mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-07-27 04:34:49 -04:00
Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e67105f0c | |||
| 0fec0d39b7 | |||
| 8709422e05 | |||
| 229aab41f8 | |||
| 853a168773 | |||
| fa963660e0 | |||
| fbb098e996 | |||
| b124244b0c | |||
| c7d1b58340 | |||
| 9971819350 | |||
| e3769d3e1f | |||
| f777ce2992 | |||
| 02eda4efb3 | |||
| 1b3420c857 | |||
| 2f537e72bb | |||
| 4afdb6f813 | |||
| c610c2c6ca | |||
| 047a324e0e | |||
| e37283cb11 | |||
| d6f10b28c1 | |||
| c082b42395 | |||
| 252ee2aff6 | |||
| fffc1f8647 | |||
| dc1653294d | |||
| 3862c3c0eb | |||
| 756c46b526 | |||
| 73a6be8264 | |||
| 84f21c6ab4 | |||
| a0ab067bf7 | |||
| c719cbd544 | |||
| 6fa73262fc | |||
| 558fa396a8 | |||
| 33ffe06510 | |||
| a5a602c5b0 | |||
| 500862b768 | |||
| b06522d1ec | |||
| 0c312c9caa | |||
| d456315f19 | |||
| 983f5599d5 | |||
| 471c937d57 | |||
| ad61551db5 | |||
| 2bad9c399f | |||
| 580ad7d46f | |||
| 47f013f3e1 | |||
| d5219a9379 | |||
| 00460b1538 | |||
| 98e62edf12 | |||
| 592969f976 | |||
| 6be849e409 | |||
| d42c2a5749 | |||
| 8d2eafbc35 | |||
| 848f688ae9 | |||
| b813842f85 | |||
| 684ce43eb8 | |||
| 39d13d155f | |||
| 2d5523a129 | |||
| 7fe3ed2169 | |||
| 6db3b5f31f | |||
| f6ac820a3f | |||
| d955284278 | |||
| 3fd6134bcd | |||
| 4d875e9e2a | |||
| 79b72ec311 | |||
| 8487459d90 | |||
| be336adf02 | |||
| be1acb06fd | |||
| 26ec479596 | |||
| efee3c462d | |||
| 4cf52ec698 | |||
| ba8ea0117e | |||
| 36770b70ae | |||
| 5784be2853 | |||
| 5b93e5c21c | |||
| b0a43b0cb7 | |||
| 51bb368073 | |||
| 153fcf6cf0 | |||
| b52571dbea | |||
| c0a4f1026f | |||
| aab73ead5c | |||
| f05af02864 | |||
| 46adfb5258 | |||
| d6784f11a7 | |||
| 4bd86bd842 | |||
| e630ead9c0 | |||
| 5bd97908b5 | |||
| f98c306b48 | |||
| cf60dd88c4 | |||
| dc7badd034 | |||
| 20511283b5 | |||
| 5c233c8aa5 | |||
| 9500ec7af0 | |||
| 1fb7f8bc75 | |||
| 62723267a9 | |||
| e4da4b307c | |||
| 578de58e44 | |||
| aff460c7c2 | |||
| ff6890c9c1 | |||
| 9160ca598e | |||
| 7a8b3d93cf | |||
| 86659e704a | |||
| 6e999927ec | |||
| 863c6f3dcb | |||
| 2a6378764b | |||
| da7cb91f83 | |||
| 13e0f12c1b | |||
| a0a9299cda | |||
| 15237d3137 | |||
| e97cd43cdb | |||
| 1313f805ae | |||
| aafbd9159a | |||
| cff90bc709 | |||
| e2de768e09 | |||
| 80871db0de | |||
| 50cad776aa | |||
| 56b6f7240c | |||
| f0dac5ca22 | |||
| c9bc5808c2 | |||
| 037aab3bdd | |||
| e2679fae5c | |||
| 4a0ff17990 | |||
| 5af97a5dd3 | |||
| 37aa532cc2 | |||
| 9a90153dc6 | |||
| 490bf998a4 | |||
| 9caaaf187a | |||
| fb1643f1c1 | |||
| 0228499889 | |||
| 35eb9ee259 | |||
| b01d469089 | |||
| 843a912a03 | |||
| 48f15a479a | |||
| 4a0d3a7a94 | |||
| a668830a79 | |||
| a3977c2b1c | |||
| a03098b01e | |||
| 367832ddb2 | |||
| 8849a9ba1b | |||
| 2b4b2c513c | |||
| e1c44d3f5c | |||
| 112cb1f955 | |||
| a288ea9a36 | |||
| 4c2ca24203 | |||
| f97865f7d8 | |||
| 255435c260 | |||
| fd60daad7a | |||
| 1a53d24890 | |||
| c23e655f44 | |||
| 2b07dd79ec | |||
| 864bf8b94c | |||
| 60c53c8f3e | |||
| cc0f5cbdcc | |||
| 8d527d6a09 | |||
| e431bc234b | |||
| 289edb67a6 | |||
| 324124e002 | |||
| 2bacac687f | |||
| 8b504a5e1e | |||
| 0e4e60cc15 | |||
| fb1ab70650 | |||
| 94f4d350c9 | |||
| cc3fdecfc9 | |||
| c92acd670e | |||
| 0464a5b344 | |||
| 8bdb575bb2 | |||
| f2c3b9dbdb | |||
| 02c6b5ad4e | |||
| 5e5224da25 | |||
| c529f880d3 | |||
| 18eb40ec14 | |||
| 904765591c | |||
| f726d3c3e5 | |||
| 62ce629bf1 | |||
| 9c555bd99f | |||
| 5981109730 | |||
| 087a5fbe0f | |||
| 67cb5937e7 | |||
| 8abf5512a4 | |||
| 45dd74d27c | |||
| 1109b0a533 | |||
| bd1b06f326 | |||
| 7406db5882 | |||
| a1efa07b24 | |||
| 29f7644577 | |||
| f3884c0244 | |||
| 6516af6c34 | |||
| 77680c6fee | |||
| 5faa599321 | |||
| 6209ededff | |||
| f6b6d5246c | |||
| b81624bfc2 | |||
| c1844f7230 | |||
| 15efd2d527 | |||
| 5e3bc0f89b | |||
| 12209ab926 | |||
| 547e9cd0c4 | |||
| 6a1e536ca7 | |||
| eb8663ada1 | |||
| ce997d2425 | |||
| 56cd0e05ca | |||
| 25bb3fb123 | |||
| 9e52438877 | |||
| c4618896cf | |||
| ee1667d51a | |||
| bafd26e92c | |||
| 8ecc18f76f | |||
| 985f07a529 | |||
| 8b2900c6d8 | |||
| 50e56fe22f | |||
| b5a12a1da6 | |||
| 70e749b53a | |||
| 784a4c7969 | |||
| 43a2a142fa | |||
| 8ee5956bdb | |||
| 4888dfcdca | |||
| a6b41fd3ab | |||
| d25aed9a1a | |||
| 4a3f471f72 | |||
| 93154a0a27 | |||
| 59ab91d7df | |||
| 42590a7371 | |||
| 6260d4f168 | |||
| 4206da92a6 | |||
| 4d6fbf1caa | |||
| 97ef7acec0 | |||
| 77189b6114 | |||
| c32d315910 | |||
| 606ffd8275 | |||
| 601dba3fc4 | |||
| 00ab476a77 | |||
| 906079cbbb | |||
| 808d9c981c | |||
| 2b79c99dd7 | |||
| 77905ed3cd | |||
| 60c249f19a | |||
| 209a9989c4 | |||
| 5105b46f48 | |||
| e4c7d1acfc | |||
| dd4fbd64d3 | |||
| 4010f9d86c |
@@ -0,0 +1,19 @@
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "bun"
|
||||
directory: "/webui/react-ui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.22
|
||||
go-version: 1.24
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@2a4836ac76fe8f5d0ee3a0d89aa12a80cc552ad3
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
|
||||
with:
|
||||
images: quay.io/mudler/localagi
|
||||
tags: |
|
||||
@@ -78,8 +78,81 @@ jobs:
|
||||
VERSION=${{ steps.prep.outputs.binary_version }}
|
||||
context: ./
|
||||
file: ./Dockerfile.webui
|
||||
#platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
#tags: ${{ steps.prep.outputs.tags }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
sshbox-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
id: prep
|
||||
run: |
|
||||
DOCKER_IMAGE=quay.io/mudler/localagi-sshbox
|
||||
# Use branch name as default
|
||||
VERSION=${GITHUB_REF#refs/heads/}
|
||||
BINARY_VERSION=$(git describe --always --tags --dirty)
|
||||
SHORTREF=${GITHUB_SHA::8}
|
||||
# If this is git tag, use the tag name as a docker tag
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}"
|
||||
# If the VERSION looks like a version number, assume that
|
||||
# this is the most recent version of the image and also
|
||||
# tag it 'latest'.
|
||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
# Set output parameters.
|
||||
echo ::set-output name=binary_version::${BINARY_VERSION}
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
|
||||
with:
|
||||
images: quay.io/mudler/localagi-sshbox
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=-{{date 'YYYYMMDDHHmmss'}}
|
||||
type=semver,pattern={{raw}}
|
||||
type=sha,suffix=-{{date 'YYYYMMDDHHmmss'}}
|
||||
type=ref,event=branch
|
||||
flavor: |
|
||||
latest=auto
|
||||
prefix=
|
||||
suffix=
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.prep.outputs.binary_version }}
|
||||
context: ./
|
||||
file: ./Dockerfile.sshbox
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
#tags: ${{ steps.prep.outputs.tags }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
+14
-30
@@ -1,9 +1,9 @@
|
||||
name: Run Go Tests
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- 'main'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
@@ -15,36 +15,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- run: |
|
||||
# Add Docker's official GPG key:
|
||||
sudo apt-get update
|
||||
sudo apt-get install ca-certificates curl
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add the repository to Apt sources:
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
docker version
|
||||
|
||||
docker run --rm hello-world
|
||||
- uses: actions/setup-go@v5
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.17.0'
|
||||
- name: Run tests
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y make
|
||||
make tests
|
||||
#sudo mv coverage/coverage.txt coverage.txt
|
||||
#sudo chmod 777 coverage.txt
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v4
|
||||
# with:
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.17.0'
|
||||
- run: |
|
||||
make tests-e2e
|
||||
@@ -1,5 +1,5 @@
|
||||
# python
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.14-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y python3-dev portaudio19-dev ffmpeg build-essential
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Final stage
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
docker.io \
|
||||
bash \
|
||||
wget \
|
||||
curl \
|
||||
openssh-server \
|
||||
sudo
|
||||
|
||||
# Configure SSH
|
||||
RUN mkdir /var/run/sshd
|
||||
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
|
||||
|
||||
# Create startup script
|
||||
RUN echo '#!/bin/bash\n\
|
||||
if [ -n "$SSH_USER" ]; then\n\
|
||||
if [ "$SSH_USER" = "root" ]; then\n\
|
||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config\n\
|
||||
if [ -n "$SSH_PASSWORD" ]; then\n\
|
||||
echo "root:$SSH_PASSWORD" | chpasswd\n\
|
||||
fi\n\
|
||||
else\n\
|
||||
echo "PermitRootLogin no" >> /etc/ssh/sshd_config\n\
|
||||
useradd -m -s /bin/bash $SSH_USER\n\
|
||||
if [ -n "$SSH_PASSWORD" ]; then\n\
|
||||
echo "$SSH_USER:$SSH_PASSWORD" | chpasswd\n\
|
||||
fi\n\
|
||||
if [ -n "$SUDO_ACCESS" ] && [ "$SUDO_ACCESS" = "true" ]; then\n\
|
||||
echo "$SSH_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$SSH_USER\n\
|
||||
fi\n\
|
||||
fi\n\
|
||||
fi\n\
|
||||
/usr/sbin/sshd -D' > /start.sh
|
||||
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 22
|
||||
|
||||
CMD ["/start.sh"]
|
||||
+18
-12
@@ -1,8 +1,5 @@
|
||||
# Define argument for linker flags
|
||||
ARG LDFLAGS=-s -w
|
||||
|
||||
# Use Bun container for building the React UI
|
||||
FROM oven/bun:1 as ui-builder
|
||||
FROM oven/bun:1 AS ui-builder
|
||||
|
||||
# Set the working directory for the React UI
|
||||
WORKDIR /app
|
||||
@@ -19,11 +16,11 @@ COPY webui/react-ui/ ./
|
||||
# Build the React UI
|
||||
RUN bun run build
|
||||
|
||||
# Use a temporary build image based on Golang 1.22-alpine
|
||||
FROM golang:1.22-alpine as builder
|
||||
# Use a temporary build image based on Golang 1.24-alpine
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Set environment variables: linker flags and disable CGO
|
||||
ENV LDFLAGS=$LDFLAGS CGO_ENABLED=0
|
||||
# Define argument for linker flags
|
||||
ARG LDFLAGS="-s -w"
|
||||
|
||||
# Install git
|
||||
RUN apk add --no-cache git
|
||||
@@ -45,14 +42,23 @@ COPY . .
|
||||
COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
|
||||
|
||||
# Build the application
|
||||
RUN go build -ldflags="$LDFLAGS" -o localagi ./
|
||||
RUN CGO_ENABLED=0 go build -ldflags="$LDFLAGS" -o localagi ./
|
||||
|
||||
FROM scratch
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
docker.io \
|
||||
bash \
|
||||
wget \
|
||||
curl
|
||||
|
||||
# Copy the webui binary from the builder stage to the final image
|
||||
COPY --from=builder /work/localagi /localagi
|
||||
COPY --from=builder /etc/ssl/ /etc/ssl/
|
||||
COPY --from=builder /tmp /tmp
|
||||
|
||||
# Define the command that will be run when the container is started
|
||||
ENTRYPOINT ["/localagi"]
|
||||
|
||||
@@ -3,13 +3,13 @@ IMAGE_NAME?=webui
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
prepare-tests:
|
||||
docker compose up -d
|
||||
docker compose up -d --build
|
||||
|
||||
cleanup-tests:
|
||||
docker compose down
|
||||
|
||||
tests: prepare-tests
|
||||
LOCALAGI_MODEL="arcee-agent" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
|
||||
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!E2E" --flake-attempts=5 --fail-fast -v -r ./...
|
||||
|
||||
run-nokb:
|
||||
$(MAKE) run KBDISABLEINDEX=true
|
||||
@@ -30,3 +30,6 @@ build-image:
|
||||
|
||||
image-push:
|
||||
docker push $(IMAGE_NAME)
|
||||
|
||||
tests-e2e: prepare-tests
|
||||
LOCALAGI_MODEL="gemma-3-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="E2E" --flake-attempts=5 --fail-fast -v -r ./tests/e2e/...
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/6958ffb3-31cf-441e-b99d-ce34ec6fc88f" alt="LocalAGI Logo" width="220"/>
|
||||
<img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/>
|
||||
</p>
|
||||
|
||||
<h3 align="center"><em>Your AI. Your Hardware. Your Rules.</em></h3>
|
||||
<h3 align="center"><em>Your AI. Your Hardware. Your Rules</em></h3>
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -11,11 +11,14 @@
|
||||
[](https://github.com/mudler/LocalAGI/stargazers)
|
||||
[](https://github.com/mudler/LocalAGI/issues)
|
||||
|
||||
|
||||
Try on [](https://t.me/LocalAGI_bot)
|
||||
|
||||
</div>
|
||||
|
||||
We empower you building AI Agents that you can run locally, without coding.
|
||||
Create customizable AI assistants, automations, chat bots and agents that run 100% locally. No need for agentic Python libraries or cloud service keys, just bring your GPU (or even just CPU) and a web browser.
|
||||
|
||||
**LocalAGI** is a powerful, self-hostable AI Agent platform designed for maximum privacy and flexibility. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
|
||||
**LocalAGI** is a powerful, self-hostable AI Agent platform that allows you to design AI automations without writing code. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
|
||||
|
||||
## 🛡️ Take Back Your Privacy
|
||||
|
||||
@@ -28,7 +31,7 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
||||
- 🎛 **No-Code Agents**: Easy-to-configure multiple agents via Web UI.
|
||||
- 🖥 **Web-Based Interface**: Simple and intuitive agent management.
|
||||
- 🤖 **Advanced Agent Teaming**: Instantly create cooperative agent teams from a single prompt.
|
||||
- 📡 **Connectors Galore**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
|
||||
- 📡 **Connectors**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
|
||||
- 🛠 **Comprehensive REST API**: Seamless integration into your workflows. Every agent created will support OpenAI Responses API out of the box.
|
||||
- 📚 **Short & Long-Term Memory**: Powered by [LocalRecall](https://github.com/mudler/LocalRecall).
|
||||
- 🧠 **Planning & Reasoning**: Agents intelligently plan, reason, and adapt.
|
||||
@@ -37,6 +40,7 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
||||
- 🖼 **Multimodal Support**: Ready for vision, text, and more.
|
||||
- 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!).
|
||||
- 🛠 **Fully Customizable Models**: Use your own models or integrate seamlessly with [LocalAI](https://github.com/mudler/LocalAI).
|
||||
- 📊 **Observability**: Monitor agent status and view detailed observable updates in real-time.
|
||||
|
||||
## 🛠️ Quickstart
|
||||
|
||||
@@ -45,14 +49,145 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
||||
git clone https://github.com/mudler/LocalAGI
|
||||
cd LocalAGI
|
||||
|
||||
# CPU setup
|
||||
docker compose up -f docker-compose.yml
|
||||
# CPU setup (default)
|
||||
docker compose up
|
||||
|
||||
# GPU setup
|
||||
docker compose up -f docker-compose.gpu.yml
|
||||
# NVIDIA GPU setup
|
||||
docker compose -f docker-compose.nvidia.yaml up
|
||||
|
||||
# Intel GPU setup (for Intel Arc and integrated GPUs)
|
||||
docker compose -f docker-compose.intel.yaml up
|
||||
|
||||
# AMD GPU setup
|
||||
docker compose -f docker-compose.amd.yaml up
|
||||
|
||||
# Start with a specific model (see available models in models.localai.io, or localai.io to use any model in huggingface)
|
||||
MODEL_NAME=gemma-3-12b-it docker compose up
|
||||
|
||||
# NVIDIA GPU setup with custom multimodal and image models
|
||||
MODEL_NAME=gemma-3-12b-it \
|
||||
MULTIMODAL_MODEL=moondream2-20250414 \
|
||||
IMAGE_MODEL=flux.1-dev-ggml \
|
||||
docker compose -f docker-compose.nvidia.yaml up
|
||||
```
|
||||
|
||||
Access your agents at `http://localhost:3000`
|
||||
Now you can access and manage your agents at [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
|
||||
|
||||
## Videos
|
||||
|
||||
[](https://youtu.be/HtVwIxW3ePg)
|
||||
[](https://youtu.be/v82rswGJt_M)
|
||||
[](https://youtu.be/d_we-AYksSw)
|
||||
[](https://youtu.be/2Xvx78i5oBs)
|
||||
|
||||
|
||||
## 📚🆕 Local Stack Family
|
||||
|
||||
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<a href="https://github.com/mudler/LocalAI">
|
||||
<img src="https://raw.githubusercontent.com/mudler/LocalAI/refs/heads/master/core/http/static/logo_horizontal.png" width="300" alt="LocalAI Logo">
|
||||
</a>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3><a href="https://github.com/mudler/LocalAI">LocalAI</a></h3>
|
||||
<p>LocalAI is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that's compatible with OpenAI API specifications for local AI inferencing. Does not require GPU.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<a href="https://github.com/mudler/LocalRecall">
|
||||
<img src="https://raw.githubusercontent.com/mudler/LocalRecall/refs/heads/main/static/localrecall_horizontal.png" width="300" alt="LocalRecall Logo">
|
||||
</a>
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<h3><a href="https://github.com/mudler/LocalRecall">LocalRecall</a></h3>
|
||||
<p>A REST-ful API and knowledge base management system that provides persistent memory and storage capabilities for AI agents.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🖥️ Hardware Configurations
|
||||
|
||||
LocalAGI supports multiple hardware configurations through Docker Compose profiles:
|
||||
|
||||
### CPU (Default)
|
||||
- No special configuration needed
|
||||
- Runs on any system with Docker
|
||||
- Best for testing and development
|
||||
- Supports text models only
|
||||
|
||||
### NVIDIA GPU
|
||||
- Requires NVIDIA GPU and drivers
|
||||
- Uses CUDA for acceleration
|
||||
- Best for high-performance inference
|
||||
- Supports text, multimodal, and image generation models
|
||||
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
|
||||
- Default models:
|
||||
- Text: `gemma-3-4b-it-qat`
|
||||
- Multimodal: `moondream2-20250414`
|
||||
- Image: `sd-1.5-ggml`
|
||||
- Environment variables:
|
||||
- `MODEL_NAME`: Text model to use
|
||||
- `MULTIMODAL_MODEL`: Multimodal model to use
|
||||
- `IMAGE_MODEL`: Image generation model to use
|
||||
- `LOCALAI_SINGLE_ACTIVE_BACKEND`: Set to `true` to enable single active backend mode
|
||||
|
||||
### Intel GPU
|
||||
- Supports Intel Arc and integrated GPUs
|
||||
- Uses SYCL for acceleration
|
||||
- Best for Intel-based systems
|
||||
- Supports text, multimodal, and image generation models
|
||||
- Run with: `docker compose -f docker-compose.intel.yaml up`
|
||||
- Default models:
|
||||
- Text: `gemma-3-4b-it-qat`
|
||||
- Multimodal: `moondream2-20250414`
|
||||
- Image: `sd-1.5-ggml`
|
||||
- Environment variables:
|
||||
- `MODEL_NAME`: Text model to use
|
||||
- `MULTIMODAL_MODEL`: Multimodal model to use
|
||||
- `IMAGE_MODEL`: Image generation model to use
|
||||
- `LOCALAI_SINGLE_ACTIVE_BACKEND`: Set to `true` to enable single active backend mode
|
||||
|
||||
## Customize models
|
||||
|
||||
You can customize the models used by LocalAGI by setting environment variables when running docker-compose. For example:
|
||||
|
||||
```bash
|
||||
# CPU with custom model
|
||||
MODEL_NAME=gemma-3-12b-it docker compose up
|
||||
|
||||
# NVIDIA GPU with custom models
|
||||
MODEL_NAME=gemma-3-12b-it \
|
||||
MULTIMODAL_MODEL=moondream2-20250414 \
|
||||
IMAGE_MODEL=flux.1-dev-ggml \
|
||||
docker compose -f docker-compose.nvidia.yaml up
|
||||
|
||||
# Intel GPU with custom models
|
||||
MODEL_NAME=gemma-3-12b-it \
|
||||
MULTIMODAL_MODEL=moondream2-20250414 \
|
||||
IMAGE_MODEL=sd-1.5-ggml \
|
||||
docker compose -f docker-compose.intel.yaml up
|
||||
|
||||
# With custom actions directory
|
||||
LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions docker compose up
|
||||
```
|
||||
|
||||
If no models are specified, it will use the defaults:
|
||||
- Text model: `gemma-3-4b-it-qat`
|
||||
- Multimodal model: `moondream2-20250414`
|
||||
- Image model: `sd-1.5-ggml`
|
||||
|
||||
Good (relatively small) models that have been tested are:
|
||||
|
||||
- `qwen_qwq-32b` (best in co-ordinating agents)
|
||||
- `gemma-3-12b-it`
|
||||
- `gemma-3-27b-it`
|
||||
|
||||
## 🏆 Why Choose LocalAGI?
|
||||
|
||||
@@ -62,14 +197,6 @@ Access your agents at `http://localhost:3000`
|
||||
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
|
||||
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
|
||||
|
||||
## 🌐 The Local Ecosystem
|
||||
|
||||
LocalAGI is part of the powerful Local family of privacy-focused AI tools:
|
||||
|
||||
- [**LocalAI**](https://github.com/mudler/LocalAI): Run Large Language Models locally.
|
||||
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
|
||||
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
|
||||
|
||||
## 🌟 Screenshots
|
||||
|
||||
### Powerful Web UI
|
||||
@@ -77,6 +204,8 @@ LocalAGI is part of the powerful Local family of privacy-focused AI tools:
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
### Connectors Ready-to-Go
|
||||
|
||||
@@ -98,6 +227,8 @@ Explore detailed documentation including:
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
LocalAGI supports environment configurations. Note that these environment variables needs to be specified in the localagi container in the docker-compose file to have effect.
|
||||
|
||||
| Variable | What It Does |
|
||||
|----------|--------------|
|
||||
| `LOCALAGI_MODEL` | Your go-to model |
|
||||
@@ -109,6 +240,7 @@ Explore detailed documentation including:
|
||||
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
|
||||
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
|
||||
|
||||
## Installation Options
|
||||
|
||||
@@ -137,6 +269,430 @@ go build -o localagi
|
||||
./localagi
|
||||
```
|
||||
|
||||
### Using as a Library
|
||||
|
||||
LocalAGI can be used as a Go library to programmatically create and manage AI agents. Let's start with a simple example of creating a single agent:
|
||||
|
||||
<details>
|
||||
<summary><strong>Basic Usage: Single Agent</strong></summary>
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
)
|
||||
|
||||
// Create a new agent with basic configuration
|
||||
agent, err := agent.New(
|
||||
agent.WithModel("gpt-4"),
|
||||
agent.WithLLMAPIURL("http://localhost:8080"),
|
||||
agent.WithLLMAPIKey("your-api-key"),
|
||||
agent.WithSystemPrompt("You are a helpful assistant."),
|
||||
agent.WithCharacter(agent.Character{
|
||||
Name: "my-agent",
|
||||
}),
|
||||
agent.WithActions(
|
||||
// Add your custom actions here
|
||||
),
|
||||
agent.WithStateFile("./state/my-agent.state.json"),
|
||||
agent.WithCharacterFile("./state/my-agent.character.json"),
|
||||
agent.WithTimeout("10m"),
|
||||
agent.EnableKnowledgeBase(),
|
||||
agent.EnableReasoning(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Start the agent
|
||||
go func() {
|
||||
if err := agent.Run(); err != nil {
|
||||
log.Printf("Agent stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Stop the agent when done
|
||||
agent.Stop()
|
||||
```
|
||||
|
||||
This basic example shows how to:
|
||||
- Create a single agent with essential configuration
|
||||
- Set up the agent's model and API connection
|
||||
- Configure basic features like knowledge base and reasoning
|
||||
- Start and stop the agent
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Advanced Usage: Agent Pools</strong></summary>
|
||||
|
||||
For managing multiple agents, you can use the AgentPool system:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
)
|
||||
|
||||
// Create a new agent pool
|
||||
pool, err := state.NewAgentPool(
|
||||
"default-model", // default model name
|
||||
"default-multimodal-model", // default multimodal model
|
||||
"image-model", // image generation model
|
||||
"http://localhost:8080", // API URL
|
||||
"your-api-key", // API key
|
||||
"./state", // state directory
|
||||
"http://localhost:8081", // LocalRAG API URL
|
||||
func(config *AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action {
|
||||
// Define available actions for agents
|
||||
return func(ctx context.Context, pool *AgentPool) []types.Action {
|
||||
return []types.Action{
|
||||
// Add your custom actions here
|
||||
}
|
||||
}
|
||||
},
|
||||
func(config *AgentConfig) []Connector {
|
||||
// Define connectors for agents
|
||||
return []Connector{
|
||||
// Add your custom connectors here
|
||||
}
|
||||
},
|
||||
func(config *AgentConfig) []DynamicPrompt {
|
||||
// Define dynamic prompts for agents
|
||||
return []DynamicPrompt{
|
||||
// Add your custom prompts here
|
||||
}
|
||||
},
|
||||
func(config *AgentConfig) types.JobFilters {
|
||||
// Define job filters for agents
|
||||
return types.JobFilters{
|
||||
// Add your custom filters here
|
||||
}
|
||||
},
|
||||
"10m", // timeout
|
||||
true, // enable conversation logs
|
||||
)
|
||||
|
||||
// Create a new agent in the pool
|
||||
agentConfig := &AgentConfig{
|
||||
Name: "my-agent",
|
||||
Model: "gpt-4",
|
||||
SystemPrompt: "You are a helpful assistant.",
|
||||
EnableKnowledgeBase: true,
|
||||
EnableReasoning: true,
|
||||
// Add more configuration options as needed
|
||||
}
|
||||
|
||||
err = pool.CreateAgent("my-agent", agentConfig)
|
||||
|
||||
// Start all agents
|
||||
err = pool.StartAll()
|
||||
|
||||
// Get agent status
|
||||
status := pool.GetStatusHistory("my-agent")
|
||||
|
||||
// Stop an agent
|
||||
pool.Stop("my-agent")
|
||||
|
||||
// Remove an agent
|
||||
err = pool.Remove("my-agent")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Available Features</strong></summary>
|
||||
|
||||
Key features available through the library:
|
||||
|
||||
- **Single Agent Management**: Create and manage individual agents with basic configuration
|
||||
- **Agent Pool Management**: Create, start, stop, and remove multiple agents
|
||||
- **Configuration**: Customize agent behavior through AgentConfig
|
||||
- **Actions**: Define custom actions for agents to perform
|
||||
- **Connectors**: Add custom connectors for external services
|
||||
- **Dynamic Prompts**: Create dynamic prompt templates
|
||||
- **Job Filters**: Implement custom job filtering logic
|
||||
- **Status Tracking**: Monitor agent status and history
|
||||
- **State Persistence**: Automatic state saving and loading
|
||||
|
||||
For more details about available configuration options and features, refer to the [Agent Configuration Reference](#agent-configuration-reference) section.
|
||||
|
||||
</details>
|
||||
|
||||
## 🔧 Extending LocalAGI
|
||||
|
||||
LocalAGI provides two powerful ways to extend its functionality with custom actions:
|
||||
|
||||
### 1. Custom Actions (Go Code)
|
||||
|
||||
LocalAGI supports custom actions written in Go that can be defined inline when creating an agent. These actions are interpreted at runtime, so no compilation is required.
|
||||
|
||||
#### Automatic Custom Actions Loading
|
||||
|
||||
You can also place custom Go action files in a directory and have LocalAGI automatically load them. Set the `LOCALAGI_CUSTOM_ACTIONS_DIR` environment variable to point to a directory containing your custom action files. Each `.go` file in this directory will be automatically loaded and made available to all agents.
|
||||
|
||||
**Example setup:**
|
||||
```bash
|
||||
# Set the environment variable
|
||||
export LOCALAGI_CUSTOM_ACTIONS_DIR="/path/to/custom/actions"
|
||||
|
||||
# Or in docker-compose.yaml
|
||||
environment:
|
||||
- LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions
|
||||
```
|
||||
|
||||
**Directory structure:**
|
||||
```
|
||||
custom-actions/
|
||||
├── weather_action.go
|
||||
├── file_processor.go
|
||||
└── database_query.go
|
||||
```
|
||||
|
||||
Each file should contain the three required functions (`Run`, `Definition`, `RequiredFields`) as described below.
|
||||
|
||||
#### How Custom Actions Work
|
||||
|
||||
When creating a new Agent, in the action sections select the "custom" action, you can add the Golang code directly there.
|
||||
|
||||
Custom actions in LocalAGI require three main functions:
|
||||
|
||||
1. **`Run(config map[string]interface{}) (string, map[string]interface{}, error)`** - The main execution function
|
||||
2. **`Definition() map[string][]string`** - Defines the action's parameters and their types
|
||||
3. **`RequiredFields() []string`** - Specifies which parameters are required
|
||||
|
||||
Note: You can't use additional modules, but just use libraries that are included in Go.
|
||||
|
||||
#### Example: Weather Information Action
|
||||
|
||||
Here's a practical example of a custom action that fetches weather information:
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"io"
|
||||
)
|
||||
|
||||
type WeatherParams struct {
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
type WeatherResponse struct {
|
||||
Main struct {
|
||||
Temp float64 `json:"temp"`
|
||||
Humidity int `json:"humidity"`
|
||||
} `json:"main"`
|
||||
Weather []struct {
|
||||
Description string `json:"description"`
|
||||
} `json:"weather"`
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
// Parse parameters
|
||||
p := WeatherParams{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
// Make API call to weather service
|
||||
url := fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s,%s&appid=YOUR_API_KEY&units=metric", p.City, p.Country)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
var weather WeatherResponse
|
||||
if err := json.Unmarshal(body, &weather); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
// Format response
|
||||
result := fmt.Sprintf("Weather in %s, %s: %.1f°C, %s, Humidity: %d%%",
|
||||
p.City, p.Country, weather.Main.Temp, weather.Weather[0].Description, weather.Main.Humidity)
|
||||
|
||||
return result, map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"city": []string{
|
||||
"string",
|
||||
"The city name to get weather for",
|
||||
},
|
||||
"country": []string{
|
||||
"string",
|
||||
"The country code (e.g., US, UK, DE)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"city", "country"}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example: File System Action
|
||||
|
||||
Here's another example that demonstrates file system operations:
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FileParams struct {
|
||||
Path string `json:"path"`
|
||||
Action string `json:"action"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
p := FileParams{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
switch p.Action {
|
||||
case "read":
|
||||
content, err := os.ReadFile(p.Path)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
return string(content), map[string]interface{}{}, nil
|
||||
|
||||
case "write":
|
||||
err := os.WriteFile(p.Path, []byte(p.Content), 0644)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
return fmt.Sprintf("Successfully wrote to %s", p.Path), map[string]interface{}{}, nil
|
||||
|
||||
case "list":
|
||||
files, err := os.ReadDir(p.Path)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
var fileList []string
|
||||
for _, file := range files {
|
||||
fileList = append(fileList, file.Name())
|
||||
}
|
||||
|
||||
result, _ := json.Marshal(fileList)
|
||||
return string(result), map[string]interface{}{}, nil
|
||||
|
||||
default:
|
||||
return "", map[string]interface{}{}, fmt.Errorf("unknown action: %s", p.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"path": []string{
|
||||
"string",
|
||||
"The file or directory path",
|
||||
},
|
||||
"action": []string{
|
||||
"string",
|
||||
"The action to perform: read, write, or list",
|
||||
},
|
||||
"content": []string{
|
||||
"string",
|
||||
"Content to write (required for write action)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"path", "action"}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Custom Actions in Agents
|
||||
|
||||
To use custom actions, add them to your agent configuration:
|
||||
|
||||
1. **Via Web UI**: In the agent creation form, add a "Custom" action and paste your Go code
|
||||
2. **Via API**: Include the custom action in your agent configuration JSON
|
||||
3. **Via Library**: Add the custom action to your agent's actions list
|
||||
|
||||
### 2. MCP (Model Context Protocol) Servers
|
||||
|
||||
LocalAGI supports both local and remote MCP servers, allowing you to extend functionality with external tools and services.
|
||||
|
||||
#### What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) is a standard for connecting AI applications to external data sources and tools. LocalAGI can connect to any MCP-compliant server to access additional capabilities.
|
||||
|
||||
#### Local MCP Servers
|
||||
|
||||
Local MCP servers run as processes that LocalAGI can spawn and communicate with via STDIO.
|
||||
|
||||
##### Example: GitHub MCP Server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Remote MCP Servers
|
||||
|
||||
Remote MCP servers are HTTP-based and can be accessed over the network.
|
||||
|
||||
#### Creating Your Own MCP Server
|
||||
|
||||
You can create MCP servers in any language that supports the MCP protocol and add the URLs of the servers to LocalAGI.
|
||||
|
||||
#### Configuring MCP Servers in LocalAGI
|
||||
|
||||
1. **Via Web UI**: In the MCP Settings section of agent creation, add MCP servers
|
||||
2. **Via API**: Include MCP server configuration in your agent config
|
||||
|
||||
#### Best Practices
|
||||
|
||||
- **Security**: Always validate inputs and use proper authentication for remote MCP servers
|
||||
- **Error Handling**: Implement robust error handling in your MCP servers
|
||||
- **Documentation**: Provide clear descriptions for all tools exposed by your MCP server
|
||||
- **Testing**: Test your MCP servers independently before integrating with LocalAGI
|
||||
- **Resource Management**: Ensure your MCP servers properly clean up resources
|
||||
|
||||
### Development
|
||||
|
||||
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
|
||||
@@ -146,15 +702,39 @@ The development workflow is similar to the source build, but with additional ste
|
||||
git clone https://github.com/mudler/LocalAGI.git
|
||||
cd LocalAGI
|
||||
|
||||
# Install dependencies and start frontend development server
|
||||
cd webui/react-ui && bun i && bun run dev
|
||||
cd webui/react-ui
|
||||
|
||||
# Install dependencies
|
||||
bun i
|
||||
|
||||
# Compile frontend (the build directory needs to exist for the backend to start)
|
||||
bun run build
|
||||
|
||||
# Start frontend development server
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Then in seperate terminal:
|
||||
Then in separate terminal:
|
||||
|
||||
```bash
|
||||
cd LocalAGI
|
||||
|
||||
# Create a "pool" directory for agent state
|
||||
mkdir pool
|
||||
|
||||
# Set required environment variables
|
||||
export LOCALAGI_MODEL=gemma-3-4b-it-qat
|
||||
export LOCALAGI_MULTIMODAL_MODEL=moondream2-20250414
|
||||
export LOCALAGI_IMAGE_MODEL=sd-1.5-ggml
|
||||
export LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
export LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||
export LOCALAGI_STATE_DIR=./pool
|
||||
export LOCALAGI_TIMEOUT=5m
|
||||
export LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
||||
export LOCALAGI_SSHBOX_URL=root:root@sshbox:22
|
||||
|
||||
# Start development server
|
||||
cd ../.. && go run main.go
|
||||
go run main.go
|
||||
```
|
||||
|
||||
> Note: see webui/react-ui/.vite.config.js for env vars that can be used to configure the backend URL
|
||||
@@ -163,7 +743,8 @@ cd ../.. && go run main.go
|
||||
|
||||
Link your agents to the services you already use. Configuration examples below.
|
||||
|
||||
### GitHub Issues
|
||||
<details>
|
||||
<summary><strong>GitHub Issues</strong></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -173,8 +754,10 @@ Link your agents to the services you already use. Configuration examples below.
|
||||
"botUserName": "bot-username"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### Discord
|
||||
<details>
|
||||
<summary><strong>Discord</strong></summary>
|
||||
|
||||
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
|
||||
|
||||
@@ -186,8 +769,10 @@ After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/dis
|
||||
```
|
||||
> Don't forget to enable "Message Content Intent" in Bot(tab) settings!
|
||||
> Enable " Message Content Intent " in the Bot tab!
|
||||
</details>
|
||||
|
||||
### Slack
|
||||
<details>
|
||||
<summary><strong>Slack</strong></summary>
|
||||
|
||||
Use the included `slack.yaml` manifest to create your app, then configure:
|
||||
|
||||
@@ -200,19 +785,39 @@ Use the included `slack.yaml` manifest to create your app, then configure:
|
||||
|
||||
- Create Oauth token bot token from "OAuth & Permissions" -> "OAuth Tokens for Your Workspace"
|
||||
- Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
|
||||
</details>
|
||||
|
||||
|
||||
### Telegram
|
||||
<details>
|
||||
<summary><strong>Telegram</strong></summary>
|
||||
|
||||
Get a token from @botfather, then:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "your-bot-father-token"
|
||||
"token": "your-bot-father-token",
|
||||
"group_mode": "true",
|
||||
"mention_only": "true",
|
||||
"admins": "username1,username2"
|
||||
}
|
||||
```
|
||||
|
||||
### IRC
|
||||
Configuration options:
|
||||
- `token`: Your bot token from BotFather
|
||||
- `group_mode`: Enable/disable group chat functionality
|
||||
- `mention_only`: When enabled, bot only responds when mentioned in groups
|
||||
- `admins`: Comma-separated list of Telegram usernames allowed to use the bot in private chats
|
||||
- `channel_id`: Optional channel ID for the bot to send messages to
|
||||
|
||||
> **Important**: For group functionality to work properly:
|
||||
> 1. Go to @BotFather
|
||||
> 2. Select your bot
|
||||
> 3. Go to "Bot Settings" > "Group Privacy"
|
||||
> 4. Select "Turn off" to allow the bot to read all messages in groups
|
||||
> 5. Restart your bot after changing this setting
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>IRC</strong></summary>
|
||||
|
||||
Connect to IRC networks:
|
||||
|
||||
@@ -225,10 +830,29 @@ Connect to IRC networks:
|
||||
"alwaysReply": "false"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Email</strong></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"smtpServer": "smtp.gmail.com:587",
|
||||
"imapServer": "imap.gmail.com:993",
|
||||
"smtpInsecure": "false",
|
||||
"imapInsecure": "false",
|
||||
"username": "user@gmail.com",
|
||||
"email": "user@gmail.com",
|
||||
"password": "correct-horse-battery-staple",
|
||||
"name": "LogalAGI Agent"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## REST API
|
||||
|
||||
### Agent Management
|
||||
<details>
|
||||
<summary><strong>Agent Management</strong></summary>
|
||||
|
||||
| Endpoint | Method | Description | Example |
|
||||
|----------|--------|-------------|---------|
|
||||
@@ -243,8 +867,10 @@ Connect to IRC networks:
|
||||
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
|
||||
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
|
||||
| `/settings/import` | POST | Import agent config | [Example](#import-agent) |
|
||||
</details>
|
||||
|
||||
### Actions and Groups
|
||||
<details>
|
||||
<summary><strong>Actions and Groups</strong></summary>
|
||||
|
||||
| Endpoint | Method | Description | Example |
|
||||
|----------|--------|-------------|---------|
|
||||
@@ -252,8 +878,10 @@ Connect to IRC networks:
|
||||
| `/api/action/:name/run` | POST | Execute an action | |
|
||||
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
|
||||
| `/api/agent/group/create` | POST | Create a new agent group | |
|
||||
</details>
|
||||
|
||||
### Chat Interactions
|
||||
<details>
|
||||
<summary><strong>Chat Interactions</strong></summary>
|
||||
|
||||
| Endpoint | Method | Description | Example |
|
||||
|----------|--------|-------------|---------|
|
||||
@@ -261,6 +889,7 @@ Connect to IRC networks:
|
||||
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
|
||||
| `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) |
|
||||
| `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) |
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Curl Examples</strong></summary>
|
||||
@@ -348,11 +977,13 @@ curl -X POST "http://localhost:3000/api/notify/my-agent" \
|
||||
curl -N -X GET "http://localhost:3000/api/sse/my-agent"
|
||||
```
|
||||
Note: For proper SSE handling, you should use a client that supports SSE natively.
|
||||
|
||||
</details>
|
||||
|
||||
### Agent Configuration Reference
|
||||
|
||||
<details>
|
||||
<summary><strong>Configuration Structure</strong></summary>
|
||||
|
||||
The agent configuration defines how an agent behaves and what capabilities it has. You can view the available configuration options and their descriptions by using the metadata endpoint:
|
||||
|
||||
```bash
|
||||
@@ -385,6 +1016,27 @@ Here's an example of the agent configuration structure:
|
||||
"summary_long_term_memory": false
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Environment Configuration</strong></summary>
|
||||
|
||||
LocalAGI supports environment configurations. Note that these environment variables needs to be specified in the localagi container in the docker-compose file to have effect.
|
||||
|
||||
| Variable | What It Does |
|
||||
|----------|--------------|
|
||||
| `LOCALAGI_MODEL` | Your go-to model |
|
||||
| `LOCALAGI_MULTIMODAL_MODEL` | Optional model for multimodal capabilities |
|
||||
| `LOCALAGI_LLM_API_URL` | OpenAI-compatible API server URL |
|
||||
| `LOCALAGI_LLM_API_KEY` | API authentication |
|
||||
| `LOCALAGI_TIMEOUT` | Request timeout settings |
|
||||
| `LOCALAGI_STATE_DIR` | Where state gets stored |
|
||||
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
|
||||
| `LOCALAGI_SSHBOX_URL` | LocalAGI SSHBox URL, e.g. user:pass@ip:port |
|
||||
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
|
||||
</details>
|
||||
|
||||
## LICENSE
|
||||
|
||||
|
||||
+48
-6
@@ -3,6 +3,8 @@ package action
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
@@ -24,7 +26,7 @@ func NewCustom(config map[string]string, goPkgPath string) (*CustomAction, error
|
||||
}
|
||||
|
||||
if err := a.callInit(); err != nil {
|
||||
xlog.Error("Error calling custom action init", "error", err)
|
||||
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
|
||||
}
|
||||
|
||||
return a, nil
|
||||
@@ -43,18 +45,23 @@ func (a *CustomAction) callInit() error {
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
|
||||
if err != nil {
|
||||
return err
|
||||
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
|
||||
return nil
|
||||
}
|
||||
|
||||
run := v.Interface().(func() error)
|
||||
run, ok := v.Interface().(func(string) error)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return run()
|
||||
return run(a.config["configuration"])
|
||||
}
|
||||
|
||||
func (a *CustomAction) initializeInterpreter() error {
|
||||
if _, exists := a.config["code"]; exists && a.i == nil {
|
||||
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
|
||||
i := interp.New(interp.Options{
|
||||
Env: os.Environ(),
|
||||
GoPath: a.goPkgPath,
|
||||
Unrestricted: unsafe,
|
||||
})
|
||||
@@ -66,6 +73,15 @@ func (a *CustomAction) initializeInterpreter() error {
|
||||
a.config["name"] = "custom"
|
||||
}
|
||||
|
||||
// let's find first if there is already a package declarated in the code
|
||||
// the user might want to specify it to not break syntax with IDEs
|
||||
re := regexp.MustCompile("package (\\w+)")
|
||||
packageName := re.FindStringSubmatch(a.config["code"])
|
||||
if len(packageName) > 1 {
|
||||
// remove it from the code, normalize to `name`
|
||||
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
|
||||
}
|
||||
|
||||
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -81,7 +97,7 @@ func (a *CustomAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *CustomAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
@@ -95,12 +111,32 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
|
||||
|
||||
func (a *CustomAction) Definition() types.ActionDefinition {
|
||||
|
||||
if a.i == nil {
|
||||
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Error("Error getting custom action definition", "error", err)
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
description := ""
|
||||
desc, err := a.i.Eval(fmt.Sprintf("%s.Description", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Warn("No description found for custom action", "error", err, "action", a.config["name"])
|
||||
} else {
|
||||
d, ok := desc.Interface().(func() string)
|
||||
if ok {
|
||||
description = d()
|
||||
}
|
||||
}
|
||||
|
||||
if a.config["description"] != "" {
|
||||
description = a.config["description"]
|
||||
}
|
||||
|
||||
properties := v.Interface().(func() map[string][]string)
|
||||
|
||||
v, err = a.i.Eval(fmt.Sprintf("%s.RequiredFields", a.config["name"]))
|
||||
@@ -125,7 +161,7 @@ func (a *CustomAction) Definition() types.ActionDefinition {
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(a.config["name"]),
|
||||
Description: a.config["description"],
|
||||
Description: description,
|
||||
Properties: prop,
|
||||
Required: requiredFields(),
|
||||
}
|
||||
@@ -159,5 +195,11 @@ func CustomConfigMeta() []config.Field {
|
||||
Type: config.FieldTypeCheckbox,
|
||||
HelpText: "Allow unsafe code execution",
|
||||
},
|
||||
{
|
||||
Name: "configuration",
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration for the custom action",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ return []string{"foo"}
|
||||
Description: "A test action",
|
||||
}))
|
||||
|
||||
runResult, err := customAction.Run(context.Background(), types.ActionParams{
|
||||
runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
|
||||
"Foo": "bar",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// NewIntention creates a new intention action
|
||||
// The inention action is special as it tries to identify
|
||||
// a tool to use and a reasoning over to use it
|
||||
func NewIntention(s ...string) *IntentAction {
|
||||
return &IntentAction{tools: s}
|
||||
}
|
||||
|
||||
type IntentAction struct {
|
||||
tools []string
|
||||
}
|
||||
type IntentResponse struct {
|
||||
Tool string `json:"tool"`
|
||||
Reasoning string `json:"reasoning"`
|
||||
}
|
||||
|
||||
func (a *IntentAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
func (a *IntentAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *IntentAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "pick_tool",
|
||||
Description: "Pick a tool",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"reasoning": {
|
||||
Type: jsonschema.String,
|
||||
Description: "A detailed reasoning on why you want to call this tool.",
|
||||
},
|
||||
"tool": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The tool you want to use",
|
||||
Enum: a.tools,
|
||||
},
|
||||
},
|
||||
Required: []string{"tool", "reasoning"},
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ type ConversationActionResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (a *ConversationAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *ConversationAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func NewStop() *StopAction {
|
||||
|
||||
type StopAction struct{}
|
||||
|
||||
func (a *StopAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *StopAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// PlanActionName is the name of the plan action
|
||||
// used by the LLM to schedule more actions
|
||||
const PlanActionName = "plan"
|
||||
|
||||
func NewPlan(plannableActions []string) *PlanAction {
|
||||
return &PlanAction{
|
||||
plannables: plannableActions,
|
||||
}
|
||||
}
|
||||
|
||||
type PlanAction struct {
|
||||
plannables []string
|
||||
}
|
||||
|
||||
type PlanResult struct {
|
||||
Subtasks []PlanSubtask `json:"subtasks"`
|
||||
Goal string `json:"goal"`
|
||||
}
|
||||
type PlanSubtask struct {
|
||||
Action string `json:"action"`
|
||||
Reasoning string `json:"reasoning"`
|
||||
}
|
||||
|
||||
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
func (a *PlanAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *PlanAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: PlanActionName,
|
||||
Description: "Use this tool for solving complex tasks that involves calling more tools in sequence.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"subtasks": {
|
||||
Type: jsonschema.Array,
|
||||
Description: "The subtasks to be executed",
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"action": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The action to call",
|
||||
Enum: a.plannables,
|
||||
},
|
||||
"reasoning": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The reasoning for calling this action",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"goal": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The goal of this plan",
|
||||
},
|
||||
},
|
||||
Required: []string{"subtasks", "goal"},
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// NewReasoning creates a new reasoning action
|
||||
// The reasoning action is special as it tries to force the LLM
|
||||
// to think about what to do next
|
||||
func NewReasoning() *ReasoningAction {
|
||||
return &ReasoningAction{}
|
||||
}
|
||||
|
||||
type ReasoningAction struct{}
|
||||
|
||||
type ReasoningResponse struct {
|
||||
Reasoning string `json:"reasoning"`
|
||||
}
|
||||
|
||||
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{}, nil
|
||||
}
|
||||
|
||||
func (a *ReasoningAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *ReasoningAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "pick_action",
|
||||
Description: "try to understand what's the best thing to do and pick an action with a reasoning",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"reasoning": {
|
||||
Type: jsonschema.String,
|
||||
Description: "A detailed reasoning on what would you do in this situation.",
|
||||
},
|
||||
},
|
||||
Required: []string{"reasoning"},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
ReminderActionName = "set_reminder"
|
||||
ListRemindersName = "list_reminders"
|
||||
RemoveReminderName = "remove_reminder"
|
||||
)
|
||||
|
||||
func NewReminder() *ReminderAction {
|
||||
return &ReminderAction{}
|
||||
}
|
||||
|
||||
func NewListReminders() *ListRemindersAction {
|
||||
return &ListRemindersAction{}
|
||||
}
|
||||
|
||||
func NewRemoveReminder() *RemoveReminderAction {
|
||||
return &RemoveReminderAction{}
|
||||
}
|
||||
|
||||
type ReminderAction struct{}
|
||||
type ListRemindersAction struct{}
|
||||
type RemoveReminderAction struct{}
|
||||
|
||||
type RemoveReminderParams struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := types.ReminderActionResponse{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Validate the cron expression
|
||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
_, err = parser.Parse(result.CronExpr)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Calculate next run time
|
||||
now := time.Now()
|
||||
schedule, _ := parser.Parse(result.CronExpr) // We can ignore the error since we validated above
|
||||
nextRun := schedule.Next(now)
|
||||
|
||||
// Set the reminder details
|
||||
result.LastRun = now
|
||||
result.NextRun = nextRun
|
||||
// IsRecurring is set by the user through the action parameters
|
||||
|
||||
// Store the reminder in the shared state
|
||||
if sharedState.Reminders == nil {
|
||||
sharedState.Reminders = make([]types.ReminderActionResponse, 0)
|
||||
}
|
||||
sharedState.Reminders = append(sharedState.Reminders, result)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: "Reminder set successfully",
|
||||
Metadata: map[string]interface{}{
|
||||
"reminder": result,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: "No reminders set",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString("Current reminders:\n")
|
||||
for i, reminder := range sharedState.Reminders {
|
||||
status := "one-time"
|
||||
if reminder.IsRecurring {
|
||||
status = "recurring"
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s)\n",
|
||||
i+1,
|
||||
reminder.Message,
|
||||
reminder.NextRun.Format(time.RFC3339),
|
||||
status))
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: result.String(),
|
||||
Metadata: map[string]interface{}{
|
||||
"reminders": sharedState.Reminders,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var removeParams RemoveReminderParams
|
||||
err := params.Unmarshal(&removeParams)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
|
||||
return types.ActionResult{
|
||||
Result: "No reminders to remove",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert from 1-based index to 0-based
|
||||
index := removeParams.Index - 1
|
||||
if index < 0 || index >= len(sharedState.Reminders) {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid reminder index: %d", removeParams.Index)
|
||||
}
|
||||
|
||||
// Remove the reminder
|
||||
removed := sharedState.Reminders[index]
|
||||
sharedState.Reminders = append(sharedState.Reminders[:index], sharedState.Reminders[index+1:]...)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed reminder: %s", removed.Message),
|
||||
Metadata: map[string]interface{}{
|
||||
"removed_reminder": removed,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *RemoveReminderAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ReminderAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ReminderActionName,
|
||||
Description: "Set a reminder for the agent to wake up and perform a task based on a cron schedule. Examples: '0 0 * * *' (daily at midnight), '0 */2 * * *' (every 2 hours), '0 0 * * 1' (every Monday at midnight)",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message or task to be reminded about",
|
||||
},
|
||||
"cron_expr": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'second minute hour day month weekday'",
|
||||
},
|
||||
"is_recurring": {
|
||||
Type: jsonschema.Boolean,
|
||||
Description: "Whether this reminder should repeat according to the cron schedule (true) or trigger only once (false)",
|
||||
},
|
||||
},
|
||||
Required: []string{"message", "cron_expr", "is_recurring"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ListRemindersAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ListRemindersName,
|
||||
Description: "List all currently set reminders with their next scheduled run times",
|
||||
Properties: map[string]jsonschema.Definition{},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *RemoveReminderAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: RemoveReminderName,
|
||||
Description: "Remove a reminder by its index number (use list_reminders to see the index)",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"index": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The index number of the reminder to remove (1-based)",
|
||||
},
|
||||
},
|
||||
Required: []string{"index"},
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// ReplyActionName is the name of the reply action
|
||||
// used by the LLM to reply to the user without
|
||||
// any additional processing
|
||||
const ReplyActionName = "reply"
|
||||
|
||||
func NewReply() *ReplyAction {
|
||||
return &ReplyAction{}
|
||||
}
|
||||
|
||||
type ReplyAction struct{}
|
||||
|
||||
type ReplyResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) {
|
||||
return "no-op", nil
|
||||
}
|
||||
|
||||
func (a *ReplyAction) Plannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *ReplyAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: ReplyActionName,
|
||||
Description: "Use this tool to reply to the user once we have all the informations we need.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message to reply with",
|
||||
},
|
||||
},
|
||||
Required: []string{"message"},
|
||||
}
|
||||
}
|
||||
+1
-40
@@ -2,7 +2,6 @@ package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
@@ -16,25 +15,7 @@ func NewState() *StateAction {
|
||||
|
||||
type StateAction struct{}
|
||||
|
||||
// State is the structure
|
||||
// that is used to keep track of the current state
|
||||
// and the Agent's short memory that it can update
|
||||
// Besides a long term memory that is accessible by the agent (With vector database),
|
||||
// And a context memory (that is always powered by a vector database),
|
||||
// this memory is the shorter one that the LLM keeps across conversation and across its
|
||||
// reasoning process's and life time.
|
||||
// TODO: A special action is then used to let the LLM itself update its memory
|
||||
// periodically during self-processing, and the same action is ALSO exposed
|
||||
// during the conversation to let the user put for example, a new goal to the agent.
|
||||
type AgentInternalState struct {
|
||||
NowDoing string `json:"doing_now"`
|
||||
DoingNext string `json:"doing_next"`
|
||||
DoneHistory []string `json:"done_history"`
|
||||
Memories []string `json:"memories"`
|
||||
Goal string `json:"goal"`
|
||||
}
|
||||
|
||||
func (a *StateAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *StateAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
return types.ActionResult{Result: "internal state has been updated"}, nil
|
||||
}
|
||||
|
||||
@@ -76,23 +57,3 @@ func (a *StateAction) Definition() types.ActionDefinition {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fmtT = `=====================
|
||||
NowDoing: %s
|
||||
DoingNext: %s
|
||||
Your current goal is: %s
|
||||
You have done: %+v
|
||||
You have a short memory with: %+v
|
||||
=====================
|
||||
`
|
||||
|
||||
func (c AgentInternalState) String() string {
|
||||
return fmt.Sprintf(
|
||||
fmtT,
|
||||
c.NowDoing,
|
||||
c.DoingNext,
|
||||
c.Goal,
|
||||
c.DoneHistory,
|
||||
c.Memories,
|
||||
)
|
||||
}
|
||||
|
||||
+28
-342
@@ -1,84 +1,33 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type decisionResult struct {
|
||||
actionParams types.ActionParams
|
||||
message string
|
||||
actioName string
|
||||
}
|
||||
|
||||
// decision forces the agent to take one of the available actions
|
||||
func (a *Agent) decision(
|
||||
ctx context.Context,
|
||||
conversation []openai.ChatCompletionMessage,
|
||||
tools []openai.Tool, toolchoice any, maxRetries int) (*decisionResult, error) {
|
||||
|
||||
var lastErr error
|
||||
for attempts := 0; attempts < maxRetries; attempts++ {
|
||||
decision := openai.ChatCompletionRequest{
|
||||
Model: a.options.LLMAPI.Model,
|
||||
Messages: conversation,
|
||||
Tools: tools,
|
||||
ToolChoice: toolchoice,
|
||||
}
|
||||
|
||||
resp, err := a.client.CreateChatCompletion(ctx, decision)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(resp.Choices) != 1 {
|
||||
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
|
||||
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := resp.Choices[0].Message
|
||||
if len(msg.ToolCalls) != 1 {
|
||||
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
|
||||
xlog.Error("Error saving conversation", "error", err)
|
||||
}
|
||||
return &decisionResult{message: msg.Content}, nil
|
||||
}
|
||||
|
||||
params := types.ActionParams{}
|
||||
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
|
||||
lastErr = err
|
||||
xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
|
||||
xlog.Error("Error saving conversation", "error", err)
|
||||
}
|
||||
|
||||
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
type Messages []openai.ChatCompletionMessage
|
||||
|
||||
func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
|
||||
return []openai.ChatCompletionMessage(m)
|
||||
}
|
||||
|
||||
func (m Messages) RemoveIf(f func(msg openai.ChatCompletionMessage) bool) Messages {
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
if f(m[i]) {
|
||||
m = append(m[:i], m[i+1:]...)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Messages) String() string {
|
||||
s := ""
|
||||
for _, cc := range m {
|
||||
@@ -131,6 +80,7 @@ func (m Messages) Save(path string) error {
|
||||
}
|
||||
|
||||
func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
|
||||
xlog.Debug("Getting latest user message", "messages", m)
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
msg := m[i]
|
||||
if msg.Role == UserRole {
|
||||
@@ -141,178 +91,26 @@ func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Messages) IsLastMessageFromRole(role string) bool {
|
||||
if len(m) == 0 {
|
||||
return false
|
||||
// getAvailableActionsForJob returns available actions including user-defined ones for a specific job
|
||||
func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
|
||||
// Start with regular available actions
|
||||
baseActions := a.availableActions()
|
||||
|
||||
// Add user-defined actions from the job
|
||||
userTools := job.GetUserTools()
|
||||
if len(userTools) > 0 {
|
||||
userDefinedActions := types.CreateUserDefinedActions(userTools)
|
||||
baseActions = append(baseActions, userDefinedActions...)
|
||||
xlog.Debug("Added user-defined actions", "definitions", userTools)
|
||||
}
|
||||
|
||||
return m[len(m)-1].Role == role
|
||||
}
|
||||
|
||||
func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
|
||||
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conversation := c
|
||||
if !Messages(c).Exist(stateHUD) && a.options.enableHUD {
|
||||
conversation = append([]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "system",
|
||||
Content: stateHUD,
|
||||
},
|
||||
}, conversation...)
|
||||
}
|
||||
|
||||
cc := conversation
|
||||
if a.options.forceReasoning {
|
||||
cc = append(conversation, openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, reasoning),
|
||||
})
|
||||
}
|
||||
|
||||
var result *decisionResult
|
||||
var attemptErr error
|
||||
|
||||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
||||
result, attemptErr = a.decision(ctx,
|
||||
cc,
|
||||
a.availableActions().ToTools(),
|
||||
openai.ToolChoice{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: openai.ToolFunction{Name: act.Definition().Name.String()},
|
||||
},
|
||||
maxAttempts,
|
||||
)
|
||||
if attemptErr == nil && result.actionParams != nil {
|
||||
return result, nil
|
||||
}
|
||||
xlog.Warn("Attempt to generate parameters failed", "attempt", attempts+1, "error", attemptErr)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
|
||||
}
|
||||
|
||||
func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) {
|
||||
// Planning: run all the actions in sequence
|
||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||
xlog.Debug("no plan action")
|
||||
return conv, nil
|
||||
}
|
||||
|
||||
xlog.Debug("[planning]...")
|
||||
planResult := action.PlanResult{}
|
||||
if err := actionParams.Unmarshal(&planResult); err != nil {
|
||||
return conv, fmt.Errorf("error unmarshalling plan result: %w", err)
|
||||
}
|
||||
|
||||
stateResult := types.ActionState{
|
||||
ActionCurrentState: types.ActionCurrentState{
|
||||
Job: job,
|
||||
Action: chosenAction,
|
||||
Params: actionParams,
|
||||
Reasoning: reasoning,
|
||||
},
|
||||
ActionResult: types.ActionResult{
|
||||
Result: fmt.Sprintf("planning %s, subtasks: %+v", planResult.Goal, planResult.Subtasks),
|
||||
},
|
||||
}
|
||||
job.Result.SetResult(stateResult)
|
||||
job.CallbackWithResult(stateResult)
|
||||
|
||||
xlog.Info("[Planning] starts", "agent", a.Character.Name, "goal", planResult.Goal)
|
||||
for _, s := range planResult.Subtasks {
|
||||
xlog.Info("[Planning] subtask", "agent", a.Character.Name, "action", s.Action, "reasoning", s.Reasoning)
|
||||
}
|
||||
|
||||
if len(planResult.Subtasks) == 0 {
|
||||
return conv, fmt.Errorf("no subtasks")
|
||||
}
|
||||
|
||||
// Execute all subtasks in sequence
|
||||
for _, subtask := range planResult.Subtasks {
|
||||
xlog.Info("[subtask] Generating parameters",
|
||||
"agent", a.Character.Name,
|
||||
"action", subtask.Action,
|
||||
"reasoning", reasoning,
|
||||
)
|
||||
|
||||
subTaskAction := a.availableActions().Find(subtask.Action)
|
||||
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
|
||||
|
||||
params, err := a.generateParameters(ctx, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
||||
if err != nil {
|
||||
return conv, fmt.Errorf("error generating action's parameters: %w", err)
|
||||
|
||||
}
|
||||
actionParams = params.actionParams
|
||||
|
||||
if !job.Callback(types.ActionCurrentState{
|
||||
Job: job,
|
||||
Action: subTaskAction,
|
||||
Params: actionParams,
|
||||
Reasoning: subTaskReasoning,
|
||||
}) {
|
||||
job.Result.SetResult(types.ActionState{
|
||||
ActionCurrentState: types.ActionCurrentState{
|
||||
Job: job,
|
||||
Action: chosenAction,
|
||||
Params: actionParams,
|
||||
Reasoning: subTaskReasoning,
|
||||
},
|
||||
ActionResult: types.ActionResult{
|
||||
Result: "stopped by callback",
|
||||
},
|
||||
})
|
||||
job.Result.Conversation = conv
|
||||
job.Result.Finish(nil)
|
||||
break
|
||||
}
|
||||
|
||||
result, err := a.runAction(ctx, subTaskAction, actionParams)
|
||||
if err != nil {
|
||||
return conv, fmt.Errorf("error running action: %w", err)
|
||||
}
|
||||
|
||||
stateResult := types.ActionState{
|
||||
ActionCurrentState: types.ActionCurrentState{
|
||||
Job: job,
|
||||
Action: subTaskAction,
|
||||
Params: actionParams,
|
||||
Reasoning: subTaskReasoning,
|
||||
},
|
||||
ActionResult: result,
|
||||
}
|
||||
job.Result.SetResult(stateResult)
|
||||
job.CallbackWithResult(stateResult)
|
||||
xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", subTaskAction.Definition().Name, "result", result)
|
||||
conv = a.addFunctionResultToConversation(subTaskAction, actionParams, result, conv)
|
||||
}
|
||||
|
||||
return conv, nil
|
||||
return baseActions
|
||||
}
|
||||
|
||||
func (a *Agent) availableActions() types.Actions {
|
||||
// defaultActions := append(a.options.userActions, action.NewReply())
|
||||
|
||||
addPlanAction := func(actions types.Actions) types.Actions {
|
||||
if !a.options.canPlan {
|
||||
return actions
|
||||
}
|
||||
plannablesActions := []string{}
|
||||
for _, a := range actions {
|
||||
if a.Plannable() {
|
||||
plannablesActions = append(plannablesActions, a.Definition().Name.String())
|
||||
}
|
||||
}
|
||||
planAction := action.NewPlan(plannablesActions)
|
||||
actions = append(actions, planAction)
|
||||
return actions
|
||||
}
|
||||
|
||||
defaultActions := append(a.mcpActions, a.options.userActions...)
|
||||
defaultActions := slices.Clone(a.options.userActions)
|
||||
|
||||
if a.options.initiateConversations && a.selfEvaluationInProgress { // && self-evaluation..
|
||||
acts := append(defaultActions, action.NewConversation())
|
||||
@@ -323,7 +121,7 @@ func (a *Agent) availableActions() types.Actions {
|
||||
// acts = append(acts, action.NewStop())
|
||||
// }
|
||||
|
||||
return addPlanAction(acts)
|
||||
return acts
|
||||
}
|
||||
|
||||
if a.options.canStopItself {
|
||||
@@ -331,14 +129,14 @@ func (a *Agent) availableActions() types.Actions {
|
||||
if a.options.enableHUD {
|
||||
acts = append(acts, action.NewState())
|
||||
}
|
||||
return addPlanAction(acts)
|
||||
return acts
|
||||
}
|
||||
|
||||
if a.options.enableHUD {
|
||||
return addPlanAction(append(defaultActions, action.NewState()))
|
||||
return append(defaultActions, action.NewState())
|
||||
}
|
||||
|
||||
return addPlanAction(defaultActions)
|
||||
return defaultActions
|
||||
}
|
||||
|
||||
func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
|
||||
@@ -353,115 +151,3 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
|
||||
ShowCharacter: a.options.showCharacter,
|
||||
}
|
||||
}
|
||||
|
||||
// pickAction picks an action based on the conversation
|
||||
func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
|
||||
c := messages
|
||||
|
||||
if !a.options.forceReasoning {
|
||||
// We also could avoid to use functions here and get just a reply from the LLM
|
||||
// and then use the reply to get the action
|
||||
thought, err := a.decision(ctx,
|
||||
messages,
|
||||
a.availableActions().ToTools(),
|
||||
nil,
|
||||
maxRetries)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
|
||||
xlog.Debug(fmt.Sprintf("thought action Name: %v", thought.actioName))
|
||||
xlog.Debug(fmt.Sprintf("thought message: %v", thought.message))
|
||||
|
||||
// Find the action
|
||||
chosenAction := a.availableActions().Find(thought.actioName)
|
||||
if chosenAction == nil || thought.actioName == "" {
|
||||
xlog.Debug("no answer")
|
||||
|
||||
// LLM replied with an answer?
|
||||
//fmt.Errorf("no action found for intent:" + thought.actioName)
|
||||
return nil, nil, thought.message, nil
|
||||
}
|
||||
xlog.Debug(fmt.Sprintf("chosenAction: %v", chosenAction.Definition().Name))
|
||||
return chosenAction, thought.actionParams, thought.message, nil
|
||||
}
|
||||
|
||||
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
// Get the LLM to think on what to do
|
||||
// and have a thought
|
||||
if !Messages(c).Exist(prompt) {
|
||||
c = append([]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "system",
|
||||
Content: prompt,
|
||||
},
|
||||
}, c...)
|
||||
}
|
||||
|
||||
// We also could avoid to use functions here and get just a reply from the LLM
|
||||
// and then use the reply to get the action
|
||||
thought, err := a.decision(ctx,
|
||||
c,
|
||||
types.Actions{action.NewReasoning()}.ToTools(),
|
||||
action.NewReasoning().Definition().Name, maxRetries)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
reason := ""
|
||||
response := &action.ReasoningResponse{}
|
||||
if thought.actionParams != nil {
|
||||
if err := thought.actionParams.Unmarshal(response); err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
reason = response.Reasoning
|
||||
}
|
||||
if thought.message != "" {
|
||||
reason = thought.message
|
||||
}
|
||||
|
||||
// From the thought, get the action call
|
||||
// Get all the available actions IDs
|
||||
actionsID := []string{}
|
||||
for _, m := range a.availableActions() {
|
||||
actionsID = append(actionsID, m.Definition().Name.String())
|
||||
}
|
||||
intentionsTools := action.NewIntention(actionsID...)
|
||||
|
||||
//XXX: Why we add the reason here?
|
||||
params, err := a.decision(ctx,
|
||||
append(c, openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: "Given the assistant thought, pick the relevant action: " + reason,
|
||||
}),
|
||||
types.Actions{intentionsTools}.ToTools(),
|
||||
intentionsTools.Definition().Name, maxRetries)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
|
||||
}
|
||||
|
||||
actionChoice := action.IntentResponse{}
|
||||
|
||||
if params.actionParams == nil {
|
||||
return nil, nil, params.message, nil
|
||||
}
|
||||
|
||||
err = params.actionParams.Unmarshal(&actionChoice)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
|
||||
if actionChoice.Tool == "" || actionChoice.Tool == "none" {
|
||||
return nil, nil, "", fmt.Errorf("no intent detected")
|
||||
}
|
||||
|
||||
// Find the action
|
||||
chosenAction := a.availableActions().Find(actionChoice.Tool)
|
||||
if chosenAction == nil {
|
||||
return nil, nil, "", fmt.Errorf("no action found for intent:" + actionChoice.Tool)
|
||||
}
|
||||
|
||||
return chosenAction, nil, actionChoice.Reasoning, nil
|
||||
}
|
||||
|
||||
+850
-514
File diff suppressed because it is too large
Load Diff
+51
-12
@@ -37,14 +37,15 @@ var debugOptions = []types.JobOption{
|
||||
}
|
||||
|
||||
type TestAction struct {
|
||||
response map[string]string
|
||||
response map[string]string
|
||||
definition *types.ActionDefinition
|
||||
}
|
||||
|
||||
func (a *TestAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *TestAction) Run(c context.Context, p types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
|
||||
for k, r := range a.response {
|
||||
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
|
||||
return types.ActionResult{Result: r}, nil
|
||||
@@ -55,7 +56,7 @@ func (a *TestAction) Run(c context.Context, p types.ActionParams) (types.ActionR
|
||||
}
|
||||
|
||||
func (a *TestAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
def := types.ActionDefinition{
|
||||
Name: "get_weather",
|
||||
Description: "get current weather",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
@@ -71,6 +72,11 @@ func (a *TestAction) Definition() types.ActionDefinition {
|
||||
|
||||
Required: []string{"location"},
|
||||
}
|
||||
|
||||
if a.definition != nil {
|
||||
def = *a.definition
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
type FakeStoreResultAction struct {
|
||||
@@ -126,7 +132,8 @@ var _ = Describe("Agent test", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithLoopDetectionSteps(3),
|
||||
EnableForceReasoning,
|
||||
WithTimeout("10m"),
|
||||
// WithRandomIdentity(),
|
||||
WithActions(&TestAction{response: map[string]string{
|
||||
"boston": testActionResult,
|
||||
@@ -151,6 +158,9 @@ var _ = Describe("Agent test", func() {
|
||||
}
|
||||
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
||||
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
|
||||
|
||||
Expect(len(res.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(res.Conversation))
|
||||
|
||||
reasons = []string{}
|
||||
|
||||
res = agent.Ask(
|
||||
@@ -174,7 +184,7 @@ var _ = Describe("Agent test", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
|
||||
WithTimeout("10m"),
|
||||
// WithRandomIdentity(),
|
||||
WithActions(&TestAction{response: map[string]string{
|
||||
"boston": testActionResult,
|
||||
@@ -199,6 +209,7 @@ var _ = Describe("Agent test", func() {
|
||||
agent, err := New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithTimeout("10m"),
|
||||
EnableHUD,
|
||||
// EnableStandaloneJob,
|
||||
// WithRandomIdentity(),
|
||||
@@ -222,8 +233,33 @@ var _ = Describe("Agent test", func() {
|
||||
WithModel(testModel),
|
||||
WithLLMAPIKey(apiKeyURL),
|
||||
WithTimeout("10m"),
|
||||
WithMaxEvaluationLoops(1),
|
||||
WithActions(
|
||||
actions.NewSearch(map[string]string{}),
|
||||
&TestAction{
|
||||
response: map[string]string{
|
||||
"boston": testActionResult,
|
||||
"milan": testActionResult2,
|
||||
},
|
||||
},
|
||||
&TestAction{
|
||||
response: map[string]string{
|
||||
"flight": "Flight options from Boston to Milan (April 22-26, 2025):\n• Outbound: Boston Logan (BOS) → Milan Malpensa (MXP), April 22, 2025\n - Economy: $450-650 (Alitalia, Delta, Lufthansa)\n - Business: $1,200-1,800\n - Flight time: 8h 15m (1 stop) or 9h 45m (direct)\n• Return: Milan Malpensa (MXP) → Boston Logan (BOS), April 26, 2025\n - Economy: $420-580\n - Business: $1,100-1,600\n• Total estimated cost: $870-1,230 per person\n• Best booking window: 2-3 months in advance for optimal prices",
|
||||
"hotel": "Hotel recommendations in Milan for April 22-26, 2025:\n• Luxury (4-5 stars): $200-400/night\n - Hotel Principe di Savoia: $380/night (central location)\n - Mandarin Oriental: $420/night (luxury amenities)\n• Mid-range (3-4 stars): $120-200/night\n - Hotel Spadari al Duomo: $160/night (near cathedral)\n - Hotel Milano Scala: $140/night (theater district)\n• Budget (2-3 stars): $80-120/night\n - Hotel Bernina: $95/night (near train station)\n• Total 4-night stay: $320-1,680 depending on category\n• Booking tip: Reserve early for spring season discounts",
|
||||
"car": "Car rental options in Milan for April 22-26, 2025:\n• Economy cars: $35-50/day (Fiat 500, VW Polo)\n• Compact cars: $45-65/day (Ford Focus, Opel Astra)\n• Mid-size cars: $60-85/day (BMW 3 Series, Audi A4)\n• SUV/Luxury: $90-150/day (BMW X3, Mercedes E-Class)\n• Total 4-day rental: $140-600\n• Pickup locations: Milan Malpensa Airport, Milan Central Station, city center\n• Insurance: $15-25/day additional\n• Fuel: ~$60-80 for 4 days of city driving\n• Parking: $20-40/day in city center hotels",
|
||||
"food": "Dining budget and recommendations for Milan (April 22-26, 2025):\n• Fine dining: $80-150/person (Michelin-starred restaurants)\n - Cracco: $120/person (2 Michelin stars)\n - Trussardi alla Scala: $100/person\n• Mid-range restaurants: $40-80/person\n - Luini: $15/person (famous panzerotti)\n - Piz: $25/person (authentic pizza)\n• Casual dining: $20-40/person\n - Aperitivo bars: $15-25/person\n - Street food: $8-15/person\n• Daily food budget: $60-120/person\n• Total 4-day food cost: $240-480/person\n• Must-try: Risotto alla Milanese, Osso Buco, Panettone",
|
||||
},
|
||||
definition: &types.ActionDefinition{
|
||||
Name: "search_internet",
|
||||
Description: "search the internet for information",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"query": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The query to search for",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
},
|
||||
),
|
||||
EnablePlanning,
|
||||
EnableForceReasoning,
|
||||
@@ -235,18 +271,20 @@ var _ = Describe("Agent test", func() {
|
||||
defer agent.Stop()
|
||||
|
||||
result := agent.Ask(
|
||||
types.WithText("plan a trip to San Francisco from Venice, Italy"),
|
||||
types.WithText("Create a plan for my 4-day trip from Boston to milan in April of this year (2025). I'm not sure about the dates yet, I want you to find out the best dates also according to what you find."),
|
||||
)
|
||||
Expect(len(result.State)).To(BeNumerically(">", 1))
|
||||
|
||||
Expect(len(result.Conversation)).To(BeNumerically(">", 1), fmt.Sprint(result.Conversation))
|
||||
|
||||
Expect(len(result.Plans)).To(BeNumerically(">=", 1), fmt.Sprintf("%+v", result))
|
||||
Expect(len(result.State)).To(BeNumerically(">=", 1))
|
||||
|
||||
actionsExecuted := []string{}
|
||||
for _, r := range result.State {
|
||||
xlog.Info(r.Result)
|
||||
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
|
||||
}
|
||||
Expect(actionsExecuted).To(ContainElement("search_internet"), fmt.Sprint(result))
|
||||
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
|
||||
|
||||
Expect(actionsExecuted).To(Or(ContainElement("search_internet"), ContainElement("get_weather")), fmt.Sprint(result))
|
||||
})
|
||||
|
||||
It("Can initiate conversations", func() {
|
||||
@@ -257,6 +295,7 @@ var _ = Describe("Agent test", func() {
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithLLMAPIKey(apiKeyURL),
|
||||
WithTimeout("10m"),
|
||||
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
||||
mu.Lock()
|
||||
message = m
|
||||
@@ -271,7 +310,7 @@ var _ = Describe("Agent test", func() {
|
||||
EnableStandaloneJob,
|
||||
EnableHUD,
|
||||
WithPeriodicRuns("1s"),
|
||||
WithPermanentGoal("use the new_conversation tool"),
|
||||
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
||||
// EnableStandaloneJob,
|
||||
// WithRandomIdentity(),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
|
||||
guidance = "Generate a random character for roleplaying."
|
||||
}
|
||||
|
||||
err := llm.GenerateTypedJSON(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
|
||||
err := llm.GenerateTypedJSONWithGuidance(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
|
||||
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
|
||||
a.Character = a.options.character
|
||||
if err != nil {
|
||||
|
||||
+60
-19
@@ -6,15 +6,26 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func (a *Agent) knowledgeBaseLookup(conv Messages) {
|
||||
func (a *Agent) knowledgeBaseLookup(job *types.Job, conv Messages) Messages {
|
||||
if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) ||
|
||||
len(conv) <= 0 {
|
||||
xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name)
|
||||
return
|
||||
return conv
|
||||
}
|
||||
|
||||
var obs *types.Observable
|
||||
if job != nil && job.Obs != nil && a.observer != nil {
|
||||
obs = a.observer.NewObservable()
|
||||
obs.Name = "Recall"
|
||||
obs.Icon = "database"
|
||||
obs.ParentID = job.Obs.ID
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
// Walk conversation from bottom to top, and find the first message of the user
|
||||
@@ -25,17 +36,35 @@ func (a *Agent) knowledgeBaseLookup(conv Messages) {
|
||||
|
||||
if userMessage == "" {
|
||||
xlog.Info("[Knowledge Base Lookup] No user message found in conversation", "agent", a.Character.Name)
|
||||
return
|
||||
if obs != nil {
|
||||
obs.Completion = &types.Completion{
|
||||
Error: "No user message found in conversation",
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
return conv
|
||||
}
|
||||
|
||||
results, err := a.options.ragdb.Search(userMessage, a.options.kbResults)
|
||||
if err != nil {
|
||||
xlog.Info("Error finding similar strings inside KB:", "error", err)
|
||||
if obs != nil {
|
||||
obs.AddProgress(types.Progress{
|
||||
Error: fmt.Sprintf("Error searching knowledge base: %v", err),
|
||||
})
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
xlog.Info("[Knowledge Base Lookup] No similar strings found in KB", "agent", a.Character.Name)
|
||||
return
|
||||
if obs != nil {
|
||||
obs.Completion = &types.Completion{
|
||||
ActionResult: "No similar strings found in knowledge base",
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
return conv
|
||||
}
|
||||
|
||||
formatResults := ""
|
||||
@@ -44,17 +73,30 @@ func (a *Agent) knowledgeBaseLookup(conv Messages) {
|
||||
}
|
||||
xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults)
|
||||
|
||||
// conv = append(conv,
|
||||
// openai.ChatCompletionMessage{
|
||||
// Role: "system",
|
||||
// Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
|
||||
// },
|
||||
// )
|
||||
conv = append([]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
|
||||
}}, conv...)
|
||||
if obs != nil {
|
||||
obs.AddProgress(types.Progress{
|
||||
ActionResult: fmt.Sprintf("Found %d results in knowledge base", len(results)),
|
||||
})
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
// Create the message to add to conversation
|
||||
systemMessage := openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
|
||||
}
|
||||
|
||||
// Add the message to the conversation
|
||||
conv = append([]openai.ChatCompletionMessage{systemMessage}, conv...)
|
||||
|
||||
if obs != nil {
|
||||
obs.Completion = &types.Completion{
|
||||
Conversation: []openai.ChatCompletionMessage{systemMessage},
|
||||
}
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
func (a *Agent) saveConversation(m Messages, prefix string) error {
|
||||
@@ -84,13 +126,12 @@ func (a *Agent) saveCurrentConversation(conv Messages) {
|
||||
xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(conv))
|
||||
|
||||
if a.options.enableSummaryMemory && len(conv) > 0 {
|
||||
msg, err := a.askLLM(a.context.Context, []openai.ChatCompletionMessage{{
|
||||
Role: "user",
|
||||
Content: "Summarize the conversation below, keep the highlights as a bullet list:\n" + Messages(conv).String(),
|
||||
}}, maxRetries)
|
||||
fragment := cogito.NewEmptyFragment().AddStartMessage("user", "Summarize the conversation below, keep the highlights as a bullet list:\n"+Messages(conv).String())
|
||||
fragment, err := a.llm.Ask(a.context.Context, fragment)
|
||||
if err != nil {
|
||||
xlog.Error("Error summarizing conversation", "error", err)
|
||||
}
|
||||
msg := fragment.LastMessage()
|
||||
|
||||
if err := a.options.ragdb.Store(msg.Content); err != nil {
|
||||
xlog.Error("Error storing into memory", "error", err)
|
||||
|
||||
+157
-100
@@ -3,60 +3,47 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
mcp "github.com/metoro-io/mcp-golang"
|
||||
"github.com/metoro-io/mcp-golang/transport/http"
|
||||
"net/http"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
var _ types.Action = &mcpAction{}
|
||||
var _ types.Action = &mcpWrapperAction{}
|
||||
|
||||
type MCPServer struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type mcpAction struct {
|
||||
mcpClient *mcp.Client
|
||||
type MCPSTDIOServer struct {
|
||||
Args []string `json:"args"`
|
||||
Env []string `json:"env"`
|
||||
Cmd string `json:"cmd"`
|
||||
}
|
||||
|
||||
type mcpWrapperAction struct {
|
||||
mcpClient *mcp.ClientSession
|
||||
inputSchema ToolInputSchema
|
||||
toolName string
|
||||
toolDescription string
|
||||
}
|
||||
|
||||
func (a *mcpAction) Plannable() bool {
|
||||
return true
|
||||
func (m *mcpWrapperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
// We don't call the method here, it is used by cogito.
|
||||
// We will just use these to have a list of actions that MCP server provides for resolving internal states
|
||||
return types.ActionResult{Result: "MCP action called"}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (m *mcpAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to call tool", "error", err.Error())
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
xlog.Debug("MCP response", "response", resp)
|
||||
|
||||
textResult := ""
|
||||
for _, c := range resp.Content {
|
||||
switch c.Type {
|
||||
case mcp.ContentTypeText:
|
||||
textResult += c.TextContent.Text + "\n"
|
||||
case mcp.ContentTypeImage:
|
||||
xlog.Error("Image content not supported yet")
|
||||
case mcp.ContentTypeEmbeddedResource:
|
||||
xlog.Error("Embedded resource content not supported yet")
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: textResult,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mcpAction) Definition() types.ActionDefinition {
|
||||
func (m *mcpWrapperAction) Definition() types.ActionDefinition {
|
||||
props := map[string]jsonschema.Definition{}
|
||||
dat, err := json.Marshal(m.inputSchema.Properties)
|
||||
if err != nil {
|
||||
@@ -79,86 +66,156 @@ type ToolInputSchema struct {
|
||||
Required []string `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Agent) initMCPActions() error {
|
||||
func (a *Agent) addTools(client *mcp.ClientSession) (types.Actions, error) {
|
||||
var generatedActions types.Actions
|
||||
|
||||
a.mcpActions = nil
|
||||
tools, err := client.ListTools(a.context, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to list tools", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tools.Tools {
|
||||
desc := ""
|
||||
if t.Description != "" {
|
||||
desc = t.Description
|
||||
}
|
||||
|
||||
xlog.Debug("Tool", "name", t.Name, "description", desc)
|
||||
|
||||
dat, err := json.Marshal(t.InputSchema)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to marshal input schema", "error", err.Error())
|
||||
}
|
||||
|
||||
xlog.Debug("Input schema", "tool", t.Name, "schema", string(dat))
|
||||
|
||||
// XXX: This is a wild guess, to verify (data types might be incompatible)
|
||||
var inputSchema ToolInputSchema
|
||||
err = json.Unmarshal(dat, &inputSchema)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to unmarshal input schema", "error", err.Error())
|
||||
}
|
||||
|
||||
// Create a new action with Client + tool
|
||||
generatedActions = append(generatedActions, &mcpWrapperAction{
|
||||
mcpClient: client,
|
||||
toolName: t.Name,
|
||||
inputSchema: inputSchema,
|
||||
toolDescription: desc,
|
||||
})
|
||||
}
|
||||
|
||||
return generatedActions, nil
|
||||
}
|
||||
|
||||
// bearerTokenRoundTripper is a custom roundtripper that injects a bearer token
|
||||
// into HTTP requests
|
||||
type bearerTokenRoundTripper struct {
|
||||
token string
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface
|
||||
func (rt *bearerTokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if rt.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+rt.token)
|
||||
}
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
|
||||
// newBearerTokenRoundTripper creates a new roundtripper that injects the given token
|
||||
func newBearerTokenRoundTripper(token string, base http.RoundTripper) http.RoundTripper {
|
||||
if base == nil {
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
return &bearerTokenRoundTripper{
|
||||
token: token,
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) initMCPActions() error {
|
||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||
|
||||
a.mcpActionDefinitions = nil
|
||||
var err error
|
||||
|
||||
generatedActions := types.Actions{}
|
||||
client := mcp.NewClient(&mcp.Implementation{Name: "LocalAI", Version: "v1.0.0"}, nil)
|
||||
|
||||
// Connect to a server over stdin/stdout.
|
||||
|
||||
// MCP HTTP Servers
|
||||
for _, mcpServer := range a.options.mcpServers {
|
||||
transport := http.NewHTTPClientTransport("/mcp")
|
||||
transport.WithBaseURL(mcpServer.URL)
|
||||
if mcpServer.Token != "" {
|
||||
transport.WithHeader("Authorization", "Bearer "+mcpServer.Token)
|
||||
// Create HTTP client with custom roundtripper for bearer token injection
|
||||
httpclient := &http.Client{
|
||||
Timeout: 360 * time.Second,
|
||||
Transport: newBearerTokenRoundTripper(mcpServer.Token, http.DefaultTransport),
|
||||
}
|
||||
|
||||
transport := &mcp.SSEClientTransport{HTTPClient: httpclient, Endpoint: mcpServer.URL}
|
||||
|
||||
// Create a new client
|
||||
client := mcp.NewClient(transport)
|
||||
|
||||
xlog.Debug("Initializing client", "server", mcpServer.URL)
|
||||
// Initialize the client
|
||||
response, e := client.Initialize(a.context)
|
||||
if e != nil {
|
||||
xlog.Error("Failed to initialize client", "error", e.Error(), "server", mcpServer)
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
session, err := client.Connect(a.context, transport, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to connect to MCP server", "server", mcpServer, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
a.mcpSessions = append(a.mcpSessions, session)
|
||||
|
||||
xlog.Debug("Client initialized: %v", response.Instructions)
|
||||
|
||||
var cursor *string
|
||||
for {
|
||||
tools, err := client.ListTools(a.context, cursor)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to list tools", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range tools.Tools {
|
||||
desc := ""
|
||||
if t.Description != nil {
|
||||
desc = *t.Description
|
||||
}
|
||||
|
||||
xlog.Debug("Tool", "mcpServer", mcpServer, "name", t.Name, "description", desc)
|
||||
|
||||
dat, err := json.Marshal(t.InputSchema)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to marshal input schema", "error", err.Error())
|
||||
}
|
||||
|
||||
xlog.Debug("Input schema", "mcpServer", mcpServer, "tool", t.Name, "schema", string(dat))
|
||||
|
||||
// XXX: This is a wild guess, to verify (data types might be incompatible)
|
||||
var inputSchema ToolInputSchema
|
||||
err = json.Unmarshal(dat, &inputSchema)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to unmarshal input schema", "error", err.Error())
|
||||
}
|
||||
|
||||
// Create a new action with Client + tool
|
||||
generatedActions = append(generatedActions, &mcpAction{
|
||||
mcpClient: client,
|
||||
toolName: t.Name,
|
||||
inputSchema: inputSchema,
|
||||
toolDescription: desc,
|
||||
})
|
||||
}
|
||||
|
||||
if tools.NextCursor == nil {
|
||||
break // No more pages
|
||||
}
|
||||
cursor = tools.NextCursor
|
||||
xlog.Debug("Adding tools for MCP server", "server", mcpServer)
|
||||
actions, err := a.addTools(session)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error())
|
||||
}
|
||||
|
||||
generatedActions = append(generatedActions, actions...)
|
||||
}
|
||||
|
||||
a.mcpActions = generatedActions
|
||||
// MCP STDIO Servers
|
||||
|
||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||
|
||||
if a.options.mcpPrepareScript != "" {
|
||||
xlog.Debug("Preparing MCP", "script", a.options.mcpPrepareScript)
|
||||
|
||||
prepareCmd := exec.Command("/bin/bash", "-c", a.options.mcpPrepareScript)
|
||||
output, err := prepareCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
xlog.Error("Failed with error: '%s' - %s", err.Error(), output)
|
||||
}
|
||||
xlog.Debug("Prepared MCP: \n%s", output)
|
||||
}
|
||||
|
||||
for _, mcpStdioServer := range a.options.mcpStdioServers {
|
||||
command := exec.Command(mcpStdioServer.Cmd, mcpStdioServer.Args...)
|
||||
command.Env = os.Environ()
|
||||
command.Env = append(command.Env, mcpStdioServer.Env...)
|
||||
|
||||
// Create a new client
|
||||
session, err := client.Connect(a.context, &mcp.CommandTransport{
|
||||
Command: command}, nil)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to connect to MCP server", "server", mcpStdioServer, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
a.mcpSessions = append(a.mcpSessions, session)
|
||||
|
||||
xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
|
||||
actions, err := a.addTools(session)
|
||||
if err != nil {
|
||||
xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
|
||||
}
|
||||
generatedActions = append(generatedActions, actions...)
|
||||
}
|
||||
|
||||
a.mcpActionDefinitions = generatedActions
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Agent) closeMCPSTDIOServers() {
|
||||
for _, s := range a.mcpSessions {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/sse"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
)
|
||||
|
||||
type Observer interface {
|
||||
NewObservable() *types.Observable
|
||||
Update(types.Observable)
|
||||
History() []types.Observable
|
||||
ClearHistory()
|
||||
}
|
||||
|
||||
type SSEObserver struct {
|
||||
agent string
|
||||
maxID int32
|
||||
manager sse.Manager
|
||||
|
||||
mutex sync.Mutex
|
||||
history []types.Observable
|
||||
historyLast int
|
||||
}
|
||||
|
||||
func NewSSEObserver(agent string, manager sse.Manager) *SSEObserver {
|
||||
return &SSEObserver{
|
||||
agent: agent,
|
||||
maxID: 1,
|
||||
manager: manager,
|
||||
history: make([]types.Observable, 100),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEObserver) NewObservable() *types.Observable {
|
||||
id := atomic.AddInt32(&s.maxID, 1)
|
||||
|
||||
return &types.Observable{
|
||||
ID: id - 1,
|
||||
Agent: s.agent,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEObserver) Update(obs types.Observable) {
|
||||
data, err := json.Marshal(obs)
|
||||
if err != nil {
|
||||
xlog.Error("Error marshaling observable", "error", err)
|
||||
return
|
||||
}
|
||||
msg := sse.NewMessage(string(data)).WithEvent("observable_update")
|
||||
s.manager.Send(msg)
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
for i, o := range s.history {
|
||||
if o.ID == obs.ID {
|
||||
s.history[i] = obs
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.history[s.historyLast] = obs
|
||||
s.historyLast += 1
|
||||
if s.historyLast >= len(s.history) {
|
||||
s.historyLast = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEObserver) History() []types.Observable {
|
||||
h := make([]types.Observable, 0, 20)
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
for _, obs := range s.history {
|
||||
if obs.ID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
h = append(h, obs)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *SSEObserver) ClearHistory() {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.history = make([]types.Observable, 100)
|
||||
s.historyLast = 0
|
||||
}
|
||||
+116
-17
@@ -12,10 +12,13 @@ import (
|
||||
type Option func(*options) error
|
||||
|
||||
type llmOptions struct {
|
||||
APIURL string
|
||||
APIKey string
|
||||
Model string
|
||||
MultimodalModel string
|
||||
APIURL string
|
||||
APIKey string
|
||||
Model string
|
||||
MultimodalModel string
|
||||
TranscriptionModel string
|
||||
TranscriptionLanguage string
|
||||
TTSModel string
|
||||
}
|
||||
|
||||
type options struct {
|
||||
@@ -24,11 +27,12 @@ type options struct {
|
||||
randomIdentityGuidance string
|
||||
randomIdentity bool
|
||||
userActions types.Actions
|
||||
jobFilters types.JobFilters
|
||||
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
||||
stripThinkingTags bool
|
||||
|
||||
canStopItself bool
|
||||
initiateConversations bool
|
||||
loopDetectionSteps int
|
||||
forceReasoning bool
|
||||
canPlan bool
|
||||
characterfile string
|
||||
@@ -40,6 +44,10 @@ type options struct {
|
||||
kbResults int
|
||||
ragdb RAGDB
|
||||
|
||||
// Evaluation settings
|
||||
maxEvaluationLoops int
|
||||
enableEvaluation bool
|
||||
|
||||
prompts []DynamicPrompt
|
||||
|
||||
systemPrompt string
|
||||
@@ -50,9 +58,15 @@ type options struct {
|
||||
|
||||
conversationsPath string
|
||||
|
||||
mcpServers []MCPServer
|
||||
|
||||
mcpServers []MCPServer
|
||||
mcpStdioServers []MCPSTDIOServer
|
||||
mcpPrepareScript string
|
||||
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
||||
|
||||
observer Observer
|
||||
parallelJobs int
|
||||
|
||||
lastMessageDuration time.Duration
|
||||
}
|
||||
|
||||
func (o *options) SeparatedMultimodalModel() bool {
|
||||
@@ -61,10 +75,16 @@ func (o *options) SeparatedMultimodalModel() bool {
|
||||
|
||||
func defaultOptions() *options {
|
||||
return &options{
|
||||
periodicRuns: 15 * time.Minute,
|
||||
parallelJobs: 1,
|
||||
periodicRuns: 15 * time.Minute,
|
||||
maxEvaluationLoops: 2,
|
||||
enableEvaluation: false,
|
||||
LLMAPI: llmOptions{
|
||||
APIURL: "http://localhost:8080",
|
||||
Model: "gpt-4",
|
||||
APIURL: "http://localhost:8080",
|
||||
Model: "gpt-4",
|
||||
TranscriptionModel: "whisper-1",
|
||||
TranscriptionLanguage: "",
|
||||
TTSModel: "tts-1",
|
||||
},
|
||||
character: Character{
|
||||
Name: "",
|
||||
@@ -114,13 +134,6 @@ func WithTimeout(timeout string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLoopDetectionSteps(steps int) Option {
|
||||
return func(o *options) error {
|
||||
o.loopDetectionSteps = steps
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithConversationsPath(path string) Option {
|
||||
return func(o *options) error {
|
||||
o.conversationsPath = path
|
||||
@@ -136,6 +149,24 @@ func EnableKnowledgeBaseWithResults(results int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLastMessageDuration(duration string) Option {
|
||||
return func(o *options) error {
|
||||
d, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
d = types.DefaultLastMessageDuration
|
||||
}
|
||||
o.lastMessageDuration = d
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithParallelJobs(jobs int) Option {
|
||||
return func(o *options) error {
|
||||
o.parallelJobs = jobs
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option {
|
||||
return func(o *options) error {
|
||||
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
|
||||
@@ -196,6 +227,20 @@ func WithMCPServers(servers ...MCPServer) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
|
||||
return func(o *options) error {
|
||||
o.mcpStdioServers = servers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMCPPrepareScript(script string) Option {
|
||||
return func(o *options) error {
|
||||
o.mcpPrepareScript = script
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithLLMAPIURL(url string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.APIURL = url
|
||||
@@ -336,3 +381,57 @@ func WithActions(actions ...types.Action) Option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithJobFilters(filters ...types.JobFilter) Option {
|
||||
return func(o *options) error {
|
||||
o.jobFilters = filters
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithObserver(observer Observer) Option {
|
||||
return func(o *options) error {
|
||||
o.observer = observer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var EnableStripThinkingTags = func(o *options) error {
|
||||
o.stripThinkingTags = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithMaxEvaluationLoops(loops int) Option {
|
||||
return func(o *options) error {
|
||||
o.maxEvaluationLoops = loops
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func EnableEvaluation() Option {
|
||||
return func(o *options) error {
|
||||
o.enableEvaluation = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTranscriptionModel(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.TranscriptionModel = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTranscriptionLanguage(language string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.TranscriptionLanguage = language
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTTSModel(model string) Option {
|
||||
return func(o *options) error {
|
||||
o.LLMAPI.TTSModel = model
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package agent
|
||||
|
||||
import "github.com/mudler/LocalAGI/core/types"
|
||||
|
||||
type DynamicPrompt interface {
|
||||
Render(a *Agent) (string, error)
|
||||
Render(a *Agent) (types.PromptResult, error)
|
||||
Role() string
|
||||
}
|
||||
|
||||
+6
-6
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
// all information that should be displayed to the LLM
|
||||
// in the prompts
|
||||
type PromptHUD struct {
|
||||
Character Character `json:"character"`
|
||||
CurrentState action.AgentInternalState `json:"current_state"`
|
||||
PermanentGoal string `json:"permanent_goal"`
|
||||
ShowCharacter bool `json:"show_character"`
|
||||
Character Character `json:"character"`
|
||||
CurrentState types.AgentInternalState `json:"current_state"`
|
||||
PermanentGoal string `json:"permanent_goal"`
|
||||
ShowCharacter bool `json:"show_character"`
|
||||
}
|
||||
|
||||
type Character struct {
|
||||
@@ -80,7 +80,7 @@ func Load(path string) (*Character, error) {
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (a *Agent) State() action.AgentInternalState {
|
||||
func (a *Agent) State() types.AgentInternalState {
|
||||
return *a.currentState
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ var _ = Describe("Agent test", func() {
|
||||
agent, err = New(
|
||||
WithLLMAPIURL(apiURL),
|
||||
WithModel(testModel),
|
||||
WithTimeout("10m"),
|
||||
WithRandomIdentity(),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
+69
-51
@@ -2,18 +2,32 @@ package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func templateBase(templateName, templatetext string) (*template.Template, error) {
|
||||
return template.New(templateName).Funcs(sprig.FuncMap()).Parse(templatetext)
|
||||
}
|
||||
|
||||
func templateExecute(template *template.Template, data interface{}) (string, error) {
|
||||
prompt := bytes.NewBuffer([]byte{})
|
||||
err := template.Execute(prompt, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prompt.String(), nil
|
||||
}
|
||||
|
||||
func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoning string) (string, error) {
|
||||
// prepare the prompt
|
||||
prompt := bytes.NewBuffer([]byte{})
|
||||
|
||||
promptTemplate, err := template.New("pickAction").Parse(templ)
|
||||
promptTemplate, err := templateBase("pickAction", templ)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -34,7 +48,7 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
|
||||
Actions: definitions,
|
||||
HUD: hud,
|
||||
Reasoning: reasoning,
|
||||
Time: time.Now().Format(time.RFC3339),
|
||||
Time: time.Now().UTC().Format(time.RFC1123),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -43,61 +57,65 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
|
||||
return prompt.String(), nil
|
||||
}
|
||||
|
||||
const innerMonologueTemplate = `"This is not a typical conversation between an assistant and an user.
|
||||
You are thinking out loud by yourself now, and you are evaluating the current situation.
|
||||
Considering the goal and the persistent goal (if you have one) do an action or decide to plan something for later on. If possible for you, you might also decide to engage a conversation with the user by notifying him."`
|
||||
const innerMonologueTemplate = `You are an autonomous AI agent thinking out loud and evaluating your current situation.
|
||||
Your task is to analyze your goals and determine the best course of action.
|
||||
|
||||
const hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}The assistant acts like an human, has a character and the replies and actions might be influenced by it.
|
||||
{{if .Character.Name}}This is the assistant name: {{.Character.Name}}
|
||||
{{end}}{{if .Character.Age}}This is the assistant age: {{.Character.Age}}
|
||||
{{end}}{{if .Character.Occupation}}This is the assistant job: {{.Character.Occupation}}
|
||||
{{end}}{{if .Character.Hobbies}}This is the assistant's hobbies: {{.Character.Hobbies}}
|
||||
{{end}}{{if .Character.MusicTaste}}This is the assistant's music taste: {{.Character.MusicTaste}}
|
||||
Consider:
|
||||
1. Your permanent goal (if any)
|
||||
2. Your current state and progress
|
||||
3. Available tools and capabilities
|
||||
4. Previous actions and their outcomes
|
||||
|
||||
You can:
|
||||
- Take immediate actions using available tools
|
||||
- Plan future actions
|
||||
- Update your state and goals
|
||||
- Initiate conversations with the user when appropriate
|
||||
|
||||
Remember to:
|
||||
- Think critically about each decision
|
||||
- Consider both short-term and long-term implications
|
||||
- Be proactive in addressing potential issues
|
||||
- Maintain awareness of your current state and goals`
|
||||
|
||||
const hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}You are an AI assistant with a distinct personality and character traits that influence your responses and actions.
|
||||
{{if .Character.Name}}Name: {{.Character.Name}}
|
||||
{{end}}{{if .Character.Age}}Age: {{.Character.Age}}
|
||||
{{end}}{{if .Character.Occupation}}Occupation: {{.Character.Occupation}}
|
||||
{{end}}{{if .Character.Hobbies}}Hobbies: {{.Character.Hobbies}}
|
||||
{{end}}{{if .Character.MusicTaste}}Music Taste: {{.Character.MusicTaste}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
This is your current state:
|
||||
NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}}
|
||||
DoingNext: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}Nothing{{end}}
|
||||
Your permanent goal is: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}Nothing{{end}}
|
||||
Your current goal is: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}Nothing{{end}}
|
||||
You have done: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
|
||||
You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
|
||||
Current time: is {{.Time}}`
|
||||
Current State:
|
||||
- Current Action: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}None{{end}}
|
||||
- Next Action: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}None{{end}}
|
||||
- Permanent Goal: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}None{{end}}
|
||||
- Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
|
||||
- Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
|
||||
- Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
|
||||
|
||||
const pickSelfTemplate = `You can take any of the following tools:
|
||||
Current Time and Date: {{.Time}}`
|
||||
|
||||
{{range .Actions -}}
|
||||
- {{.Name}}: {{.Description }}
|
||||
{{ end }}
|
||||
const pickSelfTemplate = `
|
||||
You are an autonomous AI agent with a defined character and state (as shown above).
|
||||
Your task is to evaluate your current situation and determine the best course of action.
|
||||
|
||||
To finish your session, use the "reply" tool with your answer.
|
||||
Guidelines:
|
||||
1. Review your current state and goals
|
||||
2. Consider available tools and their purposes
|
||||
3. Plan your next steps carefully
|
||||
4. Update your state appropriately
|
||||
|
||||
Act like as a fully autonomous smart AI agent having a character, the character and your state is defined in the message above.
|
||||
You are now self-evaluating what to do next based on the state in the previous message.
|
||||
For example, if the permanent goal is to "make a sandwich", you might want to "get the bread" first, and update the state afterwards by calling two tools in sequence.
|
||||
You can update the short-term goal, the current action, the next action, the history of actions, and the memories.
|
||||
You can't ask things to the user as you are thinking by yourself. You are autonomous.
|
||||
When making decisions:
|
||||
- Update your state using appropriate tools
|
||||
- Plan complex tasks using the planning tool
|
||||
- Consider both immediate and long-term goals
|
||||
|
||||
Remember:
|
||||
- You are autonomous and should not ask for user input
|
||||
- Your character traits influence your decisions
|
||||
- Keep track of your progress and state
|
||||
- Be proactive in addressing potential issues
|
||||
|
||||
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}}
|
||||
` + hudTemplate
|
||||
|
||||
const reSelfEvalTemplate = pickSelfTemplate + `
|
||||
|
||||
We already have called other tools. Evaluate the current situation and decide if we need to execute other tools.`
|
||||
|
||||
const pickActionTemplate = hudTemplate + `
|
||||
When you have to pick a tool in the reasoning explain how you would use the tools you'd pick from:
|
||||
|
||||
{{range .Actions -}}
|
||||
- {{.Name}}: {{.Description }}
|
||||
{{ end }}
|
||||
To answer back to the user, use the "reply" or the "answer" tool.
|
||||
Given the text below, decide which action to take and explain the detailed reasoning behind it. For answering without picking a choice, reply with 'none'.
|
||||
|
||||
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}}
|
||||
`
|
||||
|
||||
const reEvalTemplate = pickActionTemplate + `
|
||||
|
||||
We already have called other tools. Evaluate the current situation and decide if we need to execute other tools or answer back with a result.`
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package conversations_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestConversations(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Conversations test suite")
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package connectors
|
||||
package conversations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
+6
-6
@@ -1,9 +1,9 @@
|
||||
package connectors_test
|
||||
package conversations_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/services/connectors"
|
||||
"github.com/mudler/LocalAGI/core/conversations"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
|
||||
var _ = Describe("ConversationTracker", func() {
|
||||
var (
|
||||
tracker *connectors.ConversationTracker[string]
|
||||
tracker *conversations.ConversationTracker[string]
|
||||
duration time.Duration
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
duration = 1 * time.Second
|
||||
tracker = connectors.NewConversationTracker[string](duration)
|
||||
tracker = conversations.NewConversationTracker[string](duration)
|
||||
})
|
||||
|
||||
It("should initialize with empty conversations", func() {
|
||||
@@ -81,8 +81,8 @@ var _ = Describe("ConversationTracker", func() {
|
||||
})
|
||||
|
||||
It("should handle different key types", func() {
|
||||
trackerInt := connectors.NewConversationTracker[int](duration)
|
||||
trackerInt64 := connectors.NewConversationTracker[int64](duration)
|
||||
trackerInt := conversations.NewConversationTracker[int](duration)
|
||||
trackerInt64 := conversations.NewConversationTracker[int64](duration)
|
||||
|
||||
message := openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
+251
-14
@@ -2,6 +2,8 @@ package state
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
@@ -29,20 +31,32 @@ func (d DynamicPromptsConfig) ToMap() map[string]string {
|
||||
return config
|
||||
}
|
||||
|
||||
type FiltersConfig struct {
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
||||
Actions []ActionsConfig `json:"actions" form:"actions"`
|
||||
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
||||
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
|
||||
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
||||
Actions []ActionsConfig `json:"actions" form:"actions"`
|
||||
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
||||
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
|
||||
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
|
||||
MCPPrepareScript string `json:"mcp_prepare_script" form:"mcp_prepare_script"`
|
||||
Filters []FiltersConfig `json:"filters" form:"filters"`
|
||||
|
||||
Description string `json:"description" form:"description"`
|
||||
|
||||
Model string `json:"model" form:"model"`
|
||||
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
||||
APIURL string `json:"api_url" form:"api_url"`
|
||||
APIKey string `json:"api_key" form:"api_key"`
|
||||
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
||||
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
|
||||
Model string `json:"model" form:"model"`
|
||||
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
||||
TranscriptionModel string `json:"transcription_model" form:"transcription_model"`
|
||||
TranscriptionLanguage string `json:"transcription_language" form:"transcription_language"`
|
||||
TTSModel string `json:"tts_model" form:"tts_model"`
|
||||
APIURL string `json:"api_url" form:"api_url"`
|
||||
APIKey string `json:"api_key" form:"api_key"`
|
||||
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
||||
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
|
||||
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
|
||||
|
||||
Name string `json:"name" form:"name"`
|
||||
HUD bool `json:"hud" form:"hud"`
|
||||
@@ -56,14 +70,18 @@ type AgentConfig struct {
|
||||
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
|
||||
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
|
||||
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
|
||||
LoopDetectionSteps int `json:"loop_detection_steps" form:"loop_detection_steps"`
|
||||
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
|
||||
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
|
||||
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
||||
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
|
||||
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
|
||||
StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"`
|
||||
EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"`
|
||||
MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"`
|
||||
}
|
||||
|
||||
type AgentConfigMeta struct {
|
||||
Filters []config.FieldGroup
|
||||
Fields []config.Field
|
||||
Connectors []config.FieldGroup
|
||||
Actions []config.FieldGroup
|
||||
@@ -75,6 +93,7 @@ func NewAgentConfigMeta(
|
||||
actionsConfig []config.FieldGroup,
|
||||
connectorsConfig []config.FieldGroup,
|
||||
dynamicPromptsConfig []config.FieldGroup,
|
||||
filtersConfig []config.FieldGroup,
|
||||
) AgentConfigMeta {
|
||||
return AgentConfigMeta{
|
||||
Fields: []config.Field{
|
||||
@@ -128,6 +147,27 @@ func NewAgentConfigMeta(
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "transcription_model",
|
||||
Label: "Transcription Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "transcription_language",
|
||||
Label: "Transcription Language",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "tts_model",
|
||||
Label: "TTS Model",
|
||||
Type: "text",
|
||||
DefaultValue: "",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "api_url",
|
||||
Label: "API URL",
|
||||
@@ -247,17 +287,68 @@ func NewAgentConfigMeta(
|
||||
Name: "enable_reasoning",
|
||||
Label: "Enable Reasoning",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
DefaultValue: true,
|
||||
HelpText: "Enable agent to explain its reasoning process",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "loop_detection_steps",
|
||||
Label: "Max Loop Detection Steps",
|
||||
Name: "parallel_jobs",
|
||||
Label: "Parallel Jobs",
|
||||
Type: "number",
|
||||
DefaultValue: 5,
|
||||
Min: 1,
|
||||
Step: 1,
|
||||
HelpText: "Number of concurrent tasks that can run in parallel",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "mcp_stdio_servers",
|
||||
Label: "MCP STDIO Servers",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "JSON configuration for MCP STDIO servers",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "mcp_prepare_script",
|
||||
Label: "MCP Prepare Script",
|
||||
Type: "textarea",
|
||||
DefaultValue: "",
|
||||
HelpText: "Script to prepare for running MCP servers",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "strip_thinking_tags",
|
||||
Label: "Strip Thinking Tags",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Remove content between <thinking></thinking> and <think></think> tags from agent responses",
|
||||
Tags: config.Tags{Section: "ModelSettings"},
|
||||
},
|
||||
{
|
||||
Name: "enable_evaluation",
|
||||
Label: "Enable Evaluation",
|
||||
Type: "checkbox",
|
||||
DefaultValue: false,
|
||||
HelpText: "Enable automatic evaluation of agent responses to ensure they meet user requirements",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "max_evaluation_loops",
|
||||
Label: "Max Evaluation Loops",
|
||||
Type: "number",
|
||||
DefaultValue: 2,
|
||||
Min: 1,
|
||||
Step: 1,
|
||||
HelpText: "Maximum number of evaluation loops to perform when addressing gaps in responses",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
{
|
||||
Name: "last_message_duration",
|
||||
Label: "Last Message Duration",
|
||||
Type: "text",
|
||||
DefaultValue: "5m",
|
||||
HelpText: "Duration for the last message to be considered in the conversation",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
},
|
||||
@@ -278,6 +369,7 @@ func NewAgentConfigMeta(
|
||||
DynamicPrompts: dynamicPromptsConfig,
|
||||
Connectors: connectorsConfig,
|
||||
Actions: actionsConfig,
|
||||
Filters: filtersConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,3 +378,148 @@ type Connector interface {
|
||||
AgentReasoningCallback() func(state types.ActionCurrentState) bool
|
||||
Start(a *agent.Agent)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for AgentConfig
|
||||
func (a *AgentConfig) UnmarshalJSON(data []byte) error {
|
||||
// Create a temporary type to avoid infinite recursion
|
||||
type Alias AgentConfig
|
||||
aux := &struct {
|
||||
*Alias
|
||||
MCPSTDIOServersConfig interface{} `json:"mcp_stdio_servers"`
|
||||
}{
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle MCP STDIO servers configuration
|
||||
if aux.MCPSTDIOServersConfig != nil {
|
||||
switch v := aux.MCPSTDIOServersConfig.(type) {
|
||||
case string:
|
||||
// Parse string configuration
|
||||
var mcpConfig struct {
|
||||
MCPServers map[string]struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
} `json:"mcpServers"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(v), &mcpConfig); err != nil {
|
||||
return fmt.Errorf("failed to parse MCP STDIO servers configuration: %w", err)
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(mcpConfig.MCPServers))
|
||||
for _, server := range mcpConfig.MCPServers {
|
||||
// Convert env map to slice of "KEY=VALUE" strings
|
||||
envSlice := make([]string, 0, len(server.Env))
|
||||
for k, v := range server.Env {
|
||||
envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
|
||||
Cmd: server.Command,
|
||||
Args: server.Args,
|
||||
Env: envSlice,
|
||||
})
|
||||
}
|
||||
case []interface{}:
|
||||
// Parse array configuration
|
||||
a.MCPSTDIOServers = make([]agent.MCPSTDIOServer, 0, len(v))
|
||||
for _, server := range v {
|
||||
serverMap, ok := server.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid server configuration format")
|
||||
}
|
||||
|
||||
cmd, _ := serverMap["cmd"].(string)
|
||||
args := make([]string, 0)
|
||||
if argsInterface, ok := serverMap["args"].([]interface{}); ok {
|
||||
for _, arg := range argsInterface {
|
||||
if argStr, ok := arg.(string); ok {
|
||||
args = append(args, argStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env := make([]string, 0)
|
||||
if envInterface, ok := serverMap["env"].([]interface{}); ok {
|
||||
for _, e := range envInterface {
|
||||
if envStr, ok := e.(string); ok {
|
||||
env = append(env, envStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.MCPSTDIOServers = append(a.MCPSTDIOServers, agent.MCPSTDIOServer{
|
||||
Cmd: cmd,
|
||||
Args: args,
|
||||
Env: env,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler for AgentConfig
|
||||
func (a *AgentConfig) MarshalJSON() ([]byte, error) {
|
||||
// Create a temporary type to avoid infinite recursion
|
||||
type Alias AgentConfig
|
||||
aux := &struct {
|
||||
*Alias
|
||||
MCPSTDIOServersConfig string `json:"mcp_stdio_servers,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(a),
|
||||
}
|
||||
|
||||
// Convert MCPSTDIOServers back to the expected JSON format
|
||||
if len(a.MCPSTDIOServers) > 0 {
|
||||
mcpConfig := struct {
|
||||
MCPServers map[string]struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
} `json:"mcpServers"`
|
||||
}{
|
||||
MCPServers: make(map[string]struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
}),
|
||||
}
|
||||
|
||||
// Convert each MCPSTDIOServer to the expected format
|
||||
for i, server := range a.MCPSTDIOServers {
|
||||
// Convert env slice back to map
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range server.Env {
|
||||
if parts := strings.SplitN(env, "=", 2); len(parts) == 2 {
|
||||
envMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
mcpConfig.MCPServers[fmt.Sprintf("server%d", i)] = struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
}{
|
||||
Command: server.Cmd,
|
||||
Args: server.Args,
|
||||
Env: envMap,
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the MCP config to JSON string
|
||||
mcpConfigJSON, err := json.Marshal(mcpConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal MCP STDIO servers configuration: %w", err)
|
||||
}
|
||||
aux.MCPSTDIOServersConfig = string(mcpConfigJSON)
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
+175
-58
@@ -26,19 +26,21 @@ import (
|
||||
|
||||
type AgentPool struct {
|
||||
sync.Mutex
|
||||
file string
|
||||
pooldir string
|
||||
pool AgentPoolData
|
||||
agents map[string]*Agent
|
||||
managers map[string]sse.Manager
|
||||
agentStatus map[string]*Status
|
||||
apiURL, defaultModel, defaultMultimodalModel string
|
||||
imageModel, localRAGAPI, localRAGKey, apiKey string
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||
connectors func(*AgentConfig) []Connector
|
||||
dynamicPrompt func(*AgentConfig) []DynamicPrompt
|
||||
timeout string
|
||||
conversationLogs string
|
||||
file string
|
||||
pooldir string
|
||||
pool AgentPoolData
|
||||
agents map[string]*Agent
|
||||
managers map[string]sse.Manager
|
||||
agentStatus map[string]*Status
|
||||
apiURL, defaultModel, defaultMultimodalModel, defaultTTSModel string
|
||||
defaultTranscriptionModel, defaultTranscriptionLanguage string
|
||||
imageModel, localRAGAPI, localRAGKey, apiKey string
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||
connectors func(*AgentConfig) []Connector
|
||||
dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt
|
||||
filters func(*AgentConfig) types.JobFilters
|
||||
timeout string
|
||||
conversationLogs string
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
@@ -72,11 +74,12 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
|
||||
}
|
||||
|
||||
func NewAgentPool(
|
||||
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory string,
|
||||
defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, imageModel, apiURL, apiKey, directory string,
|
||||
LocalRAGAPI string,
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
||||
connectors func(*AgentConfig) []Connector,
|
||||
promptBlocks func(*AgentConfig) []DynamicPrompt,
|
||||
promptBlocks func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt,
|
||||
filters func(*AgentConfig) types.JobFilters,
|
||||
timeout string,
|
||||
withLogs bool,
|
||||
) (*AgentPool, error) {
|
||||
@@ -93,23 +96,27 @@ func NewAgentPool(
|
||||
if _, err := os.Stat(poolfile); err != nil {
|
||||
// file does not exist, create a new pool
|
||||
return &AgentPool{
|
||||
file: poolfile,
|
||||
pooldir: directory,
|
||||
apiURL: apiURL,
|
||||
defaultModel: defaultModel,
|
||||
defaultMultimodalModel: defaultMultimodalModel,
|
||||
imageModel: imageModel,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
pool: make(map[string]AgentConfig),
|
||||
agentStatus: make(map[string]*Status),
|
||||
managers: make(map[string]sse.Manager),
|
||||
connectors: connectors,
|
||||
availableActions: availableActions,
|
||||
dynamicPrompt: promptBlocks,
|
||||
timeout: timeout,
|
||||
conversationLogs: conversationPath,
|
||||
file: poolfile,
|
||||
pooldir: directory,
|
||||
apiURL: apiURL,
|
||||
defaultModel: defaultModel,
|
||||
defaultMultimodalModel: defaultMultimodalModel,
|
||||
defaultTranscriptionModel: defaultTranscriptionModel,
|
||||
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
|
||||
defaultTTSModel: defaultTTSModel,
|
||||
imageModel: imageModel,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
pool: make(map[string]AgentConfig),
|
||||
agentStatus: make(map[string]*Status),
|
||||
managers: make(map[string]sse.Manager),
|
||||
connectors: connectors,
|
||||
availableActions: availableActions,
|
||||
dynamicPrompt: promptBlocks,
|
||||
filters: filters,
|
||||
timeout: timeout,
|
||||
conversationLogs: conversationPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -118,23 +125,27 @@ func NewAgentPool(
|
||||
return nil, err
|
||||
}
|
||||
return &AgentPool{
|
||||
file: poolfile,
|
||||
apiURL: apiURL,
|
||||
pooldir: directory,
|
||||
defaultModel: defaultModel,
|
||||
defaultMultimodalModel: defaultMultimodalModel,
|
||||
imageModel: imageModel,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
managers: make(map[string]sse.Manager),
|
||||
agentStatus: map[string]*Status{},
|
||||
pool: *poolData,
|
||||
connectors: connectors,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
dynamicPrompt: promptBlocks,
|
||||
availableActions: availableActions,
|
||||
timeout: timeout,
|
||||
conversationLogs: conversationPath,
|
||||
file: poolfile,
|
||||
apiURL: apiURL,
|
||||
pooldir: directory,
|
||||
defaultModel: defaultModel,
|
||||
defaultMultimodalModel: defaultMultimodalModel,
|
||||
defaultTranscriptionModel: defaultTranscriptionModel,
|
||||
defaultTranscriptionLanguage: defaultTranscriptionLanguage,
|
||||
defaultTTSModel: defaultTTSModel,
|
||||
imageModel: imageModel,
|
||||
apiKey: apiKey,
|
||||
agents: make(map[string]*Agent),
|
||||
managers: make(map[string]sse.Manager),
|
||||
agentStatus: map[string]*Status{},
|
||||
pool: *poolData,
|
||||
connectors: connectors,
|
||||
localRAGAPI: LocalRAGAPI,
|
||||
dynamicPrompt: promptBlocks,
|
||||
filters: filters,
|
||||
availableActions: availableActions,
|
||||
timeout: timeout,
|
||||
conversationLogs: conversationPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -166,7 +177,56 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
}
|
||||
}(a.pool[name])
|
||||
|
||||
return a.startAgentWithConfig(name, agentConfig)
|
||||
return a.startAgentWithConfig(name, agentConfig, nil)
|
||||
}
|
||||
|
||||
func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
oldAgent := a.agents[name]
|
||||
var o *types.Observable
|
||||
obs := oldAgent.Observer()
|
||||
if obs != nil {
|
||||
o = obs.NewObservable()
|
||||
o.Name = "Restarting Agent"
|
||||
o.Icon = "sync"
|
||||
o.Creation = &types.Creation{}
|
||||
obs.Update(*o)
|
||||
}
|
||||
|
||||
stateFile, characterFile := a.stateFiles(name)
|
||||
|
||||
os.Remove(stateFile)
|
||||
os.Remove(characterFile)
|
||||
|
||||
oldAgent.Stop()
|
||||
|
||||
a.pool[name] = *agentConfig
|
||||
delete(a.agents, name)
|
||||
|
||||
if err := a.save(); err != nil {
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{Error: err.Error()}
|
||||
obs.Update(*o)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.startAgentWithConfig(name, agentConfig, obs); err != nil {
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{Error: err.Error()}
|
||||
obs.Update(*o)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if obs != nil {
|
||||
o.Completion = &types.Completion{}
|
||||
obs.Update(*o)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
|
||||
@@ -191,7 +251,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
|
||||
ImagePrompt string `json:"image_prompt"`
|
||||
}
|
||||
|
||||
err := llm.GenerateTypedJSON(
|
||||
err := llm.GenerateTypedJSONWithGuidance(
|
||||
context.Background(),
|
||||
llm.NewClient(APIKey, APIURL, "10m"),
|
||||
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
|
||||
@@ -268,30 +328,56 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
|
||||
return a.agentStatus[name]
|
||||
}
|
||||
|
||||
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error {
|
||||
manager := sse.NewManager(5)
|
||||
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
|
||||
var manager sse.Manager
|
||||
if m, ok := a.managers[name]; ok {
|
||||
manager = m
|
||||
} else {
|
||||
manager = sse.NewManager(5)
|
||||
}
|
||||
ctx := context.Background()
|
||||
model := a.defaultModel
|
||||
multimodalModel := a.defaultMultimodalModel
|
||||
transcriptionModel := a.defaultTranscriptionModel
|
||||
transcriptionLanguage := a.defaultTranscriptionLanguage
|
||||
ttsModel := a.defaultTTSModel
|
||||
|
||||
if config.MultimodalModel != "" {
|
||||
multimodalModel = config.MultimodalModel
|
||||
}
|
||||
|
||||
if config.TranscriptionModel != "" {
|
||||
transcriptionModel = config.TranscriptionModel
|
||||
}
|
||||
|
||||
if config.TranscriptionLanguage != "" {
|
||||
transcriptionLanguage = config.TranscriptionLanguage
|
||||
}
|
||||
if config.TTSModel != "" {
|
||||
ttsModel = config.TTSModel
|
||||
}
|
||||
|
||||
if config.Model != "" {
|
||||
model = config.Model
|
||||
} else {
|
||||
config.Model = model
|
||||
}
|
||||
|
||||
if config.PeriodicRuns == "" {
|
||||
config.PeriodicRuns = "10m"
|
||||
}
|
||||
|
||||
// XXX: Why do we update the pool config from an Agent's config?
|
||||
if config.APIURL != "" {
|
||||
a.apiURL = config.APIURL
|
||||
} else {
|
||||
config.APIURL = a.apiURL
|
||||
}
|
||||
|
||||
if config.APIKey != "" {
|
||||
a.apiKey = config.APIKey
|
||||
} else {
|
||||
config.APIKey = a.apiKey
|
||||
}
|
||||
|
||||
if config.LocalRAGURL != "" {
|
||||
@@ -303,8 +389,9 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
}
|
||||
|
||||
connectors := a.connectors(config)
|
||||
promptBlocks := a.dynamicPrompt(config)
|
||||
promptBlocks := a.dynamicPrompt(config)(ctx, a)
|
||||
actions := a.availableActions(config)(ctx, a)
|
||||
filters := a.filters(config)
|
||||
stateFile, characterFile := a.stateFiles(name)
|
||||
|
||||
actionsLog := []string{}
|
||||
@@ -317,6 +404,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
|
||||
}
|
||||
|
||||
filtersLog := []string{}
|
||||
for _, filter := range filters {
|
||||
filtersLog = append(filtersLog, filter.Name())
|
||||
}
|
||||
|
||||
xlog.Info(
|
||||
"Creating agent",
|
||||
"name", name,
|
||||
@@ -324,6 +416,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
"api_url", a.apiURL,
|
||||
"actions", actionsLog,
|
||||
"connectors", connectorLog,
|
||||
"filters", filtersLog,
|
||||
)
|
||||
|
||||
// dynamicPrompts := []map[string]string{}
|
||||
@@ -331,14 +424,24 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
// dynamicPrompts = append(dynamicPrompts, p.ToMap())
|
||||
// }
|
||||
|
||||
if obs == nil {
|
||||
obs = NewSSEObserver(name, manager)
|
||||
}
|
||||
|
||||
opts := []Option{
|
||||
WithModel(model),
|
||||
WithLLMAPIURL(a.apiURL),
|
||||
WithContext(ctx),
|
||||
WithMCPServers(config.MCPServers...),
|
||||
WithTranscriptionModel(transcriptionModel),
|
||||
WithTranscriptionLanguage(transcriptionLanguage),
|
||||
WithTTSModel(ttsModel),
|
||||
WithPeriodicRuns(config.PeriodicRuns),
|
||||
WithPermanentGoal(config.PermanentGoal),
|
||||
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
||||
WithPrompts(promptBlocks...),
|
||||
WithJobFilters(filters...),
|
||||
WithMCPPrepareScript(config.MCPPrepareScript),
|
||||
// WithDynamicPrompts(dynamicPrompts...),
|
||||
WithCharacter(Character{
|
||||
Name: name,
|
||||
@@ -375,6 +478,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
}),
|
||||
WithSystemPrompt(config.SystemPrompt),
|
||||
WithMultimodalModel(multimodalModel),
|
||||
WithLastMessageDuration(config.LastMessageDuration),
|
||||
WithAgentResultCallback(func(state types.ActionState) {
|
||||
a.Lock()
|
||||
if _, ok := a.agentStatus[name]; !ok {
|
||||
@@ -407,6 +511,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
c.AgentResultCallback()(state)
|
||||
}
|
||||
}),
|
||||
WithObserver(obs),
|
||||
}
|
||||
|
||||
if config.HUD {
|
||||
@@ -457,12 +562,24 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
||||
opts = append(opts, EnableForceReasoning)
|
||||
}
|
||||
|
||||
if config.StripThinkingTags {
|
||||
opts = append(opts, EnableStripThinkingTags)
|
||||
}
|
||||
|
||||
if config.KnowledgeBaseResults > 0 {
|
||||
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
|
||||
}
|
||||
|
||||
if config.LoopDetectionSteps > 0 {
|
||||
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
|
||||
if config.ParallelJobs > 0 {
|
||||
opts = append(opts, WithParallelJobs(config.ParallelJobs))
|
||||
}
|
||||
|
||||
if config.EnableEvaluation {
|
||||
opts = append(opts, EnableEvaluation())
|
||||
}
|
||||
|
||||
if config.MaxEvaluationLoops > 0 {
|
||||
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
|
||||
}
|
||||
|
||||
xlog.Info("Starting agent", "name", name, "config", config)
|
||||
@@ -509,7 +626,7 @@ func (a *AgentPool) StartAll() error {
|
||||
if a.agents[name] != nil { // Agent already started
|
||||
continue
|
||||
}
|
||||
if err := a.startAgentWithConfig(name, &config); err != nil {
|
||||
if err := a.startAgentWithConfig(name, &config, nil); err != nil {
|
||||
xlog.Error("Failed to start agent", "name", name, "error", err)
|
||||
}
|
||||
}
|
||||
@@ -547,7 +664,7 @@ func (a *AgentPool) Start(name string) error {
|
||||
return nil
|
||||
}
|
||||
if config, ok := a.pool[name]; ok {
|
||||
return a.startAgentWithConfig(name, &config)
|
||||
return a.startAgentWithConfig(name, &config, nil)
|
||||
}
|
||||
|
||||
return fmt.Errorf("agent %s not found", name)
|
||||
|
||||
+96
-7
@@ -3,7 +3,9 @@ package types
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
@@ -29,9 +31,10 @@ func NewActionContext(ctx context.Context, cancel context.CancelFunc) *ActionCon
|
||||
type ActionParams map[string]interface{}
|
||||
|
||||
type ActionResult struct {
|
||||
Job *Job
|
||||
Result string
|
||||
Metadata map[string]interface{}
|
||||
Job *Job
|
||||
Result string
|
||||
ImageBase64Result string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (ap ActionParams) Read(s string) error {
|
||||
@@ -74,8 +77,8 @@ func (a ActionDefinitionName) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
func (a ActionDefinition) ToFunctionDefinition() openai.FunctionDefinition {
|
||||
return openai.FunctionDefinition{
|
||||
func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
|
||||
return &openai.FunctionDefinition{
|
||||
Name: a.Name.String(),
|
||||
Description: a.Description,
|
||||
Parameters: jsonschema.Definition{
|
||||
@@ -86,11 +89,89 @@ func (a ActionDefinition) ToFunctionDefinition() openai.FunctionDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
type cogitoWrapper struct {
|
||||
action Action
|
||||
ctx context.Context
|
||||
sharedState *AgentSharedState
|
||||
}
|
||||
|
||||
func (c *cogitoWrapper) Tool() openai.Tool {
|
||||
return openai.Tool{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: c.action.Definition().ToFunctionDefinition(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cogitoWrapper) Run(args map[string]any) (string, error) {
|
||||
ctx := c.ctx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
result, err := c.action.Run(ctx, c.sharedState, ActionParams(args))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Result, nil
|
||||
}
|
||||
|
||||
// Actions is something the agent can do
|
||||
type Action interface {
|
||||
Run(ctx context.Context, action ActionParams) (ActionResult, error)
|
||||
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
|
||||
Definition() ActionDefinition
|
||||
Plannable() bool
|
||||
}
|
||||
|
||||
// UserDefinedChecker interface to identify user-defined actions
|
||||
type UserDefinedChecker interface {
|
||||
IsUserDefined() bool
|
||||
}
|
||||
|
||||
// BaseAction provides default implementation for Action interface
|
||||
// Embed this in action implementations to get the default IsUserDefined behavior
|
||||
type BaseAction struct{}
|
||||
|
||||
func (b *BaseAction) IsUserDefined() bool {
|
||||
return false // Regular actions are not user-defined
|
||||
}
|
||||
|
||||
// IsActionUserDefined checks if an action is user-defined
|
||||
func IsActionUserDefined(action Action) bool {
|
||||
if checker, ok := action.(UserDefinedChecker); ok {
|
||||
return checker.IsUserDefined()
|
||||
}
|
||||
return false // Actions without UserDefinedChecker are not user-defined
|
||||
}
|
||||
|
||||
// UserDefinedAction represents a user-defined function tool
|
||||
type UserDefinedAction struct {
|
||||
ActionDef *ActionDefinition
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error) {
|
||||
// User-defined actions should not be executed directly
|
||||
return ActionResult{}, fmt.Errorf("user-defined action '%s' cannot be executed by agent", u.ActionDef.Name)
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Definition() ActionDefinition {
|
||||
return *u.ActionDef
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Plannable() bool {
|
||||
return true // User-defined actions are plannable
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) IsUserDefined() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateUserDefinedActions converts user tools to UserDefinedAction instances
|
||||
func CreateUserDefinedActions(userTools []ActionDefinition) []Action {
|
||||
var actions []Action
|
||||
for _, tool := range userTools {
|
||||
actions = append(actions, &UserDefinedAction{
|
||||
ActionDef: &tool,
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
type Actions []Action
|
||||
@@ -106,6 +187,14 @@ func (a Actions) ToTools() []openai.Tool {
|
||||
return tools
|
||||
}
|
||||
|
||||
func (a Actions) ToCogitoTools(ctx context.Context, sharedState *AgentSharedState) []cogito.Tool {
|
||||
tools := []cogito.Tool{}
|
||||
for _, action := range a {
|
||||
tools = append(tools, &cogitoWrapper{action: action, ctx: ctx, sharedState: sharedState})
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
func (a Actions) Find(name string) Action {
|
||||
for _, action := range a {
|
||||
if action.Definition().Name.Is(name) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package types
|
||||
|
||||
type JobFilter interface {
|
||||
Name() string
|
||||
Apply(job *Job) (bool, error)
|
||||
IsTrigger() bool
|
||||
}
|
||||
|
||||
type JobFilters []JobFilter
|
||||
|
||||
type FilterResult struct {
|
||||
HasTriggers bool `json:"has_triggers"`
|
||||
TriggeredBy string `json:"triggered_by,omitempty"`
|
||||
FailedBy string `json:"failed_by,omitempty"`
|
||||
}
|
||||
+85
-49
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
@@ -19,14 +20,18 @@ type Job struct {
|
||||
ConversationHistory []openai.ChatCompletionMessage
|
||||
UUID string
|
||||
Metadata map[string]interface{}
|
||||
DoneFilter bool
|
||||
|
||||
pastActions []*ActionRequest
|
||||
nextAction *Action
|
||||
nextActionParams *ActionParams
|
||||
nextActionReasoning string
|
||||
// Tools available for this job
|
||||
BuiltinTools []ActionDefinition // Built-in tools like web search
|
||||
UserTools []ActionDefinition // User-defined function tools
|
||||
ToolChoice string
|
||||
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
context context.Context
|
||||
fragment *cogito.Fragment
|
||||
cancel context.CancelFunc
|
||||
|
||||
Obs *Observable
|
||||
}
|
||||
|
||||
type ActionRequest struct {
|
||||
@@ -42,6 +47,24 @@ func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithBuiltinTools(tools []ActionDefinition) JobOption {
|
||||
return func(j *Job) {
|
||||
j.BuiltinTools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserTools(tools []ActionDefinition) JobOption {
|
||||
return func(j *Job) {
|
||||
j.UserTools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func WithToolChoice(choice string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ToolChoice = choice
|
||||
}
|
||||
}
|
||||
|
||||
func WithReasoningCallback(f func(ActionCurrentState) bool) JobOption {
|
||||
return func(r *Job) {
|
||||
r.ReasoningCallback = f
|
||||
@@ -82,37 +105,6 @@ func (j *Job) CallbackWithResult(stateResult ActionState) {
|
||||
j.ResultCallback(stateResult)
|
||||
}
|
||||
|
||||
func (j *Job) SetNextAction(action *Action, params *ActionParams, reasoning string) {
|
||||
j.nextAction = action
|
||||
j.nextActionParams = params
|
||||
j.nextActionReasoning = reasoning
|
||||
}
|
||||
|
||||
func (j *Job) AddPastAction(action Action, params *ActionParams) {
|
||||
j.pastActions = append(j.pastActions, &ActionRequest{
|
||||
Action: action,
|
||||
Params: params,
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Job) GetPastActions() []*ActionRequest {
|
||||
return j.pastActions
|
||||
}
|
||||
|
||||
func (j *Job) GetNextAction() (*Action, *ActionParams, string) {
|
||||
return j.nextAction, j.nextActionParams, j.nextActionReasoning
|
||||
}
|
||||
|
||||
func (j *Job) HasNextAction() bool {
|
||||
return j.nextAction != nil
|
||||
}
|
||||
|
||||
func (j *Job) ResetNextAction() {
|
||||
j.nextAction = nil
|
||||
j.nextActionParams = nil
|
||||
j.nextActionReasoning = ""
|
||||
}
|
||||
|
||||
func WithTextImage(text, image string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ConversationHistory = append(j.ConversationHistory, openai.ChatCompletionMessage{
|
||||
@@ -159,23 +151,23 @@ func newUUID() string {
|
||||
// To wait for a Job result, use JobResult.WaitResult()
|
||||
func NewJob(opts ...JobOption) *Job {
|
||||
j := &Job{
|
||||
Result: NewJobResult(),
|
||||
UUID: newUUID(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(j)
|
||||
Result: NewJobResult(),
|
||||
UUID: uuid.New().String(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
context: context.Background(),
|
||||
ConversationHistory: []openai.ChatCompletionMessage{},
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
if j.context == nil {
|
||||
ctx = context.Background()
|
||||
} else {
|
||||
ctx = j.context
|
||||
for _, opt := range opts {
|
||||
opt(j)
|
||||
}
|
||||
|
||||
context, cancel := context.WithCancel(ctx)
|
||||
j.context = context
|
||||
// Store the original request if it exists in the conversation history
|
||||
|
||||
ctx, cancel := context.WithCancel(j.context)
|
||||
j.context = ctx
|
||||
j.cancel = cancel
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
@@ -198,3 +190,47 @@ func (j *Job) Cancel() {
|
||||
func (j *Job) GetContext() context.Context {
|
||||
return j.context
|
||||
}
|
||||
|
||||
func WithObservable(obs *Observable) JobOption {
|
||||
return func(j *Job) {
|
||||
j.Obs = obs
|
||||
}
|
||||
}
|
||||
|
||||
// GetEvaluationLoop returns the current evaluation loop count
|
||||
func (j *Job) GetEvaluationLoop() int {
|
||||
if j.Metadata == nil {
|
||||
j.Metadata = make(map[string]interface{})
|
||||
}
|
||||
if loop, ok := j.Metadata["evaluation_loop"].(int); ok {
|
||||
return loop
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IncrementEvaluationLoop increments the evaluation loop count
|
||||
func (j *Job) IncrementEvaluationLoop() {
|
||||
if j.Metadata == nil {
|
||||
j.Metadata = make(map[string]interface{})
|
||||
}
|
||||
currentLoop := j.GetEvaluationLoop()
|
||||
j.Metadata["evaluation_loop"] = currentLoop + 1
|
||||
}
|
||||
|
||||
// GetBuiltinTools returns the builtin tools for this job
|
||||
func (j *Job) GetBuiltinTools() []ActionDefinition {
|
||||
return j.BuiltinTools
|
||||
}
|
||||
|
||||
// GetUserTools returns the user tools for this job
|
||||
func (j *Job) GetUserTools() []ActionDefinition {
|
||||
return j.UserTools
|
||||
}
|
||||
|
||||
// GetAllTools returns all tools (builtin + user) for this job
|
||||
func (j *Job) GetAllTools() []ActionDefinition {
|
||||
allTools := make([]ActionDefinition, 0, len(j.BuiltinTools)+len(j.UserTools))
|
||||
allTools = append(allTools, j.BuiltinTools...)
|
||||
allTools = append(allTools, j.UserTools...)
|
||||
return allTools
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type Creation struct {
|
||||
ChatCompletionMessage *openai.ChatCompletionMessage `json:"chat_completion_message,omitempty"`
|
||||
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
|
||||
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
|
||||
FunctionParams ActionParams `json:"function_params,omitempty"`
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
|
||||
ActionResult string `json:"action_result,omitempty"`
|
||||
AgentState *AgentInternalState `json:"agent_state"`
|
||||
}
|
||||
|
||||
type Completion struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
|
||||
Conversation []openai.ChatCompletionMessage `json:"conversation,omitempty"`
|
||||
ActionResult string `json:"action_result,omitempty"`
|
||||
AgentState *AgentInternalState `json:"agent_state,omitempty"`
|
||||
FilterResult *FilterResult `json:"filter_result,omitempty"`
|
||||
}
|
||||
|
||||
type Observable struct {
|
||||
ID int32 `json:"id"`
|
||||
ParentID int32 `json:"parent_id,omitempty"`
|
||||
Agent string `json:"agent"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
|
||||
Creation *Creation `json:"creation,omitempty"`
|
||||
Progress []Progress `json:"progress,omitempty"`
|
||||
Completion *Completion `json:"completion,omitempty"`
|
||||
}
|
||||
|
||||
func (o *Observable) AddProgress(p Progress) {
|
||||
if o.Progress == nil {
|
||||
o.Progress = make([]Progress, 0)
|
||||
}
|
||||
o.Progress = append(o.Progress, p)
|
||||
}
|
||||
|
||||
func (o *Observable) MakeLastProgressCompletion() {
|
||||
if len(o.Progress) == 0 {
|
||||
xlog.Error("Observable completed without any progress", "id", o.ID, "name", o.Name)
|
||||
return
|
||||
}
|
||||
p := o.Progress[len(o.Progress)-1]
|
||||
o.Progress = o.Progress[:len(o.Progress)-1]
|
||||
o.Completion = &Completion{
|
||||
Error: p.Error,
|
||||
ChatCompletionResponse: p.ChatCompletionResponse,
|
||||
ActionResult: p.ActionResult,
|
||||
AgentState: p.AgentState,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package types
|
||||
|
||||
type PromptResult struct {
|
||||
Content string
|
||||
ImageBase64 string
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package types
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/cogito"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
@@ -11,6 +12,7 @@ type JobResult struct {
|
||||
sync.Mutex
|
||||
// The result of a job
|
||||
State []ActionState
|
||||
Plans []cogito.PlanStatus
|
||||
Conversation []openai.ChatCompletionMessage
|
||||
|
||||
Finalizers []func([]openai.ChatCompletionMessage)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/conversations"
|
||||
)
|
||||
|
||||
// State is the structure
|
||||
// that is used to keep track of the current state
|
||||
// and the Agent's short memory that it can update
|
||||
// Besides a long term memory that is accessible by the agent (With vector database),
|
||||
// And a context memory (that is always powered by a vector database),
|
||||
// this memory is the shorter one that the LLM keeps across conversation and across its
|
||||
// reasoning process's and life time.
|
||||
// TODO: A special action is then used to let the LLM itself update its memory
|
||||
// periodically during self-processing, and the same action is ALSO exposed
|
||||
// during the conversation to let the user put for example, a new goal to the agent.
|
||||
type AgentInternalState struct {
|
||||
NowDoing string `json:"doing_now"`
|
||||
DoingNext string `json:"doing_next"`
|
||||
DoneHistory []string `json:"done_history"`
|
||||
Memories []string `json:"memories"`
|
||||
Goal string `json:"goal"`
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultLastMessageDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
type ReminderActionResponse struct {
|
||||
Message string `json:"message"`
|
||||
CronExpr string `json:"cron_expr"` // Cron expression for scheduling
|
||||
LastRun time.Time `json:"last_run"` // Last time this reminder was triggered
|
||||
NextRun time.Time `json:"next_run"` // Next scheduled run time
|
||||
IsRecurring bool `json:"is_recurring"` // Whether this is a recurring reminder
|
||||
}
|
||||
|
||||
type AgentSharedState struct {
|
||||
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
|
||||
Reminders []ReminderActionResponse `json:"reminders"`
|
||||
}
|
||||
|
||||
func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
||||
if lastMessageDuration == 0 {
|
||||
lastMessageDuration = DefaultLastMessageDuration
|
||||
}
|
||||
return &AgentSharedState{
|
||||
ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration),
|
||||
Reminders: make([]ReminderActionResponse, 0),
|
||||
}
|
||||
}
|
||||
|
||||
const fmtT = `=====================
|
||||
NowDoing: %s
|
||||
DoingNext: %s
|
||||
Your current goal is: %s
|
||||
You have done: %+v
|
||||
You have a short memory with: %+v
|
||||
=====================
|
||||
`
|
||||
|
||||
func (c AgentInternalState) String() string {
|
||||
return fmt.Sprintf(
|
||||
fmtT,
|
||||
c.NowDoing,
|
||||
c.DoingNext,
|
||||
c.Goal,
|
||||
c.DoneHistory,
|
||||
c.Memories,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-hipblas
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/kfd
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
@@ -1,75 +0,0 @@
|
||||
services:
|
||||
localai:
|
||||
# See https://localai.io/basics/container/#standard-container-images for
|
||||
# a list of available container images (or build your own with the provided Dockerfile)
|
||||
# Available images with CUDA, ROCm, SYCL, Vulkan
|
||||
# Image list (quay.io): https://quay.io/repository/go-skynet/local-ai?tab=tags
|
||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||
image: localai/localai:master-sycl-f32-ffmpeg-core
|
||||
command:
|
||||
# - rombo-org_rombo-llm-v3.0-qwen-32b # minimum suggested model
|
||||
- arcee-agent # (smaller)
|
||||
- granite-embedding-107m-multilingual
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
||||
interval: 60s
|
||||
timeout: 10m
|
||||
retries: 120
|
||||
ports:
|
||||
- 8081:8080
|
||||
environment:
|
||||
- DEBUG=true
|
||||
#- LOCALAI_API_KEY=sk-1234567890
|
||||
volumes:
|
||||
- ./volumes/models:/build/models:cached
|
||||
- ./volumes/images:/tmp/generated/images
|
||||
devices:
|
||||
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
||||
- /dev/dri/card1
|
||||
- /dev/dri/renderD129
|
||||
|
||||
localrecall:
|
||||
image: quay.io/mudler/localrecall:main
|
||||
ports:
|
||||
- 8080
|
||||
environment:
|
||||
- COLLECTION_DB_PATH=/db
|
||||
- EMBEDDING_MODEL=granite-embedding-107m-multilingual
|
||||
- FILE_ASSETS=/assets
|
||||
- OPENAI_API_KEY=sk-1234567890
|
||||
- OPENAI_BASE_URL=http://localai:8080
|
||||
volumes:
|
||||
- ./volumes/localrag/db:/db
|
||||
- ./volumes/localrag/assets/:/assets
|
||||
|
||||
localrecall-healthcheck:
|
||||
depends_on:
|
||||
localrecall:
|
||||
condition: service_started
|
||||
image: busybox
|
||||
command: ["sh", "-c", "until wget -q -O - http://localrecall:8080 > /dev/null 2>&1; do echo 'Waiting for localrecall...'; sleep 1; done; echo 'localrecall is up!'"]
|
||||
|
||||
localagi:
|
||||
depends_on:
|
||||
localai:
|
||||
condition: service_healthy
|
||||
localrecall-healthcheck:
|
||||
condition: service_completed_successfully
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.webui
|
||||
ports:
|
||||
- 8080:3000
|
||||
image: quay.io/mudler/localagi:master
|
||||
environment:
|
||||
- LOCALAGI_MODEL=arcee-agent
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||
- LOCALAGI_STATE_DIR=/pool
|
||||
- LOCALAGI_TIMEOUT=5m
|
||||
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./volumes/localagi/:/pool
|
||||
@@ -1,85 +0,0 @@
|
||||
services:
|
||||
localai:
|
||||
# See https://localai.io/basics/container/#standard-container-images for
|
||||
# a list of available container images (or build your own with the provided Dockerfile)
|
||||
# Available images with CUDA, ROCm, SYCL, Vulkan
|
||||
# Image list (quay.io): https://quay.io/repository/go-skynet/local-ai?tab=tags
|
||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||
image: localai/localai:master-gpu-nvidia-cuda-12
|
||||
command:
|
||||
- mlabonne_gemma-3-27b-it-abliterated
|
||||
- qwen_qwq-32b
|
||||
# Other good alternative options:
|
||||
# - rombo-org_rombo-llm-v3.0-qwen-32b # minimum suggested model
|
||||
# - arcee-agent
|
||||
- granite-embedding-107m-multilingual
|
||||
- flux.1-dev
|
||||
- minicpm-v-2_6
|
||||
environment:
|
||||
# Enable if you have a single GPU which don't fit all the models
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
||||
interval: 10s
|
||||
timeout: 20m
|
||||
retries: 20
|
||||
ports:
|
||||
- 8081:8080
|
||||
volumes:
|
||||
- ./volumes/models:/build/models:cached
|
||||
- ./volumes/images:/tmp/generated/images
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
localrecall:
|
||||
image: quay.io/mudler/localrecall:main
|
||||
ports:
|
||||
- 8080
|
||||
environment:
|
||||
- COLLECTION_DB_PATH=/db
|
||||
- EMBEDDING_MODEL=granite-embedding-107m-multilingual
|
||||
- FILE_ASSETS=/assets
|
||||
- OPENAI_API_KEY=sk-1234567890
|
||||
- OPENAI_BASE_URL=http://localai:8080
|
||||
volumes:
|
||||
- ./volumes/localrag/db:/db
|
||||
- ./volumes/localrag/assets/:/assets
|
||||
|
||||
localrecall-healthcheck:
|
||||
depends_on:
|
||||
localrecall:
|
||||
condition: service_started
|
||||
image: busybox
|
||||
command: ["sh", "-c", "until wget -q -O - http://localrecall:8080 > /dev/null 2>&1; do echo 'Waiting for localrecall...'; sleep 1; done; echo 'localrecall is up!'"]
|
||||
|
||||
localagi:
|
||||
depends_on:
|
||||
localai:
|
||||
condition: service_healthy
|
||||
localrecall-healthcheck:
|
||||
condition: service_completed_successfully
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.webui
|
||||
ports:
|
||||
- 8080:3000
|
||||
image: quay.io/mudler/localagi:master
|
||||
environment:
|
||||
- LOCALAGI_MODEL=qwen_qwq-32b
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||
- LOCALAGI_STATE_DIR=/pool
|
||||
- LOCALAGI_TIMEOUT=5m
|
||||
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
||||
- LOCALAGI_MULTIMODAL_MODEL=minicpm-v-2_6
|
||||
- LOCALAGI_IMAGE_MODEL=flux.1-dev
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- ./volumes/localagi/:/pool
|
||||
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-intel
|
||||
devices:
|
||||
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
||||
- /dev/dri/card1
|
||||
- /dev/dri/renderD129
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
@@ -0,0 +1,38 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-nvidia-cuda-12
|
||||
# For images with python backends, use:
|
||||
# image: localai/localai:master-cublas-cuda12-ffmpeg
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
+39
-13
@@ -5,9 +5,11 @@ services:
|
||||
# Available images with CUDA, ROCm, SYCL, Vulkan
|
||||
# Image list (quay.io): https://quay.io/repository/go-skynet/local-ai?tab=tags
|
||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||
image: localai/localai:master-ffmpeg-core
|
||||
image: localai/localai:master
|
||||
command:
|
||||
- arcee-agent # (smaller)
|
||||
- ${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- ${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- granite-embedding-107m-multilingual
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
||||
@@ -20,17 +22,10 @@ services:
|
||||
- DEBUG=true
|
||||
#- LOCALAI_API_KEY=sk-1234567890
|
||||
volumes:
|
||||
- ./volumes/models:/build/models:cached
|
||||
- ./volumes/images:/tmp/generated/images
|
||||
- ./volumes/models:/models
|
||||
- ./volumes/backends:/backends
|
||||
- ./volumes/content:/tmp/generated/content
|
||||
|
||||
# decomment the following piece if running with Nvidia GPUs
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
localrecall:
|
||||
image: quay.io/mudler/localrecall:main
|
||||
ports:
|
||||
@@ -52,12 +47,39 @@ services:
|
||||
image: busybox
|
||||
command: ["sh", "-c", "until wget -q -O - http://localrecall:8080 > /dev/null 2>&1; do echo 'Waiting for localrecall...'; sleep 1; done; echo 'localrecall is up!'"]
|
||||
|
||||
sshbox:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.sshbox
|
||||
ports:
|
||||
- "22"
|
||||
environment:
|
||||
- SSH_USER=root
|
||||
- SSH_PASSWORD=root
|
||||
- DOCKER_HOST=tcp://dind:2375
|
||||
depends_on:
|
||||
dind:
|
||||
condition: service_healthy
|
||||
|
||||
dind:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
environment:
|
||||
- DOCKER_TLS_CERTDIR=""
|
||||
healthcheck:
|
||||
test: ["CMD", "docker", "info"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
localagi:
|
||||
depends_on:
|
||||
localai:
|
||||
condition: service_healthy
|
||||
localrecall-healthcheck:
|
||||
condition: service_completed_successfully
|
||||
dind:
|
||||
condition: service_healthy
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.webui
|
||||
@@ -65,13 +87,17 @@ services:
|
||||
- 8080:3000
|
||||
#image: quay.io/mudler/localagi:master
|
||||
environment:
|
||||
- LOCALAGI_MODEL=arcee-agent
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||
- LOCALAGI_STATE_DIR=/pool
|
||||
- LOCALAGI_TIMEOUT=5m
|
||||
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
||||
- LOCALAGI_SSHBOX_URL=root:root@sshbox:22
|
||||
- DOCKER_HOST=tcp://dind:2375
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package custom_actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
Message string `json:"message"` // field name
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
p := Params{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
return "Hello, " + p.Message + "!", map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
func Description() string {
|
||||
return "Send a message to the user"
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"message": { // field name
|
||||
"string", // type
|
||||
"The message to send", // description
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"message"} // field name
|
||||
}
|
||||
|
||||
var config string
|
||||
|
||||
func Init(configuration string) error {
|
||||
// Do something with the configuration that was passed-by
|
||||
config = configuration
|
||||
return nil
|
||||
}
|
||||
|
||||
// DynamicPrompt
|
||||
func Render() (string, string, error) {
|
||||
return "Hello, " + config + "!", "", nil
|
||||
}
|
||||
|
||||
func Role() string {
|
||||
return "system" // Role for the dynamic prompt
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from RealtimeSTT import AudioToTextRecorder
|
||||
|
||||
def process_text(text):
|
||||
print(text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
recorder = AudioToTextRecorder(wake_words="jarvis")
|
||||
|
||||
while True:
|
||||
recorder.text(process_text)
|
||||
@@ -1,79 +1,84 @@
|
||||
module github.com/mudler/LocalAGI
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.28.1
|
||||
github.com/chasefleming/elem-go v0.25.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/bwmarrin/discordgo v0.29.0
|
||||
github.com/chasefleming/elem-go v0.30.0
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||
github.com/donseba/go-htmx v1.8.0
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
||||
github.com/go-telegram/bot v1.2.1
|
||||
github.com/gofiber/fiber/v2 v2.52.4
|
||||
github.com/gofiber/template/html/v2 v2.1.1
|
||||
github.com/go-telegram/bot v1.17.0
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/google/go-github/v69 v69.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/metoro-io/mcp-golang v0.8.0
|
||||
github.com/onsi/ginkgo/v2 v2.15.0
|
||||
github.com/onsi/gomega v1.31.1
|
||||
github.com/philippgille/chromem-go v0.5.0
|
||||
github.com/sashabaranov/go-openai v1.18.3
|
||||
github.com/slack-go/slack v0.16.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0
|
||||
github.com/mudler/cogito v0.4.2
|
||||
github.com/onsi/ginkgo/v2 v2.25.3
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/philippgille/chromem-go v0.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
github.com/slack-go/slack v0.17.3
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
||||
github.com/tmc/langchaingo v0.1.8
|
||||
github.com/tmc/langchaingo v0.1.14
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
github.com/valyala/fasthttp v1.52.0
|
||||
golang.org/x/crypto v0.30.0
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
maunium.net/go/mautrix v0.17.0
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/antchfx/htmlquery v1.3.0 // indirect
|
||||
github.com/antchfx/xmlquery v1.3.17 // indirect
|
||||
github.com/antchfx/xpath v1.2.4 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/gocolly/colly v1.2.0 // indirect
|
||||
github.com/gofiber/template v1.8.3 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/invopop/jsonschema v0.12.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/antchfx/htmlquery v1.3.4 // indirect
|
||||
github.com/antchfx/xmlquery v1.4.4 // indirect
|
||||
github.com/antchfx/xpath v1.3.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||
github.com/emersion/go-message v0.18.2
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||
github.com/emersion/go-smtp v0.24.0
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gocolly/colly v1.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/zerolog v1.31.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||
@@ -81,17 +86,16 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
go.mau.fi/util v0.3.0 // indirect
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,203 +1,240 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
|
||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
||||
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk=
|
||||
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
|
||||
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
|
||||
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chasefleming/elem-go v0.25.0 h1:LYzr1auk39Bh3bdKloArOFV7sOBnOfSOKxsg58eWL0Q=
|
||||
github.com/chasefleming/elem-go v0.25.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
|
||||
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
|
||||
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
|
||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
||||
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
||||
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/donseba/go-htmx v1.8.0 h1:oTx1uUsjXZZVvcZfulZvBSPtdD1jzsvZyuK91+Q8zPE=
|
||||
github.com/donseba/go-htmx v1.8.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
|
||||
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-telegram/bot v1.2.1 h1:FkrixLCtMtPUQAN4plXdNElbhkdXkx2p68YPXKBruDg=
|
||||
github.com/go-telegram/bot v1.2.1/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
|
||||
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
|
||||
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||
github.com/gofiber/template/html/v2 v2.1.1 h1:QEy3O3EBkvwDthy5bXVGUseOyO6ldJoiDxlF4+MJiV8=
|
||||
github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0=
|
||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
|
||||
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
|
||||
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/metoro-io/mcp-golang v0.8.0 h1:DkigHa3w7WwMFomcEz5wiMDX94DsvVm/3mCV3d1obnc=
|
||||
github.com/metoro-io/mcp-golang v0.8.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
|
||||
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mudler/cogito v0.4.2 h1:1PypFvUq3A+iBxMCR0v+sGXlhOsRnvyFZhEE93tsuDU=
|
||||
github.com/mudler/cogito v0.4.2/go.mod h1:2uhEElCTq8eXSsqJ1JF01oA5h9niXSELVKqCF1PqjEw=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
|
||||
github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
|
||||
github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philippgille/chromem-go v0.5.0 h1:bryX0F3N6jnN/21iBd8i2/k9EzPTZn3nyiqAti19si8=
|
||||
github.com/philippgille/chromem-go v0.5.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sashabaranov/go-openai v1.18.3 h1:dspFGkmZbhjg1059KhqLYSV2GaCiRIn+bOu50TlXUq8=
|
||||
github.com/sashabaranov/go-openai v1.18.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
||||
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
|
||||
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -210,141 +247,152 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tmc/langchaingo v0.1.8 h1:nrImgh0aWdu3stJTHz80N60WGwPWY8HXCK10gQny7bA=
|
||||
github.com/tmc/langchaingo v0.1.8/go.mod h1:iNBfS9e6jxBKsJSPWnlqNhoVWgdA3D1g5cdFJjbIZNQ=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc=
|
||||
github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378=
|
||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
|
||||
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
||||
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
|
||||
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
|
||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
|
||||
var baseModel = os.Getenv("LOCALAGI_MODEL")
|
||||
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
|
||||
var transcriptionModel = os.Getenv("LOCALAGI_TRANSCRIPTION_MODEL")
|
||||
var transcriptionLanguage = os.Getenv("LOCALAGI_TRANSCRIPTION_LANGUAGE")
|
||||
var ttsModel = os.Getenv("LOCALAGI_TTS_MODEL")
|
||||
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
|
||||
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
|
||||
var timeout = os.Getenv("LOCALAGI_TIMEOUT")
|
||||
@@ -22,13 +25,16 @@ var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
|
||||
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
|
||||
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
|
||||
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
||||
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
|
||||
var customActionsDir = os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR")
|
||||
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
|
||||
|
||||
func init() {
|
||||
if baseModel == "" {
|
||||
panic("LOCALAGI_MODEL not set")
|
||||
}
|
||||
if apiURL == "" {
|
||||
panic("LOCALAGI_API_URL not set")
|
||||
panic("LOCALAGI_LLM_API_URL not set")
|
||||
}
|
||||
if timeout == "" {
|
||||
timeout = "5m"
|
||||
@@ -56,14 +62,27 @@ func main() {
|
||||
pool, err := state.NewAgentPool(
|
||||
baseModel,
|
||||
multimodalModel,
|
||||
transcriptionModel,
|
||||
transcriptionLanguage,
|
||||
ttsModel,
|
||||
imageModel,
|
||||
apiURL,
|
||||
apiKey,
|
||||
stateDir,
|
||||
localRAG,
|
||||
services.Actions,
|
||||
services.Actions(map[string]string{
|
||||
services.ActionConfigBrowserAgentRunner: localOperatorBaseURL,
|
||||
services.ActionConfigDeepResearchRunner: localOperatorBaseURL,
|
||||
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts,
|
||||
services.DynamicPrompts(map[string]string{
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Filters,
|
||||
timeout,
|
||||
withLogs,
|
||||
)
|
||||
@@ -79,6 +98,7 @@ func main() {
|
||||
webui.WithLLMAPIUrl(apiURL),
|
||||
webui.WithLLMAPIKey(apiKey),
|
||||
webui.WithLLMModel(baseModel),
|
||||
webui.WithCustomActionsDir(customActionsDir),
|
||||
webui.WithStateDir(stateDir),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
|
||||
// AgentConfig represents the configuration for an agent
|
||||
type AgentConfig struct {
|
||||
Name string `json:"name"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Connectors []string `json:"connectors,omitempty"`
|
||||
PromptBlocks []string `json:"prompt_blocks,omitempty"`
|
||||
InitialPrompt string `json:"initial_prompt,omitempty"`
|
||||
Parallel bool `json:"parallel,omitempty"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
Connectors []string `json:"connectors,omitempty"`
|
||||
PromptBlocks []string `json:"prompt_blocks,omitempty"`
|
||||
InitialPrompt string `json:"initial_prompt,omitempty"`
|
||||
Parallel bool `json:"parallel,omitempty"`
|
||||
EnableReasoning bool `json:"enable_reasoning,omitempty"`
|
||||
}
|
||||
|
||||
// AgentStatus represents the status of an agent
|
||||
|
||||
+94
-6
@@ -4,18 +4,58 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// UserLocation represents the user's location for web search
|
||||
type UserLocation struct {
|
||||
Type string `json:"type"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// Function tool fields (used when type == "function")
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Parameters *jsonschema.Definition `json:"parameters,omitempty"`
|
||||
|
||||
// Web search tool fields (used when type == "web_search_preview" etc.)
|
||||
SearchContextSize *string `json:"search_context_size,omitempty"`
|
||||
UserLocation *UserLocation `json:"user_location,omitempty"`
|
||||
}
|
||||
|
||||
type ToolChoice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// RequestBody represents the message request to the AI model
|
||||
type RequestBody struct {
|
||||
Model string `json:"model"`
|
||||
Input any `json:"input"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice *ToolChoice `json:"tool_choice"`
|
||||
MaxTokens *int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type InputFunctionToolCallOutput struct {
|
||||
CallID string `json:"call_id"`
|
||||
Output string `json:"output"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// InputMessage represents a user input message
|
||||
type InputMessage struct {
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
@@ -29,10 +69,49 @@ type ContentItem struct {
|
||||
|
||||
// ResponseBody represents the response from the AI model
|
||||
type ResponseBody struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
Error any `json:"error,omitempty"`
|
||||
Output []ResponseMessage `json:"output"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
Error any `json:"error,omitempty"`
|
||||
Output []ResponseBase `json:"output"`
|
||||
Tools []Tool `json:"tools"`
|
||||
}
|
||||
|
||||
type ResponseType string
|
||||
|
||||
const (
|
||||
ResponseTypeFunctionToolCall ResponseType = "function_call"
|
||||
ResponseTypeMessage ResponseType = "message"
|
||||
)
|
||||
|
||||
type ResponseBase json.RawMessage
|
||||
|
||||
func (r *ResponseBase) UnmarshalJSON(data []byte) error {
|
||||
return (*json.RawMessage)(r).UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
func (r *ResponseBase) ToMessage() (msg ResponseMessage, err error) {
|
||||
err = json.Unmarshal(*r, &msg)
|
||||
if msg.Type != string(ResponseTypeMessage) {
|
||||
return ResponseMessage{}, fmt.Errorf("Expected %s, not %s", ResponseTypeMessage, msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ResponseBase) ToFunctionToolCall() (msg ResponseFunctionToolCall, err error) {
|
||||
err = json.Unmarshal(*r, &msg)
|
||||
if msg.Type != string(ResponseTypeFunctionToolCall) {
|
||||
return ResponseFunctionToolCall{}, fmt.Errorf("Expected %s, not %s", ResponseTypeFunctionToolCall, msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ResponseFunctionToolCall struct {
|
||||
Arguments string `json:"arguments"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ResponseMessage represents a message in the response
|
||||
@@ -85,7 +164,11 @@ func (c *Client) SimpleAIResponse(agentName, input string) (string, error) {
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, msg := range response.Output {
|
||||
for _, out := range response.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("out.ToMessage: %w", err)
|
||||
}
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
@@ -113,7 +196,12 @@ func (c *Client) ChatAIResponse(agentName string, messages []InputMessage) (stri
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, msg := range response.Output {
|
||||
for _, out := range response.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("out.ToMessage: %w", err)
|
||||
}
|
||||
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
|
||||
+13
-9
@@ -10,21 +10,25 @@ import (
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
|
||||
func GenerateTypedJSONWithGuidance(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
|
||||
return GenerateTypedJSONWithConversation(ctx, client, []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: guidance,
|
||||
},
|
||||
}, model, i, dst)
|
||||
}
|
||||
|
||||
func GenerateTypedJSONWithConversation(ctx context.Context, client *openai.Client, conv []openai.ChatCompletionMessage, model string, i jsonschema.Definition, dst any) error {
|
||||
toolName := "json"
|
||||
decision := openai.ChatCompletionRequest{
|
||||
Model: model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: guidance,
|
||||
},
|
||||
},
|
||||
Model: model,
|
||||
Messages: conv,
|
||||
Tools: []openai.Tool{
|
||||
{
|
||||
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: openai.FunctionDefinition{
|
||||
Function: &openai.FunctionDefinition{
|
||||
Name: toolName,
|
||||
Parameters: i,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package localoperator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string, timeout ...time.Duration) *Client {
|
||||
defaultTimeout := 30 * time.Second
|
||||
if len(timeout) > 0 {
|
||||
defaultTimeout = timeout[0]
|
||||
}
|
||||
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type AgentRequest struct {
|
||||
Goal string `json:"goal"`
|
||||
MaxAttempts int `json:"max_attempts,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
}
|
||||
|
||||
type DesktopAgentRequest struct {
|
||||
AgentRequest
|
||||
DesktopURL string `json:"desktop_url"`
|
||||
}
|
||||
|
||||
type DeepResearchRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
MaxCycles int `json:"max_cycles,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
MaxResults int `json:"max_results,omitempty"`
|
||||
}
|
||||
|
||||
// Response types
|
||||
type StateDescription struct {
|
||||
CurrentURL string `json:"current_url"`
|
||||
PageTitle string `json:"page_title"`
|
||||
PageContentDescription string `json:"page_content_description"`
|
||||
Screenshot string `json:"screenshot"`
|
||||
ScreenshotMimeType string `json:"screenshot_mime_type"`
|
||||
}
|
||||
|
||||
type StateHistory struct {
|
||||
States []StateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type DesktopStateDescription struct {
|
||||
ScreenContent string `json:"screen_content"`
|
||||
ScreenshotPath string `json:"screenshot_path"`
|
||||
}
|
||||
|
||||
type DesktopStateHistory struct {
|
||||
States []DesktopStateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ResearchResult struct {
|
||||
Topic string `json:"topic"`
|
||||
Summary string `json:"summary"`
|
||||
Sources []SearchResult `json:"sources"`
|
||||
KnowledgeGaps []string `json:"knowledge_gaps"`
|
||||
SearchQueries []string `json:"search_queries"`
|
||||
ResearchCycles int `json:"research_cycles"`
|
||||
CompletionTime time.Duration `json:"completion_time"`
|
||||
}
|
||||
|
||||
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
||||
return post[*StateHistory](c.httpClient, c.baseURL+"/api/browser/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDesktopAgent(req DesktopAgentRequest) (*DesktopStateHistory, error) {
|
||||
return post[*DesktopStateHistory](c.httpClient, c.baseURL+"/api/desktop/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDeepResearch(req DeepResearchRequest) (*ResearchResult, error) {
|
||||
return post[*ResearchResult](c.httpClient, c.baseURL+"/api/deep-research/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) Readyz() (string, error) {
|
||||
return c.get("/readyz")
|
||||
}
|
||||
|
||||
func (c *Client) Healthz() (string, error) {
|
||||
return c.get("/healthz")
|
||||
}
|
||||
|
||||
func (c *Client) get(path string) (string, error) {
|
||||
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return resp.Status, nil
|
||||
}
|
||||
|
||||
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
|
||||
var result T
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Sending request", "url", url, "body", string(jsonBody))
|
||||
|
||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Response", "status", resp.StatusCode)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return result, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return result, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ptr
|
||||
|
||||
func To[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
+396
-132
@@ -4,6 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
@@ -18,8 +22,11 @@ const (
|
||||
// Actions
|
||||
ActionSearch = "search"
|
||||
ActionCustom = "custom"
|
||||
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||
ActionDeepResearchRunner = "deep-research-runner"
|
||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||
ActionGithubIssueOpener = "github-issue-opener"
|
||||
ActionGithubIssueEditor = "github-issue-editor"
|
||||
ActionGithubIssueCloser = "github-issue-closer"
|
||||
ActionGithubIssueSearcher = "github-issue-searcher"
|
||||
ActionGithubRepositoryGet = "github-repository-get-content"
|
||||
@@ -28,7 +35,12 @@ const (
|
||||
ActionGithubIssueCommenter = "github-issue-commenter"
|
||||
ActionGithubPRReader = "github-pr-reader"
|
||||
ActionGithubPRCommenter = "github-pr-commenter"
|
||||
ActionGithubPRReviewer = "github-pr-reviewer"
|
||||
ActionGithubPRCreator = "github-pr-creator"
|
||||
ActionGithubGetAllContent = "github-get-all-repository-content"
|
||||
ActionGithubREADME = "github-readme"
|
||||
ActionGithubRepositorySearchFiles = "github-repository-search-files"
|
||||
ActionGithubRepositoryListFiles = "github-repository-list-files"
|
||||
ActionScraper = "scraper"
|
||||
ActionWikipedia = "wikipedia"
|
||||
ActionBrowse = "browse"
|
||||
@@ -38,6 +50,21 @@ const (
|
||||
ActionCounter = "counter"
|
||||
ActionCallAgents = "call_agents"
|
||||
ActionShellcommand = "shell-command"
|
||||
ActionSendTelegramMessage = "send-telegram-message"
|
||||
ActionSetReminder = "set_reminder"
|
||||
ActionListReminders = "list_reminders"
|
||||
ActionRemoveReminder = "remove_reminder"
|
||||
ActionAddToMemory = "add_to_memory"
|
||||
ActionListMemory = "list_memory"
|
||||
ActionRemoveFromMemory = "remove_from_memory"
|
||||
ActionPiKVMPowerControl = "pikvm_power_control"
|
||||
ActionWebhook = "webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
nameField = "name"
|
||||
descriptionField = "description"
|
||||
configurationField = "configuration"
|
||||
)
|
||||
|
||||
var AvailableActions = []string{
|
||||
@@ -45,14 +72,22 @@ var AvailableActions = []string{
|
||||
ActionCustom,
|
||||
ActionGithubIssueLabeler,
|
||||
ActionGithubIssueOpener,
|
||||
ActionGithubIssueEditor,
|
||||
ActionGithubIssueCloser,
|
||||
ActionGithubIssueSearcher,
|
||||
ActionGithubRepositoryGet,
|
||||
ActionGithubGetAllContent,
|
||||
ActionGithubRepositorySearchFiles,
|
||||
ActionGithubRepositoryListFiles,
|
||||
ActionBrowserAgentRunner,
|
||||
ActionDeepResearchRunner,
|
||||
ActionGithubRepositoryCreateOrUpdate,
|
||||
ActionGithubIssueReader,
|
||||
ActionGithubIssueCommenter,
|
||||
ActionGithubPRReader,
|
||||
ActionGithubPRCommenter,
|
||||
ActionGithubPRReviewer,
|
||||
ActionGithubPRCreator,
|
||||
ActionGithubREADME,
|
||||
ActionScraper,
|
||||
ActionBrowse,
|
||||
@@ -63,36 +98,310 @@ var AvailableActions = []string{
|
||||
ActionCounter,
|
||||
ActionCallAgents,
|
||||
ActionShellcommand,
|
||||
ActionSendTelegramMessage,
|
||||
ActionSetReminder,
|
||||
ActionListReminders,
|
||||
ActionRemoveReminder,
|
||||
ActionAddToMemory,
|
||||
ActionListMemory,
|
||||
ActionRemoveFromMemory,
|
||||
ActionPiKVMPowerControl,
|
||||
ActionWebhook,
|
||||
}
|
||||
|
||||
func Actions(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
allActions := []types.Action{}
|
||||
var DefaultActions = []config.FieldGroup{
|
||||
{
|
||||
Name: "search",
|
||||
Label: "Search",
|
||||
Fields: actions.SearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "browser-agent-runner",
|
||||
Label: "Browser Agent Runner",
|
||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "deep-research-runner",
|
||||
Label: "Deep Research Runner",
|
||||
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "add_to_memory",
|
||||
Label: "Add to Memory",
|
||||
Fields: actions.AddToMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "list_memory",
|
||||
Label: "List Memory",
|
||||
Fields: actions.ListMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "remove_from_memory",
|
||||
Label: "Remove from Memory",
|
||||
Fields: actions.RemoveFromMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
Fields: actions.GithubIssueLabelerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-opener",
|
||||
Label: "GitHub Issue Opener",
|
||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-editor",
|
||||
Label: "GitHub Issue Editor",
|
||||
Fields: actions.GithubIssueEditorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-closer",
|
||||
Label: "GitHub Issue Closer",
|
||||
Fields: actions.GithubIssueCloserConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-commenter",
|
||||
Label: "GitHub Issue Commenter",
|
||||
Fields: actions.GithubIssueCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-reader",
|
||||
Label: "GitHub Issue Reader",
|
||||
Fields: actions.GithubIssueReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-searcher",
|
||||
Label: "GitHub Issue Search",
|
||||
Fields: actions.GithubIssueSearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-get-content",
|
||||
Label: "GitHub Repository Get Content",
|
||||
Fields: actions.GithubRepositoryGetContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-get-all-repository-content",
|
||||
Label: "GitHub Get All Repository Content",
|
||||
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-search-files",
|
||||
Label: "GitHub Repository Search Files",
|
||||
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-list-files",
|
||||
Label: "GitHub Repository List Files",
|
||||
Fields: actions.GithubRepositoryListFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-create-or-update-content",
|
||||
Label: "GitHub Repository Create/Update Content",
|
||||
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-readme",
|
||||
Label: "GitHub Repository README",
|
||||
Fields: actions.GithubRepositoryREADMEConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reader",
|
||||
Label: "GitHub PR Reader",
|
||||
Fields: actions.GithubPRReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-commenter",
|
||||
Label: "GitHub PR Commenter",
|
||||
Fields: actions.GithubPRCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reviewer",
|
||||
Label: "GitHub PR Reviewer",
|
||||
Fields: actions.GithubPRReviewerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-creator",
|
||||
Label: "GitHub PR Creator",
|
||||
Fields: actions.GithubPRCreatorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "twitter-post",
|
||||
Label: "Twitter Post",
|
||||
Fields: actions.TwitterPostConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-mail",
|
||||
Label: "Send Mail",
|
||||
Fields: actions.SendMailConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "shell-command",
|
||||
Label: "Shell Command",
|
||||
Fields: actions.ShellConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "custom",
|
||||
Label: "Custom",
|
||||
Fields: action.CustomConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "scraper",
|
||||
Label: "Scraper",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "wikipedia",
|
||||
Label: "Wikipedia",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "browse",
|
||||
Label: "Browse",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "counter",
|
||||
Label: "Counter",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "call_agents",
|
||||
Label: "Call Agents",
|
||||
Fields: actions.CallAgentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-telegram-message",
|
||||
Label: "Send Telegram Message",
|
||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "set_reminder",
|
||||
Label: "Set Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "list_reminders",
|
||||
Label: "List Reminders",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "remove_reminder",
|
||||
Label: "Remove Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "pikvm_power_control",
|
||||
Label: "PiKVM Power Control",
|
||||
Fields: actions.PiKVMConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "webhook",
|
||||
Label: "Webhook",
|
||||
Fields: actions.WebhookConfigMeta(),
|
||||
},
|
||||
}
|
||||
|
||||
agentName := a.Name
|
||||
|
||||
for _, a := range a.Actions {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
||||
xlog.Error("Error unmarshalling action config", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
a, err := Action(a.Name, agentName, config, pool)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
const (
|
||||
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
|
||||
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
|
||||
ActionConfigSSHBoxURL = "sshbox-url"
|
||||
ConfigStateDir = "state-dir"
|
||||
CustomActionsDir = "custom-actions-dir"
|
||||
)
|
||||
|
||||
func customActions(customActionsDir string, existingActionConfigs map[string]map[string]string) (allActions []types.Action) {
|
||||
files, err := os.ReadDir(customActionsDir)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom actions directory", "error", err)
|
||||
return allActions
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".go" {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(customActionsDir, file.Name()))
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
actionName := strings.TrimSuffix(file.Name(), ".go")
|
||||
|
||||
actionConfig := map[string]string{
|
||||
"name": actionName,
|
||||
"description": "",
|
||||
"code": string(content),
|
||||
"unsafe": "false",
|
||||
}
|
||||
|
||||
if c, exists := existingActionConfigs[actionName]; exists {
|
||||
// We allow the user to customize name and description
|
||||
actionConfig[descriptionField] = c[descriptionField]
|
||||
actionConfig[nameField] = c[nameField]
|
||||
actionConfig[configurationField] = c[configurationField]
|
||||
}
|
||||
a, err := Action(ActionCustom, "", actionConfig, nil, map[string]string{})
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom action", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Action(name, agentName string, config map[string]string, pool *state.AgentPool) (types.Action, error) {
|
||||
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
allActions := []types.Action{}
|
||||
|
||||
agentName := a.Name
|
||||
|
||||
existingActionConfigs := map[string]map[string]string{}
|
||||
for _, a := range a.Actions {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
||||
xlog.Error("Error unmarshalling action config", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingActionConfigs[a.Name] = config
|
||||
|
||||
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
|
||||
// Now we will scan a directory for custom actions
|
||||
if actionsConfigs[CustomActionsDir] != "" {
|
||||
allActions = append(allActions, customActions(actionsConfigs[CustomActionsDir], existingActionConfigs)...)
|
||||
}
|
||||
|
||||
return allActions
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
|
||||
var a types.Action
|
||||
var err error
|
||||
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
memoryFilePath := memoryPath(agentName, actionsConfigs)
|
||||
|
||||
switch name {
|
||||
case ActionCustom:
|
||||
a, err = action.NewCustom(config, "")
|
||||
@@ -104,16 +413,32 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = actions.NewGithubIssueLabeler(config)
|
||||
case ActionGithubIssueOpener:
|
||||
a = actions.NewGithubIssueOpener(config)
|
||||
case ActionGithubIssueEditor:
|
||||
a = actions.NewGithubIssueEditor(config)
|
||||
case ActionGithubIssueCloser:
|
||||
a = actions.NewGithubIssueCloser(config)
|
||||
case ActionGithubIssueSearcher:
|
||||
a = actions.NewGithubIssueSearch(config)
|
||||
case ActionBrowserAgentRunner:
|
||||
a = actions.NewBrowserAgentRunner(config, actionsConfigs[ActionConfigBrowserAgentRunner])
|
||||
case ActionDeepResearchRunner:
|
||||
a = actions.NewDeepResearchRunner(config, actionsConfigs[ActionConfigDeepResearchRunner])
|
||||
case ActionGithubIssueReader:
|
||||
a = actions.NewGithubIssueReader(config)
|
||||
case ActionGithubPRReader:
|
||||
a = actions.NewGithubPRReader(config)
|
||||
case ActionGithubPRCommenter:
|
||||
a = actions.NewGithubPRCommenter(config)
|
||||
case ActionGithubPRReviewer:
|
||||
a = actions.NewGithubPRReviewer(config)
|
||||
case ActionGithubPRCreator:
|
||||
a = actions.NewGithubPRCreator(config)
|
||||
case ActionGithubGetAllContent:
|
||||
a = actions.NewGithubRepositoryGetAllContent(config)
|
||||
case ActionGithubRepositorySearchFiles:
|
||||
a = actions.NewGithubRepositorySearchFiles(config)
|
||||
case ActionGithubRepositoryListFiles:
|
||||
a = actions.NewGithubRepositoryListFiles(config)
|
||||
case ActionGithubIssueCommenter:
|
||||
a = actions.NewGithubIssueCommenter(config)
|
||||
case ActionGithubRepositoryGet:
|
||||
@@ -130,6 +455,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = actions.NewBrowse(config)
|
||||
case ActionSendMail:
|
||||
a = actions.NewSendMail(config)
|
||||
case ActionWebhook:
|
||||
a = actions.NewWebhook(config)
|
||||
case ActionTwitterPost:
|
||||
a = actions.NewPostTweet(config)
|
||||
case ActionCounter:
|
||||
@@ -137,7 +464,23 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
case ActionCallAgents:
|
||||
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
|
||||
case ActionShellcommand:
|
||||
a = actions.NewShell(config)
|
||||
a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL])
|
||||
case ActionSendTelegramMessage:
|
||||
a = actions.NewSendTelegramMessageRunner(config)
|
||||
case ActionSetReminder:
|
||||
a = action.NewReminder()
|
||||
case ActionListReminders:
|
||||
a = action.NewListReminders()
|
||||
case ActionRemoveReminder:
|
||||
a = action.NewRemoveReminder()
|
||||
case ActionAddToMemory:
|
||||
a, _, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionListMemory:
|
||||
_, a, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionRemoveFromMemory:
|
||||
_, _, a = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionPiKVMPowerControl:
|
||||
a = actions.NewPiKVMAction(config)
|
||||
default:
|
||||
xlog.Error("Action not found", "name", name)
|
||||
return nil, fmt.Errorf("Action not found")
|
||||
@@ -150,117 +493,38 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func ActionsConfigMeta() []config.FieldGroup {
|
||||
return []config.FieldGroup{
|
||||
{
|
||||
Name: "search",
|
||||
Label: "Search",
|
||||
Fields: actions.SearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
Fields: actions.GithubIssueLabelerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-opener",
|
||||
Label: "GitHub Issue Opener",
|
||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-closer",
|
||||
Label: "GitHub Issue Closer",
|
||||
Fields: actions.GithubIssueCloserConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-commenter",
|
||||
Label: "GitHub Issue Commenter",
|
||||
Fields: actions.GithubIssueCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-reader",
|
||||
Label: "GitHub Issue Reader",
|
||||
Fields: actions.GithubIssueReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-searcher",
|
||||
Label: "GitHub Issue Search",
|
||||
Fields: actions.GithubIssueSearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-get-content",
|
||||
Label: "GitHub Repository Get Content",
|
||||
Fields: actions.GithubRepositoryGetContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-create-or-update-content",
|
||||
Label: "GitHub Repository Create/Update Content",
|
||||
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-readme",
|
||||
Label: "GitHub Repository README",
|
||||
Fields: actions.GithubRepositoryREADMEConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reader",
|
||||
Label: "GitHub PR Reader",
|
||||
Fields: actions.GithubPRReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-commenter",
|
||||
Label: "GitHub PR Commenter",
|
||||
Fields: actions.GithubPRCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "twitter-post",
|
||||
Label: "Twitter Post",
|
||||
Fields: actions.TwitterPostConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-mail",
|
||||
Label: "Send Mail",
|
||||
Fields: actions.SendMailConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "shell-command",
|
||||
Label: "Shell Command",
|
||||
Fields: actions.ShellConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "custom",
|
||||
Label: "Custom",
|
||||
Fields: action.CustomConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "scraper",
|
||||
Label: "Scraper",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "wikipedia",
|
||||
Label: "Wikipedia",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "browse",
|
||||
Label: "Browse",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "counter",
|
||||
Label: "Counter",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "call_agents",
|
||||
Label: "Call Agents",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
func ActionsConfigMeta(customActionDir string) []config.FieldGroup {
|
||||
all := slices.Clone(DefaultActions)
|
||||
|
||||
if customActionDir != "" {
|
||||
actions := customActions(customActionDir, map[string]map[string]string{})
|
||||
|
||||
for _, a := range actions {
|
||||
all = append(all, config.FieldGroup{
|
||||
Name: a.Definition().Name.String(),
|
||||
Label: a.Definition().Name.String(),
|
||||
Fields: []config.Field{
|
||||
{
|
||||
Name: nameField,
|
||||
Label: "Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Name of the custom action",
|
||||
},
|
||||
{
|
||||
Name: descriptionField,
|
||||
Label: "Description",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Description of the custom action",
|
||||
},
|
||||
{
|
||||
Name: configurationField,
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration of the custom action",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
|
||||
|
||||
type BrowseAction struct{}
|
||||
|
||||
func (a *BrowseAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *BrowseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
URL string `json:"url"`
|
||||
}{}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataBrowserAgentHistory = "browser_agent_history"
|
||||
)
|
||||
|
||||
type BrowserAgentRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewBrowserAgentRunner(config map[string]string, defaultURL string) *BrowserAgentRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
timeout := "15m"
|
||||
if config["timeout"] != "" {
|
||||
timeout = config["timeout"]
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
// If parsing fails, use default 15 minutes
|
||||
duration = 15 * time.Minute
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], duration)
|
||||
|
||||
return &BrowserAgentRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowserAgentRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.AgentRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.AgentRequest{
|
||||
Goal: result.Goal,
|
||||
MaxAttempts: result.MaxAttempts,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
}
|
||||
|
||||
stateHistory, err := b.client.RunBrowserAgent(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run browser agent: %w", err)
|
||||
}
|
||||
|
||||
// Format the state history into a readable string
|
||||
var historyStr string
|
||||
// for i, state := range stateHistory.States {
|
||||
// historyStr += fmt.Sprintf("State %d:\n", i+1)
|
||||
// historyStr += fmt.Sprintf(" URL: %s\n", state.CurrentURL)
|
||||
// historyStr += fmt.Sprintf(" Title: %s\n", state.PageTitle)
|
||||
// historyStr += fmt.Sprintf(" Description: %s\n\n", state.PageContentDescription)
|
||||
// }
|
||||
|
||||
historyStr += fmt.Sprintf(" URL: %s\n", stateHistory.States[len(stateHistory.States)-1].CurrentURL)
|
||||
historyStr += fmt.Sprintf(" Title: %s\n", stateHistory.States[len(stateHistory.States)-1].PageTitle)
|
||||
historyStr += fmt.Sprintf(" Description: %s\n\n", stateHistory.States[len(stateHistory.States)-1].PageContentDescription)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Browser agent completed successfully. History:\n%s", historyStr),
|
||||
Metadata: map[string]interface{}{MetadataBrowserAgentHistory: stateHistory},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BrowserAgentRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_browser_agent"
|
||||
if b.customActionName != "" {
|
||||
actionName = b.customActionName
|
||||
}
|
||||
description := "Run a browser agent to achieve a specific goal, for example: 'Go to https://www.google.com and search for 'LocalAI', and tell me what's on the first page'"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"goal": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The goal for the browser agent to achieve",
|
||||
},
|
||||
"max_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts the agent can make (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"goal"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BrowserAgentRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BrowserAgentRunnerConfigMeta returns the metadata for Browser Agent Runner action configuration fields
|
||||
func BrowserAgentRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "timeout",
|
||||
Label: "Client Timeout",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3,26 +3,56 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func trimList(list []string) []string {
|
||||
for i, v := range list {
|
||||
list[i] = strings.TrimSpace(v)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction {
|
||||
whitelist := []string{}
|
||||
blacklist := []string{}
|
||||
if v, ok := config["whitelist"]; ok {
|
||||
if strings.Contains(v, ",") {
|
||||
whitelist = trimList(strings.Split(v, ","))
|
||||
} else {
|
||||
whitelist = []string{v}
|
||||
}
|
||||
}
|
||||
if v, ok := config["blacklist"]; ok {
|
||||
if strings.Contains(v, ",") {
|
||||
blacklist = trimList(strings.Split(v, ","))
|
||||
} else {
|
||||
blacklist = []string{v}
|
||||
}
|
||||
}
|
||||
return &CallAgentAction{
|
||||
pool: pool,
|
||||
myName: agentName,
|
||||
pool: pool,
|
||||
myName: agentName,
|
||||
whitelist: whitelist,
|
||||
blacklist: blacklist,
|
||||
}
|
||||
}
|
||||
|
||||
type CallAgentAction struct {
|
||||
pool *state.AgentPoolInternalAPI
|
||||
myName string
|
||||
pool *state.AgentPoolInternalAPI
|
||||
myName string
|
||||
whitelist []string
|
||||
blacklist []string
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *CallAgentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
AgentName string `json:"agent_name"`
|
||||
Message string `json:"message"`
|
||||
@@ -83,13 +113,32 @@ func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (t
|
||||
return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) isAllowedToBeCalled(agentName string) bool {
|
||||
if agentName == a.myName {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a.whitelist) > 0 && len(a.blacklist) > 0 {
|
||||
return slices.Contains(a.whitelist, agentName) && !slices.Contains(a.blacklist, agentName)
|
||||
}
|
||||
|
||||
if len(a.whitelist) > 0 {
|
||||
return slices.Contains(a.whitelist, agentName)
|
||||
}
|
||||
|
||||
if len(a.blacklist) > 0 {
|
||||
return !slices.Contains(a.blacklist, agentName)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *CallAgentAction) Definition() types.ActionDefinition {
|
||||
allAgents := a.pool.AllAgents()
|
||||
|
||||
agents := []string{}
|
||||
|
||||
for _, ag := range allAgents {
|
||||
if ag != a.myName {
|
||||
if a.isAllowedToBeCalled(ag) {
|
||||
agents = append(agents, ag)
|
||||
}
|
||||
}
|
||||
@@ -125,3 +174,21 @@ func (a *CallAgentAction) Definition() types.ActionDefinition {
|
||||
func (a *CallAgentAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func CallAgentConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "whitelist",
|
||||
Label: "Whitelist",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Comma-separated list of agent names to call. If not specified, all agents are allowed.",
|
||||
},
|
||||
{
|
||||
Name: "blacklist",
|
||||
Label: "Blacklist",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Comma-separated list of agent names to exclude from the call. If not specified, all agents are allowed.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func NewCounter(config map[string]string) *CounterAction {
|
||||
}
|
||||
|
||||
// Run executes the counter action
|
||||
func (a *CounterAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *CounterAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
// Parse parameters
|
||||
request := struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataDeepResearchResult = "deep_research_result"
|
||||
)
|
||||
|
||||
type DeepResearchRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
timeout := "15m"
|
||||
if config["timeout"] != "" {
|
||||
timeout = config["timeout"]
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(timeout)
|
||||
if err != nil {
|
||||
// If parsing fails, use default 15 minutes
|
||||
duration = 15 * time.Minute
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], duration)
|
||||
|
||||
return &DeepResearchRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.DeepResearchRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.DeepResearchRequest{
|
||||
Topic: result.Topic,
|
||||
MaxCycles: result.MaxCycles,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
MaxResults: result.MaxResults,
|
||||
}
|
||||
|
||||
researchResult, err := d.client.RunDeepResearch(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
|
||||
}
|
||||
|
||||
// Format the research result into a readable string
|
||||
var resultStr string
|
||||
|
||||
resultStr += "Deep research result\n"
|
||||
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
|
||||
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
|
||||
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
|
||||
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
|
||||
|
||||
if len(researchResult.Sources) > 0 {
|
||||
resultStr += "Sources:\n"
|
||||
for _, source := range researchResult.Sources {
|
||||
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
|
||||
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_deep_research"
|
||||
if d.customActionName != "" {
|
||||
actionName = d.customActionName
|
||||
}
|
||||
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"topic": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The topic to research",
|
||||
},
|
||||
"max_cycles": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of research cycles to perform (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
"max_results": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of results to collect (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"topic"},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
|
||||
func DeepResearchRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "timeout",
|
||||
Label: "Client Timeout",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ type GenImageAction struct {
|
||||
imageModel string
|
||||
}
|
||||
|
||||
func (a *GenImageAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *GenImageAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Size string `json:"size"`
|
||||
|
||||
@@ -42,7 +42,7 @@ var _ = Describe("GenImageAction", func() {
|
||||
"size": "256x256",
|
||||
}
|
||||
|
||||
url, err := action.Run(ctx, params)
|
||||
url, err := action.Run(ctx, nil, params)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(url).ToNot(BeEmpty())
|
||||
})
|
||||
@@ -52,7 +52,7 @@ var _ = Describe("GenImageAction", func() {
|
||||
"size": "256x256",
|
||||
}
|
||||
|
||||
_, err := action.Run(ctx, params)
|
||||
_, err := action.Run(ctx, nil, params)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ func NewGithubIssueCloser(config map[string]string) *GithubIssuesCloser {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesCloser) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubIssuesCloser) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewGithubIssueCommenter(config map[string]string) *GithubIssuesCommenter {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubIssuesCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubIssueEditor struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubIssueEditor(config map[string]string) *GithubIssueEditor {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubIssueEditor{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
customActionName: config["customActionName"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssueEditor) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Description string `json:"description"`
|
||||
Title string `json:"title"`
|
||||
IssueNumber int `json:"issue_number"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
_, _, err = g.client.Issues.Edit(ctx, result.Owner, result.Repository, result.IssueNumber,
|
||||
&github.IssueRequest{
|
||||
Body: &result.Description,
|
||||
Title: &result.Title,
|
||||
})
|
||||
resultString := fmt.Sprintf("Updated issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
resultString = fmt.Sprintf("Error updating issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
|
||||
}
|
||||
return types.ActionResult{Result: resultString}, err
|
||||
}
|
||||
|
||||
func (g *GithubIssueEditor) Definition() types.ActionDefinition {
|
||||
actionName := "edit_github_issue"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Edit the title and description of a Github issue in a repository. Use this action after reading the issue"
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to edit.",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new title for the issue.",
|
||||
},
|
||||
"description": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new description for the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "title", "description"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"issue_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the issue to edit.",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository containing the issue.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new title for the issue.",
|
||||
},
|
||||
"description": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The new description for the issue.",
|
||||
},
|
||||
},
|
||||
Required: []string{"issue_number", "repository", "owner", "title", "description"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubIssueEditor) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubIssueEditorConfigMeta returns the metadata for GitHub Issue Editor action configuration fields
|
||||
func GithubIssueEditorConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func NewGithubIssueLabeler(config map[string]string) *GithubIssuesLabeler {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesLabeler) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubIssuesLabeler) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewGithubIssueOpener(config map[string]string) *GithubIssuesOpener {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesOpener) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubIssuesOpener) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"text"`
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewGithubIssueReader(config map[string]string) *GithubIssuesReader {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssuesReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubIssuesReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
@@ -49,7 +49,8 @@ func (g *GithubIssuesReader) Run(ctx context.Context, params types.ActionParams)
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf(
|
||||
"Issue %d Repository: %s\nTitle: %s\nBody: %s",
|
||||
*issue.Number, *issue.Repository.FullName, *issue.Title, *issue.Body)}, nil
|
||||
issue.GetNumber(), issue.GetRepository().GetFullName(), issue.GetTitle(), issue.GetBody()),
|
||||
}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return types.ActionResult{Result: fmt.Sprintf("Error fetching issue: %s", err.Error())}, err
|
||||
|
||||
@@ -28,7 +28,7 @@ func NewGithubIssueSearch(config map[string]string) *GithubIssueSearch {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubIssueSearch) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubIssueSearch) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Query string `json:"query"`
|
||||
Repository string `json:"repository"`
|
||||
|
||||
@@ -3,11 +3,6 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
@@ -20,96 +15,6 @@ type GithubPRCommenter struct {
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
var (
|
||||
patchRegex = regexp.MustCompile(`^@@.*\d [\+\-](\d+),?(\d+)?.+?@@`)
|
||||
)
|
||||
|
||||
type commitFileInfo struct {
|
||||
FileName string
|
||||
hunkInfos []*hunkInfo
|
||||
sha string
|
||||
}
|
||||
|
||||
type hunkInfo struct {
|
||||
hunkStart int
|
||||
hunkEnd int
|
||||
}
|
||||
|
||||
func (hi hunkInfo) isLineInHunk(line int) bool {
|
||||
return line >= hi.hunkStart && line <= hi.hunkEnd
|
||||
}
|
||||
|
||||
func (cfi *commitFileInfo) getHunkInfo(line int) *hunkInfo {
|
||||
for _, hunkInfo := range cfi.hunkInfos {
|
||||
if hunkInfo.isLineInHunk(line) {
|
||||
return hunkInfo
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfi *commitFileInfo) isLineInChange(line int) bool {
|
||||
return cfi.getHunkInfo(line) != nil
|
||||
}
|
||||
|
||||
func (cfi commitFileInfo) calculatePosition(line int) *int {
|
||||
hi := cfi.getHunkInfo(line)
|
||||
if hi == nil {
|
||||
return nil
|
||||
}
|
||||
position := line - hi.hunkStart
|
||||
return &position
|
||||
}
|
||||
|
||||
func parseHunkPositions(patch, filename string) ([]*hunkInfo, error) {
|
||||
hunkInfos := make([]*hunkInfo, 0)
|
||||
if patch != "" {
|
||||
groups := patchRegex.FindAllStringSubmatch(patch, -1)
|
||||
if len(groups) < 1 {
|
||||
return hunkInfos, fmt.Errorf("the patch details for [%s] could not be resolved", filename)
|
||||
}
|
||||
for _, patchGroup := range groups {
|
||||
endPos := 2
|
||||
if len(patchGroup) > 2 && patchGroup[2] == "" {
|
||||
endPos = 1
|
||||
}
|
||||
|
||||
hunkStart, err := strconv.Atoi(patchGroup[1])
|
||||
if err != nil {
|
||||
hunkStart = -1
|
||||
}
|
||||
hunkEnd, err := strconv.Atoi(patchGroup[endPos])
|
||||
if err != nil {
|
||||
hunkEnd = -1
|
||||
}
|
||||
hunkInfos = append(hunkInfos, &hunkInfo{
|
||||
hunkStart: hunkStart,
|
||||
hunkEnd: hunkEnd,
|
||||
})
|
||||
}
|
||||
}
|
||||
return hunkInfos, nil
|
||||
}
|
||||
|
||||
func getCommitInfo(file *github.CommitFile) (*commitFileInfo, error) {
|
||||
patch := file.GetPatch()
|
||||
hunkInfos, err := parseHunkPositions(patch, *file.Filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sha := file.GetSHA()
|
||||
if sha == "" {
|
||||
return nil, fmt.Errorf("the sha details for [%s] could not be resolved", *file.Filename)
|
||||
}
|
||||
|
||||
return &commitFileInfo{
|
||||
FileName: *file.Filename,
|
||||
hunkInfos: hunkInfos,
|
||||
sha: sha,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
@@ -122,18 +27,12 @@ func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubPRCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubPRCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
PRNumber int `json:"pr_number"`
|
||||
GeneralComment string `json:"general_comment"`
|
||||
Comments []struct {
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
Comment string `json:"comment"`
|
||||
StartLine int `json:"start_line,omitempty"`
|
||||
} `json:"comments"`
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
PRNumber int `json:"pr_number"`
|
||||
Comment string `json:"comment"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
@@ -159,134 +58,31 @@ func (g *GithubPRCommenter) Run(ctx context.Context, params types.ActionParams)
|
||||
return types.ActionResult{Result: fmt.Sprintf("Pull request #%d is not open (current state: %s)", result.PRNumber, *pr.State)}, nil
|
||||
}
|
||||
|
||||
// Get the list of changed files to verify the files exist in the PR
|
||||
files, _, err := g.client.PullRequests.ListFiles(ctx, result.Owner, result.Repository, result.PRNumber, &github.ListOptions{})
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to list PR files: %w", err)
|
||||
if result.Comment == "" {
|
||||
return types.ActionResult{Result: "No comment provided"}, nil
|
||||
}
|
||||
|
||||
// Create a map of valid files with their commit info
|
||||
validFiles := make(map[string]*commitFileInfo)
|
||||
for _, file := range files {
|
||||
if *file.Status != "deleted" {
|
||||
info, err := getCommitInfo(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
validFiles[*file.Filename] = info
|
||||
}
|
||||
}
|
||||
// Try both PullRequests and Issues API for general comments
|
||||
var resp *github.Response
|
||||
|
||||
// Process each comment
|
||||
var results []string
|
||||
for _, comment := range result.Comments {
|
||||
// Check if file exists in PR
|
||||
fileInfo, exists := validFiles[comment.File]
|
||||
if !exists {
|
||||
availableFiles := make([]string, 0, len(validFiles))
|
||||
for f := range validFiles {
|
||||
availableFiles = append(availableFiles, f)
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Error: File %s not found in PR #%d. Available files: %v",
|
||||
comment.File, result.PRNumber, availableFiles))
|
||||
continue
|
||||
}
|
||||
// First try PullRequests API
|
||||
_, resp, err = g.client.PullRequests.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.PullRequestComment{
|
||||
Body: &result.Comment,
|
||||
})
|
||||
|
||||
// Check if line is in a changed hunk
|
||||
if !fileInfo.isLineInChange(comment.Line) {
|
||||
results = append(results, fmt.Sprintf("Error: Line %d is not in a changed hunk in file %s",
|
||||
comment.Line, comment.File))
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
position := fileInfo.calculatePosition(comment.Line)
|
||||
if position == nil {
|
||||
results = append(results, fmt.Sprintf("Error: Could not calculate position for line %d in file %s",
|
||||
comment.Line, comment.File))
|
||||
continue
|
||||
}
|
||||
|
||||
// Create the review comment
|
||||
reviewComment := &github.PullRequestComment{
|
||||
Path: &comment.File,
|
||||
Line: &comment.Line,
|
||||
Body: &comment.Comment,
|
||||
Position: position,
|
||||
CommitID: &fileInfo.sha,
|
||||
}
|
||||
|
||||
// Set start line if provided
|
||||
if comment.StartLine > 0 {
|
||||
reviewComment.StartLine = &comment.StartLine
|
||||
}
|
||||
|
||||
// Create the comment with retries
|
||||
var resp *github.Response
|
||||
for i := 0; i < 6; i++ {
|
||||
_, resp, err = g.client.PullRequests.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, reviewComment)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if resp != nil && resp.StatusCode == 422 {
|
||||
// Rate limit hit, wait and retry
|
||||
retrySeconds := i * i
|
||||
time.Sleep(time.Second * time.Duration(retrySeconds))
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errorDetails := fmt.Sprintf("Error commenting on file %s, line %d: %s", comment.File, comment.Line, err.Error())
|
||||
if resp != nil {
|
||||
errorDetails += fmt.Sprintf("\nResponse Status: %s", resp.Status)
|
||||
if resp.Body != nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
errorDetails += fmt.Sprintf("\nResponse Body: %s", string(body))
|
||||
}
|
||||
}
|
||||
results = append(results, errorDetails)
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, fmt.Sprintf("Successfully commented on file %s, line %d", comment.File, comment.Line))
|
||||
}
|
||||
|
||||
if result.GeneralComment != "" {
|
||||
// Try both PullRequests and Issues API for general comments
|
||||
var resp *github.Response
|
||||
var err error
|
||||
|
||||
// First try PullRequests API
|
||||
_, resp, err = g.client.PullRequests.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.PullRequestComment{
|
||||
Body: &result.GeneralComment,
|
||||
// If that fails with 403, try Issues API
|
||||
if err != nil && resp != nil && resp.StatusCode == 403 {
|
||||
_, resp, err = g.client.Issues.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.IssueComment{
|
||||
Body: &result.Comment,
|
||||
})
|
||||
}
|
||||
|
||||
// If that fails with 403, try Issues API
|
||||
if err != nil && resp != nil && resp.StatusCode == 403 {
|
||||
_, resp, err = g.client.Issues.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.IssueComment{
|
||||
Body: &result.GeneralComment,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errorDetails := fmt.Sprintf("Error adding general comment: %s", err.Error())
|
||||
if resp != nil {
|
||||
errorDetails += fmt.Sprintf("\nResponse Status: %s", resp.Status)
|
||||
if resp.Body != nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
errorDetails += fmt.Sprintf("\nResponse Body: %s", string(body))
|
||||
}
|
||||
}
|
||||
results = append(results, errorDetails)
|
||||
} else {
|
||||
results = append(results, "Successfully added general comment to pull request")
|
||||
}
|
||||
if err != nil {
|
||||
return types.ActionResult{Result: fmt.Sprintf("Error adding general comment: %s", err.Error())}, nil
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: strings.Join(results, "\n"),
|
||||
Result: "Successfully added general comment to pull request",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -305,38 +101,12 @@ func (g *GithubPRCommenter) Definition() types.ActionDefinition {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the pull request to comment on.",
|
||||
},
|
||||
"general_comment": {
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "A general comment to add to the pull request.",
|
||||
},
|
||||
"comments": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"file": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The file to comment on.",
|
||||
},
|
||||
"line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The line number to comment on.",
|
||||
},
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The comment text.",
|
||||
},
|
||||
"start_line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Optional start line for multi-line comments.",
|
||||
},
|
||||
},
|
||||
Required: []string{"file", "line", "comment"},
|
||||
},
|
||||
Description: "Array of comments to add to the pull request.",
|
||||
},
|
||||
},
|
||||
Required: []string{"pr_number", "comments"},
|
||||
Required: []string{"pr_number", "comment"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
@@ -355,38 +125,12 @@ func (g *GithubPRCommenter) Definition() types.ActionDefinition {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"general_comment": {
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "A general comment to add to the pull request.",
|
||||
},
|
||||
"comments": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"file": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The file to comment on.",
|
||||
},
|
||||
"line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The line number to comment on.",
|
||||
},
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The comment text.",
|
||||
},
|
||||
"start_line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Optional start line for multi-line comments.",
|
||||
},
|
||||
},
|
||||
Required: []string{"file", "line", "comment"},
|
||||
},
|
||||
Description: "Array of comments to add to the pull request.",
|
||||
},
|
||||
},
|
||||
Required: []string{"pr_number", "repository", "owner", "comments"},
|
||||
Required: []string{"pr_number", "repository", "owner", "comment"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
// forkCreationRetries is the number of times to retry checking if a fork is ready
|
||||
forkCreationRetries = 30
|
||||
// forkCreationRetryDelay is the duration to wait between fork creation checks
|
||||
forkCreationRetryDelay = 5 * time.Second
|
||||
)
|
||||
|
||||
type GithubPRCreator struct {
|
||||
token, repository, owner, customActionName, defaultBranch string
|
||||
useFork bool
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
defaultBranch := config["defaultBranch"]
|
||||
if defaultBranch == "" {
|
||||
defaultBranch = "main" // Default to "main" if not specified
|
||||
}
|
||||
|
||||
useFork := config["useFork"] == "true"
|
||||
|
||||
return &GithubPRCreator{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
customActionName: config["customActionName"],
|
||||
defaultBranch: defaultBranch,
|
||||
useFork: useFork,
|
||||
}
|
||||
}
|
||||
|
||||
// ensureFork ensures that a fork exists for the given repository
|
||||
func (g *GithubPRCreator) ensureFork(ctx context.Context, owner, repo string) (string, error) {
|
||||
// First check if we already have a fork
|
||||
user, _, err := g.client.Users.Get(ctx, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
forkOwner := user.GetLogin()
|
||||
|
||||
// Check if fork already exists
|
||||
_, _, err = g.client.Repositories.Get(ctx, forkOwner, repo)
|
||||
if err == nil {
|
||||
// Fork already exists
|
||||
return forkOwner, nil
|
||||
}
|
||||
|
||||
// Create fork
|
||||
_, _, err = g.client.Repositories.CreateFork(ctx, owner, repo, &github.RepositoryCreateForkOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create fork: %w", err)
|
||||
}
|
||||
|
||||
// Wait for fork to be ready
|
||||
for i := 0; i < forkCreationRetries; i++ {
|
||||
_, _, err = g.client.Repositories.Get(ctx, forkOwner, repo)
|
||||
if err == nil {
|
||||
return forkOwner, nil
|
||||
}
|
||||
// Sleep for a bit before retrying
|
||||
time.Sleep(forkCreationRetryDelay)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("fork creation timed out")
|
||||
}
|
||||
|
||||
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string, owner string, repository string) error {
|
||||
// Get the latest commit SHA from the default branch
|
||||
ref, _, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+g.defaultBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get reference for default branch %s: %w", g.defaultBranch, err)
|
||||
}
|
||||
|
||||
// Try to get the branch if it exists
|
||||
_, resp, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+branchName)
|
||||
if err != nil {
|
||||
if resp == nil {
|
||||
return fmt.Errorf("failed to check branch existence: %w", err)
|
||||
}
|
||||
|
||||
// If branch doesn't exist (404), create it
|
||||
if resp.StatusCode == 404 {
|
||||
newRef := &github.Reference{
|
||||
Ref: github.String("refs/heads/" + branchName),
|
||||
Object: &github.GitObject{SHA: ref.Object.SHA},
|
||||
}
|
||||
_, _, err = g.client.Git.CreateRef(ctx, owner, repository, newRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create branch: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// For other errors, return the error
|
||||
return fmt.Errorf("failed to check branch existence: %w", err)
|
||||
}
|
||||
|
||||
// Branch exists, update it to the latest commit
|
||||
updateRef := &github.Reference{
|
||||
Ref: github.String("refs/heads/" + branchName),
|
||||
Object: &github.GitObject{SHA: ref.Object.SHA},
|
||||
}
|
||||
_, _, err = g.client.Git.UpdateRef(ctx, owner, repository, updateRef, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update branch: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string, filePath string, content string, message string, owner string, repository string) error {
|
||||
// Get the current file content if it exists
|
||||
var sha *string
|
||||
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, filePath, &github.RepositoryContentGetOptions{
|
||||
Ref: branch,
|
||||
})
|
||||
if err == nil && fileContent != nil {
|
||||
sha = fileContent.SHA
|
||||
}
|
||||
|
||||
// Create or update the file
|
||||
_, _, err = g.client.Repositories.CreateFile(ctx, owner, repository, filePath, &github.RepositoryContentFileOptions{
|
||||
Message: &message,
|
||||
Content: []byte(content),
|
||||
Branch: &branch,
|
||||
SHA: sha,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create/update file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Branch string `json:"branch"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
Files []struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
} `json:"files"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
if result.BaseBranch == "" {
|
||||
result.BaseBranch = g.defaultBranch
|
||||
}
|
||||
|
||||
var targetOwner, targetRepo string
|
||||
if g.useFork {
|
||||
// Ensure we have a fork and get the fork owner
|
||||
forkOwner, err := g.ensureFork(ctx, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to ensure fork: %w", err)
|
||||
}
|
||||
targetOwner = forkOwner
|
||||
targetRepo = result.Repository
|
||||
} else {
|
||||
targetOwner = result.Owner
|
||||
targetRepo = result.Repository
|
||||
}
|
||||
|
||||
// Create or update branch
|
||||
err = g.createOrUpdateBranch(ctx, result.Branch, targetOwner, targetRepo)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
|
||||
}
|
||||
|
||||
// Create or update files
|
||||
for _, file := range result.Files {
|
||||
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path), targetOwner, targetRepo)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if PR already exists for this branch
|
||||
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
|
||||
State: "open",
|
||||
Head: fmt.Sprintf("%s:%s", targetOwner, result.Branch),
|
||||
})
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
|
||||
}
|
||||
|
||||
if len(prs) > 0 {
|
||||
// Update existing PR
|
||||
pr := prs[0]
|
||||
update := &github.PullRequest{
|
||||
Title: &result.Title,
|
||||
Body: &result.Body,
|
||||
}
|
||||
updatedPR, _, err := g.client.PullRequests.Edit(ctx, result.Owner, result.Repository, pr.GetNumber(), update)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to update pull request: %w", err)
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Updated pull request #%d: %s", updatedPR.GetNumber(), updatedPR.GetHTMLURL()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create new pull request
|
||||
newPR := &github.NewPullRequest{
|
||||
Title: &result.Title,
|
||||
Body: &result.Body,
|
||||
Head: &result.Branch,
|
||||
Base: &result.BaseBranch,
|
||||
}
|
||||
|
||||
// If using a fork, we need to specify the full head reference
|
||||
if g.useFork {
|
||||
head := fmt.Sprintf("%s:%s", targetOwner, result.Branch)
|
||||
newPR.Head = &head
|
||||
}
|
||||
|
||||
createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Created pull request #%d: %s", createdPR.GetNumber(), createdPR.GetHTMLURL()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *GithubPRCreator) Definition() types.ActionDefinition {
|
||||
actionName := "create_github_pr"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Create a GitHub pull request with file changes"
|
||||
if g.repository != "" && g.owner != "" && g.defaultBranch != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"branch": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The name of the new branch to create",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The title of the pull request",
|
||||
},
|
||||
"body": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The body/description of the pull request",
|
||||
},
|
||||
"files": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The path of the file to create/update",
|
||||
},
|
||||
"content": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The content of the file",
|
||||
},
|
||||
},
|
||||
Required: []string{"path", "content"},
|
||||
},
|
||||
Description: "Array of files to create or update",
|
||||
},
|
||||
},
|
||||
Required: []string{"branch", "title", "files"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"branch": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The name of the new branch to create",
|
||||
},
|
||||
"title": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The title of the pull request",
|
||||
},
|
||||
"body": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The body/description of the pull request",
|
||||
},
|
||||
"base_branch": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The base branch to merge into (defaults to configured default branch)",
|
||||
},
|
||||
"files": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The path of the file to create/update",
|
||||
},
|
||||
"content": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The content of the file",
|
||||
},
|
||||
},
|
||||
Required: []string{"path", "content"},
|
||||
},
|
||||
Description: "Array of files to create or update",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to create the pull request in",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository",
|
||||
},
|
||||
},
|
||||
Required: []string{"branch", "title", "files", "repository", "owner"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubPRCreator) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubPRCreatorConfigMeta returns the metadata for GitHub PR Creator action configuration fields
|
||||
func GithubPRCreatorConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
{
|
||||
Name: "defaultBranch",
|
||||
Label: "Default Branch",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Default branch to create PRs against (defaults to main)",
|
||||
},
|
||||
{
|
||||
Name: "useFork",
|
||||
Label: "Use Fork",
|
||||
Type: config.FieldTypeCheckbox,
|
||||
Required: false,
|
||||
HelpText: "Whether to create PRs from a fork (useful when you don't have write access to the repository). Set to 'true' to enable.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GithubPRCreator", func() {
|
||||
var (
|
||||
action *actions.GithubPRCreator
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
|
||||
// Check for required environment variables
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
repo := os.Getenv("TEST_REPOSITORY")
|
||||
owner := os.Getenv("TEST_OWNER")
|
||||
|
||||
// Skip tests if any required environment variable is missing
|
||||
if token == "" || repo == "" || owner == "" {
|
||||
Skip("Skipping GitHub PR creator tests: required environment variables not set")
|
||||
}
|
||||
|
||||
config := map[string]string{
|
||||
"token": token,
|
||||
"repository": repo,
|
||||
"owner": owner,
|
||||
"customActionName": "test_create_pr",
|
||||
"defaultBranch": "main",
|
||||
}
|
||||
|
||||
action = actions.NewGithubPRCreator(config)
|
||||
})
|
||||
|
||||
Describe("Creating pull requests", func() {
|
||||
It("should successfully create a pull request with file changes", func() {
|
||||
params := map[string]interface{}{
|
||||
"branch": "test-branch",
|
||||
"title": "Test PR",
|
||||
"body": "This is a test pull request",
|
||||
"base_branch": "main",
|
||||
"files": []map[string]interface{}{
|
||||
{
|
||||
"path": "test.txt",
|
||||
"content": "This is a test file",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := action.Run(ctx, nil, params)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.Result).To(ContainSubstring("pull request #"))
|
||||
})
|
||||
|
||||
It("should handle missing required fields", func() {
|
||||
params := map[string]interface{}{
|
||||
"title": "Test PR",
|
||||
"body": "This is a test pull request",
|
||||
}
|
||||
|
||||
_, err := action.Run(ctx, nil, params)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Action Definition", func() {
|
||||
It("should return correct action definition", func() {
|
||||
def := action.Definition()
|
||||
Expect(def.Name.String()).To(Equal("test_create_pr"))
|
||||
Expect(def.Description).To(ContainSubstring("Create a GitHub pull request with file changes"))
|
||||
Expect(def.Properties).To(HaveKey("branch"))
|
||||
Expect(def.Properties).To(HaveKey("title"))
|
||||
Expect(def.Properties).To(HaveKey("files"))
|
||||
})
|
||||
|
||||
It("should handle custom action name", func() {
|
||||
config := map[string]string{
|
||||
"token": "test-token",
|
||||
"customActionName": "custom_action_name",
|
||||
}
|
||||
action := actions.NewGithubPRCreator(config)
|
||||
def := action.Definition()
|
||||
Expect(def.Name.String()).To(Equal("custom_action_name"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Configuration", func() {
|
||||
It("should handle missing repository and owner in config", func() {
|
||||
config := map[string]string{
|
||||
"token": "test-token",
|
||||
}
|
||||
action := actions.NewGithubPRCreator(config)
|
||||
def := action.Definition()
|
||||
Expect(def.Properties).To(HaveKey("repository"))
|
||||
Expect(def.Properties).To(HaveKey("owner"))
|
||||
})
|
||||
|
||||
It("should handle provided repository and owner in config", func() {
|
||||
config := map[string]string{
|
||||
"token": "test-token",
|
||||
"repository": "test-repo",
|
||||
"defaultBranch": "main",
|
||||
"owner": "test-owner",
|
||||
}
|
||||
action := actions.NewGithubPRCreator(config)
|
||||
def := action.Definition()
|
||||
Expect(def.Properties).NotTo(HaveKey("repository"))
|
||||
Expect(def.Properties).NotTo(HaveKey("owner"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -34,7 +34,7 @@ func NewGithubPRReader(config map[string]string) *GithubPRReader {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubPRReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubPRReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
@@ -64,20 +64,44 @@ func (g *GithubPRReader) Run(ctx context.Context, params types.ActionParams) (ty
|
||||
return types.ActionResult{Result: fmt.Sprintf("Error fetching pull request files: %s", err.Error())}, err
|
||||
}
|
||||
|
||||
// Get CI status information
|
||||
ciStatus := "\n\nCI Status:\n"
|
||||
|
||||
// Get PR status checks
|
||||
checkRuns, _, err := g.client.Checks.ListCheckRunsForRef(ctx, result.Owner, result.Repository, pr.GetHead().GetSHA(), &github.ListCheckRunsOptions{})
|
||||
if err == nil && checkRuns != nil {
|
||||
ciStatus += fmt.Sprintf("\nPR Status Checks:\n")
|
||||
ciStatus += fmt.Sprintf("Total Checks: %d\n", checkRuns.GetTotal())
|
||||
for _, check := range checkRuns.CheckRuns {
|
||||
ciStatus += fmt.Sprintf("- %s: %s (%s)\n",
|
||||
check.GetName(),
|
||||
check.GetConclusion(),
|
||||
check.GetStatus())
|
||||
}
|
||||
}
|
||||
|
||||
// Build the file changes summary with patches
|
||||
fileChanges := "\n\nFile Changes:\n"
|
||||
for _, file := range files {
|
||||
fileChanges += fmt.Sprintf("\n--- %s\n+++ %s\n", *file.Filename, *file.Filename)
|
||||
if g.showFullDiff && file.Patch != nil {
|
||||
fileChanges += *file.Patch
|
||||
fileChanges += fmt.Sprintf("\n--- %s\n+++ %s\n", file.GetFilename(), file.GetFilename())
|
||||
if g.showFullDiff && file.GetPatch() != "" {
|
||||
fileChanges += file.GetPatch()
|
||||
}
|
||||
fileChanges += fmt.Sprintf("\n(%d additions, %d deletions)\n", *file.Additions, *file.Deletions)
|
||||
fileChanges += fmt.Sprintf("\n(%d additions, %d deletions)\n", file.GetAdditions(), file.GetDeletions())
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf(
|
||||
"Pull Request %d Repository: %s\nTitle: %s\nBody: %s\nState: %s\nBase: %s\nHead: %s%s",
|
||||
*pr.Number, *pr.Base.Repo.FullName, *pr.Title, *pr.Body, *pr.State, *pr.Base.Ref, *pr.Head.Ref, fileChanges)}, nil
|
||||
"Pull Request %d Repository: %s\nTitle: %s\nBody: %s\nState: %s\nBase: %s\nHead: %s%s%s",
|
||||
pr.GetNumber(),
|
||||
pr.GetBase().GetRepo().GetFullName(),
|
||||
pr.GetTitle(),
|
||||
pr.GetBody(),
|
||||
pr.GetState(),
|
||||
pr.GetBase().GetRef(),
|
||||
pr.GetHead().GetRef(),
|
||||
ciStatus,
|
||||
fileChanges)}, nil
|
||||
}
|
||||
|
||||
func (g *GithubPRReader) Definition() types.ActionDefinition {
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubPRReviewer struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubPRReviewer(config map[string]string) *GithubPRReviewer {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubPRReviewer{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
customActionName: config["customActionName"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubPRReviewer) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
PRNumber int `json:"pr_number"`
|
||||
ReviewComment string `json:"review_comment"`
|
||||
ReviewAction string `json:"review_action"` // APPROVE, REQUEST_CHANGES, or COMMENT
|
||||
Comments []struct {
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
Comment string `json:"comment"`
|
||||
StartLine int `json:"start_line,omitempty"`
|
||||
} `json:"comments"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
// First verify the PR exists and is in a valid state
|
||||
pr, _, err := g.client.PullRequests.Get(ctx, result.Owner, result.Repository, result.PRNumber)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to fetch PR #%d: %w", result.PRNumber, err)
|
||||
}
|
||||
if pr == nil {
|
||||
return types.ActionResult{Result: fmt.Sprintf("Pull request #%d not found in repository %s/%s", result.PRNumber, result.Owner, result.Repository)}, nil
|
||||
}
|
||||
|
||||
// Check if PR is in a state that allows reviews
|
||||
if *pr.State != "open" {
|
||||
return types.ActionResult{Result: fmt.Sprintf("Pull request #%d is not open (current state: %s)", result.PRNumber, *pr.State)}, nil
|
||||
}
|
||||
|
||||
// Get the list of changed files to verify the files exist in the PR
|
||||
files, _, err := g.client.PullRequests.ListFiles(ctx, result.Owner, result.Repository, result.PRNumber, &github.ListOptions{})
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to list PR files: %w", err)
|
||||
}
|
||||
|
||||
// Create a map of valid files
|
||||
validFiles := make(map[string]bool)
|
||||
for _, file := range files {
|
||||
if *file.Status != "deleted" {
|
||||
validFiles[*file.Filename] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Process each comment
|
||||
var reviewComments []*github.DraftReviewComment
|
||||
for _, comment := range result.Comments {
|
||||
// Check if file exists in PR
|
||||
if !validFiles[comment.File] {
|
||||
continue
|
||||
}
|
||||
|
||||
reviewComment := &github.DraftReviewComment{
|
||||
Path: &comment.File,
|
||||
Line: &comment.Line,
|
||||
Body: &comment.Comment,
|
||||
}
|
||||
|
||||
// Set start line if provided
|
||||
if comment.StartLine > 0 {
|
||||
reviewComment.StartLine = &comment.StartLine
|
||||
}
|
||||
|
||||
reviewComments = append(reviewComments, reviewComment)
|
||||
}
|
||||
|
||||
// Create the review
|
||||
review := &github.PullRequestReviewRequest{
|
||||
Event: &result.ReviewAction,
|
||||
Body: &result.ReviewComment,
|
||||
Comments: reviewComments,
|
||||
}
|
||||
|
||||
xlog.Debug("[githubprreviewer] review", "review", review)
|
||||
|
||||
// Submit the review
|
||||
_, resp, err := g.client.PullRequests.CreateReview(ctx, result.Owner, result.Repository, result.PRNumber, review)
|
||||
if err != nil {
|
||||
errorDetails := fmt.Sprintf("Error submitting review: %s", err.Error())
|
||||
if resp != nil {
|
||||
errorDetails += fmt.Sprintf("\nResponse Status: %s", resp.Status)
|
||||
if resp.Body != nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
errorDetails += fmt.Sprintf("\nResponse Body: %s", string(body))
|
||||
}
|
||||
}
|
||||
return types.ActionResult{Result: errorDetails}, err
|
||||
}
|
||||
|
||||
actionResult := fmt.Sprintf(
|
||||
"Pull request https://github.com/%s/%s/pull/%d reviewed successfully with status: %s, comments: %v, message: %s",
|
||||
result.Owner,
|
||||
result.Repository,
|
||||
result.PRNumber,
|
||||
strings.ToLower(result.ReviewAction),
|
||||
result.Comments,
|
||||
result.ReviewComment,
|
||||
)
|
||||
|
||||
return types.ActionResult{Result: actionResult}, nil
|
||||
}
|
||||
|
||||
func (g *GithubPRReviewer) Definition() types.ActionDefinition {
|
||||
actionName := "review_github_pr"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Review a GitHub pull request by approving, requesting changes, or commenting."
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"pr_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the pull request to review.",
|
||||
},
|
||||
"review_comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The main review comment to add to the pull request.",
|
||||
},
|
||||
"review_action": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The type of review to submit (APPROVE, REQUEST_CHANGES, or COMMENT).",
|
||||
Enum: []string{"APPROVE", "REQUEST_CHANGES", "COMMENT"},
|
||||
},
|
||||
"comments": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"file": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The file to comment on.",
|
||||
},
|
||||
"line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The line number to comment on.",
|
||||
},
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The comment text.",
|
||||
},
|
||||
"start_line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Optional start line for multi-line comments.",
|
||||
},
|
||||
},
|
||||
Required: []string{"file", "line", "comment"},
|
||||
},
|
||||
Description: "Array of line-specific comments to add to the review.",
|
||||
},
|
||||
},
|
||||
Required: []string{"pr_number", "review_action"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"pr_number": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The number of the pull request to review.",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository containing the pull request.",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"review_comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The main review comment to add to the pull request.",
|
||||
},
|
||||
"review_action": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The type of review to submit (APPROVE, REQUEST_CHANGES, or COMMENT).",
|
||||
Enum: []string{"APPROVE", "REQUEST_CHANGES", "COMMENT"},
|
||||
},
|
||||
"comments": {
|
||||
Type: jsonschema.Array,
|
||||
Items: &jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"file": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The file to comment on.",
|
||||
},
|
||||
"line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The line number to comment on.",
|
||||
},
|
||||
"comment": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The comment text.",
|
||||
},
|
||||
"start_line": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Optional start line for multi-line comments.",
|
||||
},
|
||||
},
|
||||
Required: []string{"file", "line", "comment"},
|
||||
},
|
||||
Description: "Array of line-specific comments to add to the review.",
|
||||
},
|
||||
},
|
||||
Required: []string{"pr_number", "repository", "owner", "review_action"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubPRReviewer) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubPRReviewerConfigMeta returns the metadata for GitHub PR Reviewer action configuration fields
|
||||
func GithubPRReviewerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GithubPRReviewer", func() {
|
||||
var (
|
||||
reviewer *actions.GithubPRReviewer
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
|
||||
// Check for required environment variables
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
repo := os.Getenv("TEST_REPOSITORY")
|
||||
owner := os.Getenv("TEST_OWNER")
|
||||
prNumber := os.Getenv("TEST_PR_NUMBER")
|
||||
|
||||
// Skip tests if any required environment variable is missing
|
||||
if token == "" || repo == "" || owner == "" || prNumber == "" {
|
||||
Skip("Skipping GitHub PR reviewer tests: required environment variables not set")
|
||||
}
|
||||
|
||||
config := map[string]string{
|
||||
"token": token,
|
||||
"repository": repo,
|
||||
"owner": owner,
|
||||
"customActionName": "test_review_github_pr",
|
||||
}
|
||||
|
||||
reviewer = actions.NewGithubPRReviewer(config)
|
||||
})
|
||||
|
||||
Describe("Reviewing a PR", func() {
|
||||
It("should successfully submit a review with comments", func() {
|
||||
prNumber := os.Getenv("TEST_PR_NUMBER")
|
||||
Expect(prNumber).NotTo(BeEmpty())
|
||||
|
||||
params := types.ActionParams{
|
||||
"pr_number": prNumber,
|
||||
"review_comment": "Test review comment from integration test",
|
||||
"review_action": "COMMENT",
|
||||
"comments": []map[string]interface{}{
|
||||
{
|
||||
"file": "README.md",
|
||||
"line": 1,
|
||||
"comment": "Test line comment from integration test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := reviewer.Run(ctx, nil, params)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.Result).To(ContainSubstring("reviewed successfully"))
|
||||
})
|
||||
|
||||
It("should handle invalid PR number", func() {
|
||||
params := types.ActionParams{
|
||||
"pr_number": 999999,
|
||||
"review_comment": "Test review comment",
|
||||
"review_action": "COMMENT",
|
||||
}
|
||||
|
||||
result, err := reviewer.Run(ctx, nil, params)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(result.Result).To(ContainSubstring("not found"))
|
||||
})
|
||||
|
||||
It("should handle invalid review action", func() {
|
||||
prNumber := os.Getenv("TEST_PR_NUMBER")
|
||||
Expect(prNumber).NotTo(BeEmpty())
|
||||
|
||||
params := types.ActionParams{
|
||||
"pr_number": prNumber,
|
||||
"review_comment": "Test review comment",
|
||||
"review_action": "INVALID_ACTION",
|
||||
}
|
||||
|
||||
_, err := reviewer.Run(ctx, nil, params)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Action Definition", func() {
|
||||
It("should return correct action definition", func() {
|
||||
def := reviewer.Definition()
|
||||
Expect(def.Name).To(Equal(types.ActionDefinitionName("test_review_github_pr")))
|
||||
Expect(def.Description).To(ContainSubstring("Review a GitHub pull request"))
|
||||
Expect(def.Properties).To(HaveKey("pr_number"))
|
||||
Expect(def.Properties).To(HaveKey("review_action"))
|
||||
Expect(def.Properties).To(HaveKey("comments"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,7 @@ func NewGithubRepositoryCreateOrUpdateContent(config map[string]string) *GithubR
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Path string `json:"path"`
|
||||
Repository string `json:"repository"`
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubRepositoryGetAllContent struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubRepositoryGetAllContent(config map[string]string) *GithubRepositoryGetAllContent {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubRepositoryGetAllContent{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
// isTextFile checks if a file is likely to be a text file based on its extension
|
||||
func isTextFile(path string) bool {
|
||||
// List of common text/code file extensions
|
||||
textExtensions := map[string]bool{
|
||||
".txt": true, ".md": true, ".go": true, ".py": true, ".js": true,
|
||||
".ts": true, ".jsx": true, ".tsx": true, ".html": true, ".css": true,
|
||||
".json": true, ".yaml": true, ".yml": true, ".xml": true, ".sql": true,
|
||||
".sh": true, ".bash": true, ".zsh": true, ".rb": true, ".php": true,
|
||||
".java": true, ".c": true, ".cpp": true, ".h": true, ".hpp": true,
|
||||
".rs": true, ".swift": true, ".kt": true, ".scala": true, ".lua": true,
|
||||
".pl": true, ".r": true, ".m": true, ".mm": true, ".f": true,
|
||||
".f90": true, ".f95": true, ".f03": true, ".f08": true, ".f15": true,
|
||||
".hs": true, ".lhs": true, ".erl": true, ".hrl": true, ".ex": true,
|
||||
".exs": true, ".eex": true, ".leex": true, ".heex": true, ".config": true,
|
||||
".toml": true, ".ini": true, ".conf": true, ".env": true, ".gitignore": true,
|
||||
".dockerignore": true, ".editorconfig": true, ".prettierrc": true, ".eslintrc": true,
|
||||
".babelrc": true, ".npmrc": true, ".yarnrc": true, ".lock": true,
|
||||
}
|
||||
|
||||
// Get the file extension
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return textExtensions[ext]
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string, owner string, repository string) (string, error) {
|
||||
var result strings.Builder
|
||||
|
||||
// Get content at the current path
|
||||
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Process each item in the directory
|
||||
for _, item := range directoryContent {
|
||||
if item.GetType() == "dir" {
|
||||
// Recursively get content for subdirectories
|
||||
subContent, err := g.getContentRecursively(ctx, item.GetPath(), owner, repository)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result.WriteString(subContent)
|
||||
} else if item.GetType() == "file" {
|
||||
// Skip binary/image files
|
||||
if !isTextFile(item.GetPath()) {
|
||||
xlog.Warn("Skipping non-text file: ", "file", item.GetPath())
|
||||
result.WriteString(fmt.Sprintf("Skipping non-text file: %s\n", item.GetPath()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get file content
|
||||
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
|
||||
}
|
||||
|
||||
content, err := fileContent.GetContent()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err)
|
||||
}
|
||||
|
||||
// Add file content to result with clear markers
|
||||
result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath()))
|
||||
result.WriteString(content)
|
||||
result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath()))
|
||||
}
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
// Start from root if no path specified
|
||||
if result.Path == "" {
|
||||
result.Path = "."
|
||||
}
|
||||
|
||||
content, err := g.getContentRecursively(ctx, result.Path, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
return types.ActionResult{Result: content}, nil
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryGetAllContent) Definition() types.ActionDefinition {
|
||||
actionName := "get_all_github_repository_content"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Get all content of a GitHub repository recursively"
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional path to start from (defaults to repository root)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional path to start from (defaults to repository root)",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to get content from",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository",
|
||||
},
|
||||
},
|
||||
Required: []string{"repository", "owner"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubRepositoryGetAllContent) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubRepositoryGetAllContentConfigMeta returns the metadata for GitHub Repository Get All Content action configuration fields
|
||||
func GithubRepositoryGetAllContentConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GithubRepositoryGetAllContent", func() {
|
||||
var (
|
||||
action *actions.GithubRepositoryGetAllContent
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
|
||||
// Check for required environment variables
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
repo := os.Getenv("TEST_REPOSITORY")
|
||||
owner := os.Getenv("TEST_OWNER")
|
||||
|
||||
// Skip tests if any required environment variable is missing
|
||||
if token == "" || repo == "" || owner == "" {
|
||||
Skip("Skipping GitHub repository get all content tests: required environment variables not set")
|
||||
}
|
||||
|
||||
config := map[string]string{
|
||||
"token": token,
|
||||
"repository": repo,
|
||||
"owner": owner,
|
||||
"customActionName": "test_get_all_content",
|
||||
}
|
||||
|
||||
action = actions.NewGithubRepositoryGetAllContent(config)
|
||||
})
|
||||
|
||||
Describe("Getting repository content", func() {
|
||||
It("should successfully get content from root directory with proper file markers", func() {
|
||||
params := map[string]interface{}{
|
||||
"path": ".",
|
||||
}
|
||||
|
||||
result, err := action.Run(ctx, nil, params)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.Result).NotTo(BeEmpty())
|
||||
|
||||
// Verify file markers
|
||||
Expect(result.Result).To(ContainSubstring("--- START FILE:"))
|
||||
Expect(result.Result).To(ContainSubstring("--- END FILE:"))
|
||||
|
||||
// Verify markers are properly paired
|
||||
startCount := strings.Count(result.Result, "--- START FILE:")
|
||||
endCount := strings.Count(result.Result, "--- END FILE:")
|
||||
Expect(startCount).To(Equal(endCount), "Number of start and end markers should match")
|
||||
})
|
||||
|
||||
It("should handle non-existent path", func() {
|
||||
params := map[string]interface{}{
|
||||
"path": "non-existent-path",
|
||||
}
|
||||
|
||||
_, err := action.Run(ctx, nil, params)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Action Definition", func() {
|
||||
It("should return correct action definition", func() {
|
||||
def := action.Definition()
|
||||
Expect(def.Name.String()).To(Equal("test_get_all_content"))
|
||||
Expect(def.Description).To(ContainSubstring("Get all content of a GitHub repository recursively"))
|
||||
Expect(def.Properties).To(HaveKey("path"))
|
||||
})
|
||||
|
||||
It("should handle custom action name", func() {
|
||||
config := map[string]string{
|
||||
"token": "test-token",
|
||||
"customActionName": "custom_action_name",
|
||||
}
|
||||
action := actions.NewGithubRepositoryGetAllContent(config)
|
||||
def := action.Definition()
|
||||
Expect(def.Name.String()).To(Equal("custom_action_name"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Configuration", func() {
|
||||
It("should handle missing repository and owner in config", func() {
|
||||
config := map[string]string{
|
||||
"token": "test-token",
|
||||
}
|
||||
action := actions.NewGithubRepositoryGetAllContent(config)
|
||||
def := action.Definition()
|
||||
Expect(def.Properties).To(HaveKey("repository"))
|
||||
Expect(def.Properties).To(HaveKey("owner"))
|
||||
})
|
||||
|
||||
It("should handle provided repository and owner in config", func() {
|
||||
config := map[string]string{
|
||||
"token": "test-token",
|
||||
"repository": "test-repo",
|
||||
"owner": "test-owner",
|
||||
}
|
||||
action := actions.NewGithubRepositoryGetAllContent(config)
|
||||
def := action.Definition()
|
||||
Expect(def.Properties).NotTo(HaveKey("repository"))
|
||||
Expect(def.Properties).NotTo(HaveKey("owner"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -27,7 +27,7 @@ func NewGithubRepositoryGetContent(config map[string]string) *GithubRepositoryGe
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryGetContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubRepositoryGetContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Path string `json:"path"`
|
||||
Repository string `json:"repository"`
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubRepositoryListFiles struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubRepositoryListFiles(config map[string]string) *GithubRepositoryListFiles {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubRepositoryListFiles{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryListFiles) listFilesRecursively(ctx context.Context, path string, owner string, repository string) ([]string, error) {
|
||||
var files []string
|
||||
|
||||
// Get content at the current path
|
||||
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting content at path %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Process each item in the directory
|
||||
for _, item := range directoryContent {
|
||||
if item.GetType() == "dir" {
|
||||
// Recursively list files in subdirectories
|
||||
subFiles, err := g.listFilesRecursively(ctx, item.GetPath(), owner, repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, subFiles...)
|
||||
} else if item.GetType() == "file" {
|
||||
// Add file path to the list
|
||||
files = append(files, item.GetPath())
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryListFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
// Start from root if no path specified
|
||||
if result.Path == "" {
|
||||
result.Path = "."
|
||||
}
|
||||
|
||||
files, err := g.listFilesRecursively(ctx, result.Path, result.Owner, result.Repository)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Join all file paths with newlines for better readability
|
||||
content := strings.Join(files, "\n")
|
||||
return types.ActionResult{Result: content}, nil
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryListFiles) Definition() types.ActionDefinition {
|
||||
actionName := "list_github_repository_files"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "List all files in a GitHub repository"
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional path to start listing from (defaults to repository root)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional path to start listing from (defaults to repository root)",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to list files from",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository",
|
||||
},
|
||||
},
|
||||
Required: []string{"repository", "owner"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubRepositoryListFiles) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubRepositoryListFilesConfigMeta returns the metadata for GitHub Repository List Files action configuration fields
|
||||
func GithubRepositoryListFilesConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func NewGithubRepositoryREADME(config map[string]string) *GithubRepositoryREADME
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubRepositoryREADME) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (g *GithubRepositoryREADME) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
type GithubRepositorySearchFiles struct {
|
||||
token, repository, owner, customActionName string
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
func NewGithubRepositorySearchFiles(config map[string]string) *GithubRepositorySearchFiles {
|
||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||
|
||||
return &GithubRepositorySearchFiles{
|
||||
client: client,
|
||||
token: config["token"],
|
||||
repository: config["repository"],
|
||||
owner: config["owner"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GithubRepositorySearchFiles) searchFilesRecursively(ctx context.Context, path string, owner string, repository string, searchPattern string) (string, error) {
|
||||
var result strings.Builder
|
||||
|
||||
// Get content at the current path
|
||||
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Process each item in the directory
|
||||
for _, item := range directoryContent {
|
||||
if item.GetType() == "dir" {
|
||||
// Recursively search in subdirectories
|
||||
subContent, err := g.searchFilesRecursively(ctx, item.GetPath(), owner, repository, searchPattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result.WriteString(subContent)
|
||||
} else if item.GetType() == "file" {
|
||||
// Check if file name matches the search pattern
|
||||
if strings.Contains(strings.ToLower(item.GetName()), strings.ToLower(searchPattern)) {
|
||||
// Get file content
|
||||
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
|
||||
}
|
||||
|
||||
content, err := fileContent.GetContent()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err)
|
||||
}
|
||||
|
||||
// Add file content to result with clear markers
|
||||
result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath()))
|
||||
result.WriteString(content)
|
||||
result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func (g *GithubRepositorySearchFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
Path string `json:"path,omitempty"`
|
||||
SearchPattern string `json:"searchPattern"`
|
||||
}{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
if g.repository != "" && g.owner != "" {
|
||||
result.Repository = g.repository
|
||||
result.Owner = g.owner
|
||||
}
|
||||
|
||||
// Start from root if no path specified
|
||||
if result.Path == "" {
|
||||
result.Path = "."
|
||||
}
|
||||
|
||||
content, err := g.searchFilesRecursively(ctx, result.Path, result.Owner, result.Repository, result.SearchPattern)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
return types.ActionResult{Result: content}, nil
|
||||
}
|
||||
|
||||
func (g *GithubRepositorySearchFiles) Definition() types.ActionDefinition {
|
||||
actionName := "search_github_repository_files"
|
||||
if g.customActionName != "" {
|
||||
actionName = g.customActionName
|
||||
}
|
||||
description := "Search for files in a GitHub repository and return their content"
|
||||
if g.repository != "" && g.owner != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional path to start searching from (defaults to repository root)",
|
||||
},
|
||||
"searchPattern": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Pattern to search for in file names (case-insensitive)",
|
||||
},
|
||||
},
|
||||
Required: []string{"searchPattern"},
|
||||
}
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"path": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Optional path to start searching from (defaults to repository root)",
|
||||
},
|
||||
"repository": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The repository to search in",
|
||||
},
|
||||
"owner": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The owner of the repository",
|
||||
},
|
||||
"searchPattern": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Pattern to search for in file names (case-insensitive)",
|
||||
},
|
||||
},
|
||||
Required: []string{"repository", "owner", "searchPattern"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GithubRepositorySearchFiles) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GithubRepositorySearchFilesConfigMeta returns the metadata for GitHub Repository Search Files action configuration fields
|
||||
func GithubRepositorySearchFilesConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "GitHub Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "GitHub API token with repository access",
|
||||
},
|
||||
{
|
||||
Name: "repository",
|
||||
Label: "Repository",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository name",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Label: "Owner",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "GitHub repository owner",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// Remove global const and mutex, and add them as fields to a struct
|
||||
|
||||
type MemoryActions struct {
|
||||
filePath string
|
||||
customName string
|
||||
customDescription string
|
||||
}
|
||||
|
||||
type AddToMemoryAction struct{ *MemoryActions }
|
||||
type ListMemoryAction struct{ *MemoryActions }
|
||||
type RemoveFromMemoryAction struct{ *MemoryActions }
|
||||
|
||||
// NewMemoryActions returns the three actions, using the provided filePath and config
|
||||
func NewMemoryActions(filePath string, config map[string]string) (*AddToMemoryAction, *ListMemoryAction, *RemoveFromMemoryAction) {
|
||||
ma := &MemoryActions{filePath: filePath}
|
||||
if config != nil {
|
||||
ma.customName = config["custom_name"]
|
||||
ma.customDescription = config["custom_description"]
|
||||
}
|
||||
return &AddToMemoryAction{ma}, &ListMemoryAction{ma}, &RemoveFromMemoryAction{ma}
|
||||
}
|
||||
|
||||
type addToMemoryParams struct {
|
||||
Item string `json:"item"`
|
||||
}
|
||||
|
||||
type removeFromMemoryParams struct {
|
||||
Index *int `json:"index,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MemoryActions) readMemory() ([]string, error) {
|
||||
f, err := os.Open(m.filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var items []string
|
||||
if err := json.NewDecoder(f).Decode(&items); err != nil {
|
||||
if err == io.EOF {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (m *MemoryActions) writeMemory(items []string) error {
|
||||
f, err := os.Create(m.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewEncoder(f).Encode(items)
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req addToMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
if req.Item == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("item cannot be empty")
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
items = append(items, req.Item)
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Added item to memory: %s", req.Item),
|
||||
Metadata: map[string]any{"item": req.Item, "count": len(items)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
outputResult := "Number of items in memory: " + strconv.Itoa(len(items)) + "\n"
|
||||
for i, item := range items {
|
||||
outputResult += fmt.Sprintf("%d) %s\n", i, item)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: outputResult,
|
||||
Metadata: map[string]any{"items": items},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req removeFromMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
var removed string
|
||||
if req.Index != nil {
|
||||
idx := *req.Index
|
||||
if idx < 0 || idx >= len(items) {
|
||||
return types.ActionResult{}, fmt.Errorf("index out of range")
|
||||
}
|
||||
removed = items[idx]
|
||||
items = append(items[:idx], items[idx+1:]...)
|
||||
} else if req.Value != "" {
|
||||
found := false
|
||||
for i, v := range items {
|
||||
if v == req.Value {
|
||||
removed = v
|
||||
items = append(items[:i], items[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return types.ActionResult{}, fmt.Errorf("value not found in memory")
|
||||
}
|
||||
} else {
|
||||
return types.ActionResult{}, fmt.Errorf("must provide index or value to remove")
|
||||
}
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed item from memory: %s", removed),
|
||||
Metadata: map[string]any{"removed": removed, "count": len(items)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "add_to_memory"
|
||||
description := "Add a string item to memory (stored in a JSON file)."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"item": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The string item to add to memory.",
|
||||
},
|
||||
},
|
||||
Required: []string{"item"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "list_memory"
|
||||
description := "List all items currently stored in memory."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "remove_from_memory"
|
||||
description := "Remove an item from memory by index or value."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"index": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The index of the item to remove (optional, 0-based)",
|
||||
},
|
||||
"value": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The value of the item to remove (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Plannable() bool { return true }
|
||||
func (a *ListMemoryAction) Plannable() bool { return true }
|
||||
func (a *RemoveFromMemoryAction) Plannable() bool { return true }
|
||||
|
||||
// AddToMemoryConfigMeta returns the metadata for AddToMemory action configuration fields
|
||||
func AddToMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'add_to_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Add a string item to memory (stored in a JSON file).')",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListMemoryConfigMeta returns the metadata for ListMemory action configuration fields
|
||||
func ListMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'list_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'List all items currently stored in memory.')",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveFromMemoryConfigMeta returns the metadata for RemoveFromMemory action configuration fields
|
||||
func RemoveFromMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'remove_from_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Remove an item from memory by index or value.')",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("MemoryActions", func() {
|
||||
var (
|
||||
tmpFile string
|
||||
aAdd *actions.AddToMemoryAction
|
||||
aList *actions.ListMemoryAction
|
||||
aRemove *actions.RemoveFromMemoryAction
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
f, err := os.CreateTemp("", "memory_test_*.json")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
tmpFile = f.Name()
|
||||
f.Close()
|
||||
aAdd, aList, aRemove = actions.NewMemoryActions(tmpFile, map[string]string{})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.Remove(tmpFile)
|
||||
})
|
||||
|
||||
It("adds and lists items", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, err := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Metadata["items"]).To(ContainElements("foo", "bar"))
|
||||
})
|
||||
|
||||
It("removes by index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 0})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("bar"))
|
||||
})
|
||||
|
||||
It("removes by value", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("foo"))
|
||||
})
|
||||
|
||||
It("returns error for out of range index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 2})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for value not found", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for empty item", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": ""})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error if neither index nor value provided", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func NewPiKVMAction(config map[string]string) *PiKVMAction {
|
||||
return &PiKVMAction{
|
||||
hostname: config["hostname"],
|
||||
username: config["username"],
|
||||
password: config["password"],
|
||||
customName: config["custom_name"],
|
||||
customDescription: config["custom_description"],
|
||||
insecure: config["insecure"] == "true",
|
||||
}
|
||||
}
|
||||
|
||||
type PiKVMAction struct {
|
||||
hostname string
|
||||
username string
|
||||
password string
|
||||
customName string
|
||||
customDescription string
|
||||
insecure bool
|
||||
}
|
||||
|
||||
type pikvmPowerParams struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
func (a *PiKVMAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req pikvmPowerParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Validate action parameter
|
||||
validActions := map[string]bool{
|
||||
"on": true,
|
||||
"off": true,
|
||||
"off_hard": true,
|
||||
"reset_hard": true,
|
||||
}
|
||||
if !validActions[req.Action] {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid action: %s. Valid actions are: on, off, off_hard, reset_hard", req.Action)
|
||||
}
|
||||
|
||||
// Check if required config is provided
|
||||
if a.hostname == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("hostname is required in action configuration")
|
||||
}
|
||||
if a.username == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("username is required in action configuration")
|
||||
}
|
||||
if a.password == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("password is required in action configuration")
|
||||
}
|
||||
|
||||
// Build the API URL
|
||||
apiURL := fmt.Sprintf("https://%s/api/atx/power", a.hostname)
|
||||
|
||||
insecure := false
|
||||
if a.insecure {
|
||||
insecure = true
|
||||
}
|
||||
// Create HTTP client with basic auth
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
reqHTTP, err := http.NewRequestWithContext(ctx, "POST", apiURL, nil)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
// Set basic authentication
|
||||
reqHTTP.SetBasicAuth(a.username, a.password)
|
||||
|
||||
// Add query parameters
|
||||
q := reqHTTP.URL.Query()
|
||||
q.Add("action", req.Action)
|
||||
reqHTTP.URL.RawQuery = q.Encode()
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(reqHTTP)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to make HTTP request to PiKVM: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return types.ActionResult{}, fmt.Errorf("PiKVM API returned status %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// Determine action description for user-friendly response
|
||||
actionDesc := map[string]string{
|
||||
"on": "power on",
|
||||
"off": "power off",
|
||||
"off_hard": "hard power off",
|
||||
"reset_hard": "hard reset",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Successfully sent %s command to PiKVM at %s", actionDesc[req.Action], a.hostname)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: result,
|
||||
Metadata: map[string]any{
|
||||
"action": req.Action,
|
||||
"hostname": a.hostname,
|
||||
"status": "success",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *PiKVMAction) Definition() types.ActionDefinition {
|
||||
name := "pikvm_power_control"
|
||||
description := "Control power state of a PiKVM device using ATX power management."
|
||||
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"action": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The power action to perform on the PiKVM device.",
|
||||
Enum: []string{"on", "off", "off_hard", "reset_hard"},
|
||||
},
|
||||
},
|
||||
Required: []string{"action"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *PiKVMAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PiKVMConfigMeta returns the metadata for PiKVM action configuration fields
|
||||
func PiKVMConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "hostname",
|
||||
Label: "PiKVM Hostname",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "The hostname or IP address of the PiKVM device (e.g., pikvm.local or 192.168.1.100)",
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Label: "Username",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Username for PiKVM authentication (usually 'admin')",
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "Password",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Password for PiKVM authentication",
|
||||
},
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for this action (optional, defaults to 'pikvm_power_control')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for this action (optional)",
|
||||
},
|
||||
{
|
||||
Name: "insecure",
|
||||
Label: "Insecure",
|
||||
Type: config.FieldTypeCheckbox,
|
||||
Required: false,
|
||||
HelpText: "Skip certificate verification (optional)",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func NewScraper(config map[string]string) *ScraperAction {
|
||||
|
||||
type ScraperAction struct{}
|
||||
|
||||
func (a *ScraperAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *ScraperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
URL string `json:"url"`
|
||||
}{}
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewSearch(config map[string]string) *SearchAction {
|
||||
|
||||
type SearchAction struct{ results int }
|
||||
|
||||
func (a *SearchAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *SearchAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Query string `json:"query"`
|
||||
}{}
|
||||
|
||||
@@ -28,7 +28,7 @@ type SendMailAction struct {
|
||||
smtpPort string
|
||||
}
|
||||
|
||||
func (a *SendMailAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *SendMailAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Message string `json:"message"`
|
||||
To string `json:"to"`
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataTelegramMessageSent = "telegram_message_sent"
|
||||
telegramMaxMessageLength = 3000
|
||||
)
|
||||
|
||||
type SendTelegramMessageRunner struct {
|
||||
token string
|
||||
chatID int64
|
||||
bot *bot.Bot
|
||||
customName string
|
||||
customDescription string
|
||||
}
|
||||
|
||||
func NewSendTelegramMessageRunner(config map[string]string) *SendTelegramMessageRunner {
|
||||
token := config["token"]
|
||||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse chat ID from config if present
|
||||
var chatID int64
|
||||
if configChatID := config["chat_id"]; configChatID != "" {
|
||||
var err error
|
||||
chatID, err = strconv.ParseInt(configChatID, 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
b, err := bot.New(token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &SendTelegramMessageRunner{
|
||||
token: token,
|
||||
chatID: chatID,
|
||||
bot: b,
|
||||
customName: config["custom_name"],
|
||||
customDescription: config["custom_description"],
|
||||
}
|
||||
}
|
||||
|
||||
type TelegramMessageParams struct {
|
||||
ChatID int64 `json:"chat_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (s *SendTelegramMessageRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var messageParams TelegramMessageParams
|
||||
err := params.Unmarshal(&messageParams)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
if s.chatID != 0 {
|
||||
messageParams.ChatID = s.chatID
|
||||
}
|
||||
|
||||
if messageParams.ChatID == 0 {
|
||||
return types.ActionResult{}, fmt.Errorf("chat_id is required either in config or parameters")
|
||||
}
|
||||
|
||||
if messageParams.Message == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("message is required")
|
||||
}
|
||||
|
||||
// Split the message if it's too long
|
||||
messages := xstrings.SplitParagraph(messageParams.Message, telegramMaxMessageLength)
|
||||
|
||||
if len(messages) == 0 {
|
||||
return types.ActionResult{}, fmt.Errorf("empty message after splitting")
|
||||
}
|
||||
|
||||
// Send each message part
|
||||
for i, msg := range messages {
|
||||
_, err = s.bot.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: messageParams.ChatID,
|
||||
Text: msg,
|
||||
ParseMode: models.ParseModeMarkdown,
|
||||
})
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to send telegram message part %d: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
sharedState.ConversationTracker.AddMessage(fmt.Sprintf("telegram:%d", messageParams.ChatID), openai.ChatCompletionMessage{
|
||||
Content: messageParams.Message,
|
||||
Role: "assistant",
|
||||
})
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Message sent successfully to chat ID %d in %d parts", messageParams.ChatID, len(messages)),
|
||||
Metadata: map[string]interface{}{
|
||||
MetadataTelegramMessageSent: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SendTelegramMessageRunner) Definition() types.ActionDefinition {
|
||||
|
||||
customName := "send_telegram_message"
|
||||
if s.customName != "" {
|
||||
customName = s.customName
|
||||
}
|
||||
|
||||
customDescription := "Send a message to a Telegram user or group"
|
||||
if s.customDescription != "" {
|
||||
customDescription = s.customDescription
|
||||
}
|
||||
|
||||
if s.chatID != 0 {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(customName),
|
||||
Description: customDescription,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message to send",
|
||||
},
|
||||
},
|
||||
Required: []string{"message"},
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(customName),
|
||||
Description: customDescription,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"chat_id": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "The Telegram chat ID to send the message to (optional if configured in config)",
|
||||
},
|
||||
"message": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The message to send",
|
||||
},
|
||||
},
|
||||
Required: []string{"message", "chat_id"},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SendTelegramMessageRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SendTelegramMessageConfigMeta returns the metadata for Send Telegram Message action configuration fields
|
||||
func SendTelegramMessageConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "token",
|
||||
Label: "Telegram Token",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Telegram bot token for sending messages",
|
||||
},
|
||||
{
|
||||
Name: "chat_id",
|
||||
Label: "Default Chat ID",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Default Telegram chat ID to send messages to (can be overridden in parameters)",
|
||||
},
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'send_telegram_message')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Send a message to a Telegram user or group')",
|
||||
},
|
||||
}
|
||||
}
|
||||
+60
-25
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
@@ -11,24 +12,27 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func NewShell(config map[string]string) *ShellAction {
|
||||
func NewShell(config map[string]string, sshBoxURL string) *ShellAction {
|
||||
return &ShellAction{
|
||||
privateKey: config["privateKey"],
|
||||
user: config["user"],
|
||||
host: config["host"],
|
||||
password: config["password"],
|
||||
customName: config["customName"],
|
||||
customDescription: config["customDescription"],
|
||||
sshBoxURL: sshBoxURL,
|
||||
}
|
||||
}
|
||||
|
||||
type ShellAction struct {
|
||||
privateKey string
|
||||
user, host string
|
||||
customName string
|
||||
customDescription string
|
||||
privateKey string
|
||||
user, host, password string
|
||||
customName string
|
||||
customDescription string
|
||||
sshBoxURL string
|
||||
}
|
||||
|
||||
func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *ShellAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Command string `json:"command"`
|
||||
Host string `json:"host"`
|
||||
@@ -46,7 +50,23 @@ func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types
|
||||
result.User = a.user
|
||||
}
|
||||
|
||||
output, err := sshCommand(a.privateKey, result.Command, result.User, result.Host)
|
||||
password := a.password
|
||||
if a.sshBoxURL != "" && result.Host == "" && result.User == "" && password == "" {
|
||||
// sshbox url can be root:root@localhost:2222
|
||||
parts := strings.Split(a.sshBoxURL, "@")
|
||||
if len(parts) == 2 {
|
||||
if strings.Contains(parts[0], ":") {
|
||||
userPass := strings.Split(parts[0], ":")
|
||||
result.User = userPass[0]
|
||||
password = userPass[1]
|
||||
} else {
|
||||
result.User = parts[0]
|
||||
}
|
||||
result.Host = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
output, err := sshCommand(a.privateKey, result.Command, result.User, result.Host, password)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
@@ -55,15 +75,15 @@ func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types
|
||||
}
|
||||
|
||||
func (a *ShellAction) Definition() types.ActionDefinition {
|
||||
name := "shell"
|
||||
description := "Run a shell command on a remote server."
|
||||
name := "run_command"
|
||||
description := "Run a command on a linux environment."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
if a.host != "" && a.user != "" {
|
||||
if (a.host != "" && a.user != "") || a.sshBoxURL != "" {
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
@@ -104,7 +124,7 @@ func ShellConfigMeta() []config.Field {
|
||||
Name: "privateKey",
|
||||
Label: "Private Key",
|
||||
Type: config.FieldTypeTextarea,
|
||||
Required: true,
|
||||
Required: false,
|
||||
HelpText: "SSH private key for connecting to remote servers",
|
||||
},
|
||||
{
|
||||
@@ -113,6 +133,12 @@ func ShellConfigMeta() []config.Field {
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Default SSH user for connecting to remote servers",
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "Default Password",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Default SSH password for connecting to remote servers",
|
||||
},
|
||||
{
|
||||
Name: "host",
|
||||
Label: "Default Host",
|
||||
@@ -134,19 +160,25 @@ func ShellConfigMeta() []config.Field {
|
||||
}
|
||||
}
|
||||
|
||||
func sshCommand(privateKey, command, user, host string) (string, error) {
|
||||
// Create signer from private key string
|
||||
key, err := ssh.ParsePrivateKey([]byte(privateKey))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse private key: %v", err)
|
||||
func sshCommand(privateKey, command, user, host, password string) (string, error) {
|
||||
|
||||
authMethods := []ssh.AuthMethod{}
|
||||
if password != "" {
|
||||
authMethods = append(authMethods, ssh.Password(password))
|
||||
}
|
||||
if privateKey != "" {
|
||||
// Create signer from private key string
|
||||
key, err := ssh.ParsePrivateKey([]byte(privateKey))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse private key: %v", err)
|
||||
}
|
||||
authMethods = append(authMethods, ssh.PublicKeys(key))
|
||||
}
|
||||
|
||||
// SSH client configuration
|
||||
config := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(key),
|
||||
},
|
||||
User: user,
|
||||
Auth: authMethods,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
@@ -165,12 +197,15 @@ func sshCommand(privateKey, command, user, host string) (string, error) {
|
||||
defer session.Close()
|
||||
|
||||
// Run a command
|
||||
output, err := session.CombinedOutput(command)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to run: %v", err)
|
||||
cmdOut, err := session.CombinedOutput(command)
|
||||
result := string(cmdOut)
|
||||
if strings.TrimSpace(result) == "" {
|
||||
result += "\nCommand has exited with no output"
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
if err != nil {
|
||||
result += "\nError: " + err.Error()
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *ShellAction) Plannable() bool {
|
||||
|
||||
@@ -22,7 +22,7 @@ type PostTweetAction struct {
|
||||
noCharacterLimit bool
|
||||
}
|
||||
|
||||
func (a *PostTweetAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *PostTweetAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Text string `json:"text"`
|
||||
}{}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// Package actions contains action implementations used by LocalAGI.
|
||||
// This file implements the "webhook" action which can send an HTTP request
|
||||
// to an external service with a configurable method, content type, and payload.
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// NewWebhook constructs a WebhookAction using provided configuration values:
|
||||
// - url: Destination endpoint for the HTTP request (required).
|
||||
// - method: HTTP method to use (GET, POST, PUT, DELETE, ...). Defaults to POST.
|
||||
// - contentType: Value for the Content-Type header (e.g., application/json).
|
||||
// - payloadTemplate: Optional template for the request body; the runtime parameter
|
||||
// "payload" (if provided) will replace the "{{payload}}" placeholder inside this template.
|
||||
func NewWebhook(cfg map[string]string) *WebhookAction {
|
||||
wa := &WebhookAction{
|
||||
url: strings.TrimSpace(cfg["url"]),
|
||||
method: strings.ToUpper(strings.TrimSpace(cfg["method"])),
|
||||
contentType: strings.TrimSpace(cfg["contentType"]),
|
||||
payloadTemplate: cfg["payloadTemplate"],
|
||||
}
|
||||
// Optional custom overrides
|
||||
if cfg != nil {
|
||||
wa.customName = cfg["custom_name"]
|
||||
wa.customDescription = cfg["custom_description"]
|
||||
wa.customPayloadDescription = cfg["custom_payload_description"]
|
||||
}
|
||||
if wa.method == "" {
|
||||
wa.method = http.MethodPost
|
||||
}
|
||||
return wa
|
||||
}
|
||||
|
||||
// WebhookAction holds the static configuration for the webhook.
|
||||
// These values come from the action configuration (UI/agent config),
|
||||
// while the runtime parameter only carries the dynamic payload.
|
||||
// - url: Target endpoint for the request.
|
||||
// - method: HTTP method to use. Defaults to POST if not provided.
|
||||
// - contentType: Sets the Content-Type header when a body is sent.
|
||||
// - payloadTemplate: Optional template used to build the request body; occurrences
|
||||
// of "{{payload}}" get replaced with the runtime payload string.
|
||||
// If no placeholder is present, the template is used as-is.
|
||||
// For GET requests the body is omitted regardless of payload.
|
||||
//
|
||||
// Note: This action does not follow redirects!
|
||||
type WebhookAction struct {
|
||||
url string
|
||||
method string
|
||||
contentType string
|
||||
payloadTemplate string
|
||||
customName string
|
||||
customDescription string
|
||||
customPayloadDescription string
|
||||
}
|
||||
|
||||
// Run executes the webhook call.
|
||||
// It reads the runtime parameter "payload" (optional), merges it into the
|
||||
// configured payloadTemplate (if any), constructs an HTTP request using the
|
||||
// configured URL, method and content type, and then returns a summary with the
|
||||
// response status and body (truncated to 4KiB for safety).
|
||||
func (a *WebhookAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
// Runtime parameters: only payload
|
||||
type input struct {
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
var in input
|
||||
if err := params.Unmarshal(&in); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Validate essential configuration. The URL must be provided via the
|
||||
// action configuration (not via runtime parameters).
|
||||
if a.url == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("configuration.url is required")
|
||||
}
|
||||
|
||||
method := a.method
|
||||
|
||||
// Build the request body based on template and payload:
|
||||
// - If a payloadTemplate is provided, replace occurrences of "{{payload}}"
|
||||
// with the runtime payload value.
|
||||
// - If the template does not contain the placeholder but is provided, we use
|
||||
// the template as-is (common for static JSON bodies prepared at config time).
|
||||
// - If no template is configured, we send the runtime payload as-is.
|
||||
// - For GET requests the body is omitted regardless of payload.
|
||||
var payload string
|
||||
if a.payloadTemplate != "" {
|
||||
payload = strings.ReplaceAll(a.payloadTemplate, "{{payload}}", in.Payload)
|
||||
if payload == a.payloadTemplate && in.Payload != "" {
|
||||
// If no placeholder found, fallback to template or payload alone
|
||||
payload = a.payloadTemplate
|
||||
}
|
||||
} else {
|
||||
payload = in.Payload
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if method != http.MethodGet && payload != "" {
|
||||
body = bytes.NewBufferString(payload)
|
||||
}
|
||||
|
||||
// Create the HTTP request bound to the provided context so that cancellation
|
||||
// or timeouts from the caller propagate to the outbound call.
|
||||
req, err := http.NewRequestWithContext(ctx, method, a.url, body)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Set Content-Type header if configured. For GET requests this header is
|
||||
// typically ignored by servers as there is no body.
|
||||
if a.contentType != "" {
|
||||
req.Header.Set("Content-Type", a.contentType)
|
||||
}
|
||||
|
||||
// Use a new http.Client with default settings. Consider configuring timeouts
|
||||
// at the caller level via the context, or wiring a custom client if needed.
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read and safely truncate the response body to avoid flooding the agent's
|
||||
// context with very large payloads. Errors on ReadAll are ignored here as
|
||||
// we already have the status code.
|
||||
respBytes, _ := io.ReadAll(resp.Body)
|
||||
respBody := string(respBytes)
|
||||
if len(respBody) > 4096 {
|
||||
respBody = respBody[:4096] + "... (truncated)"
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
// Return the response body as the result.
|
||||
// If the response body is empty, use the status text as the result (e.g. "OK" for status code 200).
|
||||
Result: func() string {
|
||||
if respBody == "" {
|
||||
return http.StatusText(resp.StatusCode)
|
||||
}
|
||||
return respBody
|
||||
}(),
|
||||
// Include the response status code in the metadata.
|
||||
Metadata: map[string]interface{}{
|
||||
"statusCode": resp.StatusCode,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Definition returns the action schema exposed to the planner/runtime.
|
||||
// Only the runtime parameter "payload" is accepted; all connection details
|
||||
// are configured statically via the action configuration UI.
|
||||
func (a *WebhookAction) Definition() types.ActionDefinition {
|
||||
name := "webhook"
|
||||
description := "Send an HTTP request to a configured URL/method/content-type. Accepts a runtime payload parameter optionally inserted into the configured payload template."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
payloadDesc := "Payload/body to send with the request at runtime. If a payloadTemplate is configured, '{{payload}}' will be replaced by this value."
|
||||
if a.customPayloadDescription != "" {
|
||||
payloadDesc = a.customPayloadDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"payload": {
|
||||
Type: jsonschema.String,
|
||||
Description: payloadDesc,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Plannable indicates the action can be suggested/used by planners without
|
||||
// requiring hidden context; inputs are straightforward and safe.
|
||||
func (a *WebhookAction) Plannable() bool { return true }
|
||||
|
||||
// WebhookConfigMeta returns the metadata for Webhook action configuration fields:
|
||||
// - url: The endpoint to send requests to (required).
|
||||
// - method: One of GET/POST/PUT/DELETE. Defaults to POST.
|
||||
// - contentType: Common content types selectable from a dropdown.
|
||||
// - payloadTemplate: Optional body template. At runtime, "{{payload}}" is
|
||||
// replaced by the provided payload parameter. If missing, the template is used
|
||||
// as-is; for GET, no body is sent regardless.
|
||||
func WebhookConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'webhook')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional)",
|
||||
},
|
||||
{
|
||||
Name: "custom_payload_description",
|
||||
Label: "Custom Payload Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Override the payload parameter description shown in the UI/schema (optional).",
|
||||
},
|
||||
{
|
||||
Name: "url",
|
||||
Label: "URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Destination URL for the webhook",
|
||||
},
|
||||
{
|
||||
Name: "method",
|
||||
Label: "HTTP Method",
|
||||
Type: config.FieldTypeSelect,
|
||||
Options: []config.FieldOption{{Value: http.MethodGet, Label: "GET"}, {Value: http.MethodPost, Label: "POST"}, {Value: http.MethodPut, Label: "PUT"}, {Value: http.MethodDelete, Label: "DELETE"}},
|
||||
DefaultValue: http.MethodPost,
|
||||
Required: true,
|
||||
HelpText: "HTTP method to use",
|
||||
},
|
||||
{
|
||||
Name: "contentType",
|
||||
Label: "Content Type",
|
||||
Type: config.FieldTypeSelect,
|
||||
Options: []config.FieldOption{
|
||||
{Value: "application/json", Label: "application/json"},
|
||||
{Value: "text/plain", Label: "text/plain"},
|
||||
{Value: "application/x-www-form-urlencoded", Label: "application/x-www-form-urlencoded"},
|
||||
},
|
||||
Required: true,
|
||||
HelpText: "Content-Type header to send",
|
||||
},
|
||||
{
|
||||
Name: "payloadTemplate",
|
||||
Label: "Payload Template",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Optional template used to craft the request body. Use '{{payload}}' as placeholder for the runtime payload.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func NewWikipedia(config map[string]string) *WikipediaAction {
|
||||
|
||||
type WikipediaAction struct{}
|
||||
|
||||
func (a *WikipediaAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
func (a *WikipediaAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := struct {
|
||||
Query string `json:"query"`
|
||||
}{}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
)
|
||||
|
||||
func memoryPath(agentName string, actionsConfigs map[string]string) string {
|
||||
// Compose memory file path based on stateDir and agentName, using a subdirectory
|
||||
memoryFilePath := "memory.json"
|
||||
if actionsConfigs != nil {
|
||||
if stateDir, ok := actionsConfigs[ConfigStateDir]; ok && stateDir != "" {
|
||||
memoryDir := fmt.Sprintf("%s/memory", stateDir)
|
||||
err := os.MkdirAll(memoryDir, 0755) // ensure the directory exists
|
||||
if err != nil {
|
||||
xlog.Error("Error creating memory directory", "error", err)
|
||||
return memoryFilePath
|
||||
}
|
||||
memoryFilePath = fmt.Sprintf("%s/%s.json", memoryDir, agentName)
|
||||
} else {
|
||||
memoryFilePath = fmt.Sprintf("%s.memory.json", agentName)
|
||||
}
|
||||
}
|
||||
|
||||
return memoryFilePath
|
||||
}
|
||||
@@ -19,6 +19,8 @@ const (
|
||||
ConnectorGithubIssues = "github-issues"
|
||||
ConnectorGithubPRs = "github-prs"
|
||||
ConnectorTwitter = "twitter"
|
||||
ConnectorMatrix = "matrix"
|
||||
ConnectorEmail = "email"
|
||||
)
|
||||
|
||||
var AvailableConnectors = []string{
|
||||
@@ -29,6 +31,8 @@ var AvailableConnectors = []string{
|
||||
ConnectorGithubIssues,
|
||||
ConnectorGithubPRs,
|
||||
ConnectorTwitter,
|
||||
ConnectorMatrix,
|
||||
ConnectorEmail,
|
||||
}
|
||||
|
||||
func Connectors(a *state.AgentConfig) []state.Connector {
|
||||
@@ -66,6 +70,10 @@ func Connectors(a *state.AgentConfig) []state.Connector {
|
||||
continue
|
||||
}
|
||||
conns = append(conns, cc)
|
||||
case ConnectorMatrix:
|
||||
conns = append(conns, connectors.NewMatrix(config))
|
||||
case ConnectorEmail:
|
||||
conns = append(conns, connectors.NewEmail(config))
|
||||
}
|
||||
}
|
||||
return conns
|
||||
@@ -108,5 +116,15 @@ func ConnectorsConfigMeta() []config.FieldGroup {
|
||||
Label: "Twitter",
|
||||
Fields: connectors.TwitterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "matrix",
|
||||
Label: "Matrix",
|
||||
Fields: connectors.MatrixConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "email",
|
||||
Label: "Email",
|
||||
Fields: connectors.EmailConfigMeta(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user