Compare commits
142 Commits
intel-imag
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5697ff62c6 | ||
|
|
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 | ||
|
|
0fda6e38db | ||
|
|
bffb5bd852 | ||
|
|
4d722c35d3 | ||
|
|
8dd0c3883b | ||
|
|
c2ec333777 | ||
|
|
2f19feff5e |
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -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"
|
||||||
2
.github/workflows/goreleaser.yml
vendored
2
.github/workflows/goreleaser.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.22
|
go-version: 1.24
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
153
.github/workflows/image.yml
vendored
153
.github/workflows/image.yml
vendored
@@ -3,7 +3,7 @@ name: 'build container images'
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@2a4836ac76fe8f5d0ee3a0d89aa12a80cc552ad3
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804
|
||||||
with:
|
with:
|
||||||
images: quay.io/mudler/localagi
|
images: quay.io/mudler/localagi
|
||||||
tags: |
|
tags: |
|
||||||
@@ -78,8 +78,153 @@ jobs:
|
|||||||
VERSION=${{ steps.prep.outputs.binary_version }}
|
VERSION=${{ steps.prep.outputs.binary_version }}
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile.webui
|
file: ./Dockerfile.webui
|
||||||
#platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
platforms: linux/amd64
|
push: true
|
||||||
|
#tags: ${{ steps.prep.outputs.tags }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
mcpbox-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
DOCKER_IMAGE=quay.io/mudler/localagi-mcpbox
|
||||||
|
# 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-mcpbox
|
||||||
|
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.mcpbox
|
||||||
|
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
|
push: true
|
||||||
#tags: ${{ steps.prep.outputs.tags }}
|
#tags: ${{ steps.prep.outputs.tags }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@@ -3,7 +3,7 @@ name: Run Go Tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- 'main'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
# Add Docker's official GPG key:
|
# Add Docker's official GPG key:
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -30,16 +30,24 @@ jobs:
|
|||||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make
|
||||||
docker version
|
docker version
|
||||||
|
|
||||||
docker run --rm hello-world
|
docker run --rm hello-world
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.17.0'
|
go-version: '>=1.17.0'
|
||||||
|
- name: Free up disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo apt-get clean
|
||||||
|
docker system prune -af || true
|
||||||
|
df -h
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y make
|
|
||||||
make tests
|
make tests
|
||||||
#sudo mv coverage/coverage.txt coverage.txt
|
#sudo mv coverage/coverage.txt coverage.txt
|
||||||
#sudo chmod 777 coverage.txt
|
#sudo chmod 777 coverage.txt
|
||||||
|
|||||||
49
Dockerfile.mcpbox
Normal file
49
Dockerfile.mcpbox
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
#RUN adduser -D -g '' appuser
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binary from builder
|
||||||
|
COPY --from=builder /app/mcpbox .
|
||||||
|
|
||||||
|
# Use non-root user
|
||||||
|
#USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set entrypoint
|
||||||
|
ENTRYPOINT ["/app/mcpbox"]
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["-addr", ":8080"]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# python
|
# python
|
||||||
FROM python:3.10-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt-get update && apt-get install -y python3-dev portaudio19-dev ffmpeg build-essential
|
RUN apt-get update && apt-get install -y python3-dev portaudio19-dev ffmpeg build-essential
|
||||||
|
|||||||
46
Dockerfile.sshbox
Normal file
46
Dockerfile.sshbox
Normal file
@@ -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"]
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
# Define argument for linker flags
|
|
||||||
ARG LDFLAGS=-s -w
|
|
||||||
|
|
||||||
# Use Bun container for building the React UI
|
# 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
|
# Set the working directory for the React UI
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -19,11 +16,11 @@ COPY webui/react-ui/ ./
|
|||||||
# Build the React UI
|
# Build the React UI
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# Use a temporary build image based on Golang 1.22-alpine
|
# Use a temporary build image based on Golang 1.24-alpine
|
||||||
FROM golang:1.22-alpine as builder
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
# Set environment variables: linker flags and disable CGO
|
# Define argument for linker flags
|
||||||
ENV LDFLAGS=$LDFLAGS CGO_ENABLED=0
|
ARG LDFLAGS="-s -w"
|
||||||
|
|
||||||
# Install git
|
# Install git
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
@@ -45,7 +42,7 @@ COPY . .
|
|||||||
COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
|
COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
|
||||||
|
|
||||||
# Build the application
|
# Build the application
|
||||||
RUN go build -ldflags="$LDFLAGS" -o localagi ./
|
RUN CGO_ENABLED=0 go build -ldflags="$LDFLAGS" -o localagi ./
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
|
|||||||
16
Makefile
16
Makefile
@@ -1,15 +1,17 @@
|
|||||||
GOCMD?=go
|
GOCMD?=go
|
||||||
IMAGE_NAME?=webui
|
IMAGE_NAME?=webui
|
||||||
|
MCPBOX_IMAGE_NAME?=mcpbox
|
||||||
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
|
||||||
prepare-tests:
|
prepare-tests: build-mcpbox
|
||||||
docker compose up -d
|
docker compose up -d --build
|
||||||
|
docker run -d -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 --rm -ti $(MCPBOX_IMAGE_NAME)
|
||||||
|
|
||||||
cleanup-tests:
|
cleanup-tests:
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
tests: prepare-tests
|
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_MCPBOX_URL="http://localhost:9090" 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 --fail-fast -v -r ./...
|
||||||
|
|
||||||
run-nokb:
|
run-nokb:
|
||||||
$(MAKE) run KBDISABLEINDEX=true
|
$(MAKE) run KBDISABLEINDEX=true
|
||||||
@@ -23,10 +25,16 @@ build: webui/react-ui/dist
|
|||||||
|
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: webui/react-ui/dist
|
run: webui/react-ui/dist
|
||||||
$(GOCMD) run ./
|
LOCALAGI_MCPBOX_URL="http://localhost:9090" $(GOCMD) run ./
|
||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
docker build -t $(IMAGE_NAME) -f Dockerfile.webui .
|
docker build -t $(IMAGE_NAME) -f Dockerfile.webui .
|
||||||
|
|
||||||
image-push:
|
image-push:
|
||||||
docker push $(IMAGE_NAME)
|
docker push $(IMAGE_NAME)
|
||||||
|
|
||||||
|
build-mcpbox:
|
||||||
|
docker build -t $(MCPBOX_IMAGE_NAME) -f Dockerfile.mcpbox .
|
||||||
|
|
||||||
|
run-mcpbox:
|
||||||
|
docker run -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 -ti mcpbox
|
||||||
417
README.md
417
README.md
@@ -1,8 +1,8 @@
|
|||||||
<p align="center">
|
<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>
|
</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">
|
<div align="center">
|
||||||
|
|
||||||
@@ -11,11 +11,14 @@
|
|||||||
[](https://github.com/mudler/LocalAGI/stargazers)
|
[](https://github.com/mudler/LocalAGI/stargazers)
|
||||||
[](https://github.com/mudler/LocalAGI/issues)
|
[](https://github.com/mudler/LocalAGI/issues)
|
||||||
|
|
||||||
|
|
||||||
|
Try on [](https://t.me/LocalAGI_bot)
|
||||||
|
|
||||||
</div>
|
</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
|
## 🛡️ Take Back Your Privacy
|
||||||
|
|
||||||
@@ -30,13 +33,14 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
|||||||
- 🤖 **Advanced Agent Teaming**: Instantly create cooperative agent teams from a single prompt.
|
- 🤖 **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 Galore**: 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.
|
- 🛠 **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 [LocalRAG](https://github.com/mudler/LocalRAG).
|
- 📚 **Short & Long-Term Memory**: Powered by [LocalRecall](https://github.com/mudler/LocalRecall).
|
||||||
- 🧠 **Planning & Reasoning**: Agents intelligently plan, reason, and adapt.
|
- 🧠 **Planning & Reasoning**: Agents intelligently plan, reason, and adapt.
|
||||||
- 🔄 **Periodic Tasks**: Schedule tasks with cron-like syntax.
|
- 🔄 **Periodic Tasks**: Schedule tasks with cron-like syntax.
|
||||||
- 💾 **Memory Management**: Control memory usage with options for long-term and summary memory.
|
- 💾 **Memory Management**: Control memory usage with options for long-term and summary memory.
|
||||||
- 🖼 **Multimodal Support**: Ready for vision, text, and more.
|
- 🖼 **Multimodal Support**: Ready for vision, text, and more.
|
||||||
- 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!).
|
- 🔧 **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).
|
- 🛠 **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
|
## 🛠️ Quickstart
|
||||||
|
|
||||||
@@ -45,14 +49,139 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
|||||||
git clone https://github.com/mudler/LocalAGI
|
git clone https://github.com/mudler/LocalAGI
|
||||||
cd LocalAGI
|
cd LocalAGI
|
||||||
|
|
||||||
# CPU setup
|
# CPU setup (default)
|
||||||
docker compose up -f docker-compose.yml
|
docker compose up
|
||||||
|
|
||||||
# GPU setup
|
# NVIDIA GPU setup
|
||||||
docker compose up -f docker-compose.gpu.yml
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
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?
|
## 🏆 Why Choose LocalAGI?
|
||||||
|
|
||||||
@@ -62,21 +191,15 @@ Access your agents at `http://localhost:3000`
|
|||||||
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
|
- **✓ 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.
|
- **✓ 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.
|
|
||||||
- [**LocalRAG**](https://github.com/mudler/LocalRAG): Retrieval-Augmented Generation with local storage.
|
|
||||||
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
|
|
||||||
|
|
||||||
## 🌟 Screenshots
|
## 🌟 Screenshots
|
||||||
|
|
||||||
### Powerful Web UI
|
### Powerful Web UI
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
### Connectors Ready-to-Go
|
### Connectors Ready-to-Go
|
||||||
|
|
||||||
@@ -98,6 +221,8 @@ Explore detailed documentation including:
|
|||||||
|
|
||||||
### Environment Configuration
|
### 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 |
|
| Variable | What It Does |
|
||||||
|----------|--------------|
|
|----------|--------------|
|
||||||
| `LOCALAGI_MODEL` | Your go-to model |
|
| `LOCALAGI_MODEL` | Your go-to model |
|
||||||
@@ -106,7 +231,7 @@ Explore detailed documentation including:
|
|||||||
| `LOCALAGI_LLM_API_KEY` | API authentication |
|
| `LOCALAGI_LLM_API_KEY` | API authentication |
|
||||||
| `LOCALAGI_TIMEOUT` | Request timeout settings |
|
| `LOCALAGI_TIMEOUT` | Request timeout settings |
|
||||||
| `LOCALAGI_STATE_DIR` | Where state gets stored |
|
| `LOCALAGI_STATE_DIR` | Where state gets stored |
|
||||||
| `LOCALAGI_LOCALRAG_URL` | LocalRAG connection |
|
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
|
||||||
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||||
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||||
|
|
||||||
@@ -137,6 +262,158 @@ go build -o localagi
|
|||||||
./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
|
||||||
|
"", // MCP box URL (optional)
|
||||||
|
"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>
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
|
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
|
||||||
@@ -150,7 +427,7 @@ cd LocalAGI
|
|||||||
cd webui/react-ui && bun i && bun run dev
|
cd webui/react-ui && bun i && bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Then in seperate terminal:
|
Then in separate terminal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start development server
|
# Start development server
|
||||||
@@ -163,7 +440,8 @@ cd ../.. && go run main.go
|
|||||||
|
|
||||||
Link your agents to the services you already use. Configuration examples below.
|
Link your agents to the services you already use. Configuration examples below.
|
||||||
|
|
||||||
### GitHub Issues
|
<details>
|
||||||
|
<summary><strong>GitHub Issues</strong></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -173,8 +451,10 @@ Link your agents to the services you already use. Configuration examples below.
|
|||||||
"botUserName": "bot-username"
|
"botUserName": "bot-username"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### Discord
|
<details>
|
||||||
|
<summary><strong>Discord</strong></summary>
|
||||||
|
|
||||||
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
|
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
|
||||||
|
|
||||||
@@ -186,8 +466,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!
|
> Don't forget to enable "Message Content Intent" in Bot(tab) settings!
|
||||||
> Enable " Message Content Intent " in the Bot tab!
|
> 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:
|
Use the included `slack.yaml` manifest to create your app, then configure:
|
||||||
|
|
||||||
@@ -200,19 +482,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 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 ))
|
- Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
### Telegram
|
<summary><strong>Telegram</strong></summary>
|
||||||
|
|
||||||
Get a token from @botfather, then:
|
Get a token from @botfather, then:
|
||||||
|
|
||||||
```json
|
```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:
|
Connect to IRC networks:
|
||||||
|
|
||||||
@@ -225,10 +527,29 @@ Connect to IRC networks:
|
|||||||
"alwaysReply": "false"
|
"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
|
## REST API
|
||||||
|
|
||||||
### Agent Management
|
<details>
|
||||||
|
<summary><strong>Agent Management</strong></summary>
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -243,8 +564,10 @@ Connect to IRC networks:
|
|||||||
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
|
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
|
||||||
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
|
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
|
||||||
| `/settings/import` | POST | Import agent config | [Example](#import-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 |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -252,8 +575,10 @@ Connect to IRC networks:
|
|||||||
| `/api/action/:name/run` | POST | Execute an action | |
|
| `/api/action/:name/run` | POST | Execute an action | |
|
||||||
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
|
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
|
||||||
| `/api/agent/group/create` | POST | Create a new agent group | |
|
| `/api/agent/group/create` | POST | Create a new agent group | |
|
||||||
|
</details>
|
||||||
|
|
||||||
### Chat Interactions
|
<details>
|
||||||
|
<summary><strong>Chat Interactions</strong></summary>
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -261,6 +586,7 @@ Connect to IRC networks:
|
|||||||
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
|
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
|
||||||
| `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) |
|
| `/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) |
|
| `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) |
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Curl Examples</strong></summary>
|
<summary><strong>Curl Examples</strong></summary>
|
||||||
@@ -348,11 +674,13 @@ curl -X POST "http://localhost:3000/api/notify/my-agent" \
|
|||||||
curl -N -X GET "http://localhost:3000/api/sse/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.
|
Note: For proper SSE handling, you should use a client that supports SSE natively.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Agent Configuration Reference
|
### 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:
|
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
|
```bash
|
||||||
@@ -385,6 +713,27 @@ Here's an example of the agent configuration structure:
|
|||||||
"summary_long_term_memory": false
|
"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_MCPBOX_URL` | LocalAGI MCPBox URL, e.g. http://mcpbox:8080 |
|
||||||
|
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||||
|
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||||
|
</details>
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
|
|||||||
38
cmd/mcpbox/main.go
Normal file
38
cmd/mcpbox/main.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/pkg/stdio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Parse command line flags
|
||||||
|
addr := flag.String("addr", ":8080", "HTTP server address")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Create and start the server
|
||||||
|
server := stdio.NewServer()
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("Starting server on %s", *addr)
|
||||||
|
if err := server.Start(*addr); err != nil {
|
||||||
|
log.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for shutdown signal
|
||||||
|
<-sigChan
|
||||||
|
log.Println("Shutting down server...")
|
||||||
|
|
||||||
|
// TODO: Implement graceful shutdown if needed
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ func (a *CustomAction) Plannable() bool {
|
|||||||
return true
|
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"]))
|
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, err
|
return types.ActionResult{}, err
|
||||||
@@ -95,6 +95,11 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
|
|||||||
|
|
||||||
func (a *CustomAction) Definition() types.ActionDefinition {
|
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"]))
|
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error getting custom action definition", "error", err)
|
xlog.Error("Error getting custom action definition", "error", err)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ return []string{"foo"}
|
|||||||
Description: "A test action",
|
Description: "A test action",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
runResult, err := customAction.Run(context.Background(), types.ActionParams{
|
runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
|
||||||
"Foo": "bar",
|
"Foo": "bar",
|
||||||
})
|
})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|||||||
48
core/action/goal.go
Normal file
48
core/action/goal.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGoal 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 NewGoal() *GoalAction {
|
||||||
|
return &GoalAction{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalAction struct {
|
||||||
|
}
|
||||||
|
type GoalResponse struct {
|
||||||
|
Goal string `json:"goal"`
|
||||||
|
Achieved bool `json:"achieved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GoalAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
|
return types.ActionResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GoalAction) Plannable() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GoalAction) Definition() types.ActionDefinition {
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: "goal",
|
||||||
|
Description: "Check if the goal is achieved",
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"goal": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The goal to check if it is achieved.",
|
||||||
|
},
|
||||||
|
"achieved": {
|
||||||
|
Type: jsonschema.Boolean,
|
||||||
|
Description: "Whether the goal is achieved",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"goal", "achieved"},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ type IntentResponse struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *IntentAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type ConversationActionResponse struct {
|
|||||||
Message string `json:"message"`
|
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
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func NewStop() *StopAction {
|
|||||||
|
|
||||||
type StopAction struct{}
|
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
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type PlanSubtask struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *PlanAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func (a *PlanAction) Plannable() bool {
|
|||||||
func (a *PlanAction) Definition() types.ActionDefinition {
|
func (a *PlanAction) Definition() types.ActionDefinition {
|
||||||
return types.ActionDefinition{
|
return types.ActionDefinition{
|
||||||
Name: PlanActionName,
|
Name: PlanActionName,
|
||||||
Description: "Use this tool for solving complex tasks that involves calling more tools in sequence.",
|
Description: "Use it for situations that involves doing more actions in sequence.",
|
||||||
Properties: map[string]jsonschema.Definition{
|
Properties: map[string]jsonschema.Definition{
|
||||||
"subtasks": {
|
"subtasks": {
|
||||||
Type: jsonschema.Array,
|
Type: jsonschema.Array,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type ReasoningResponse struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
193
core/action/reminder.go
Normal file
193
core/action/reminder.go
Normal file
@@ -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"},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ type ReplyResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) {
|
func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) {
|
||||||
return "no-op", nil
|
return "no-op", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package action
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
@@ -16,25 +15,7 @@ func NewState() *StateAction {
|
|||||||
|
|
||||||
type StateAction struct{}
|
type StateAction struct{}
|
||||||
|
|
||||||
// State is the structure
|
func (a *StateAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
// 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) {
|
|
||||||
return types.ActionResult{Result: "internal state has been updated"}, nil
|
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/action"
|
"github.com/mudler/LocalAGI/core/action"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
@@ -12,39 +13,98 @@ import (
|
|||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const parameterReasoningPrompt = `You are tasked with generating the optimal parameters for the action "%s". The action requires the following parameters:
|
||||||
|
%s
|
||||||
|
|
||||||
|
Your task is to:
|
||||||
|
1. Generate the best possible values for each required parameter
|
||||||
|
2. If the parameter requires code, provide complete, working code
|
||||||
|
3. If the parameter requires text or documentation, provide comprehensive, well-structured content
|
||||||
|
4. Ensure all parameters are complete and ready to be used
|
||||||
|
|
||||||
|
Focus on quality and completeness. Do not explain your reasoning or analyze the action's purpose - just provide the best possible parameter values.`
|
||||||
|
|
||||||
type decisionResult struct {
|
type decisionResult struct {
|
||||||
actionParams types.ActionParams
|
actionParams types.ActionParams
|
||||||
message string
|
message string
|
||||||
actioName string
|
actionName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// decision forces the agent to take one of the available actions
|
// decision forces the agent to take one of the available actions
|
||||||
func (a *Agent) decision(
|
func (a *Agent) decision(
|
||||||
ctx context.Context,
|
job *types.Job,
|
||||||
conversation []openai.ChatCompletionMessage,
|
conversation []openai.ChatCompletionMessage,
|
||||||
tools []openai.Tool, toolchoice any, maxRetries int) (*decisionResult, error) {
|
tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) {
|
||||||
|
|
||||||
|
var choice *openai.ToolChoice
|
||||||
|
|
||||||
|
if toolchoice != "" {
|
||||||
|
choice = &openai.ToolChoice{
|
||||||
|
Type: openai.ToolTypeFunction,
|
||||||
|
Function: openai.ToolFunction{Name: toolchoice},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decision := openai.ChatCompletionRequest{
|
||||||
|
Model: a.options.LLMAPI.Model,
|
||||||
|
Messages: conversation,
|
||||||
|
Tools: tools,
|
||||||
|
}
|
||||||
|
|
||||||
|
if choice != nil {
|
||||||
|
decision.ToolChoice = *choice
|
||||||
|
}
|
||||||
|
|
||||||
|
var obs *types.Observable
|
||||||
|
if job.Obs != nil {
|
||||||
|
obs = a.observer.NewObservable()
|
||||||
|
obs.Name = "decision"
|
||||||
|
obs.ParentID = job.Obs.ID
|
||||||
|
obs.Icon = "brain"
|
||||||
|
obs.Creation = &types.Creation{
|
||||||
|
ChatCompletionRequest: &decision,
|
||||||
|
}
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for attempts := 0; attempts < maxRetries; attempts++ {
|
for attempts := 0; attempts < maxRetries; attempts++ {
|
||||||
decision := openai.ChatCompletionRequest{
|
resp, err := a.client.CreateChatCompletion(job.GetContext(), decision)
|
||||||
Model: a.options.LLMAPI.Model,
|
|
||||||
Messages: conversation,
|
|
||||||
Tools: tools,
|
|
||||||
ToolChoice: toolchoice,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := a.client.CreateChatCompletion(ctx, decision)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err)
|
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", err)
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.Progress = append(obs.Progress, types.Progress{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jsonResp, _ := json.Marshal(resp)
|
||||||
|
xlog.Debug("Decision response", "response", string(jsonResp))
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.AddProgress(types.Progress{
|
||||||
|
ChatCompletionResponse: &resp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if len(resp.Choices) != 1 {
|
if len(resp.Choices) != 1 {
|
||||||
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
|
lastErr = fmt.Errorf("no choices: %d", len(resp.Choices))
|
||||||
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr)
|
xlog.Warn("Attempt to make a decision failed", "attempt", attempts+1, "error", lastErr)
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.Progress[len(obs.Progress)-1].Error = lastErr.Error()
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +113,12 @@ func (a *Agent) decision(
|
|||||||
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
|
if err := a.saveConversation(append(conversation, msg), "decision"); err != nil {
|
||||||
xlog.Error("Error saving conversation", "error", err)
|
xlog.Error("Error saving conversation", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.MakeLastProgressCompletion()
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
return &decisionResult{message: msg.Content}, nil
|
return &decisionResult{message: msg.Content}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +126,12 @@ func (a *Agent) decision(
|
|||||||
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
|
if err := params.Read(msg.ToolCalls[0].Function.Arguments); err != nil {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err)
|
xlog.Warn("Attempt to parse action parameters failed", "attempt", attempts+1, "error", err)
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.Progress[len(obs.Progress)-1].Error = lastErr.Error()
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +139,12 @@ func (a *Agent) decision(
|
|||||||
xlog.Error("Error saving conversation", "error", err)
|
xlog.Error("Error saving conversation", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
if obs != nil {
|
||||||
|
obs.MakeLastProgressCompletion()
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &decisionResult{actionParams: params, actionName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
|
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
|
||||||
@@ -79,6 +156,15 @@ func (m Messages) ToOpenAI() []openai.ChatCompletionMessage {
|
|||||||
return []openai.ChatCompletionMessage(m)
|
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 {
|
func (m Messages) String() string {
|
||||||
s := ""
|
s := ""
|
||||||
for _, cc := range m {
|
for _, cc := range m {
|
||||||
@@ -149,7 +235,15 @@ func (m Messages) IsLastMessageFromRole(role string) bool {
|
|||||||
return m[len(m)-1].Role == role
|
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) {
|
func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
|
||||||
|
|
||||||
|
if len(act.Definition().Properties) > 0 {
|
||||||
|
xlog.Debug("Action has properties", "action", act.Definition().Name, "properties", act.Definition().Properties)
|
||||||
|
} else {
|
||||||
|
xlog.Debug("Action has no properties", "action", act.Definition().Name)
|
||||||
|
return &decisionResult{actionParams: types.ActionParams{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -167,9 +261,32 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act
|
|||||||
|
|
||||||
cc := conversation
|
cc := conversation
|
||||||
if a.options.forceReasoning {
|
if a.options.forceReasoning {
|
||||||
|
// First, get the LLM to reason about optimal parameter usage
|
||||||
|
parameterReasoningPrompt := fmt.Sprintf(parameterReasoningPrompt,
|
||||||
|
act.Definition().Name,
|
||||||
|
formatProperties(act.Definition().Properties))
|
||||||
|
|
||||||
|
// Get initial reasoning about parameters using askLLM
|
||||||
|
paramReasoningMsg, err := a.askLLM(job.GetContext(),
|
||||||
|
append(conversation, openai.ChatCompletionMessage{
|
||||||
|
Role: "system",
|
||||||
|
Content: parameterReasoningPrompt,
|
||||||
|
}),
|
||||||
|
maxAttempts,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Warn("Failed to get parameter reasoning", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine original reasoning with parameter-specific reasoning
|
||||||
|
enhancedReasoning := reasoning
|
||||||
|
if paramReasoningMsg.Content != "" {
|
||||||
|
enhancedReasoning = fmt.Sprintf("%s\n\nParameter Analysis:\n%s", reasoning, paramReasoningMsg.Content)
|
||||||
|
}
|
||||||
|
|
||||||
cc = append(conversation, openai.ChatCompletionMessage{
|
cc = append(conversation, openai.ChatCompletionMessage{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, reasoning),
|
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, enhancedReasoning),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +294,10 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act
|
|||||||
var attemptErr error
|
var attemptErr error
|
||||||
|
|
||||||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
for attempts := 0; attempts < maxAttempts; attempts++ {
|
||||||
result, attemptErr = a.decision(ctx,
|
result, attemptErr = a.decision(job,
|
||||||
cc,
|
cc,
|
||||||
a.availableActions().ToTools(),
|
a.availableActions().ToTools(),
|
||||||
openai.ToolChoice{
|
act.Definition().Name.String(),
|
||||||
Type: openai.ToolTypeFunction,
|
|
||||||
Function: openai.ToolFunction{Name: act.Definition().Name.String()},
|
|
||||||
},
|
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
)
|
)
|
||||||
if attemptErr == nil && result.actionParams != nil {
|
if attemptErr == nil && result.actionParams != nil {
|
||||||
@@ -195,6 +309,15 @@ func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act
|
|||||||
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
|
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to format properties for the prompt
|
||||||
|
func formatProperties(props map[string]jsonschema.Definition) string {
|
||||||
|
var result strings.Builder
|
||||||
|
for name, prop := range props {
|
||||||
|
result.WriteString(fmt.Sprintf("- %s: %s\n", name, prop.Description))
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) {
|
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
|
// Planning: run all the actions in sequence
|
||||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||||
@@ -242,8 +365,9 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
|
|||||||
subTaskAction := a.availableActions().Find(subtask.Action)
|
subTaskAction := a.availableActions().Find(subtask.Action)
|
||||||
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
|
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
|
||||||
|
|
||||||
params, err := a.generateParameters(ctx, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
params, err := a.generateParameters(job, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
xlog.Error("error generating action's parameters", "error", err)
|
||||||
return conv, fmt.Errorf("error generating action's parameters: %w", err)
|
return conv, fmt.Errorf("error generating action's parameters: %w", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -271,8 +395,9 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := a.runAction(ctx, subTaskAction, actionParams)
|
result, err := a.runAction(job, subTaskAction, actionParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
xlog.Error("error running action", "error", err)
|
||||||
return conv, fmt.Errorf("error running action: %w", err)
|
return conv, fmt.Errorf("error running action: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,27 +480,32 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pickAction picks an action based on the conversation
|
// 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) {
|
func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
|
||||||
c := messages
|
c := messages
|
||||||
|
|
||||||
|
xlog.Debug("[pickAction] picking action starts", "messages", messages)
|
||||||
|
|
||||||
|
// Identify the goal of this conversation
|
||||||
|
|
||||||
if !a.options.forceReasoning {
|
if !a.options.forceReasoning {
|
||||||
|
xlog.Debug("not forcing reasoning")
|
||||||
// We also could avoid to use functions here and get just a reply from the LLM
|
// 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
|
// and then use the reply to get the action
|
||||||
thought, err := a.decision(ctx,
|
thought, err := a.decision(job,
|
||||||
messages,
|
messages,
|
||||||
a.availableActions().ToTools(),
|
a.availableActions().ToTools(),
|
||||||
nil,
|
"",
|
||||||
maxRetries)
|
maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug(fmt.Sprintf("thought action Name: %v", thought.actioName))
|
xlog.Debug("thought action Name", "actionName", thought.actionName)
|
||||||
xlog.Debug(fmt.Sprintf("thought message: %v", thought.message))
|
xlog.Debug("thought message", "message", thought.message)
|
||||||
|
|
||||||
// Find the action
|
// Find the action
|
||||||
chosenAction := a.availableActions().Find(thought.actioName)
|
chosenAction := a.availableActions().Find(thought.actionName)
|
||||||
if chosenAction == nil || thought.actioName == "" {
|
if chosenAction == nil || thought.actionName == "" {
|
||||||
xlog.Debug("no answer")
|
xlog.Debug("no answer")
|
||||||
|
|
||||||
// LLM replied with an answer?
|
// LLM replied with an answer?
|
||||||
@@ -386,6 +516,9 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
|
|||||||
return chosenAction, thought.actionParams, thought.message, nil
|
return chosenAction, thought.actionParams, thought.message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force the LLM to think and we extract a "reasoning" to pick a specific action and with which parameters
|
||||||
|
xlog.Debug("[pickAction] forcing reasoning")
|
||||||
|
|
||||||
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
|
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", err
|
return nil, nil, "", err
|
||||||
@@ -401,67 +534,91 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
|
|||||||
}, c...)
|
}, c...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We also could avoid to use functions here and get just a reply from the LLM
|
// Create a detailed prompt for reasoning that includes available actions and their properties
|
||||||
// and then use the reply to get the action
|
reasoningPrompt := "Analyze the current situation and determine the best course of action. Consider the following:\n\n"
|
||||||
thought, err := a.decision(ctx,
|
reasoningPrompt += "Available Actions:\n"
|
||||||
c,
|
for _, act := range a.availableActions() {
|
||||||
types.Actions{action.NewReasoning()}.ToTools(),
|
reasoningPrompt += fmt.Sprintf("- %s: %s\n", act.Definition().Name, act.Definition().Description)
|
||||||
action.NewReasoning().Definition().Name, maxRetries)
|
if len(act.Definition().Properties) > 0 {
|
||||||
if err != nil {
|
reasoningPrompt += " Properties:\n"
|
||||||
return nil, nil, "", err
|
for name, prop := range act.Definition().Properties {
|
||||||
}
|
reasoningPrompt += fmt.Sprintf(" - %s: %s\n", name, prop.Description)
|
||||||
reason := ""
|
}
|
||||||
response := &action.ReasoningResponse{}
|
|
||||||
if thought.actionParams != nil {
|
|
||||||
if err := thought.actionParams.Unmarshal(response); err != nil {
|
|
||||||
return nil, nil, "", err
|
|
||||||
}
|
}
|
||||||
reason = response.Reasoning
|
reasoningPrompt += "\n"
|
||||||
}
|
}
|
||||||
if thought.message != "" {
|
reasoningPrompt += "\nProvide a detailed reasoning about what action would be most appropriate in this situation and why. You can also just reply with a simple message by choosing the 'reply' or 'answer' action."
|
||||||
reason = thought.message
|
|
||||||
|
// Get reasoning using askLLM
|
||||||
|
reasoningMsg, err := a.askLLM(job.GetContext(),
|
||||||
|
append(c, openai.ChatCompletionMessage{
|
||||||
|
Role: "system",
|
||||||
|
Content: reasoningPrompt,
|
||||||
|
}),
|
||||||
|
maxRetries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "", fmt.Errorf("failed to get reasoning: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// From the thought, get the action call
|
originalReasoning := reasoningMsg.Content
|
||||||
// Get all the available actions IDs
|
|
||||||
actionsID := []string{}
|
xlog.Debug("[pickAction] picking action", "messages", c)
|
||||||
|
|
||||||
|
actionsID := []string{"reply"}
|
||||||
for _, m := range a.availableActions() {
|
for _, m := range a.availableActions() {
|
||||||
actionsID = append(actionsID, m.Definition().Name.String())
|
actionsID = append(actionsID, m.Definition().Name.String())
|
||||||
}
|
}
|
||||||
intentionsTools := action.NewIntention(actionsID...)
|
|
||||||
|
|
||||||
//XXX: Why we add the reason here?
|
xlog.Debug("[pickAction] actionsID", "actionsID", actionsID)
|
||||||
params, err := a.decision(ctx,
|
|
||||||
|
intentionsTools := action.NewIntention(actionsID...)
|
||||||
|
// TODO: FORCE to select ana ction here
|
||||||
|
// NOTE: we do not give the full conversation here to pick the action
|
||||||
|
// to avoid hallucinations
|
||||||
|
|
||||||
|
// Extract an action
|
||||||
|
params, err := a.decision(job,
|
||||||
append(c, openai.ChatCompletionMessage{
|
append(c, openai.ChatCompletionMessage{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
Content: "Given the assistant thought, pick the relevant action: " + reason,
|
Content: "Pick the relevant action given the following reasoning: " + originalReasoning,
|
||||||
}),
|
}),
|
||||||
types.Actions{intentionsTools}.ToTools(),
|
types.Actions{intentionsTools}.ToTools(),
|
||||||
intentionsTools.Definition().Name, maxRetries)
|
intentionsTools.Definition().Name.String(), maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
|
return nil, nil, "", fmt.Errorf("failed to get the action tool parameters: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actionChoice := action.IntentResponse{}
|
|
||||||
|
|
||||||
if params.actionParams == nil {
|
if params.actionParams == nil {
|
||||||
|
xlog.Debug("[pickAction] no action params found")
|
||||||
return nil, nil, params.message, nil
|
return nil, nil, params.message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actionChoice := action.IntentResponse{}
|
||||||
err = params.actionParams.Unmarshal(&actionChoice)
|
err = params.actionParams.Unmarshal(&actionChoice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if actionChoice.Tool == "" || actionChoice.Tool == "none" {
|
if actionChoice.Tool == "" || actionChoice.Tool == "reply" {
|
||||||
return nil, nil, "", fmt.Errorf("no intent detected")
|
xlog.Debug("[pickAction] no action found, replying")
|
||||||
|
return nil, nil, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the action
|
|
||||||
chosenAction := a.availableActions().Find(actionChoice.Tool)
|
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
|
xlog.Debug("[pickAction] chosenAction", "chosenAction", chosenAction, "actionName", actionChoice.Tool)
|
||||||
|
|
||||||
|
// // Let's double check if the action is correct by asking the LLM to judge it
|
||||||
|
|
||||||
|
// if chosenAction!= nil {
|
||||||
|
// promptString:= "Given the following goal and thoughts, is the action correct? \n\n"
|
||||||
|
// promptString+= fmt.Sprintf("Goal: %s\n", goalResponse.Goal)
|
||||||
|
// promptString+= fmt.Sprintf("Thoughts: %s\n", originalReasoning)
|
||||||
|
// promptString+= fmt.Sprintf("Action: %s\n", chosenAction.Definition().Name.String())
|
||||||
|
// promptString+= fmt.Sprintf("Action description: %s\n", chosenAction.Definition().Description)
|
||||||
|
// promptString+= fmt.Sprintf("Action parameters: %s\n", params.actionParams)
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
return chosenAction, nil, originalReasoning, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,6 +15,7 @@ import (
|
|||||||
"github.com/mudler/LocalAGI/core/action"
|
"github.com/mudler/LocalAGI/core/action"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@ type Agent struct {
|
|||||||
jobQueue chan *types.Job
|
jobQueue chan *types.Job
|
||||||
context *types.ActionContext
|
context *types.ActionContext
|
||||||
|
|
||||||
currentState *action.AgentInternalState
|
currentState *types.AgentInternalState
|
||||||
|
|
||||||
selfEvaluationInProgress bool
|
selfEvaluationInProgress bool
|
||||||
pause bool
|
pause bool
|
||||||
@@ -41,6 +45,10 @@ type Agent struct {
|
|||||||
|
|
||||||
subscriberMutex sync.Mutex
|
subscriberMutex sync.Mutex
|
||||||
newMessagesSubscribers []func(openai.ChatCompletionMessage)
|
newMessagesSubscribers []func(openai.ChatCompletionMessage)
|
||||||
|
|
||||||
|
observer Observer
|
||||||
|
|
||||||
|
sharedState *types.AgentSharedState
|
||||||
}
|
}
|
||||||
|
|
||||||
type RAGDB interface {
|
type RAGDB interface {
|
||||||
@@ -69,10 +77,16 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
options: options,
|
options: options,
|
||||||
client: client,
|
client: client,
|
||||||
Character: options.character,
|
Character: options.character,
|
||||||
currentState: &action.AgentInternalState{},
|
currentState: &types.AgentInternalState{},
|
||||||
context: types.NewActionContext(ctx, cancel),
|
context: types.NewActionContext(ctx, cancel),
|
||||||
newConversations: make(chan openai.ChatCompletionMessage),
|
newConversations: make(chan openai.ChatCompletionMessage),
|
||||||
newMessagesSubscribers: options.newConversationsSubscribers,
|
newMessagesSubscribers: options.newConversationsSubscribers,
|
||||||
|
sharedState: types.NewAgentSharedState(options.lastMessageDuration),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize observer if provided
|
||||||
|
if options.observer != nil {
|
||||||
|
a.observer = options.observer
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.options.statefile != "" {
|
if a.options.statefile != "" {
|
||||||
@@ -108,6 +122,10 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Agent) SharedState() *types.AgentSharedState {
|
||||||
|
return a.sharedState
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) startNewConversationsConsumer() {
|
func (a *Agent) startNewConversationsConsumer() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@@ -146,6 +164,14 @@ func (a *Agent) Ask(opts ...types.JobOption) *types.JobResult {
|
|||||||
xlog.Debug("Agent has finished being asked", "agent", a.Character.Name)
|
xlog.Debug("Agent has finished being asked", "agent", a.Character.Name)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if a.observer != nil {
|
||||||
|
obs := a.observer.NewObservable()
|
||||||
|
obs.Name = "job"
|
||||||
|
obs.Icon = "plug"
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
opts = append(opts, types.WithObservable(obs))
|
||||||
|
}
|
||||||
|
|
||||||
return a.Execute(types.NewJob(
|
return a.Execute(types.NewJob(
|
||||||
append(
|
append(
|
||||||
opts,
|
opts,
|
||||||
@@ -163,6 +189,26 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
|
|||||||
xlog.Debug("Agent has finished", "agent", a.Character.Name)
|
xlog.Debug("Agent has finished", "agent", a.Character.Name)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if j.Obs != nil {
|
||||||
|
if len(j.ConversationHistory) > 0 {
|
||||||
|
m := j.ConversationHistory[len(j.ConversationHistory)-1]
|
||||||
|
j.Obs.Creation = &types.Creation{ChatCompletionMessage: &m}
|
||||||
|
a.observer.Update(*j.Obs)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
|
||||||
|
j.Obs.Completion = &types.Completion{
|
||||||
|
Conversation: ccm,
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.Result.Error != nil {
|
||||||
|
j.Obs.Completion.Error = j.Result.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
a.observer.Update(*j.Obs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
a.Enqueue(j)
|
a.Enqueue(j)
|
||||||
return j.Result.WaitResult()
|
return j.Result.WaitResult()
|
||||||
}
|
}
|
||||||
@@ -211,6 +257,7 @@ func (a *Agent) Stop() {
|
|||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
xlog.Debug("Stopping agent", "agent", a.Character.Name)
|
xlog.Debug("Stopping agent", "agent", a.Character.Name)
|
||||||
|
a.closeMCPSTDIOServers()
|
||||||
a.context.Cancel()
|
a.context.Cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,39 +284,90 @@ func (a *Agent) Memory() RAGDB {
|
|||||||
return a.options.ragdb
|
return a.options.ragdb
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) runAction(ctx context.Context, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) {
|
func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) {
|
||||||
|
var obs *types.Observable
|
||||||
|
if job.Obs != nil {
|
||||||
|
obs = a.observer.NewObservable()
|
||||||
|
obs.Name = "action"
|
||||||
|
obs.Icon = "bolt"
|
||||||
|
obs.ParentID = job.Obs.ID
|
||||||
|
obs.Creation = &types.Creation{
|
||||||
|
FunctionDefinition: chosenAction.Definition().ToFunctionDefinition(),
|
||||||
|
FunctionParams: params,
|
||||||
|
}
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Info("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String())
|
||||||
|
|
||||||
for _, act := range a.availableActions() {
|
for _, act := range a.availableActions() {
|
||||||
if act.Definition().Name == chosenAction.Definition().Name {
|
if act.Definition().Name == chosenAction.Definition().Name {
|
||||||
res, err := act.Run(ctx, params)
|
res, err := act.Run(job.GetContext(), a.sharedState, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if obs != nil {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return types.ActionResult{}, fmt.Errorf("error running action: %w", err)
|
return types.ActionResult{}, fmt.Errorf("error running action: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.Progress = append(obs.Progress, types.Progress{
|
||||||
|
ActionResult: res.Result,
|
||||||
|
})
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
result = res
|
result = res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Info("Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name)
|
|
||||||
|
|
||||||
if chosenAction.Definition().Name.Is(action.StateActionName) {
|
if chosenAction.Definition().Name.Is(action.StateActionName) {
|
||||||
// We need to store the result in the state
|
// We need to store the result in the state
|
||||||
state := action.AgentInternalState{}
|
state := types.AgentInternalState{}
|
||||||
|
|
||||||
err = params.Unmarshal(&state)
|
err = params.Unmarshal(&state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("error unmarshalling state of the agent: %w", err)
|
werr := fmt.Errorf("error unmarshalling state of the agent: %w", err)
|
||||||
|
if obs != nil {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
Error: werr.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.ActionResult{}, werr
|
||||||
}
|
}
|
||||||
// update the current state with the one we just got from the action
|
// update the current state with the one we just got from the action
|
||||||
a.currentState = &state
|
a.currentState = &state
|
||||||
|
if obs != nil {
|
||||||
|
obs.Progress = append(obs.Progress, types.Progress{
|
||||||
|
AgentState: &state,
|
||||||
|
})
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
// update the state file
|
// update the state file
|
||||||
if a.options.statefile != "" {
|
if a.options.statefile != "" {
|
||||||
if err := a.SaveState(a.options.statefile); err != nil {
|
if err := a.SaveState(a.options.statefile); err != nil {
|
||||||
|
if obs != nil {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return types.ActionResult{}, err
|
return types.ActionResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result)
|
||||||
|
|
||||||
|
if obs != nil {
|
||||||
|
obs.MakeLastProgressCompletion()
|
||||||
|
a.observer.Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,13 +500,84 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me
|
|||||||
return conv
|
return conv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) consumeJob(job *types.Job, role string) {
|
func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
|
||||||
|
hasTriggers := false
|
||||||
|
triggeredBy := ""
|
||||||
|
failedBy := ""
|
||||||
|
|
||||||
|
if job.DoneFilter {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
job.DoneFilter = true
|
||||||
|
|
||||||
|
if len(a.options.jobFilters) < 1 {
|
||||||
|
xlog.Debug("No filters")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range a.options.jobFilters {
|
||||||
|
name := filter.Name()
|
||||||
|
if triggeredBy != "" && filter.IsTrigger() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = filter.Apply(job)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error in job filter", "filter", name, "error", err)
|
||||||
|
failedBy = name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.IsTrigger() {
|
||||||
|
hasTriggers = true
|
||||||
|
if ok {
|
||||||
|
triggeredBy = name
|
||||||
|
xlog.Info("Job triggered by filter", "filter", name)
|
||||||
|
}
|
||||||
|
} else if !ok {
|
||||||
|
failedBy = name
|
||||||
|
xlog.Info("Job failed filter", "filter", name)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
xlog.Debug("Job passed filter", "filter", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Observer() != nil {
|
||||||
|
obs := a.Observer().NewObservable()
|
||||||
|
obs.Name = "filter"
|
||||||
|
obs.Icon = "shield"
|
||||||
|
obs.ParentID = job.Obs.ID
|
||||||
|
if err == nil {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
FilterResult: &types.FilterResult{
|
||||||
|
HasTriggers: hasTriggers,
|
||||||
|
TriggeredBy: triggeredBy,
|
||||||
|
FailedBy: failedBy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.Observer().Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||||
if err := job.GetContext().Err(); err != nil {
|
if err := job.GetContext().Err(); err != nil {
|
||||||
job.Result.Finish(fmt.Errorf("expired"))
|
job.Result.Finish(fmt.Errorf("expired"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if retries < 1 {
|
||||||
|
job.Result.Finish(fmt.Errorf("Exceeded recursive retries"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
a.Lock()
|
a.Lock()
|
||||||
paused := a.pause
|
paused := a.pause
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
@@ -438,10 +607,18 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conv = a.processPrompts(conv)
|
conv = a.processPrompts(conv)
|
||||||
|
if ok, err := a.filterJob(job); !ok || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("Error in job filter: %w", err))
|
||||||
|
} else {
|
||||||
|
job.Result.Finish(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
conv = a.processUserInputs(job, role, conv)
|
conv = a.processUserInputs(job, role, conv)
|
||||||
|
|
||||||
// RAG
|
// RAG
|
||||||
a.knowledgeBaseLookup(conv)
|
conv = a.knowledgeBaseLookup(job, conv)
|
||||||
|
|
||||||
var pickTemplate string
|
var pickTemplate string
|
||||||
var reEvaluationTemplate string
|
var reEvaluationTemplate string
|
||||||
@@ -466,12 +643,12 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
chosenAction = *action
|
chosenAction = *action
|
||||||
reasoning = reason
|
reasoning = reason
|
||||||
if params == nil {
|
if params == nil {
|
||||||
p, err := a.generateParameters(job.GetContext(), pickTemplate, chosenAction, conv, reasoning, maxRetries)
|
p, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error generating parameters, trying again", "error", err)
|
xlog.Error("Error generating parameters, trying again", "error", err)
|
||||||
// try again
|
// try again
|
||||||
job.SetNextAction(&chosenAction, nil, reasoning)
|
job.SetNextAction(&chosenAction, nil, reasoning)
|
||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role, retries-1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionParams = p.actionParams
|
actionParams = p.actionParams
|
||||||
@@ -481,7 +658,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
job.ResetNextAction()
|
job.ResetNextAction()
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
chosenAction, actionParams, reasoning, err = a.pickAction(job.GetContext(), pickTemplate, conv, maxRetries)
|
chosenAction, actionParams, reasoning, err = a.pickAction(job, pickTemplate, conv, maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error picking action", "error", err)
|
xlog.Error("Error picking action", "error", err)
|
||||||
job.Result.Finish(err)
|
job.Result.Finish(err)
|
||||||
@@ -489,36 +666,44 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the agent is looping over the same action
|
|
||||||
// if so, we need to stop it
|
|
||||||
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
|
|
||||||
count := map[string]int{}
|
|
||||||
for i := len(job.GetPastActions()) - 1; i >= 0; i-- {
|
|
||||||
pastAction := job.GetPastActions()[i]
|
|
||||||
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
|
|
||||||
pastAction.Params.String() == actionParams.String() {
|
|
||||||
count[chosenAction.Definition().Name.String()]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count[chosenAction.Definition().Name.String()] > a.options.loopDetectionSteps {
|
|
||||||
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
|
|
||||||
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning)
|
|
||||||
if chosenAction == nil {
|
if chosenAction == nil {
|
||||||
// If no action was picked up, the reasoning is the message returned by the assistant
|
// If no action was picked up, the reasoning is the message returned by the assistant
|
||||||
// so we can consume it as if it was a reply.
|
// so we can consume it as if it was a reply.
|
||||||
//job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""})
|
|
||||||
//job.Result.Finish(fmt.Errorf("no action to do"))\
|
|
||||||
xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning)
|
xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning)
|
||||||
|
|
||||||
conv = append(conv, openai.ChatCompletionMessage{
|
if reasoning != "" {
|
||||||
Role: "assistant",
|
conv = append(conv, openai.ChatCompletionMessage{
|
||||||
Content: reasoning,
|
Role: "assistant",
|
||||||
})
|
Content: a.cleanupLLMResponse(reasoning),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
xlog.Info("No reasoning, just reply", "agent", a.Character.Name)
|
||||||
|
msg, err := a.askLLM(job.GetContext(), conv, maxRetries)
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg.Content = a.cleanupLLMResponse(msg.Content)
|
||||||
|
conv = append(conv, msg)
|
||||||
|
reasoning = msg.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
var satisfied bool
|
||||||
|
var err error
|
||||||
|
// Evaluate the response
|
||||||
|
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !satisfied {
|
||||||
|
// If not satisfied, continue with the conversation
|
||||||
|
job.ConversationHistory = conv
|
||||||
|
job.IncrementEvaluationLoop()
|
||||||
|
a.consumeJob(job, role, retries)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv))
|
xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv))
|
||||||
job.Result.Conversation = conv
|
job.Result.Conversation = conv
|
||||||
@@ -544,12 +729,12 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
"reasoning", reasoning,
|
"reasoning", reasoning,
|
||||||
)
|
)
|
||||||
|
|
||||||
params, err := a.generateParameters(job.GetContext(), pickTemplate, chosenAction, conv, reasoning, maxRetries)
|
params, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error generating parameters, trying again", "error", err)
|
xlog.Error("Error generating parameters, trying again", "error", err)
|
||||||
// try again
|
// try again
|
||||||
job.SetNextAction(&chosenAction, nil, reasoning)
|
job.SetNextAction(&chosenAction, nil, reasoning)
|
||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role, retries-1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionParams = params.actionParams
|
actionParams = params.actionParams
|
||||||
@@ -569,6 +754,22 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
|
||||||
|
count := 0
|
||||||
|
for _, pastAction := range job.GetPastActions() {
|
||||||
|
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
|
||||||
|
pastAction.Params.String() == actionParams.String() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count > a.options.loopDetectionSteps {
|
||||||
|
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
|
||||||
|
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Debug("Checked for loops", "action", chosenAction.Definition().Name, "count", count)
|
||||||
|
}
|
||||||
|
|
||||||
job.AddPastAction(chosenAction, &actionParams)
|
job.AddPastAction(chosenAction, &actionParams)
|
||||||
|
|
||||||
if !job.Callback(types.ActionCurrentState{
|
if !job.Callback(types.ActionCurrentState{
|
||||||
@@ -592,7 +793,11 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
var err error
|
var err error
|
||||||
conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv)
|
conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
job.Result.Finish(fmt.Errorf("error running action: %w", err))
|
xlog.Error("error handling planning", "error", err)
|
||||||
|
a.reply(job, role, append(conv, openai.ChatCompletionMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: fmt.Sprintf("Error handling planning: %v", err),
|
||||||
|
}), actionParams, chosenAction, reasoning)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,11 +838,8 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||||
result, err := a.runAction(job.GetContext(), chosenAction, actionParams)
|
result, err := a.runAction(job, chosenAction, actionParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//job.Result.Finish(fmt.Errorf("error running action: %w", err))
|
|
||||||
//return
|
|
||||||
// make the LLM aware of the error of running the action instead of stopping the job here
|
|
||||||
result.Result = fmt.Sprintf("Error running tool: %v", err)
|
result.Result = fmt.Sprintf("Error running tool: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,12 +859,8 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
conv = a.addFunctionResultToConversation(chosenAction, actionParams, result, conv)
|
conv = a.addFunctionResultToConversation(chosenAction, actionParams, result, conv)
|
||||||
}
|
}
|
||||||
|
|
||||||
//conv = append(conv, messages...)
|
// given the result, we can now re-evaluate the conversation
|
||||||
//conv = messages
|
followingAction, followingParams, reasoning, err := a.pickAction(job, reEvaluationTemplate, conv, maxRetries)
|
||||||
|
|
||||||
// given the result, we can now ask OpenAI to complete the conversation or
|
|
||||||
// to continue using another tool given the result
|
|
||||||
followingAction, followingParams, reasoning, err := a.pickAction(job.GetContext(), reEvaluationTemplate, conv, maxRetries)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
job.Result.Conversation = conv
|
job.Result.Conversation = conv
|
||||||
job.Result.Finish(fmt.Errorf("error picking action: %w", err))
|
job.Result.Finish(fmt.Errorf("error picking action: %w", err))
|
||||||
@@ -674,67 +872,60 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
!chosenAction.Definition().Name.Is(action.ReplyActionName) {
|
!chosenAction.Definition().Name.Is(action.ReplyActionName) {
|
||||||
|
|
||||||
xlog.Info("Following action", "action", followingAction.Definition().Name, "agent", a.Character.Name)
|
xlog.Info("Following action", "action", followingAction.Definition().Name, "agent", a.Character.Name)
|
||||||
|
job.ConversationHistory = conv
|
||||||
|
|
||||||
// We need to do another action (?)
|
// We need to do another action (?)
|
||||||
// The agent decided to do another action
|
// The agent decided to do another action
|
||||||
// call ourselves again
|
// call ourselves again
|
||||||
job.SetNextAction(&followingAction, &followingParams, reasoning)
|
job.SetNextAction(&followingAction, &followingParams, reasoning)
|
||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role, retries)
|
||||||
return
|
return
|
||||||
} else if followingAction == nil {
|
}
|
||||||
xlog.Info("Not following another action", "agent", a.Character.Name)
|
|
||||||
|
|
||||||
if !a.options.forceReasoning {
|
// Evaluate the final response
|
||||||
xlog.Info("Finish conversation with reasoning", "reasoning", reasoning, "agent", a.Character.Name)
|
var satisfied bool
|
||||||
|
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
msg := openai.ChatCompletionMessage{
|
if !satisfied {
|
||||||
Role: "assistant",
|
// If not satisfied, continue with the conversation
|
||||||
Content: reasoning,
|
job.ConversationHistory = conv
|
||||||
}
|
job.IncrementEvaluationLoop()
|
||||||
|
a.consumeJob(job, role, retries)
|
||||||
conv = append(conv, msg)
|
return
|
||||||
job.Result.SetResponse(msg.Content)
|
|
||||||
job.Result.Conversation = conv
|
|
||||||
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
|
||||||
a.saveCurrentConversation(conv)
|
|
||||||
})
|
|
||||||
job.Result.Finish(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripThinkingTags(content string) string {
|
||||||
|
// Remove content between <thinking> and </thinking> (including multi-line)
|
||||||
|
content = regexp.MustCompile(`(?s)<thinking>.*?</thinking>`).ReplaceAllString(content, "")
|
||||||
|
// Remove content between <think> and </think> (including multi-line)
|
||||||
|
content = regexp.MustCompile(`(?s)<think>.*?</think>`).ReplaceAllString(content, "")
|
||||||
|
// Clean up any extra whitespace
|
||||||
|
content = strings.TrimSpace(content)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) cleanupLLMResponse(content string) string {
|
||||||
|
if a.options.stripThinkingTags {
|
||||||
|
content = stripThinkingTags(content)
|
||||||
|
}
|
||||||
|
// Future post-processing options can be added here
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) {
|
func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) {
|
||||||
job.Result.Conversation = conv
|
job.Result.Conversation = conv
|
||||||
|
|
||||||
// At this point can only be a reply action
|
// At this point can only be a reply action
|
||||||
xlog.Info("Computing reply", "agent", a.Character.Name)
|
xlog.Info("Computing reply", "agent", a.Character.Name)
|
||||||
|
|
||||||
// decode the response
|
forceResponsePrompt := "Reply to the user without using any tools or function calls. Just reply with the message."
|
||||||
replyResponse := action.ReplyResponse{}
|
|
||||||
|
|
||||||
if err := actionParams.Unmarshal(&replyResponse); err != nil {
|
|
||||||
job.Result.Conversation = conv
|
|
||||||
job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have already a reply from the action, just return it.
|
|
||||||
// Otherwise generate a full conversation to get a proper message response
|
|
||||||
// if chosenAction.Definition().Name.Is(action.ReplyActionName) {
|
|
||||||
// replyResponse := action.ReplyResponse{}
|
|
||||||
// if err := params.actionParams.Unmarshal(&replyResponse); err != nil {
|
|
||||||
// job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if replyResponse.Message != "" {
|
|
||||||
// job.Result.SetResponse(replyResponse.Message)
|
|
||||||
// job.Result.Finish(nil)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// If we have a hud, display it when answering normally
|
// If we have a hud, display it when answering normally
|
||||||
if a.options.enableHUD {
|
if a.options.enableHUD {
|
||||||
@@ -750,39 +941,19 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
|
|||||||
Role: "system",
|
Role: "system",
|
||||||
Content: prompt,
|
Content: prompt,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: forceResponsePrompt,
|
||||||
|
},
|
||||||
}, conv...)
|
}, conv...)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
conv = append([]openai.ChatCompletionMessage{
|
||||||
// Generate a human-readable response
|
{
|
||||||
// resp, err := a.client.CreateChatCompletion(ctx,
|
Role: "system",
|
||||||
// openai.ChatCompletionRequest{
|
Content: forceResponsePrompt,
|
||||||
// Model: a.options.LLMAPI.Model,
|
},
|
||||||
// Messages: append(conv,
|
}, conv...)
|
||||||
// openai.ChatCompletionMessage{
|
|
||||||
// Role: "system",
|
|
||||||
// Content: "Assistant thought: " + replyResponse.Message,
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
|
|
||||||
if replyResponse.Message != "" {
|
|
||||||
xlog.Info("Return reply message", "reply", replyResponse.Message, "agent", a.Character.Name)
|
|
||||||
|
|
||||||
msg := openai.ChatCompletionMessage{
|
|
||||||
Role: "assistant",
|
|
||||||
Content: replyResponse.Message,
|
|
||||||
}
|
|
||||||
|
|
||||||
conv = append(conv, msg)
|
|
||||||
job.Result.Conversation = conv
|
|
||||||
job.Result.SetResponse(msg.Content)
|
|
||||||
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
|
||||||
a.saveCurrentConversation(conv)
|
|
||||||
})
|
|
||||||
job.Result.Finish(nil)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name)
|
xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name)
|
||||||
@@ -795,13 +966,21 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't got any message, we can use the response from the action
|
msg.Content = a.cleanupLLMResponse(msg.Content)
|
||||||
if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" {
|
|
||||||
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
|
|
||||||
|
|
||||||
msg = openai.ChatCompletionMessage{
|
if msg.Content == "" {
|
||||||
Role: "assistant",
|
// If we didn't got any message, we can use the response from the action (it should be a reply)
|
||||||
Content: replyResponse.Message,
|
|
||||||
|
replyResponse := action.ReplyResponse{}
|
||||||
|
if err := actionParams.Unmarshal(&replyResponse); err != nil {
|
||||||
|
job.Result.Conversation = conv
|
||||||
|
job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosenAction.Definition().Name.Is(action.ReplyActionName) && replyResponse.Message != "" {
|
||||||
|
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
|
||||||
|
msg.Content = a.cleanupLLMResponse(replyResponse.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,25 +1027,83 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
|
|
||||||
xlog.Debug("Agent is running periodically", "agent", a.Character.Name)
|
xlog.Debug("Agent is running periodically", "agent", a.Character.Name)
|
||||||
|
|
||||||
// TODO: Would be nice if we have a special action to
|
// Check for reminders that need to be triggered
|
||||||
// contact the user. This would actually make sure that
|
now := time.Now()
|
||||||
// if the agent wants to initiate a conversation, it can do so.
|
var triggeredReminders []types.ReminderActionResponse
|
||||||
// This would be a special action that would be picked up by the agent
|
var remainingReminders []types.ReminderActionResponse
|
||||||
// and would be used to contact the user.
|
|
||||||
|
|
||||||
// if len(conv()) != 0 {
|
for _, reminder := range a.sharedState.Reminders {
|
||||||
// // Here the LLM could decide to store some part of the conversation too in the memory
|
xlog.Debug("Checking reminder", "reminder", reminder)
|
||||||
// evaluateMemory := NewJob(
|
if now.After(reminder.NextRun) {
|
||||||
// WithText(
|
triggeredReminders = append(triggeredReminders, reminder)
|
||||||
// `Evaluate the current conversation and decide if we need to store some relevant informations from it`,
|
xlog.Debug("Reminder triggered", "reminder", reminder)
|
||||||
// ),
|
// Calculate next run time for recurring reminders
|
||||||
// WithReasoningCallback(a.options.reasoningCallback),
|
if reminder.IsRecurring {
|
||||||
// WithResultCallback(a.options.resultCallback),
|
xlog.Debug("Reminder is recurring", "reminder", reminder)
|
||||||
// )
|
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||||
// a.consumeJob(evaluateMemory, SystemRole)
|
schedule, err := parser.Parse(reminder.CronExpr)
|
||||||
|
if err == nil {
|
||||||
|
nextRun := schedule.Next(now)
|
||||||
|
xlog.Debug("Next run time", "reminder", reminder, "nextRun", nextRun)
|
||||||
|
reminder.LastRun = now
|
||||||
|
reminder.NextRun = nextRun
|
||||||
|
remainingReminders = append(remainingReminders, reminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
xlog.Debug("Reminder not triggered", "reminder", reminder)
|
||||||
|
remainingReminders = append(remainingReminders, reminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// a.ResetConversation()
|
// Update the reminders list
|
||||||
// }
|
a.sharedState.Reminders = remainingReminders
|
||||||
|
|
||||||
|
// Handle triggered reminders
|
||||||
|
for _, reminder := range triggeredReminders {
|
||||||
|
xlog.Info("Processing triggered reminder", "agent", a.Character.Name, "message", reminder.Message)
|
||||||
|
|
||||||
|
// Create a more natural conversation flow for the reminder
|
||||||
|
reminderJob := types.NewJob(
|
||||||
|
types.WithText(fmt.Sprintf("I have a reminder for you: %s", reminder.Message)),
|
||||||
|
types.WithReasoningCallback(a.options.reasoningCallback),
|
||||||
|
types.WithResultCallback(a.options.resultCallback),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add the reminder message to the job's metadata
|
||||||
|
reminderJob.Metadata = map[string]interface{}{
|
||||||
|
"message": reminder.Message,
|
||||||
|
"is_reminder": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the reminder as a normal conversation
|
||||||
|
a.consumeJob(reminderJob, UserRole, a.options.loopDetectionSteps)
|
||||||
|
|
||||||
|
// After the reminder job is complete, ensure the user is notified
|
||||||
|
if reminderJob.Result != nil && reminderJob.Result.Conversation != nil {
|
||||||
|
// Get the last assistant message from the conversation
|
||||||
|
var lastAssistantMsg *openai.ChatCompletionMessage
|
||||||
|
for i := len(reminderJob.Result.Conversation) - 1; i >= 0; i-- {
|
||||||
|
if reminderJob.Result.Conversation[i].Role == AssistantRole {
|
||||||
|
lastAssistantMsg = &reminderJob.Result.Conversation[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastAssistantMsg != nil && lastAssistantMsg.Content != "" {
|
||||||
|
// Send the reminder response to the user
|
||||||
|
msg := openai.ChatCompletionMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: fmt.Sprintf("Reminder Update: %s\n\n%s", reminder.Message, lastAssistantMsg.Content),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(agent *Agent) {
|
||||||
|
xlog.Info("Sending reminder response to user", "agent", agent.Character.Name, "message", msg.Content)
|
||||||
|
agent.newConversations <- msg
|
||||||
|
}(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !a.options.standaloneJob {
|
if !a.options.standaloneJob {
|
||||||
return
|
return
|
||||||
@@ -878,44 +1115,17 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
// - evaluating the result
|
// - evaluating the result
|
||||||
// - asking the agent to do something else based on the result
|
// - asking the agent to do something else based on the result
|
||||||
|
|
||||||
// whatNext := NewJob(WithText("Decide what to do based on the state"))
|
|
||||||
whatNext := types.NewJob(
|
whatNext := types.NewJob(
|
||||||
types.WithText(innerMonologueTemplate),
|
types.WithText(innerMonologueTemplate),
|
||||||
types.WithReasoningCallback(a.options.reasoningCallback),
|
types.WithReasoningCallback(a.options.reasoningCallback),
|
||||||
types.WithResultCallback(a.options.resultCallback),
|
types.WithResultCallback(a.options.resultCallback),
|
||||||
)
|
)
|
||||||
a.consumeJob(whatNext, SystemRole)
|
a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps)
|
||||||
|
|
||||||
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
||||||
|
|
||||||
// Save results from state
|
|
||||||
|
|
||||||
// a.ResetConversation()
|
|
||||||
|
|
||||||
// doWork := NewJob(WithText("Select the tool to use based on your goal and the current state."))
|
|
||||||
// a.consumeJob(doWork, SystemRole)
|
|
||||||
|
|
||||||
// results := []string{}
|
|
||||||
// for _, v := range doWork.Result.State {
|
|
||||||
// results = append(results, v.Result)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// a.ResetConversation()
|
|
||||||
|
|
||||||
// // Here the LLM could decide to do something based on the result of our automatic action
|
|
||||||
// evaluateAction := NewJob(
|
|
||||||
// WithText(
|
|
||||||
// `Evaluate the current situation and decide if we need to execute other tools (for instance to store results into permanent, or short memory).
|
|
||||||
// We have done the following actions:
|
|
||||||
// ` + strings.Join(results, "\n"),
|
|
||||||
// ))
|
|
||||||
// a.consumeJob(evaluateAction, SystemRole)
|
|
||||||
|
|
||||||
// a.ResetConversation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) Run() error {
|
func (a *Agent) Run() error {
|
||||||
|
|
||||||
a.startNewConversationsConsumer()
|
a.startNewConversationsConsumer()
|
||||||
xlog.Debug("Agent is now running", "agent", a.Character.Name)
|
xlog.Debug("Agent is now running", "agent", a.Character.Name)
|
||||||
// The agent run does two things:
|
// The agent run does two things:
|
||||||
@@ -930,32 +1140,68 @@ func (a *Agent) Run() error {
|
|||||||
|
|
||||||
// Expose a REST API to interact with the agent to ask it things
|
// Expose a REST API to interact with the agent to ask it things
|
||||||
|
|
||||||
//todoTimer := time.NewTicker(a.options.periodicRuns)
|
|
||||||
timer := time.NewTimer(a.options.periodicRuns)
|
timer := time.NewTimer(a.options.periodicRuns)
|
||||||
|
|
||||||
|
// we fire the periodicalRunner only once.
|
||||||
|
go a.periodicalRunRunner(timer)
|
||||||
|
var errs []error
|
||||||
|
var muErr sync.Mutex
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
parallelJobs := a.options.parallelJobs
|
||||||
|
if a.options.parallelJobs == 0 {
|
||||||
|
parallelJobs = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < parallelJobs; i++ {
|
||||||
|
xlog.Debug("Starting agent worker", "worker", i)
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
e := a.run(timer)
|
||||||
|
muErr.Lock()
|
||||||
|
errs = append(errs, e)
|
||||||
|
muErr.Unlock()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) run(timer *time.Timer) error {
|
||||||
for {
|
for {
|
||||||
xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name)
|
xlog.Debug("Agent is now waiting for a new job", "agent", a.Character.Name)
|
||||||
select {
|
select {
|
||||||
case job := <-a.jobQueue:
|
case job := <-a.jobQueue:
|
||||||
a.loop(timer, job)
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
|
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
|
||||||
|
a.consumeJob(job, UserRole, a.options.loopDetectionSteps)
|
||||||
|
timer.Reset(a.options.periodicRuns)
|
||||||
case <-a.context.Done():
|
case <-a.context.Done():
|
||||||
// Agent has been canceled, return error
|
// Agent has been canceled, return error
|
||||||
xlog.Warn("Agent has been canceled", "agent", a.Character.Name)
|
xlog.Warn("Agent has been canceled", "agent", a.Character.Name)
|
||||||
return ErrContextCanceled
|
return ErrContextCanceled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) periodicalRunRunner(timer *time.Timer) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.context.Done():
|
||||||
|
// Agent has been canceled, return error
|
||||||
|
xlog.Warn("periodicalRunner has been canceled", "agent", a.Character.Name)
|
||||||
|
return
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
a.periodicallyRun(timer)
|
a.periodicallyRun(timer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) loop(timer *time.Timer, job *types.Job) {
|
func (a *Agent) Observer() Observer {
|
||||||
// Remember always to reset the timer - if we don't the agent will stop..
|
return a.observer
|
||||||
defer timer.Reset(a.options.periodicRuns)
|
|
||||||
// Consume the job and generate a response
|
|
||||||
// TODO: Give a short-term memory to the agent
|
|
||||||
// stop and drain the timer
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
|
|
||||||
a.consumeJob(job, UserRole)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (a *TestAction) Plannable() bool {
|
|||||||
return true
|
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 {
|
for k, r := range a.response {
|
||||||
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
|
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
|
||||||
return types.ActionResult{Result: r}, nil
|
return types.ActionResult{Result: r}, nil
|
||||||
@@ -126,6 +126,8 @@ var _ = Describe("Agent test", func() {
|
|||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
EnableForceReasoning,
|
||||||
|
WithTimeout("10m"),
|
||||||
WithLoopDetectionSteps(3),
|
WithLoopDetectionSteps(3),
|
||||||
// WithRandomIdentity(),
|
// WithRandomIdentity(),
|
||||||
WithActions(&TestAction{response: map[string]string{
|
WithActions(&TestAction{response: map[string]string{
|
||||||
@@ -174,7 +176,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
WithTimeout("10m"),
|
||||||
// WithRandomIdentity(),
|
// WithRandomIdentity(),
|
||||||
WithActions(&TestAction{response: map[string]string{
|
WithActions(&TestAction{response: map[string]string{
|
||||||
"boston": testActionResult,
|
"boston": testActionResult,
|
||||||
@@ -199,6 +201,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
WithTimeout("10m"),
|
||||||
EnableHUD,
|
EnableHUD,
|
||||||
// EnableStandaloneJob,
|
// EnableStandaloneJob,
|
||||||
// WithRandomIdentity(),
|
// WithRandomIdentity(),
|
||||||
@@ -221,8 +224,12 @@ var _ = Describe("Agent test", func() {
|
|||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithLLMAPIKey(apiKeyURL),
|
WithLLMAPIKey(apiKeyURL),
|
||||||
|
WithTimeout("10m"),
|
||||||
WithActions(
|
WithActions(
|
||||||
actions.NewSearch(map[string]string{}),
|
&TestAction{response: map[string]string{
|
||||||
|
"boston": testActionResult,
|
||||||
|
"milan": testActionResult2,
|
||||||
|
}},
|
||||||
),
|
),
|
||||||
EnablePlanning,
|
EnablePlanning,
|
||||||
EnableForceReasoning,
|
EnableForceReasoning,
|
||||||
@@ -234,18 +241,21 @@ var _ = Describe("Agent test", func() {
|
|||||||
defer agent.Stop()
|
defer agent.Stop()
|
||||||
|
|
||||||
result := agent.Ask(
|
result := agent.Ask(
|
||||||
types.WithText("plan a trip to San Francisco from Venice, Italy"),
|
types.WithText("Use the plan tool to do two actions in sequence: search for the weather in boston and search for the weather in milan"),
|
||||||
)
|
)
|
||||||
Expect(len(result.State)).To(BeNumerically(">", 1))
|
Expect(len(result.State)).To(BeNumerically(">", 1))
|
||||||
|
|
||||||
actionsExecuted := []string{}
|
actionsExecuted := []string{}
|
||||||
|
actionResults := []string{}
|
||||||
for _, r := range result.State {
|
for _, r := range result.State {
|
||||||
xlog.Info(r.Result)
|
xlog.Info(r.Result)
|
||||||
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
|
actionsExecuted = append(actionsExecuted, r.Action.Definition().Name.String())
|
||||||
|
actionResults = append(actionResults, r.ActionResult.Result)
|
||||||
}
|
}
|
||||||
Expect(actionsExecuted).To(ContainElement("search_internet"), fmt.Sprint(result))
|
Expect(actionsExecuted).To(ContainElement("get_weather"), fmt.Sprint(result))
|
||||||
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
|
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
|
||||||
|
Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result))
|
||||||
|
Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Can initiate conversations", func() {
|
It("Can initiate conversations", func() {
|
||||||
@@ -256,6 +266,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithLLMAPIKey(apiKeyURL),
|
WithLLMAPIKey(apiKeyURL),
|
||||||
|
WithTimeout("10m"),
|
||||||
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
message = m
|
message = m
|
||||||
@@ -270,7 +281,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
EnableStandaloneJob,
|
EnableStandaloneJob,
|
||||||
EnableHUD,
|
EnableHUD,
|
||||||
WithPeriodicRuns("1s"),
|
WithPeriodicRuns("1s"),
|
||||||
WithPermanentGoal("use the new_conversation tool"),
|
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
||||||
// EnableStandaloneJob,
|
// EnableStandaloneJob,
|
||||||
// WithRandomIdentity(),
|
// WithRandomIdentity(),
|
||||||
)
|
)
|
||||||
|
|||||||
162
core/agent/evaluation.go
Normal file
162
core/agent/evaluation.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EvaluationResult struct {
|
||||||
|
Satisfied bool `json:"satisfied"`
|
||||||
|
Gaps []string `json:"gaps"`
|
||||||
|
Reasoning string `json:"reasoning"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalExtraction struct {
|
||||||
|
Goal string `json:"goal"`
|
||||||
|
Constraints []string `json:"constraints"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) extractGoal(job *types.Job, conv []openai.ChatCompletionMessage) (*GoalExtraction, error) {
|
||||||
|
// Create the goal extraction schema
|
||||||
|
schema := jsonschema.Definition{
|
||||||
|
Type: jsonschema.Object,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"goal": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The main goal or request from the user",
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
Type: jsonschema.Array,
|
||||||
|
Items: &jsonschema.Definition{
|
||||||
|
Type: jsonschema.String,
|
||||||
|
},
|
||||||
|
Description: "Any constraints or requirements specified by the user",
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Additional context that might be relevant for understanding the goal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"goal", "constraints", "context"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the goal extraction prompt
|
||||||
|
prompt := `Analyze the conversation and extract the user's main goal, any constraints, and relevant context.
|
||||||
|
Consider the entire conversation history to understand the complete context and requirements.
|
||||||
|
Focus on identifying the primary objective and any specific requirements or limitations mentioned.`
|
||||||
|
|
||||||
|
var result GoalExtraction
|
||||||
|
err := llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
|
||||||
|
append(
|
||||||
|
[]openai.ChatCompletionMessage{
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conv...), a.options.LLMAPI.Model, schema, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting goal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) evaluateJob(job *types.Job, conv []openai.ChatCompletionMessage) (*EvaluationResult, error) {
|
||||||
|
if !a.options.enableEvaluation {
|
||||||
|
return &EvaluationResult{Satisfied: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the goal first
|
||||||
|
goal, err := a.extractGoal(job, conv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting goal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the evaluation schema
|
||||||
|
schema := jsonschema.Definition{
|
||||||
|
Type: jsonschema.Object,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"satisfied": {
|
||||||
|
Type: jsonschema.Boolean,
|
||||||
|
},
|
||||||
|
"gaps": {
|
||||||
|
Type: jsonschema.Array,
|
||||||
|
Items: &jsonschema.Definition{
|
||||||
|
Type: jsonschema.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"satisfied", "gaps", "reasoning"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the evaluation prompt
|
||||||
|
prompt := fmt.Sprintf(`Evaluate if the assistant has satisfied the user's request. Consider:
|
||||||
|
1. The identified goal: %s
|
||||||
|
2. Constraints and requirements: %v
|
||||||
|
3. Context: %s
|
||||||
|
4. The conversation history
|
||||||
|
5. Any gaps or missing information
|
||||||
|
6. Whether the response fully addresses the user's needs
|
||||||
|
|
||||||
|
Provide a detailed evaluation with specific gaps if any are found.`,
|
||||||
|
goal.Goal,
|
||||||
|
goal.Constraints,
|
||||||
|
goal.Context)
|
||||||
|
|
||||||
|
var result EvaluationResult
|
||||||
|
err = llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
|
||||||
|
append(
|
||||||
|
[]openai.ChatCompletionMessage{
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conv...),
|
||||||
|
a.options.LLMAPI.Model, schema, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating evaluation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleEvaluation(job *types.Job, conv []openai.ChatCompletionMessage, currentLoop int) (bool, []openai.ChatCompletionMessage, error) {
|
||||||
|
if !a.options.enableEvaluation || currentLoop >= a.options.maxEvaluationLoops {
|
||||||
|
return true, conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := a.evaluateJob(job, conv)
|
||||||
|
if err != nil {
|
||||||
|
return false, conv, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Satisfied {
|
||||||
|
return true, conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are gaps, we need to address them
|
||||||
|
if len(result.Gaps) > 0 {
|
||||||
|
// Add the evaluation result to the conversation
|
||||||
|
conv = append(conv, openai.ChatCompletionMessage{
|
||||||
|
Role: "system",
|
||||||
|
Content: fmt.Sprintf("Evaluation found gaps that need to be addressed:\n%s\nReasoning: %s",
|
||||||
|
result.Gaps, result.Reasoning),
|
||||||
|
})
|
||||||
|
|
||||||
|
xlog.Debug("Evaluation found gaps, incrementing loop count", "loop", currentLoop+1)
|
||||||
|
return false, conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, conv, nil
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
|
|||||||
guidance = "Generate a random character for roleplaying."
|
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)
|
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
|
||||||
a.Character = a.options.character
|
a.Character = a.options.character
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,15 +6,25 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/sashabaranov/go-openai"
|
"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) ||
|
if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) ||
|
||||||
len(conv) <= 0 {
|
len(conv) <= 0 {
|
||||||
xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name)
|
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
|
// Walk conversation from bottom to top, and find the first message of the user
|
||||||
@@ -25,17 +35,35 @@ func (a *Agent) knowledgeBaseLookup(conv Messages) {
|
|||||||
|
|
||||||
if userMessage == "" {
|
if userMessage == "" {
|
||||||
xlog.Info("[Knowledge Base Lookup] No user message found in conversation", "agent", a.Character.Name)
|
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)
|
results, err := a.options.ragdb.Search(userMessage, a.options.kbResults)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Info("Error finding similar strings inside KB:", "error", err)
|
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 {
|
if len(results) == 0 {
|
||||||
xlog.Info("[Knowledge Base Lookup] No similar strings found in KB", "agent", a.Character.Name)
|
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 := ""
|
formatResults := ""
|
||||||
@@ -44,17 +72,30 @@ func (a *Agent) knowledgeBaseLookup(conv Messages) {
|
|||||||
}
|
}
|
||||||
xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults)
|
xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults)
|
||||||
|
|
||||||
// conv = append(conv,
|
if obs != nil {
|
||||||
// openai.ChatCompletionMessage{
|
obs.AddProgress(types.Progress{
|
||||||
// Role: "system",
|
ActionResult: fmt.Sprintf("Found %d results in knowledge base", len(results)),
|
||||||
// Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
|
})
|
||||||
// },
|
a.observer.Update(*obs)
|
||||||
// )
|
}
|
||||||
conv = append([]openai.ChatCompletionMessage{
|
|
||||||
{
|
// Create the message to add to conversation
|
||||||
Role: "system",
|
systemMessage := openai.ChatCompletionMessage{
|
||||||
Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults),
|
Role: "system",
|
||||||
}}, conv...)
|
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 {
|
func (a *Agent) saveConversation(m Messages, prefix string) error {
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package agent
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
|
|
||||||
mcp "github.com/metoro-io/mcp-golang"
|
mcp "github.com/metoro-io/mcp-golang"
|
||||||
"github.com/metoro-io/mcp-golang/transport/http"
|
"github.com/metoro-io/mcp-golang/transport/http"
|
||||||
|
stdioTransport "github.com/metoro-io/mcp-golang/transport/stdio"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/stdio"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +21,12 @@ type MCPServer struct {
|
|||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MCPSTDIOServer struct {
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env []string `json:"env"`
|
||||||
|
Cmd string `json:"cmd"`
|
||||||
|
}
|
||||||
|
|
||||||
type mcpAction struct {
|
type mcpAction struct {
|
||||||
mcpClient *mcp.Client
|
mcpClient *mcp.Client
|
||||||
inputSchema ToolInputSchema
|
inputSchema ToolInputSchema
|
||||||
@@ -30,7 +38,7 @@ func (a *mcpAction) Plannable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mcpAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
|
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Failed to call tool", "error", err.Error())
|
xlog.Error("Failed to call tool", "error", err.Error())
|
||||||
@@ -79,6 +87,68 @@ type ToolInputSchema struct {
|
|||||||
Required []string `json:"required,omitempty"`
|
Required []string `json:"required,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Agent) addTools(client *mcp.Client) (types.Actions, error) {
|
||||||
|
|
||||||
|
var generatedActions types.Actions
|
||||||
|
xlog.Debug("Initializing client")
|
||||||
|
// Initialize the client
|
||||||
|
response, e := client.Initialize(a.context)
|
||||||
|
if e != nil {
|
||||||
|
xlog.Error("Failed to initialize client", "error", e.Error())
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range tools.Tools {
|
||||||
|
desc := ""
|
||||||
|
if t.Description != nil {
|
||||||
|
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, &mcpAction{
|
||||||
|
mcpClient: client,
|
||||||
|
toolName: t.Name,
|
||||||
|
inputSchema: inputSchema,
|
||||||
|
toolDescription: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if tools.NextCursor == nil {
|
||||||
|
break // No more pages
|
||||||
|
}
|
||||||
|
cursor = tools.NextCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatedActions, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) initMCPActions() error {
|
func (a *Agent) initMCPActions() error {
|
||||||
|
|
||||||
a.mcpActions = nil
|
a.mcpActions = nil
|
||||||
@@ -86,6 +156,7 @@ func (a *Agent) initMCPActions() error {
|
|||||||
|
|
||||||
generatedActions := types.Actions{}
|
generatedActions := types.Actions{}
|
||||||
|
|
||||||
|
// MCP HTTP Servers
|
||||||
for _, mcpServer := range a.options.mcpServers {
|
for _, mcpServer := range a.options.mcpServers {
|
||||||
transport := http.NewHTTPClientTransport("/mcp")
|
transport := http.NewHTTPClientTransport("/mcp")
|
||||||
transport.WithBaseURL(mcpServer.URL)
|
transport.WithBaseURL(mcpServer.URL)
|
||||||
@@ -95,70 +166,60 @@ func (a *Agent) initMCPActions() error {
|
|||||||
|
|
||||||
// Create a new client
|
// Create a new client
|
||||||
client := mcp.NewClient(transport)
|
client := mcp.NewClient(transport)
|
||||||
|
xlog.Debug("Adding tools for MCP server", "server", mcpServer)
|
||||||
|
actions, err := a.addTools(client)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error())
|
||||||
|
}
|
||||||
|
generatedActions = append(generatedActions, actions...)
|
||||||
|
}
|
||||||
|
|
||||||
xlog.Debug("Initializing client", "server", mcpServer.URL)
|
// MCP STDIO Servers
|
||||||
// Initialize the client
|
|
||||||
response, e := client.Initialize(a.context)
|
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
||||||
if e != nil {
|
|
||||||
xlog.Error("Failed to initialize client", "error", e.Error(), "server", mcpServer)
|
if a.options.mcpPrepareScript != "" {
|
||||||
if err == nil {
|
xlog.Debug("Preparing MCP box", "script", a.options.mcpPrepareScript)
|
||||||
err = e
|
client := stdio.NewClient(a.options.mcpBoxURL)
|
||||||
} else {
|
client.RunProcess(a.context, "/bin/bash", []string{"-c", a.options.mcpPrepareScript}, []string{})
|
||||||
err = errors.Join(err, e)
|
}
|
||||||
}
|
|
||||||
|
for _, mcpStdioServer := range a.options.mcpStdioServers {
|
||||||
|
client := stdio.NewClient(a.options.mcpBoxURL)
|
||||||
|
p, err := client.CreateProcess(a.context,
|
||||||
|
mcpStdioServer.Cmd,
|
||||||
|
mcpStdioServer.Args,
|
||||||
|
mcpStdioServer.Env,
|
||||||
|
a.Character.Name)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Failed to create process", "error", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
read, writer, err := client.GetProcessIO(p.ID)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Failed to get process IO", "error", err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug("Client initialized: %v", response.Instructions)
|
transport := stdioTransport.NewStdioServerTransportWithIO(read, writer)
|
||||||
|
|
||||||
var cursor *string
|
// Create a new client
|
||||||
for {
|
mcpClient := mcp.NewClient(transport)
|
||||||
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 {
|
xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
|
||||||
desc := ""
|
actions, err := a.addTools(mcpClient)
|
||||||
if t.Description != nil {
|
if err != nil {
|
||||||
desc = *t.Description
|
xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
generatedActions = append(generatedActions, actions...)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.mcpActions = generatedActions
|
a.mcpActions = generatedActions
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Agent) closeMCPSTDIOServers() {
|
||||||
|
client := stdio.NewClient(a.options.mcpBoxURL)
|
||||||
|
client.StopGroup(a.Character.Name)
|
||||||
|
}
|
||||||
|
|||||||
88
core/agent/observer.go
Normal file
88
core/agent/observer.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -24,7 +24,9 @@ type options struct {
|
|||||||
randomIdentityGuidance string
|
randomIdentityGuidance string
|
||||||
randomIdentity bool
|
randomIdentity bool
|
||||||
userActions types.Actions
|
userActions types.Actions
|
||||||
|
jobFilters types.JobFilters
|
||||||
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
||||||
|
stripThinkingTags bool
|
||||||
|
|
||||||
canStopItself bool
|
canStopItself bool
|
||||||
initiateConversations bool
|
initiateConversations bool
|
||||||
@@ -40,6 +42,10 @@ type options struct {
|
|||||||
kbResults int
|
kbResults int
|
||||||
ragdb RAGDB
|
ragdb RAGDB
|
||||||
|
|
||||||
|
// Evaluation settings
|
||||||
|
maxEvaluationLoops int
|
||||||
|
enableEvaluation bool
|
||||||
|
|
||||||
prompts []DynamicPrompt
|
prompts []DynamicPrompt
|
||||||
|
|
||||||
systemPrompt string
|
systemPrompt string
|
||||||
@@ -50,9 +56,16 @@ type options struct {
|
|||||||
|
|
||||||
conversationsPath string
|
conversationsPath string
|
||||||
|
|
||||||
mcpServers []MCPServer
|
mcpServers []MCPServer
|
||||||
|
mcpStdioServers []MCPSTDIOServer
|
||||||
|
mcpBoxURL string
|
||||||
|
mcpPrepareScript string
|
||||||
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
newConversationsSubscribers []func(openai.ChatCompletionMessage)
|
||||||
|
|
||||||
|
observer Observer
|
||||||
|
parallelJobs int
|
||||||
|
|
||||||
|
lastMessageDuration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *options) SeparatedMultimodalModel() bool {
|
func (o *options) SeparatedMultimodalModel() bool {
|
||||||
@@ -61,7 +74,11 @@ func (o *options) SeparatedMultimodalModel() bool {
|
|||||||
|
|
||||||
func defaultOptions() *options {
|
func defaultOptions() *options {
|
||||||
return &options{
|
return &options{
|
||||||
periodicRuns: 15 * time.Minute,
|
parallelJobs: 1,
|
||||||
|
periodicRuns: 15 * time.Minute,
|
||||||
|
loopDetectionSteps: 10,
|
||||||
|
maxEvaluationLoops: 2,
|
||||||
|
enableEvaluation: false,
|
||||||
LLMAPI: llmOptions{
|
LLMAPI: llmOptions{
|
||||||
APIURL: "http://localhost:8080",
|
APIURL: "http://localhost:8080",
|
||||||
Model: "gpt-4",
|
Model: "gpt-4",
|
||||||
@@ -136,6 +153,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 {
|
func WithNewConversationSubscriber(sub func(openai.ChatCompletionMessage)) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
|
o.newConversationsSubscribers = append(o.newConversationsSubscribers, sub)
|
||||||
@@ -196,6 +231,27 @@ func WithMCPServers(servers ...MCPServer) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithMCPSTDIOServers(servers ...MCPSTDIOServer) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.mcpStdioServers = servers
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMCPBoxURL(url string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.mcpBoxURL = url
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMCPPrepareScript(script string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.mcpPrepareScript = script
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithLLMAPIURL(url string) Option {
|
func WithLLMAPIURL(url string) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.LLMAPI.APIURL = url
|
o.LLMAPI.APIURL = url
|
||||||
@@ -336,3 +392,36 @@ func WithActions(actions ...types.Action) Option {
|
|||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/action"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
// all information that should be displayed to the LLM
|
// all information that should be displayed to the LLM
|
||||||
// in the prompts
|
// in the prompts
|
||||||
type PromptHUD struct {
|
type PromptHUD struct {
|
||||||
Character Character `json:"character"`
|
Character Character `json:"character"`
|
||||||
CurrentState action.AgentInternalState `json:"current_state"`
|
CurrentState types.AgentInternalState `json:"current_state"`
|
||||||
PermanentGoal string `json:"permanent_goal"`
|
PermanentGoal string `json:"permanent_goal"`
|
||||||
ShowCharacter bool `json:"show_character"`
|
ShowCharacter bool `json:"show_character"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Character struct {
|
type Character struct {
|
||||||
@@ -80,7 +80,7 @@ func Load(path string) (*Character, error) {
|
|||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) State() action.AgentInternalState {
|
func (a *Agent) State() types.AgentInternalState {
|
||||||
return *a.currentState
|
return *a.currentState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
agent, err = New(
|
agent, err = New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
WithTimeout("10m"),
|
||||||
WithRandomIdentity(),
|
WithRandomIdentity(),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|||||||
@@ -43,61 +43,104 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
|
|||||||
return prompt.String(), nil
|
return prompt.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const innerMonologueTemplate = `"This is not a typical conversation between an assistant and an user.
|
const innerMonologueTemplate = `You are an autonomous AI agent thinking out loud and evaluating your current situation.
|
||||||
You are thinking out loud by yourself now, and you are evaluating the current situation.
|
Your task is to analyze your goals and determine the best course of action.
|
||||||
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 hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}The assistant acts like an human, has a character and the replies and actions might be influenced by it.
|
Consider:
|
||||||
{{if .Character.Name}}This is the assistant name: {{.Character.Name}}
|
1. Your permanent goal (if any)
|
||||||
{{end}}{{if .Character.Age}}This is the assistant age: {{.Character.Age}}
|
2. Your current state and progress
|
||||||
{{end}}{{if .Character.Occupation}}This is the assistant job: {{.Character.Occupation}}
|
3. Available tools and capabilities
|
||||||
{{end}}{{if .Character.Hobbies}}This is the assistant's hobbies: {{.Character.Hobbies}}
|
4. Previous actions and their outcomes
|
||||||
{{end}}{{if .Character.MusicTaste}}This is the assistant's music taste: {{.Character.MusicTaste}}
|
|
||||||
|
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}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
This is your current state:
|
Current State:
|
||||||
NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}}
|
- Current Action: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}None{{end}}
|
||||||
DoingNext: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}Nothing{{end}}
|
- Next Action: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}None{{end}}
|
||||||
Your permanent goal is: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}Nothing{{end}}
|
- Permanent Goal: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}None{{end}}
|
||||||
Your current goal is: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}Nothing{{end}}
|
- Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
|
||||||
You have done: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
|
- Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
|
||||||
You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
|
- Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
|
||||||
Current time: is {{.Time}}`
|
Current Time: {{.Time}}`
|
||||||
|
|
||||||
const pickSelfTemplate = `You can take any of the following tools:
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
When making decisions:
|
||||||
|
- Use the "reply" tool to provide final responses
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Available Tools:
|
||||||
{{range .Actions -}}
|
{{range .Actions -}}
|
||||||
- {{.Name}}: {{.Description }}
|
- {{.Name}}: {{.Description }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
To finish your session, use the "reply" tool with your answer.
|
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}}
|
|
||||||
` + hudTemplate
|
` + hudTemplate
|
||||||
|
|
||||||
const reSelfEvalTemplate = pickSelfTemplate + `
|
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 + `
|
const pickActionTemplate = hudTemplate + `
|
||||||
When you have to pick a tool in the reasoning explain how you would use the tools you'd pick from:
|
Your only task is to analyze the conversation and determine a goal and the best tool to use, or just a final response if we have fullfilled the goal.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
1. Review the current state, what was done already and context
|
||||||
|
2. Consider available tools and their purposes
|
||||||
|
3. Plan your approach carefully
|
||||||
|
4. Explain your reasoning clearly
|
||||||
|
|
||||||
|
When choosing actions:
|
||||||
|
- Use "reply" or "answer" tools for direct responses
|
||||||
|
- Select appropriate tools for specific tasks
|
||||||
|
- Consider the impact of each action
|
||||||
|
- Plan for potential challenges
|
||||||
|
|
||||||
|
Decision Process:
|
||||||
|
1. Analyze the situation
|
||||||
|
2. Consider available options
|
||||||
|
3. Choose the best course of action
|
||||||
|
4. Explain your reasoning
|
||||||
|
5. Execute the chosen action
|
||||||
|
|
||||||
|
Available Tools:
|
||||||
{{range .Actions -}}
|
{{range .Actions -}}
|
||||||
- {{.Name}}: {{.Description }}
|
- {{.Name}}: {{.Description }}
|
||||||
{{ end }}
|
{{ 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}}
|
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}`
|
||||||
`
|
|
||||||
|
|
||||||
const reEvalTemplate = pickActionTemplate + `
|
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.`
|
|
||||||
|
|||||||
13
core/conversations/conversations_suite_test.go
Normal file
13
core/conversations/conversations_suite_test.go
Normal file
@@ -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,4 +1,4 @@
|
|||||||
package connectors
|
package conversations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package connectors_test
|
package conversations_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/services/connectors"
|
"github.com/mudler/LocalAGI/core/conversations"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
@@ -11,13 +11,13 @@ import (
|
|||||||
|
|
||||||
var _ = Describe("ConversationTracker", func() {
|
var _ = Describe("ConversationTracker", func() {
|
||||||
var (
|
var (
|
||||||
tracker *connectors.ConversationTracker[string]
|
tracker *conversations.ConversationTracker[string]
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
)
|
)
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
duration = 1 * time.Second
|
duration = 1 * time.Second
|
||||||
tracker = connectors.NewConversationTracker[string](duration)
|
tracker = conversations.NewConversationTracker[string](duration)
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should initialize with empty conversations", func() {
|
It("should initialize with empty conversations", func() {
|
||||||
@@ -81,8 +81,8 @@ var _ = Describe("ConversationTracker", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("should handle different key types", func() {
|
It("should handle different key types", func() {
|
||||||
trackerInt := connectors.NewConversationTracker[int](duration)
|
trackerInt := conversations.NewConversationTracker[int](duration)
|
||||||
trackerInt64 := connectors.NewConversationTracker[int64](duration)
|
trackerInt64 := conversations.NewConversationTracker[int64](duration)
|
||||||
|
|
||||||
message := openai.ChatCompletionMessage{
|
message := openai.ChatCompletionMessage{
|
||||||
Role: openai.ChatMessageRoleUser,
|
Role: openai.ChatMessageRoleUser,
|
||||||
@@ -2,6 +2,8 @@ package state
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/agent"
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
@@ -29,20 +31,30 @@ func (d DynamicPromptsConfig) ToMap() map[string]string {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FiltersConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config string `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
||||||
Actions []ActionsConfig `json:"actions" form:"actions"`
|
Actions []ActionsConfig `json:"actions" form:"actions"`
|
||||||
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
DynamicPrompts []DynamicPromptsConfig `json:"dynamic_prompts" form:"dynamic_prompts"`
|
||||||
MCPServers []agent.MCPServer `json:"mcp_servers" form:"mcp_servers"`
|
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"`
|
||||||
|
MCPBoxURL string `json:"mcp_box_url" form:"mcp_box_url"`
|
||||||
|
Filters []FiltersConfig `json:"filters" form:"filters"`
|
||||||
|
|
||||||
Description string `json:"description" form:"description"`
|
Description string `json:"description" form:"description"`
|
||||||
|
|
||||||
Model string `json:"model" form:"model"`
|
Model string `json:"model" form:"model"`
|
||||||
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
||||||
APIURL string `json:"api_url" form:"api_url"`
|
APIURL string `json:"api_url" form:"api_url"`
|
||||||
APIKey string `json:"api_key" form:"api_key"`
|
APIKey string `json:"api_key" form:"api_key"`
|
||||||
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
||||||
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
|
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"`
|
Name string `json:"name" form:"name"`
|
||||||
HUD bool `json:"hud" form:"hud"`
|
HUD bool `json:"hud" form:"hud"`
|
||||||
@@ -61,9 +73,14 @@ type AgentConfig struct {
|
|||||||
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
|
SystemPrompt string `json:"system_prompt" form:"system_prompt"`
|
||||||
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
||||||
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_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 {
|
type AgentConfigMeta struct {
|
||||||
|
Filters []config.FieldGroup
|
||||||
Fields []config.Field
|
Fields []config.Field
|
||||||
Connectors []config.FieldGroup
|
Connectors []config.FieldGroup
|
||||||
Actions []config.FieldGroup
|
Actions []config.FieldGroup
|
||||||
@@ -75,6 +92,7 @@ func NewAgentConfigMeta(
|
|||||||
actionsConfig []config.FieldGroup,
|
actionsConfig []config.FieldGroup,
|
||||||
connectorsConfig []config.FieldGroup,
|
connectorsConfig []config.FieldGroup,
|
||||||
dynamicPromptsConfig []config.FieldGroup,
|
dynamicPromptsConfig []config.FieldGroup,
|
||||||
|
filtersConfig []config.FieldGroup,
|
||||||
) AgentConfigMeta {
|
) AgentConfigMeta {
|
||||||
return AgentConfigMeta{
|
return AgentConfigMeta{
|
||||||
Fields: []config.Field{
|
Fields: []config.Field{
|
||||||
@@ -247,7 +265,7 @@ func NewAgentConfigMeta(
|
|||||||
Name: "enable_reasoning",
|
Name: "enable_reasoning",
|
||||||
Label: "Enable Reasoning",
|
Label: "Enable Reasoning",
|
||||||
Type: "checkbox",
|
Type: "checkbox",
|
||||||
DefaultValue: false,
|
DefaultValue: true,
|
||||||
HelpText: "Enable agent to explain its reasoning process",
|
HelpText: "Enable agent to explain its reasoning process",
|
||||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
},
|
},
|
||||||
@@ -260,6 +278,66 @@ func NewAgentConfigMeta(
|
|||||||
Step: 1,
|
Step: 1,
|
||||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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 the MCP box",
|
||||||
|
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"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
MCPServers: []config.Field{
|
MCPServers: []config.Field{
|
||||||
{
|
{
|
||||||
@@ -278,6 +356,7 @@ func NewAgentConfigMeta(
|
|||||||
DynamicPrompts: dynamicPromptsConfig,
|
DynamicPrompts: dynamicPromptsConfig,
|
||||||
Connectors: connectorsConfig,
|
Connectors: connectorsConfig,
|
||||||
Actions: actionsConfig,
|
Actions: actionsConfig,
|
||||||
|
Filters: filtersConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,3 +365,148 @@ type Connector interface {
|
|||||||
AgentReasoningCallback() func(state types.ActionCurrentState) bool
|
AgentReasoningCallback() func(state types.ActionCurrentState) bool
|
||||||
Start(a *agent.Agent)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ type AgentPool struct {
|
|||||||
managers map[string]sse.Manager
|
managers map[string]sse.Manager
|
||||||
agentStatus map[string]*Status
|
agentStatus map[string]*Status
|
||||||
apiURL, defaultModel, defaultMultimodalModel string
|
apiURL, defaultModel, defaultMultimodalModel string
|
||||||
|
mcpBoxURL string
|
||||||
imageModel, localRAGAPI, localRAGKey, apiKey string
|
imageModel, localRAGAPI, localRAGKey, apiKey string
|
||||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||||
connectors func(*AgentConfig) []Connector
|
connectors func(*AgentConfig) []Connector
|
||||||
dynamicPrompt func(*AgentConfig) []DynamicPrompt
|
dynamicPrompt func(*AgentConfig) []DynamicPrompt
|
||||||
|
filters func(*AgentConfig) types.JobFilters
|
||||||
timeout string
|
timeout string
|
||||||
conversationLogs string
|
conversationLogs string
|
||||||
}
|
}
|
||||||
@@ -72,11 +74,12 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentPool(
|
func NewAgentPool(
|
||||||
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory string,
|
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory, mcpBoxURL string,
|
||||||
LocalRAGAPI string,
|
LocalRAGAPI string,
|
||||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
||||||
connectors func(*AgentConfig) []Connector,
|
connectors func(*AgentConfig) []Connector,
|
||||||
promptBlocks func(*AgentConfig) []DynamicPrompt,
|
promptBlocks func(*AgentConfig) []DynamicPrompt,
|
||||||
|
filters func(*AgentConfig) types.JobFilters,
|
||||||
timeout string,
|
timeout string,
|
||||||
withLogs bool,
|
withLogs bool,
|
||||||
) (*AgentPool, error) {
|
) (*AgentPool, error) {
|
||||||
@@ -98,6 +101,7 @@ func NewAgentPool(
|
|||||||
apiURL: apiURL,
|
apiURL: apiURL,
|
||||||
defaultModel: defaultModel,
|
defaultModel: defaultModel,
|
||||||
defaultMultimodalModel: defaultMultimodalModel,
|
defaultMultimodalModel: defaultMultimodalModel,
|
||||||
|
mcpBoxURL: mcpBoxURL,
|
||||||
imageModel: imageModel,
|
imageModel: imageModel,
|
||||||
localRAGAPI: LocalRAGAPI,
|
localRAGAPI: LocalRAGAPI,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
@@ -108,6 +112,7 @@ func NewAgentPool(
|
|||||||
connectors: connectors,
|
connectors: connectors,
|
||||||
availableActions: availableActions,
|
availableActions: availableActions,
|
||||||
dynamicPrompt: promptBlocks,
|
dynamicPrompt: promptBlocks,
|
||||||
|
filters: filters,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
conversationLogs: conversationPath,
|
conversationLogs: conversationPath,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -123,6 +128,7 @@ func NewAgentPool(
|
|||||||
pooldir: directory,
|
pooldir: directory,
|
||||||
defaultModel: defaultModel,
|
defaultModel: defaultModel,
|
||||||
defaultMultimodalModel: defaultMultimodalModel,
|
defaultMultimodalModel: defaultMultimodalModel,
|
||||||
|
mcpBoxURL: mcpBoxURL,
|
||||||
imageModel: imageModel,
|
imageModel: imageModel,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
agents: make(map[string]*Agent),
|
agents: make(map[string]*Agent),
|
||||||
@@ -132,6 +138,7 @@ func NewAgentPool(
|
|||||||
connectors: connectors,
|
connectors: connectors,
|
||||||
localRAGAPI: LocalRAGAPI,
|
localRAGAPI: LocalRAGAPI,
|
||||||
dynamicPrompt: promptBlocks,
|
dynamicPrompt: promptBlocks,
|
||||||
|
filters: filters,
|
||||||
availableActions: availableActions,
|
availableActions: availableActions,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
conversationLogs: conversationPath,
|
conversationLogs: conversationPath,
|
||||||
@@ -166,7 +173,56 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
|
|||||||
}
|
}
|
||||||
}(a.pool[name])
|
}(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 {
|
func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agent AgentConfig) error {
|
||||||
@@ -191,7 +247,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
|
|||||||
ImagePrompt string `json:"image_prompt"`
|
ImagePrompt string `json:"image_prompt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := llm.GenerateTypedJSON(
|
err := llm.GenerateTypedJSONWithGuidance(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
llm.NewClient(APIKey, APIURL, "10m"),
|
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,
|
"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,8 +324,13 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
|
|||||||
return a.agentStatus[name]
|
return a.agentStatus[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error {
|
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
|
||||||
manager := sse.NewManager(5)
|
var manager sse.Manager
|
||||||
|
if m, ok := a.managers[name]; ok {
|
||||||
|
manager = m
|
||||||
|
} else {
|
||||||
|
manager = sse.NewManager(5)
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
model := a.defaultModel
|
model := a.defaultModel
|
||||||
multimodalModel := a.defaultMultimodalModel
|
multimodalModel := a.defaultMultimodalModel
|
||||||
@@ -280,18 +341,29 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
|
|
||||||
if config.Model != "" {
|
if config.Model != "" {
|
||||||
model = config.Model
|
model = config.Model
|
||||||
|
} else {
|
||||||
|
config.Model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MCPBoxURL != "" {
|
||||||
|
a.mcpBoxURL = config.MCPBoxURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PeriodicRuns == "" {
|
if config.PeriodicRuns == "" {
|
||||||
config.PeriodicRuns = "10m"
|
config.PeriodicRuns = "10m"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: Why do we update the pool config from an Agent's config?
|
||||||
if config.APIURL != "" {
|
if config.APIURL != "" {
|
||||||
a.apiURL = config.APIURL
|
a.apiURL = config.APIURL
|
||||||
|
} else {
|
||||||
|
config.APIURL = a.apiURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.APIKey != "" {
|
if config.APIKey != "" {
|
||||||
a.apiKey = config.APIKey
|
a.apiKey = config.APIKey
|
||||||
|
} else {
|
||||||
|
config.APIKey = a.apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.LocalRAGURL != "" {
|
if config.LocalRAGURL != "" {
|
||||||
@@ -305,6 +377,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
connectors := a.connectors(config)
|
connectors := a.connectors(config)
|
||||||
promptBlocks := a.dynamicPrompt(config)
|
promptBlocks := a.dynamicPrompt(config)
|
||||||
actions := a.availableActions(config)(ctx, a)
|
actions := a.availableActions(config)(ctx, a)
|
||||||
|
filters := a.filters(config)
|
||||||
stateFile, characterFile := a.stateFiles(name)
|
stateFile, characterFile := a.stateFiles(name)
|
||||||
|
|
||||||
actionsLog := []string{}
|
actionsLog := []string{}
|
||||||
@@ -317,6 +390,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
|
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filtersLog := []string{}
|
||||||
|
for _, filter := range filters {
|
||||||
|
filtersLog = append(filtersLog, filter.Name())
|
||||||
|
}
|
||||||
|
|
||||||
xlog.Info(
|
xlog.Info(
|
||||||
"Creating agent",
|
"Creating agent",
|
||||||
"name", name,
|
"name", name,
|
||||||
@@ -324,6 +402,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
"api_url", a.apiURL,
|
"api_url", a.apiURL,
|
||||||
"actions", actionsLog,
|
"actions", actionsLog,
|
||||||
"connectors", connectorLog,
|
"connectors", connectorLog,
|
||||||
|
"filters", filtersLog,
|
||||||
)
|
)
|
||||||
|
|
||||||
// dynamicPrompts := []map[string]string{}
|
// dynamicPrompts := []map[string]string{}
|
||||||
@@ -331,6 +410,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
// dynamicPrompts = append(dynamicPrompts, p.ToMap())
|
// dynamicPrompts = append(dynamicPrompts, p.ToMap())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if obs == nil {
|
||||||
|
obs = NewSSEObserver(name, manager)
|
||||||
|
}
|
||||||
|
|
||||||
opts := []Option{
|
opts := []Option{
|
||||||
WithModel(model),
|
WithModel(model),
|
||||||
WithLLMAPIURL(a.apiURL),
|
WithLLMAPIURL(a.apiURL),
|
||||||
@@ -338,7 +421,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
WithMCPServers(config.MCPServers...),
|
WithMCPServers(config.MCPServers...),
|
||||||
WithPeriodicRuns(config.PeriodicRuns),
|
WithPeriodicRuns(config.PeriodicRuns),
|
||||||
WithPermanentGoal(config.PermanentGoal),
|
WithPermanentGoal(config.PermanentGoal),
|
||||||
|
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
||||||
|
WithMCPBoxURL(a.mcpBoxURL),
|
||||||
WithPrompts(promptBlocks...),
|
WithPrompts(promptBlocks...),
|
||||||
|
WithJobFilters(filters...),
|
||||||
|
WithMCPPrepareScript(config.MCPPrepareScript),
|
||||||
// WithDynamicPrompts(dynamicPrompts...),
|
// WithDynamicPrompts(dynamicPrompts...),
|
||||||
WithCharacter(Character{
|
WithCharacter(Character{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -375,6 +462,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
}),
|
}),
|
||||||
WithSystemPrompt(config.SystemPrompt),
|
WithSystemPrompt(config.SystemPrompt),
|
||||||
WithMultimodalModel(multimodalModel),
|
WithMultimodalModel(multimodalModel),
|
||||||
|
WithLastMessageDuration(config.LastMessageDuration),
|
||||||
WithAgentResultCallback(func(state types.ActionState) {
|
WithAgentResultCallback(func(state types.ActionState) {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
if _, ok := a.agentStatus[name]; !ok {
|
if _, ok := a.agentStatus[name]; !ok {
|
||||||
@@ -407,6 +495,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
c.AgentResultCallback()(state)
|
c.AgentResultCallback()(state)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
WithObserver(obs),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HUD {
|
if config.HUD {
|
||||||
@@ -457,6 +546,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
opts = append(opts, EnableForceReasoning)
|
opts = append(opts, EnableForceReasoning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.StripThinkingTags {
|
||||||
|
opts = append(opts, EnableStripThinkingTags)
|
||||||
|
}
|
||||||
|
|
||||||
if config.KnowledgeBaseResults > 0 {
|
if config.KnowledgeBaseResults > 0 {
|
||||||
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
|
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
|
||||||
}
|
}
|
||||||
@@ -465,6 +558,17 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
|
|||||||
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
|
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)
|
xlog.Info("Starting agent", "name", name, "config", config)
|
||||||
|
|
||||||
agent, err := New(opts...)
|
agent, err := New(opts...)
|
||||||
@@ -509,7 +613,7 @@ func (a *AgentPool) StartAll() error {
|
|||||||
if a.agents[name] != nil { // Agent already started
|
if a.agents[name] != nil { // Agent already started
|
||||||
continue
|
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)
|
xlog.Error("Failed to start agent", "name", name, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,7 +651,7 @@ func (a *AgentPool) Start(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if config, ok := a.pool[name]; ok {
|
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)
|
return fmt.Errorf("agent %s not found", name)
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ func (a ActionDefinitionName) String() string {
|
|||||||
return string(a)
|
return string(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a ActionDefinition) ToFunctionDefinition() openai.FunctionDefinition {
|
func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
|
||||||
return openai.FunctionDefinition{
|
return &openai.FunctionDefinition{
|
||||||
Name: a.Name.String(),
|
Name: a.Name.String(),
|
||||||
Description: a.Description,
|
Description: a.Description,
|
||||||
Parameters: jsonschema.Definition{
|
Parameters: jsonschema.Definition{
|
||||||
@@ -88,7 +88,7 @@ func (a ActionDefinition) ToFunctionDefinition() openai.FunctionDefinition {
|
|||||||
|
|
||||||
// Actions is something the agent can do
|
// Actions is something the agent can do
|
||||||
type Action interface {
|
type Action interface {
|
||||||
Run(ctx context.Context, action ActionParams) (ActionResult, error)
|
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
|
||||||
Definition() ActionDefinition
|
Definition() ActionDefinition
|
||||||
Plannable() bool
|
Plannable() bool
|
||||||
}
|
}
|
||||||
|
|||||||
15
core/types/filters.go
Normal file
15
core/types/filters.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ type Job struct {
|
|||||||
ConversationHistory []openai.ChatCompletionMessage
|
ConversationHistory []openai.ChatCompletionMessage
|
||||||
UUID string
|
UUID string
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
|
DoneFilter bool
|
||||||
|
|
||||||
pastActions []*ActionRequest
|
pastActions []*ActionRequest
|
||||||
nextAction *Action
|
nextAction *Action
|
||||||
@@ -27,6 +28,8 @@ type Job struct {
|
|||||||
|
|
||||||
context context.Context
|
context context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
Obs *Observable
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionRequest struct {
|
type ActionRequest struct {
|
||||||
@@ -159,23 +162,23 @@ func newUUID() string {
|
|||||||
// To wait for a Job result, use JobResult.WaitResult()
|
// To wait for a Job result, use JobResult.WaitResult()
|
||||||
func NewJob(opts ...JobOption) *Job {
|
func NewJob(opts ...JobOption) *Job {
|
||||||
j := &Job{
|
j := &Job{
|
||||||
Result: NewJobResult(),
|
Result: NewJobResult(),
|
||||||
UUID: newUUID(),
|
UUID: uuid.New().String(),
|
||||||
}
|
Metadata: make(map[string]interface{}),
|
||||||
for _, o := range opts {
|
context: context.Background(),
|
||||||
o(j)
|
ConversationHistory: []openai.ChatCompletionMessage{},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx context.Context
|
for _, opt := range opts {
|
||||||
if j.context == nil {
|
opt(j)
|
||||||
ctx = context.Background()
|
|
||||||
} else {
|
|
||||||
ctx = j.context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context, cancel := context.WithCancel(ctx)
|
// Store the original request if it exists in the conversation history
|
||||||
j.context = context
|
|
||||||
|
ctx, cancel := context.WithCancel(j.context)
|
||||||
|
j.context = ctx
|
||||||
j.cancel = cancel
|
j.cancel = cancel
|
||||||
|
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,3 +201,29 @@ func (j *Job) Cancel() {
|
|||||||
func (j *Job) GetContext() context.Context {
|
func (j *Job) GetContext() context.Context {
|
||||||
return j.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
|
||||||
|
}
|
||||||
|
|||||||
63
core/types/observable.go
Normal file
63
core/types/observable.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
73
core/types/state.go
Normal file
73
core/types/state.go
Normal file
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,77 +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
|
|
||||||
- flux.1-dev-ggml
|
|
||||||
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
|
|
||||||
- LOCALAGI_IMAGE_MODEL=flux.1-dev-ggml
|
|
||||||
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
|
|
||||||
38
docker-compose.intel.yaml
Normal file
38
docker-compose.intel.yaml
Normal file
@@ -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-sycl-f32
|
||||||
|
devices:
|
||||||
|
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
||||||
|
- /dev/dri/card1
|
||||||
|
- /dev/dri/renderD129
|
||||||
|
|
||||||
|
mcpbox:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yaml
|
||||||
|
service: mcpbox
|
||||||
|
|
||||||
|
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
|
||||||
43
docker-compose.nvidia.yaml
Normal file
43
docker-compose.nvidia.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
services:
|
||||||
|
localai:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yaml
|
||||||
|
service: localai
|
||||||
|
environment:
|
||||||
|
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||||
|
- DEBUG=true
|
||||||
|
image: localai/localai:master-cublas-cuda12
|
||||||
|
# For images with python backends, use:
|
||||||
|
# image: localai/localai:master-cublas-cuda12-ffmpeg
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
mcpbox:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yaml
|
||||||
|
service: mcpbox
|
||||||
|
|
||||||
|
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
|
||||||
@@ -5,9 +5,11 @@ services:
|
|||||||
# Available images with CUDA, ROCm, SYCL, Vulkan
|
# Available images with CUDA, ROCm, SYCL, Vulkan
|
||||||
# Image list (quay.io): https://quay.io/repository/go-skynet/local-ai?tab=tags
|
# 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 list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||||
image: localai/localai:master-ffmpeg-core
|
image: localai/localai:master
|
||||||
command:
|
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
|
- granite-embedding-107m-multilingual
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
|
||||||
@@ -23,14 +25,6 @@ services:
|
|||||||
- ./volumes/models:/build/models:cached
|
- ./volumes/models:/build/models:cached
|
||||||
- ./volumes/images:/tmp/generated/images
|
- ./volumes/images:/tmp/generated/images
|
||||||
|
|
||||||
# decomment the following piece if running with Nvidia GPUs
|
|
||||||
# deploy:
|
|
||||||
# resources:
|
|
||||||
# reservations:
|
|
||||||
# devices:
|
|
||||||
# - driver: nvidia
|
|
||||||
# count: 1
|
|
||||||
# capabilities: [gpu]
|
|
||||||
localrecall:
|
localrecall:
|
||||||
image: quay.io/mudler/localrecall:main
|
image: quay.io/mudler/localrecall:main
|
||||||
ports:
|
ports:
|
||||||
@@ -52,26 +46,76 @@ services:
|
|||||||
image: busybox
|
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!'"]
|
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
|
||||||
|
|
||||||
|
mcpbox:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.mcpbox
|
||||||
|
ports:
|
||||||
|
- "8080"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/mcpbox:/app/data
|
||||||
|
environment:
|
||||||
|
- DOCKER_HOST=tcp://dind:2375
|
||||||
|
depends_on:
|
||||||
|
dind:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/processes"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
dind:
|
||||||
|
image: docker:dind
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
- DOCKER_TLS_CERTDIR=""
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "docker", "info"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
localagi:
|
localagi:
|
||||||
depends_on:
|
depends_on:
|
||||||
localai:
|
localai:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
localrecall-healthcheck:
|
localrecall-healthcheck:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
mcpbox:
|
||||||
|
condition: service_healthy
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.webui
|
dockerfile: Dockerfile.webui
|
||||||
ports:
|
ports:
|
||||||
- 8080:3000
|
- 8080:3000
|
||||||
image: quay.io/mudler/localagi:master
|
#image: quay.io/mudler/localagi:master
|
||||||
environment:
|
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_URL=http://localai:8080
|
||||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||||
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
- LOCALAGI_LOCALRAG_URL=http://localrecall:8080
|
||||||
- LOCALAGI_STATE_DIR=/pool
|
- LOCALAGI_STATE_DIR=/pool
|
||||||
- LOCALAGI_TIMEOUT=5m
|
- LOCALAGI_TIMEOUT=5m
|
||||||
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
- LOCALAGI_ENABLE_CONVERSATIONS_LOGGING=false
|
||||||
|
- LOCALAGI_MCPBOX_URL=http://mcpbox:8080
|
||||||
|
- LOCALAGI_SSHBOX_URL=root:root@sshbox:22
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
132
go.mod
132
go.mod
@@ -1,79 +1,98 @@
|
|||||||
module github.com/mudler/LocalAGI
|
module github.com/mudler/LocalAGI
|
||||||
|
|
||||||
go 1.22.0
|
go 1.24
|
||||||
|
|
||||||
toolchain go1.22.2
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.28.1
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
github.com/chasefleming/elem-go v0.25.0
|
github.com/chasefleming/elem-go v0.30.0
|
||||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||||
github.com/donseba/go-htmx v1.8.0
|
github.com/donseba/go-htmx v1.12.0
|
||||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
||||||
github.com/go-telegram/bot v1.2.1
|
github.com/go-telegram/bot v1.15.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.4
|
github.com/gofiber/fiber/v2 v2.52.8
|
||||||
github.com/gofiber/template/html/v2 v2.1.1
|
github.com/gofiber/template/html/v2 v2.1.3
|
||||||
github.com/google/go-github/v69 v69.2.0
|
github.com/google/go-github/v69 v69.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/metoro-io/mcp-golang v0.8.0
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/onsi/ginkgo/v2 v2.15.0
|
github.com/metoro-io/mcp-golang v0.13.0
|
||||||
github.com/onsi/gomega v1.31.1
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/philippgille/chromem-go v0.5.0
|
github.com/onsi/gomega v1.37.0
|
||||||
github.com/sashabaranov/go-openai v1.18.3
|
github.com/philippgille/chromem-go v0.7.0
|
||||||
github.com/slack-go/slack v0.16.0
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/sashabaranov/go-openai v1.40.0
|
||||||
|
github.com/slack-go/slack v0.17.1
|
||||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
||||||
github.com/tmc/langchaingo v0.1.8
|
github.com/tmc/langchaingo v0.1.13
|
||||||
github.com/traefik/yaegi v0.16.1
|
github.com/traefik/yaegi v0.16.1
|
||||||
github.com/valyala/fasthttp v1.52.0
|
github.com/valyala/fasthttp v1.62.0
|
||||||
golang.org/x/crypto v0.30.0
|
golang.org/x/crypto v0.39.0
|
||||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||||
|
maunium.net/go/mautrix v0.24.0
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/JohannesKaufmann/dom v0.2.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
|
require (
|
||||||
github.com/antchfx/xpath v1.2.4 // indirect
|
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // 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/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/go-logr/logr v1.3.0 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/emersion/go-message v0.18.2
|
||||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/emersion/go-smtp v0.22.0
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/goccy/go-json v0.9.7 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gocolly/colly v1.2.0 // indirect
|
github.com/gocolly/colly v1.2.0 // indirect
|
||||||
github.com/gofiber/template v1.8.3 // indirect
|
github.com/gofiber/template v1.8.3 // indirect
|
||||||
github.com/gofiber/utils v1.1.0 // indirect
|
github.com/gofiber/utils v1.1.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // 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/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||||
github.com/invopop/jsonschema v0.12.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||||
github.com/klauspost/compress v1.17.7 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
@@ -81,17 +100,20 @@ require (
|
|||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // 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
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
go.mau.fi/util v0.8.7 // indirect
|
||||||
golang.org/x/net v0.32.0 // indirect
|
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/arch v0.16.0 // indirect
|
||||||
golang.org/x/tools v0.28.0 // indirect
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.26.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/protobuf v1.32.0 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
404
go.sum
404
go.sum
@@ -1,152 +1,153 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3 h1:r3fokGFRDk/8pHmwLwJ8zsX4qiqfS1/1TZm2BH8ueY8=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.3/go.mod h1:HtsP+1Fchp4dVvaiIsLHAl/yqL3H1YLwqLC9kNwqQEg=
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
|
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
|
||||||
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
|
||||||
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
|
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||||
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
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/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 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
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.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
github.com/chasefleming/elem-go v0.25.0 h1:LYzr1auk39Bh3bdKloArOFV7sOBnOfSOKxsg58eWL0Q=
|
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/chasefleming/elem-go v0.25.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
|
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/donseba/go-htmx v1.8.0 h1:oTx1uUsjXZZVvcZfulZvBSPtdD1jzsvZyuK91+Q8zPE=
|
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
|
||||||
github.com/donseba/go-htmx v1.8.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
|
github.com/donseba/go-htmx v1.12.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/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
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.22.0 h1:/d3HWxkZZ4riB+0kzfoODh9X+xyCrLEezMnAAa1LEMU=
|
||||||
|
github.com/emersion/go-smtp v0.22.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 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
||||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-telegram/bot v1.2.1 h1:FkrixLCtMtPUQAN4plXdNElbhkdXkx2p68YPXKBruDg=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-telegram/bot v1.2.1/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
|
||||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-telegram/bot v1.15.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 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
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.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
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 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.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
|
||||||
github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0=
|
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
||||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
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/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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
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/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
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.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.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 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
|
||||||
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
|
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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
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/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-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||||
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
|
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||||
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
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/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/metoro-io/mcp-golang v0.8.0 h1:DkigHa3w7WwMFomcEz5wiMDX94DsvVm/3mCV3d1obnc=
|
github.com/metoro-io/mcp-golang v0.13.0 h1:54TFBJIW76VRB55CJovQQje9x4GnXg0BQQwGRtXrbCE=
|
||||||
github.com/metoro-io/mcp-golang v0.8.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
|
github.com/metoro-io/mcp-golang v0.13.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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -154,48 +155,54 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
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/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.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
|
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||||
github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
|
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philippgille/chromem-go v0.5.0 h1:bryX0F3N6jnN/21iBd8i2/k9EzPTZn3nyiqAti19si8=
|
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||||
github.com/philippgille/chromem-go v0.5.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
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/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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
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 h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
||||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
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/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.40.0 h1:Peg9Iag5mUJtPW00aYatlsn97YML0iNULiLNe74iPrU=
|
||||||
github.com/sashabaranov/go-openai v1.18.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
github.com/sashabaranov/go-openai v1.40.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||||
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
|
||||||
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||||
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
|
github.com/slack-go/slack v0.17.1 h1:x0Mnc6biHBea5vfxLR+x4JFl/Rm3eIo0iS3xDZenX+o=
|
||||||
|
github.com/slack-go/slack v0.17.1/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
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/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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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.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.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.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.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
||||||
@@ -210,141 +217,138 @@ 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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
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.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
|
||||||
github.com/tmc/langchaingo v0.1.8/go.mod h1:iNBfS9e6jxBKsJSPWnlqNhoVWgdA3D1g5cdFJjbIZNQ=
|
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
|
||||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||||
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
||||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
|
||||||
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 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
go.mau.fi/util v0.8.7 h1:ywKarPxouJQEEijTs4mPlxC7F4AWEKokEpWc+2TYy6c=
|
||||||
|
go.mau.fi/util v0.8.7/go.mod h1:j6R3cENakc1f8HpQeFl0N15UiSTcNmIfDBNJUbL71RY=
|
||||||
|
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=
|
||||||
|
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||||
|
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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/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/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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-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-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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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.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/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-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-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-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-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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
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-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-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.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
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.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
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.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.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
||||||
|
maunium.net/go/mautrix v0.24.0 h1:kBeyWhgL1W8/d8BEFlBSlgIpItPgP1l37hzF8cN3R70=
|
||||||
|
maunium.net/go/mautrix v0.24.0/go.mod h1:HqA1HUutQYJkrYRPkK64itARDz79PCec1oWVEB72HVQ=
|
||||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||||
|
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||||
|
|||||||
23
main.go
23
main.go
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/mudler/LocalAGI/webui"
|
"github.com/mudler/LocalAGI/webui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testModel = os.Getenv("LOCALAGI_MODEL")
|
var baseModel = os.Getenv("LOCALAGI_MODEL")
|
||||||
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
|
var multimodalModel = os.Getenv("LOCALAGI_MULTIMODAL_MODEL")
|
||||||
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
|
var apiURL = os.Getenv("LOCALAGI_LLM_API_URL")
|
||||||
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
|
var apiKey = os.Getenv("LOCALAGI_LLM_API_KEY")
|
||||||
@@ -22,13 +22,16 @@ var withLogs = os.Getenv("LOCALAGI_ENABLE_CONVERSATIONS_LOGGING") == "true"
|
|||||||
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
|
var apiKeysEnv = os.Getenv("LOCALAGI_API_KEYS")
|
||||||
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
|
var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
|
||||||
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
||||||
|
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
|
||||||
|
var mcpboxURL = os.Getenv("LOCALAGI_MCPBOX_URL")
|
||||||
|
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if testModel == "" {
|
if baseModel == "" {
|
||||||
testModel = "hermes-2-pro-mistral"
|
panic("LOCALAGI_MODEL not set")
|
||||||
}
|
}
|
||||||
if apiURL == "" {
|
if apiURL == "" {
|
||||||
apiURL = "http://192.168.68.113:8080"
|
panic("LOCALAGI_API_URL not set")
|
||||||
}
|
}
|
||||||
if timeout == "" {
|
if timeout == "" {
|
||||||
timeout = "5m"
|
timeout = "5m"
|
||||||
@@ -54,16 +57,22 @@ func main() {
|
|||||||
|
|
||||||
// Create the agent pool
|
// Create the agent pool
|
||||||
pool, err := state.NewAgentPool(
|
pool, err := state.NewAgentPool(
|
||||||
testModel,
|
baseModel,
|
||||||
multimodalModel,
|
multimodalModel,
|
||||||
imageModel,
|
imageModel,
|
||||||
apiURL,
|
apiURL,
|
||||||
apiKey,
|
apiKey,
|
||||||
stateDir,
|
stateDir,
|
||||||
|
mcpboxURL,
|
||||||
localRAG,
|
localRAG,
|
||||||
services.Actions,
|
services.Actions(map[string]string{
|
||||||
|
services.ActionConfigBrowserAgentRunner: localOperatorBaseURL,
|
||||||
|
services.ActionConfigDeepResearchRunner: localOperatorBaseURL,
|
||||||
|
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||||
|
}),
|
||||||
services.Connectors,
|
services.Connectors,
|
||||||
services.DynamicPrompts,
|
services.DynamicPrompts,
|
||||||
|
services.Filters,
|
||||||
timeout,
|
timeout,
|
||||||
withLogs,
|
withLogs,
|
||||||
)
|
)
|
||||||
@@ -78,7 +87,7 @@ func main() {
|
|||||||
webui.WithApiKeys(apiKeys...),
|
webui.WithApiKeys(apiKeys...),
|
||||||
webui.WithLLMAPIUrl(apiURL),
|
webui.WithLLMAPIUrl(apiURL),
|
||||||
webui.WithLLMAPIKey(apiKey),
|
webui.WithLLMAPIKey(apiKey),
|
||||||
webui.WithLLMModel(testModel),
|
webui.WithLLMModel(baseModel),
|
||||||
webui.WithStateDir(stateDir),
|
webui.WithStateDir(stateDir),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,21 +10,25 @@ import (
|
|||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"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"
|
toolName := "json"
|
||||||
decision := openai.ChatCompletionRequest{
|
decision := openai.ChatCompletionRequest{
|
||||||
Model: model,
|
Model: model,
|
||||||
Messages: []openai.ChatCompletionMessage{
|
Messages: conv,
|
||||||
{
|
|
||||||
Role: "user",
|
|
||||||
Content: guidance,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tools: []openai.Tool{
|
Tools: []openai.Tool{
|
||||||
{
|
{
|
||||||
|
|
||||||
Type: openai.ToolTypeFunction,
|
Type: openai.ToolTypeFunction,
|
||||||
Function: openai.FunctionDefinition{
|
Function: &openai.FunctionDefinition{
|
||||||
Name: toolName,
|
Name: toolName,
|
||||||
Parameters: i,
|
Parameters: i,
|
||||||
},
|
},
|
||||||
|
|||||||
149
pkg/localoperator/client.go
Normal file
149
pkg/localoperator/client.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
325
pkg/stdio/client.go
Normal file
325
pkg/stdio/client.go
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
package stdio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client implements the transport.Interface for stdio processes
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
processes map[string]*Process
|
||||||
|
groups map[string][]string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new stdio transport client
|
||||||
|
func NewClient(baseURL string) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: baseURL,
|
||||||
|
processes: make(map[string]*Process),
|
||||||
|
groups: make(map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProcess starts a new process in a group
|
||||||
|
func (c *Client) CreateProcess(ctx context.Context, command string, args []string, env []string, groupID string) (*Process, error) {
|
||||||
|
log.Printf("Creating process: command=%s, args=%v, groupID=%s", command, args, groupID)
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env []string `json:"env"`
|
||||||
|
GroupID string `json:"group_id"`
|
||||||
|
}{
|
||||||
|
Command: command,
|
||||||
|
Args: args,
|
||||||
|
Env: env,
|
||||||
|
GroupID: groupID,
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/processes", c.baseURL)
|
||||||
|
log.Printf("Sending POST request to %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to start process: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Printf("Received response with status: %d", resp.StatusCode)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %w. body: %s", err, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully created process with ID: %s", result.ID)
|
||||||
|
|
||||||
|
process := &Process{
|
||||||
|
ID: result.ID,
|
||||||
|
GroupID: groupID,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.processes[process.ID] = process
|
||||||
|
if groupID != "" {
|
||||||
|
c.groups[groupID] = append(c.groups[groupID], process.ID)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
return process, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProcess returns a process by ID
|
||||||
|
func (c *Client) GetProcess(id string) (*Process, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
process, exists := c.processes[id]
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("process not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return process, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroupProcesses returns all processes in a group
|
||||||
|
func (c *Client) GetGroupProcesses(groupID string) ([]*Process, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
processIDs, exists := c.groups[groupID]
|
||||||
|
if !exists {
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return nil, fmt.Errorf("group not found: %s", groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
processes := make([]*Process, 0, len(processIDs))
|
||||||
|
for _, pid := range processIDs {
|
||||||
|
if process, exists := c.processes[pid]; exists {
|
||||||
|
processes = append(processes, process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return processes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopProcess stops a single process
|
||||||
|
func (c *Client) StopProcess(id string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
process, exists := c.processes[id]
|
||||||
|
if !exists {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return fmt.Errorf("process not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from group if it exists
|
||||||
|
if process.GroupID != "" {
|
||||||
|
groupProcesses := c.groups[process.GroupID]
|
||||||
|
for i, pid := range groupProcesses {
|
||||||
|
if pid == id {
|
||||||
|
c.groups[process.GroupID] = append(groupProcesses[:i], groupProcesses[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(c.groups[process.GroupID]) == 0 {
|
||||||
|
delete(c.groups, process.GroupID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(c.processes, id)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(
|
||||||
|
"DELETE",
|
||||||
|
fmt.Sprintf("%s/processes/%s", c.baseURL, id),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop process: %w", err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopGroup stops all processes in a group
|
||||||
|
func (c *Client) StopGroup(groupID string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
processIDs, exists := c.groups[groupID]
|
||||||
|
if !exists {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return fmt.Errorf("group not found: %s", groupID)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, pid := range processIDs {
|
||||||
|
if err := c.StopProcess(pid); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop process %s in group %s: %w", pid, groupID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGroups returns all group IDs
|
||||||
|
func (c *Client) ListGroups() []string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
groups := make([]string, 0, len(c.groups))
|
||||||
|
for groupID := range c.groups {
|
||||||
|
groups = append(groups, groupID)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProcessIO returns io.Reader and io.Writer for a process
|
||||||
|
func (c *Client) GetProcessIO(id string) (io.Reader, io.Writer, error) {
|
||||||
|
log.Printf("Getting IO for process: %s", id)
|
||||||
|
|
||||||
|
process, err := c.GetProcess(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the base URL to get the host
|
||||||
|
baseURL, err := url.Parse(c.baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to parse base URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "ws",
|
||||||
|
Host: baseURL.Host,
|
||||||
|
Path: fmt.Sprintf("/ws/%s", process.ID),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Connecting to WebSocket at: %s", u.String())
|
||||||
|
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to connect to WebSocket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully connected to WebSocket for process: %s", id)
|
||||||
|
|
||||||
|
// Create reader and writer
|
||||||
|
reader := &websocketReader{conn: conn}
|
||||||
|
writer := &websocketWriter{conn: conn}
|
||||||
|
|
||||||
|
return reader, writer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// websocketReader implements io.Reader for WebSocket
|
||||||
|
type websocketReader struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *websocketReader) Read(p []byte) (n int, err error) {
|
||||||
|
_, message, err := r.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
n = copy(p, message)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// websocketWriter implements io.Writer for WebSocket
|
||||||
|
type websocketWriter struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *websocketWriter) Write(p []byte) (n int, err error) {
|
||||||
|
// Use BinaryMessage type for better compatibility
|
||||||
|
err = w.conn.WriteMessage(websocket.BinaryMessage, p)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to write WebSocket message: %w", err)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all connections and stops all processes
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Stop all processes
|
||||||
|
for id := range c.processes {
|
||||||
|
if err := c.StopProcess(id); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop process %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunProcess executes a command and returns its output
|
||||||
|
func (c *Client) RunProcess(ctx context.Context, command string, args []string, env []string) (string, error) {
|
||||||
|
log.Printf("Running one-time process: command=%s, args=%v", command, args)
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env []string `json:"env"`
|
||||||
|
}{
|
||||||
|
Command: command,
|
||||||
|
Args: args,
|
||||||
|
Env: env,
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/run", c.baseURL)
|
||||||
|
log.Printf("Sending POST request to %s", url)
|
||||||
|
|
||||||
|
resp, err := http.Post(url, "application/json", bytes.NewReader(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to execute process: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Printf("Received response with status: %d", resp.StatusCode)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", fmt.Errorf("failed to decode response: %w. body: %s", err, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully executed process with output length: %d", len(result.Output))
|
||||||
|
return result.Output, nil
|
||||||
|
}
|
||||||
28
pkg/stdio/client_suite_test.go
Normal file
28
pkg/stdio/client_suite_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package stdio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSTDIOTransport(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "STDIOTransport test suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseURL string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
baseURL = os.Getenv("LOCALAGI_MCPBOX_URL")
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "http://localhost:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = AfterSuite(func() {
|
||||||
|
client := NewClient(baseURL)
|
||||||
|
client.StopGroup("test-group")
|
||||||
|
})
|
||||||
235
pkg/stdio/client_test.go
Normal file
235
pkg/stdio/client_test.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package stdio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mcp "github.com/metoro-io/mcp-golang"
|
||||||
|
"github.com/metoro-io/mcp-golang/transport/stdio"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Client", func() {
|
||||||
|
var (
|
||||||
|
client *Client
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
client = NewClient(baseURL)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
if client != nil {
|
||||||
|
Expect(client.Close()).To(Succeed())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("Process Management", func() {
|
||||||
|
It("should create and stop a process", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
// Use a command that doesn't exit immediately
|
||||||
|
process, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Hello, World!'; sleep 10"}, []string{}, "test-group")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process).NotTo(BeNil())
|
||||||
|
Expect(process.ID).NotTo(BeEmpty())
|
||||||
|
|
||||||
|
// Get process IO
|
||||||
|
reader, writer, err := client.GetProcessIO(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(reader).NotTo(BeNil())
|
||||||
|
Expect(writer).NotTo(BeNil())
|
||||||
|
|
||||||
|
// Write to process
|
||||||
|
_, err = writer.Write([]byte("test input\n"))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Read from process with timeout
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
readDone := make(chan struct{})
|
||||||
|
var readErr error
|
||||||
|
var readN int
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
readN, readErr = reader.Read(buf)
|
||||||
|
close(readDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for read with timeout
|
||||||
|
select {
|
||||||
|
case <-readDone:
|
||||||
|
Expect(readErr).NotTo(HaveOccurred())
|
||||||
|
Expect(readN).To(BeNumerically(">", 0))
|
||||||
|
Expect(string(buf[:readN])).To(ContainSubstring("Hello, World!"))
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
Fail("Timeout waiting for process output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the process
|
||||||
|
err = client.StopProcess(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should manage process groups", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
groupID := "test-group"
|
||||||
|
|
||||||
|
// Create multiple processes in the same group
|
||||||
|
process1, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Process 1'; sleep 1"}, []string{}, groupID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process1).NotTo(BeNil())
|
||||||
|
|
||||||
|
process2, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Process 2'; sleep 1"}, []string{}, groupID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process2).NotTo(BeNil())
|
||||||
|
|
||||||
|
// Get group processes
|
||||||
|
processes, err := client.GetGroupProcesses(groupID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(processes).To(HaveLen(2))
|
||||||
|
|
||||||
|
// List groups
|
||||||
|
groups := client.ListGroups()
|
||||||
|
Expect(groups).To(ContainElement(groupID))
|
||||||
|
|
||||||
|
// Stop the group
|
||||||
|
err = client.StopGroup(groupID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should run a one-time process", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
output, err := client.RunProcess(ctx, "echo", []string{"One-time process"}, []string{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(output).To(ContainSubstring("One-time process"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle process with environment variables", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
env := []string{"TEST_VAR=test_value"}
|
||||||
|
process, err := client.CreateProcess(ctx, "sh", []string{"-c", "env | grep TEST_VAR; sleep 1"}, env, "test-group")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process).NotTo(BeNil())
|
||||||
|
|
||||||
|
// Get process IO
|
||||||
|
reader, _, err := client.GetProcessIO(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Read environment variables with timeout
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
readDone := make(chan struct{})
|
||||||
|
var readErr error
|
||||||
|
var readN int
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
readN, readErr = reader.Read(buf)
|
||||||
|
close(readDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for read with timeout
|
||||||
|
select {
|
||||||
|
case <-readDone:
|
||||||
|
Expect(readErr).NotTo(HaveOccurred())
|
||||||
|
Expect(readN).To(BeNumerically(">", 0))
|
||||||
|
Expect(string(buf[:readN])).To(ContainSubstring("TEST_VAR=test_value"))
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
Fail("Timeout waiting for process output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the process
|
||||||
|
err = client.StopProcess(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle long-running processes", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
process, err := client.CreateProcess(ctx, "sh", []string{"-c", "echo 'Starting long process'; sleep 5"}, []string{}, "test-group")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process).NotTo(BeNil())
|
||||||
|
|
||||||
|
// Get process IO
|
||||||
|
reader, _, err := client.GetProcessIO(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Read initial output
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
readDone := make(chan struct{})
|
||||||
|
var readErr error
|
||||||
|
var readN int
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
readN, readErr = reader.Read(buf)
|
||||||
|
close(readDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for read with timeout
|
||||||
|
select {
|
||||||
|
case <-readDone:
|
||||||
|
Expect(readErr).NotTo(HaveOccurred())
|
||||||
|
Expect(readN).To(BeNumerically(">", 0))
|
||||||
|
Expect(string(buf[:readN])).To(ContainSubstring("Starting long process"))
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
Fail("Timeout waiting for process output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit to ensure process is running
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
// Stop the process
|
||||||
|
err = client.StopProcess(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("MCP", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
process, err := client.CreateProcess(ctx,
|
||||||
|
"docker", []string{"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"},
|
||||||
|
[]string{"GITHUB_PERSONAL_ACCESS_TOKEN=test"}, "test-group")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(process).NotTo(BeNil())
|
||||||
|
Expect(process.ID).NotTo(BeEmpty())
|
||||||
|
|
||||||
|
defer client.StopProcess(process.ID)
|
||||||
|
|
||||||
|
// MCP client
|
||||||
|
|
||||||
|
read, writer, err := client.GetProcessIO(process.ID)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(read).NotTo(BeNil())
|
||||||
|
Expect(writer).NotTo(BeNil())
|
||||||
|
|
||||||
|
transport := stdio.NewStdioServerTransportWithIO(read, writer)
|
||||||
|
|
||||||
|
// Create a new client
|
||||||
|
mcpClient := mcp.NewClient(transport)
|
||||||
|
// Initialize the client
|
||||||
|
response, e := mcpClient.Initialize(ctx)
|
||||||
|
Expect(e).NotTo(HaveOccurred())
|
||||||
|
Expect(response).NotTo(BeNil())
|
||||||
|
|
||||||
|
Expect(mcpClient.Ping(ctx)).To(Succeed())
|
||||||
|
|
||||||
|
xlog.Debug("Client initialized: %v", response.Instructions)
|
||||||
|
|
||||||
|
alltools := []mcp.ToolRetType{}
|
||||||
|
var cursor *string
|
||||||
|
for {
|
||||||
|
tools, err := mcpClient.ListTools(ctx, cursor)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(tools).NotTo(BeNil())
|
||||||
|
Expect(tools.Tools).NotTo(BeEmpty())
|
||||||
|
alltools = append(alltools, tools.Tools...)
|
||||||
|
|
||||||
|
if tools.NextCursor == nil {
|
||||||
|
break // No more pages
|
||||||
|
}
|
||||||
|
cursor = tools.NextCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range alltools {
|
||||||
|
xlog.Debug("Tool: %v", tool)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
473
pkg/stdio/server.go
Normal file
473
pkg/stdio/server.go
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
package stdio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Process represents a running process with its stdio streams
|
||||||
|
type Process struct {
|
||||||
|
ID string
|
||||||
|
GroupID string
|
||||||
|
Cmd *exec.Cmd
|
||||||
|
Stdin io.WriteCloser
|
||||||
|
Stdout io.ReadCloser
|
||||||
|
Stderr io.ReadCloser
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server handles process management and stdio streaming
|
||||||
|
type Server struct {
|
||||||
|
processes map[string]*Process
|
||||||
|
groups map[string][]string // maps group ID to process IDs
|
||||||
|
mu sync.RWMutex
|
||||||
|
upgrader websocket.Upgrader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new stdio server
|
||||||
|
func NewServer() *Server {
|
||||||
|
return &Server{
|
||||||
|
processes: make(map[string]*Process),
|
||||||
|
groups: make(map[string][]string),
|
||||||
|
upgrader: websocket.Upgrader{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartProcess starts a new process and returns its ID
|
||||||
|
func (s *Server) StartProcess(ctx context.Context, command string, args []string, env []string, groupID string) (string, error) {
|
||||||
|
xlog.Debug("Starting process", "command", command, "args", args, "groupID", groupID)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, command, args...)
|
||||||
|
|
||||||
|
if len(env) > 0 {
|
||||||
|
cmd.Env = append(os.Environ(), env...)
|
||||||
|
xlog.Debug("Process environment", "env", cmd.Env)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to start process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
process := &Process{
|
||||||
|
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||||
|
GroupID: groupID,
|
||||||
|
Cmd: cmd,
|
||||||
|
Stdin: stdin,
|
||||||
|
Stdout: stdout,
|
||||||
|
Stderr: stderr,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.processes[process.ID] = process
|
||||||
|
if groupID != "" {
|
||||||
|
s.groups[groupID] = append(s.groups[groupID], process.ID)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
xlog.Debug("Successfully started process", "id", process.ID, "pid", cmd.Process.Pid)
|
||||||
|
return process.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopProcess stops a running process
|
||||||
|
func (s *Server) StopProcess(id string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
process, exists := s.processes[id]
|
||||||
|
if !exists {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return fmt.Errorf("process not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Debug("Stopping process", "processID", id, "pid", process.Cmd.Process.Pid)
|
||||||
|
|
||||||
|
// Remove from group if it exists
|
||||||
|
if process.GroupID != "" {
|
||||||
|
groupProcesses := s.groups[process.GroupID]
|
||||||
|
for i, pid := range groupProcesses {
|
||||||
|
if pid == id {
|
||||||
|
s.groups[process.GroupID] = append(groupProcesses[:i], groupProcesses[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s.groups[process.GroupID]) == 0 {
|
||||||
|
delete(s.groups, process.GroupID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.processes, id)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if err := process.Cmd.Process.Kill(); err != nil {
|
||||||
|
xlog.Debug("Failed to kill process", "processID", id, "pid", process.Cmd.Process.Pid, "error", err)
|
||||||
|
return fmt.Errorf("failed to kill process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Debug("Successfully killed process", "processID", id, "pid", process.Cmd.Process.Pid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopGroup stops all processes in a group
|
||||||
|
func (s *Server) StopGroup(groupID string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
processIDs, exists := s.groups[groupID]
|
||||||
|
if !exists {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return fmt.Errorf("group not found: %s", groupID)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
for _, pid := range processIDs {
|
||||||
|
if err := s.StopProcess(pid); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop process %s in group %s: %w", pid, groupID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroupProcesses returns all processes in a group
|
||||||
|
func (s *Server) GetGroupProcesses(groupID string) ([]*Process, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
processIDs, exists := s.groups[groupID]
|
||||||
|
if !exists {
|
||||||
|
s.mu.RUnlock()
|
||||||
|
return nil, fmt.Errorf("group not found: %s", groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
processes := make([]*Process, 0, len(processIDs))
|
||||||
|
for _, pid := range processIDs {
|
||||||
|
if process, exists := s.processes[pid]; exists {
|
||||||
|
processes = append(processes, process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
return processes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGroups returns all group IDs
|
||||||
|
func (s *Server) ListGroups() []string {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
groups := make([]string, 0, len(s.groups))
|
||||||
|
for groupID := range s.groups {
|
||||||
|
groups = append(groups, groupID)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProcess returns a process by ID
|
||||||
|
func (s *Server) GetProcess(id string) (*Process, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
process, exists := s.processes[id]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("process not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return process, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProcesses returns all running processes
|
||||||
|
func (s *Server) ListProcesses() []*Process {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
processes := make([]*Process, 0, len(s.processes))
|
||||||
|
for _, p := range s.processes {
|
||||||
|
processes = append(processes, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunProcess executes a command and returns its output
|
||||||
|
func (s *Server) RunProcess(ctx context.Context, command string, args []string, env []string) (string, error) {
|
||||||
|
cmd := exec.CommandContext(ctx, command, args...)
|
||||||
|
|
||||||
|
if len(env) > 0 {
|
||||||
|
cmd.Env = append(os.Environ(), env...)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return string(output), fmt.Errorf("process failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTP server
|
||||||
|
func (s *Server) Start(addr string) error {
|
||||||
|
http.HandleFunc("/processes", s.handleProcesses)
|
||||||
|
http.HandleFunc("/processes/", s.handleProcess)
|
||||||
|
http.HandleFunc("/ws/", s.handleWebSocket)
|
||||||
|
http.HandleFunc("/groups", s.handleGroups)
|
||||||
|
http.HandleFunc("/groups/", s.handleGroup)
|
||||||
|
http.HandleFunc("/run", s.handleRun)
|
||||||
|
|
||||||
|
return http.ListenAndServe(addr, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleProcesses(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("Handling /processes request: method=%s", r.Method)
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
processes := s.ListProcesses()
|
||||||
|
json.NewEncoder(w).Encode(processes)
|
||||||
|
case http.MethodPost:
|
||||||
|
var req struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env []string `json:"env"`
|
||||||
|
GroupID string `json:"group_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := s.StartProcess(context.Background(), req.Command, req.Args, req.Env, req.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"id": id})
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleProcess(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.URL.Path[len("/processes/"):]
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
process, err := s.GetProcess(id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(process)
|
||||||
|
case http.MethodDelete:
|
||||||
|
if err := s.StopProcess(id); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := r.URL.Path[len("/ws/"):]
|
||||||
|
xlog.Debug("Handling WebSocket connection", "processID", id)
|
||||||
|
|
||||||
|
process, err := s.GetProcess(id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if process.Cmd.ProcessState != nil && process.Cmd.ProcessState.Exited() {
|
||||||
|
xlog.Debug("Process already exited", "processID", id)
|
||||||
|
http.Error(w, "Process already exited", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Debug("Process is running", "processID", id, "pid", process.Cmd.Process.Pid)
|
||||||
|
|
||||||
|
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
xlog.Debug("WebSocket connection established", "processID", id)
|
||||||
|
|
||||||
|
// Create a done channel to signal process completion
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Handle stdin
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
xlog.Debug("Process stdin handler done", "processID", id)
|
||||||
|
default:
|
||||||
|
xlog.Debug("WebSocket stdin connection closed", "processID", id)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
||||||
|
xlog.Debug("WebSocket stdin unexpected error", "processID", id, "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Debug("Received message", "processID", id, "message", string(message))
|
||||||
|
if _, err := process.Stdin.Write(message); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
xlog.Debug("WebSocket stdin write error", "processID", id, "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Debug("Message sent to process", "processID", id, "message", string(message))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Handle stdout and stderr
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
xlog.Debug("Process output handler done", "processID", id)
|
||||||
|
default:
|
||||||
|
xlog.Debug("WebSocket output connection closed", "processID", id)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a buffer for reading
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
reader := io.MultiReader(process.Stdout, process.Stderr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := reader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
xlog.Debug("Read error", "processID", id, "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
xlog.Debug("Sending message", "processID", id, "size", n)
|
||||||
|
if err := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||||
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
|
||||||
|
xlog.Debug("WebSocket output write error", "processID", id, "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Debug("Message sent to client", "processID", id, "size", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for process to exit
|
||||||
|
xlog.Debug("Waiting for process to exit", "processID", id)
|
||||||
|
err = process.Cmd.Wait()
|
||||||
|
close(done) // Signal that the process is done
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
xlog.Debug("Process exited with error",
|
||||||
|
"processID", id,
|
||||||
|
"pid", process.Cmd.Process.Pid,
|
||||||
|
"error", err)
|
||||||
|
} else {
|
||||||
|
xlog.Debug("Process exited successfully",
|
||||||
|
"processID", id,
|
||||||
|
"pid", process.Cmd.Process.Pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new handlers for group management
|
||||||
|
func (s *Server) handleGroups(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
groups := s.ListGroups()
|
||||||
|
json.NewEncoder(w).Encode(groups)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
groupID := r.URL.Path[len("/groups/"):]
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
processes, err := s.GetGroupProcesses(groupID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(processes)
|
||||||
|
case http.MethodDelete:
|
||||||
|
if err := s.StopGroup(groupID); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleRun(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Handling /run request")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env []string `json:"env"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Executing one-time process: command=%s, args=%v", req.Command, req.Args)
|
||||||
|
|
||||||
|
output, err := s.RunProcess(r.Context(), req.Command, req.Args, req.Env)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("One-time process completed with output length: %d", len(output))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"output": output,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -18,15 +18,25 @@ const (
|
|||||||
// Actions
|
// Actions
|
||||||
ActionSearch = "search"
|
ActionSearch = "search"
|
||||||
ActionCustom = "custom"
|
ActionCustom = "custom"
|
||||||
|
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||||
|
ActionDeepResearchRunner = "deep-research-runner"
|
||||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||||
ActionGithubIssueOpener = "github-issue-opener"
|
ActionGithubIssueOpener = "github-issue-opener"
|
||||||
|
ActionGithubIssueEditor = "github-issue-editor"
|
||||||
ActionGithubIssueCloser = "github-issue-closer"
|
ActionGithubIssueCloser = "github-issue-closer"
|
||||||
ActionGithubIssueSearcher = "github-issue-searcher"
|
ActionGithubIssueSearcher = "github-issue-searcher"
|
||||||
ActionGithubRepositoryGet = "github-repository-get-content"
|
ActionGithubRepositoryGet = "github-repository-get-content"
|
||||||
ActionGithubRepositoryCreateOrUpdate = "github-repository-create-or-update-content"
|
ActionGithubRepositoryCreateOrUpdate = "github-repository-create-or-update-content"
|
||||||
ActionGithubIssueReader = "github-issue-reader"
|
ActionGithubIssueReader = "github-issue-reader"
|
||||||
ActionGithubIssueCommenter = "github-issue-commenter"
|
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"
|
ActionGithubREADME = "github-readme"
|
||||||
|
ActionGithubRepositorySearchFiles = "github-repository-search-files"
|
||||||
|
ActionGithubRepositoryListFiles = "github-repository-list-files"
|
||||||
ActionScraper = "scraper"
|
ActionScraper = "scraper"
|
||||||
ActionWikipedia = "wikipedia"
|
ActionWikipedia = "wikipedia"
|
||||||
ActionBrowse = "browse"
|
ActionBrowse = "browse"
|
||||||
@@ -36,6 +46,10 @@ const (
|
|||||||
ActionCounter = "counter"
|
ActionCounter = "counter"
|
||||||
ActionCallAgents = "call_agents"
|
ActionCallAgents = "call_agents"
|
||||||
ActionShellcommand = "shell-command"
|
ActionShellcommand = "shell-command"
|
||||||
|
ActionSendTelegramMessage = "send-telegram-message"
|
||||||
|
ActionSetReminder = "set_reminder"
|
||||||
|
ActionListReminders = "list_reminders"
|
||||||
|
ActionRemoveReminder = "remove_reminder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableActions = []string{
|
var AvailableActions = []string{
|
||||||
@@ -43,12 +57,22 @@ var AvailableActions = []string{
|
|||||||
ActionCustom,
|
ActionCustom,
|
||||||
ActionGithubIssueLabeler,
|
ActionGithubIssueLabeler,
|
||||||
ActionGithubIssueOpener,
|
ActionGithubIssueOpener,
|
||||||
|
ActionGithubIssueEditor,
|
||||||
ActionGithubIssueCloser,
|
ActionGithubIssueCloser,
|
||||||
ActionGithubIssueSearcher,
|
ActionGithubIssueSearcher,
|
||||||
ActionGithubRepositoryGet,
|
ActionGithubRepositoryGet,
|
||||||
|
ActionGithubGetAllContent,
|
||||||
|
ActionGithubRepositorySearchFiles,
|
||||||
|
ActionGithubRepositoryListFiles,
|
||||||
|
ActionBrowserAgentRunner,
|
||||||
|
ActionDeepResearchRunner,
|
||||||
ActionGithubRepositoryCreateOrUpdate,
|
ActionGithubRepositoryCreateOrUpdate,
|
||||||
ActionGithubIssueReader,
|
ActionGithubIssueReader,
|
||||||
ActionGithubIssueCommenter,
|
ActionGithubIssueCommenter,
|
||||||
|
ActionGithubPRReader,
|
||||||
|
ActionGithubPRCommenter,
|
||||||
|
ActionGithubPRReviewer,
|
||||||
|
ActionGithubPRCreator,
|
||||||
ActionGithubREADME,
|
ActionGithubREADME,
|
||||||
ActionScraper,
|
ActionScraper,
|
||||||
ActionBrowse,
|
ActionBrowse,
|
||||||
@@ -59,36 +83,53 @@ var AvailableActions = []string{
|
|||||||
ActionCounter,
|
ActionCounter,
|
||||||
ActionCallAgents,
|
ActionCallAgents,
|
||||||
ActionShellcommand,
|
ActionShellcommand,
|
||||||
|
ActionSendTelegramMessage,
|
||||||
|
ActionSetReminder,
|
||||||
|
ActionListReminders,
|
||||||
|
ActionRemoveReminder,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Actions(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
const (
|
||||||
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
|
||||||
allActions := []types.Action{}
|
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
|
||||||
|
ActionConfigSSHBoxURL = "sshbox-url"
|
||||||
|
)
|
||||||
|
|
||||||
agentName := a.Name
|
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{}
|
||||||
|
|
||||||
for _, a := range a.Actions {
|
agentName := a.Name
|
||||||
var config map[string]string
|
|
||||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
for _, a := range a.Actions {
|
||||||
xlog.Error("Error unmarshalling action config", "error", err)
|
var config map[string]string
|
||||||
continue
|
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, actionsConfigs)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allActions = append(allActions, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := Action(a.Name, agentName, config, pool)
|
return allActions
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allActions = append(allActions, a)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allActions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Action(name, agentName string, config map[string]string, pool *state.AgentPool) (types.Action, error) {
|
func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
|
||||||
var a types.Action
|
var a types.Action
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
config = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
case ActionCustom:
|
case ActionCustom:
|
||||||
a, err = action.NewCustom(config, "")
|
a, err = action.NewCustom(config, "")
|
||||||
@@ -100,12 +141,32 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewGithubIssueLabeler(config)
|
a = actions.NewGithubIssueLabeler(config)
|
||||||
case ActionGithubIssueOpener:
|
case ActionGithubIssueOpener:
|
||||||
a = actions.NewGithubIssueOpener(config)
|
a = actions.NewGithubIssueOpener(config)
|
||||||
|
case ActionGithubIssueEditor:
|
||||||
|
a = actions.NewGithubIssueEditor(config)
|
||||||
case ActionGithubIssueCloser:
|
case ActionGithubIssueCloser:
|
||||||
a = actions.NewGithubIssueCloser(config)
|
a = actions.NewGithubIssueCloser(config)
|
||||||
case ActionGithubIssueSearcher:
|
case ActionGithubIssueSearcher:
|
||||||
a = actions.NewGithubIssueSearch(config)
|
a = actions.NewGithubIssueSearch(config)
|
||||||
|
case ActionBrowserAgentRunner:
|
||||||
|
a = actions.NewBrowserAgentRunner(config, actionsConfigs[ActionConfigBrowserAgentRunner])
|
||||||
|
case ActionDeepResearchRunner:
|
||||||
|
a = actions.NewDeepResearchRunner(config, actionsConfigs[ActionConfigDeepResearchRunner])
|
||||||
case ActionGithubIssueReader:
|
case ActionGithubIssueReader:
|
||||||
a = actions.NewGithubIssueReader(config)
|
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:
|
case ActionGithubIssueCommenter:
|
||||||
a = actions.NewGithubIssueCommenter(config)
|
a = actions.NewGithubIssueCommenter(config)
|
||||||
case ActionGithubRepositoryGet:
|
case ActionGithubRepositoryGet:
|
||||||
@@ -129,7 +190,15 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
case ActionCallAgents:
|
case ActionCallAgents:
|
||||||
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
|
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
|
||||||
case ActionShellcommand:
|
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()
|
||||||
default:
|
default:
|
||||||
xlog.Error("Action not found", "name", name)
|
xlog.Error("Action not found", "name", name)
|
||||||
return nil, fmt.Errorf("Action not found")
|
return nil, fmt.Errorf("Action not found")
|
||||||
@@ -149,6 +218,16 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Search",
|
Label: "Search",
|
||||||
Fields: actions.SearchConfigMeta(),
|
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",
|
Name: "generate_image",
|
||||||
Label: "Generate Image",
|
Label: "Generate Image",
|
||||||
@@ -164,6 +243,11 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "GitHub Issue Opener",
|
Label: "GitHub Issue Opener",
|
||||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "github-issue-editor",
|
||||||
|
Label: "GitHub Issue Editor",
|
||||||
|
Fields: actions.GithubIssueEditorConfigMeta(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "github-issue-closer",
|
Name: "github-issue-closer",
|
||||||
Label: "GitHub Issue Closer",
|
Label: "GitHub Issue Closer",
|
||||||
@@ -189,6 +273,21 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "GitHub Repository Get Content",
|
Label: "GitHub Repository Get Content",
|
||||||
Fields: actions.GithubRepositoryGetContentConfigMeta(),
|
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",
|
Name: "github-repository-create-or-update-content",
|
||||||
Label: "GitHub Repository Create/Update Content",
|
Label: "GitHub Repository Create/Update Content",
|
||||||
@@ -199,6 +298,26 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "GitHub Repository README",
|
Label: "GitHub Repository README",
|
||||||
Fields: actions.GithubRepositoryREADMEConfigMeta(),
|
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",
|
Name: "twitter-post",
|
||||||
Label: "Twitter Post",
|
Label: "Twitter Post",
|
||||||
@@ -242,6 +361,26 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
{
|
{
|
||||||
Name: "call_agents",
|
Name: "call_agents",
|
||||||
Label: "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{},
|
Fields: []config.Field{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
|
|||||||
|
|
||||||
type BrowseAction struct{}
|
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 {
|
result := struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
140
services/actions/browseragentrunner.go
Normal file
140
services/actions/browseragentrunner.go
Normal file
@@ -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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/state"
|
"github.com/mudler/LocalAGI/core/state"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"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 {
|
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{
|
return &CallAgentAction{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
myName: agentName,
|
myName: agentName,
|
||||||
|
whitelist: whitelist,
|
||||||
|
blacklist: blacklist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CallAgentAction struct {
|
type CallAgentAction struct {
|
||||||
pool *state.AgentPoolInternalAPI
|
pool *state.AgentPoolInternalAPI
|
||||||
myName string
|
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 {
|
result := struct {
|
||||||
AgentName string `json:"agent_name"`
|
AgentName string `json:"agent_name"`
|
||||||
Message string `json:"message"`
|
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
|
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 {
|
func (a *CallAgentAction) Definition() types.ActionDefinition {
|
||||||
allAgents := a.pool.AllAgents()
|
allAgents := a.pool.AllAgents()
|
||||||
|
|
||||||
agents := []string{}
|
agents := []string{}
|
||||||
|
|
||||||
for _, ag := range allAgents {
|
for _, ag := range allAgents {
|
||||||
if ag != a.myName {
|
if a.isAllowedToBeCalled(ag) {
|
||||||
agents = append(agents, ag)
|
agents = append(agents, ag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,3 +174,21 @@ func (a *CallAgentAction) Definition() types.ActionDefinition {
|
|||||||
func (a *CallAgentAction) Plannable() bool {
|
func (a *CallAgentAction) Plannable() bool {
|
||||||
return true
|
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
|
// 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
|
// Parse parameters
|
||||||
request := struct {
|
request := struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
148
services/actions/deepresearchrunner.go
Normal file
148
services/actions/deepresearchrunner.go
Normal file
@@ -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
|
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 {
|
result := struct {
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
Size string `json:"size"`
|
Size string `json:"size"`
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ var _ = Describe("GenImageAction", func() {
|
|||||||
"size": "256x256",
|
"size": "256x256",
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := action.Run(ctx, params)
|
url, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(url).ToNot(BeEmpty())
|
Expect(url).ToNot(BeEmpty())
|
||||||
})
|
})
|
||||||
@@ -52,7 +52,7 @@ var _ = Describe("GenImageAction", func() {
|
|||||||
"size": "256x256",
|
"size": "256x256",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := action.Run(ctx, params)
|
_, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).To(HaveOccurred())
|
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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
151
services/actions/githubissueedit.go
Normal file
151
services/actions/githubissueedit.go
Normal file
@@ -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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
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 {
|
result := struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Body string `json:"text"`
|
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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
@@ -49,7 +49,8 @@ func (g *GithubIssuesReader) Run(ctx context.Context, params types.ActionParams)
|
|||||||
return types.ActionResult{
|
return types.ActionResult{
|
||||||
Result: fmt.Sprintf(
|
Result: fmt.Sprintf(
|
||||||
"Issue %d Repository: %s\nTitle: %s\nBody: %s",
|
"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 {
|
if err != nil {
|
||||||
return types.ActionResult{Result: fmt.Sprintf("Error fetching issue: %s", err.Error())}, err
|
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 {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
|||||||
172
services/actions/githubprcommenter.go
Normal file
172
services/actions/githubprcommenter.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
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 GithubPRCommenter struct {
|
||||||
|
token, repository, owner, customActionName string
|
||||||
|
client *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
||||||
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
|
return &GithubPRCommenter{
|
||||||
|
client: client,
|
||||||
|
token: config["token"],
|
||||||
|
customActionName: config["customActionName"],
|
||||||
|
repository: config["repository"],
|
||||||
|
owner: config["owner"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
}{}
|
||||||
|
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 comments
|
||||||
|
if *pr.State != "open" {
|
||||||
|
return types.ActionResult{Result: fmt.Sprintf("Pull request #%d is not open (current state: %s)", result.PRNumber, *pr.State)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Comment == "" {
|
||||||
|
return types.ActionResult{Result: "No comment provided"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try both PullRequests and Issues API for general comments
|
||||||
|
var resp *github.Response
|
||||||
|
|
||||||
|
// First try PullRequests API
|
||||||
|
_, resp, err = g.client.PullRequests.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.PullRequestComment{
|
||||||
|
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.Comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{Result: fmt.Sprintf("Error adding general comment: %s", err.Error())}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ActionResult{
|
||||||
|
Result: "Successfully added general comment to pull request",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubPRCommenter) Definition() types.ActionDefinition {
|
||||||
|
actionName := "comment_github_pr"
|
||||||
|
if g.customActionName != "" {
|
||||||
|
actionName = g.customActionName
|
||||||
|
}
|
||||||
|
description := "Add comments to a GitHub pull request, including line-specific feedback. Often used after reading a PR to provide a peer review."
|
||||||
|
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 comment on.",
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "A general comment to add to the pull request.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"pr_number", "comment"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 comment on.",
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The repository containing the pull request.",
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The owner of the repository.",
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "A general comment to add to the pull request.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"pr_number", "repository", "owner", "comment"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GithubPRCommenter) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubPRCommenterConfigMeta returns the metadata for GitHub PR Commenter action configuration fields
|
||||||
|
func GithubPRCommenterConfigMeta() []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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
400
services/actions/githubprcreator.go
Normal file
400
services/actions/githubprcreator.go
Normal file
@@ -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.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
118
services/actions/githubprcreator_test.go
Normal file
118
services/actions/githubprcreator_test.go
Normal file
@@ -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"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
188
services/actions/githubprreader.go
Normal file
188
services/actions/githubprreader.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
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 GithubPRReader struct {
|
||||||
|
token, repository, owner, customActionName string
|
||||||
|
showFullDiff bool
|
||||||
|
client *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGithubPRReader(config map[string]string) *GithubPRReader {
|
||||||
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
|
showFullDiff := false
|
||||||
|
if config["showFullDiff"] == "true" {
|
||||||
|
showFullDiff = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GithubPRReader{
|
||||||
|
client: client,
|
||||||
|
token: config["token"],
|
||||||
|
customActionName: config["customActionName"],
|
||||||
|
repository: config["repository"],
|
||||||
|
owner: config["owner"],
|
||||||
|
showFullDiff: showFullDiff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
PRNumber int `json:"pr_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
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, _, err := g.client.PullRequests.Get(ctx, result.Owner, result.Repository, result.PRNumber)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{Result: fmt.Sprintf("Error fetching pull request: %s", err.Error())}, err
|
||||||
|
}
|
||||||
|
if pr == nil {
|
||||||
|
return types.ActionResult{Result: fmt.Sprintf("No pull request found")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list of changed files
|
||||||
|
files, _, err := g.client.PullRequests.ListFiles(ctx, result.Owner, result.Repository, result.PRNumber, &github.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
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.GetFilename(), file.GetFilename())
|
||||||
|
if g.showFullDiff && file.GetPatch() != "" {
|
||||||
|
fileChanges += file.GetPatch()
|
||||||
|
}
|
||||||
|
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%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 {
|
||||||
|
actionName := "read_github_pr"
|
||||||
|
if g.customActionName != "" {
|
||||||
|
actionName = g.customActionName
|
||||||
|
}
|
||||||
|
description := "Read a GitHub pull request."
|
||||||
|
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 read.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"pr_number"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 read.",
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The repository containing the pull request.",
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The owner of the repository.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"pr_number", "repository", "owner"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GithubPRReader) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubPRReaderConfigMeta returns the metadata for GitHub PR Reader action configuration fields
|
||||||
|
func GithubPRReaderConfigMeta() []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: "showFullDiff",
|
||||||
|
Label: "Show Full Diff",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
HelpText: "Whether to show the full diff content or just the summary",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
288
services/actions/githubprreviewer.go
Normal file
288
services/actions/githubprreviewer.go
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
103
services/actions/githubprreviewer_test.go
Normal file
103
services/actions/githubprreviewer_test.go
Normal file
@@ -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 {
|
result := struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
|||||||
207
services/actions/githubrepositorygetallcontent.go
Normal file
207
services/actions/githubrepositorygetallcontent.go
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
114
services/actions/githubrepositorygetallcontent_test.go
Normal file
114
services/actions/githubrepositorygetallcontent_test.go
Normal file
@@ -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 {
|
result := struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
|||||||
163
services/actions/githubrepositorylistfiles.go
Normal file
163
services/actions/githubrepositorylistfiles.go
Normal file
@@ -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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
187
services/actions/githubrepositorysearchfiles.go
Normal file
187
services/actions/githubrepositorysearchfiles.go
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ func NewScraper(config map[string]string) *ScraperAction {
|
|||||||
|
|
||||||
type ScraperAction struct{}
|
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 {
|
result := struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func NewSearch(config map[string]string) *SearchAction {
|
|||||||
|
|
||||||
type SearchAction struct{ results int }
|
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 {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type SendMailAction struct {
|
|||||||
smtpPort string
|
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 {
|
result := struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
To string `json:"to"`
|
To string `json:"to"`
|
||||||
|
|||||||
195
services/actions/sendtelegrammessage.go
Normal file
195
services/actions/sendtelegrammessage.go
Normal file
@@ -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')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
@@ -11,24 +12,27 @@ import (
|
|||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewShell(config map[string]string) *ShellAction {
|
func NewShell(config map[string]string, sshBoxURL string) *ShellAction {
|
||||||
return &ShellAction{
|
return &ShellAction{
|
||||||
privateKey: config["privateKey"],
|
privateKey: config["privateKey"],
|
||||||
user: config["user"],
|
user: config["user"],
|
||||||
host: config["host"],
|
host: config["host"],
|
||||||
|
password: config["password"],
|
||||||
customName: config["customName"],
|
customName: config["customName"],
|
||||||
customDescription: config["customDescription"],
|
customDescription: config["customDescription"],
|
||||||
|
sshBoxURL: sshBoxURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShellAction struct {
|
type ShellAction struct {
|
||||||
privateKey string
|
privateKey string
|
||||||
user, host string
|
user, host, password string
|
||||||
customName string
|
customName string
|
||||||
customDescription 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 {
|
result := struct {
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
@@ -46,7 +50,23 @@ func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types
|
|||||||
result.User = a.user
|
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 {
|
if err != nil {
|
||||||
return types.ActionResult{}, err
|
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 {
|
func (a *ShellAction) Definition() types.ActionDefinition {
|
||||||
name := "shell"
|
name := "run_command"
|
||||||
description := "Run a shell command on a remote server."
|
description := "Run a command on a linux environment."
|
||||||
if a.customName != "" {
|
if a.customName != "" {
|
||||||
name = a.customName
|
name = a.customName
|
||||||
}
|
}
|
||||||
if a.customDescription != "" {
|
if a.customDescription != "" {
|
||||||
description = a.customDescription
|
description = a.customDescription
|
||||||
}
|
}
|
||||||
if a.host != "" && a.user != "" {
|
if (a.host != "" && a.user != "") || a.sshBoxURL != "" {
|
||||||
return types.ActionDefinition{
|
return types.ActionDefinition{
|
||||||
Name: types.ActionDefinitionName(name),
|
Name: types.ActionDefinitionName(name),
|
||||||
Description: description,
|
Description: description,
|
||||||
@@ -104,7 +124,7 @@ func ShellConfigMeta() []config.Field {
|
|||||||
Name: "privateKey",
|
Name: "privateKey",
|
||||||
Label: "Private Key",
|
Label: "Private Key",
|
||||||
Type: config.FieldTypeTextarea,
|
Type: config.FieldTypeTextarea,
|
||||||
Required: true,
|
Required: false,
|
||||||
HelpText: "SSH private key for connecting to remote servers",
|
HelpText: "SSH private key for connecting to remote servers",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -113,6 +133,12 @@ func ShellConfigMeta() []config.Field {
|
|||||||
Type: config.FieldTypeText,
|
Type: config.FieldTypeText,
|
||||||
HelpText: "Default SSH user for connecting to remote servers",
|
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",
|
Name: "host",
|
||||||
Label: "Default Host",
|
Label: "Default Host",
|
||||||
@@ -134,19 +160,25 @@ func ShellConfigMeta() []config.Field {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshCommand(privateKey, command, user, host string) (string, error) {
|
func sshCommand(privateKey, command, user, host, password string) (string, error) {
|
||||||
// Create signer from private key string
|
|
||||||
key, err := ssh.ParsePrivateKey([]byte(privateKey))
|
authMethods := []ssh.AuthMethod{}
|
||||||
if err != nil {
|
if password != "" {
|
||||||
log.Fatalf("failed to parse private key: %v", err)
|
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
|
// SSH client configuration
|
||||||
config := &ssh.ClientConfig{
|
config := &ssh.ClientConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Auth: []ssh.AuthMethod{
|
Auth: authMethods,
|
||||||
ssh.PublicKeys(key),
|
|
||||||
},
|
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,12 +197,15 @@ func sshCommand(privateKey, command, user, host string) (string, error) {
|
|||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
// Run a command
|
// Run a command
|
||||||
output, err := session.CombinedOutput(command)
|
cmdOut, err := session.CombinedOutput(command)
|
||||||
if err != nil {
|
result := string(cmdOut)
|
||||||
return "", fmt.Errorf("failed to run: %v", err)
|
if strings.TrimSpace(result) == "" {
|
||||||
|
result += "\nCommand has exited with no output"
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
return string(output), nil
|
result += "\nError: " + err.Error()
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ShellAction) Plannable() bool {
|
func (a *ShellAction) Plannable() bool {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type PostTweetAction struct {
|
|||||||
noCharacterLimit bool
|
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 {
|
result := struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func NewWikipedia(config map[string]string) *WikipediaAction {
|
|||||||
|
|
||||||
type WikipediaAction struct{}
|
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 {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const (
|
|||||||
ConnectorGithubIssues = "github-issues"
|
ConnectorGithubIssues = "github-issues"
|
||||||
ConnectorGithubPRs = "github-prs"
|
ConnectorGithubPRs = "github-prs"
|
||||||
ConnectorTwitter = "twitter"
|
ConnectorTwitter = "twitter"
|
||||||
|
ConnectorMatrix = "matrix"
|
||||||
|
ConnectorEmail = "email"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableConnectors = []string{
|
var AvailableConnectors = []string{
|
||||||
@@ -29,6 +31,8 @@ var AvailableConnectors = []string{
|
|||||||
ConnectorGithubIssues,
|
ConnectorGithubIssues,
|
||||||
ConnectorGithubPRs,
|
ConnectorGithubPRs,
|
||||||
ConnectorTwitter,
|
ConnectorTwitter,
|
||||||
|
ConnectorMatrix,
|
||||||
|
ConnectorEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Connectors(a *state.AgentConfig) []state.Connector {
|
func Connectors(a *state.AgentConfig) []state.Connector {
|
||||||
@@ -66,6 +70,10 @@ func Connectors(a *state.AgentConfig) []state.Connector {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
conns = append(conns, cc)
|
conns = append(conns, cc)
|
||||||
|
case ConnectorMatrix:
|
||||||
|
conns = append(conns, connectors.NewMatrix(config))
|
||||||
|
case ConnectorEmail:
|
||||||
|
conns = append(conns, connectors.NewEmail(config))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conns
|
return conns
|
||||||
@@ -108,5 +116,15 @@ func ConnectorsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Twitter",
|
Label: "Twitter",
|
||||||
Fields: connectors.TwitterConfigMeta(),
|
Fields: connectors.TwitterConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "matrix",
|
||||||
|
Label: "Matrix",
|
||||||
|
Fields: connectors.MatrixConfigMeta(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "email",
|
||||||
|
Label: "Email",
|
||||||
|
Fields: connectors.EmailConfigMeta(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package connectors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/mudler/LocalAGI/core/agent"
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
@@ -14,9 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Discord struct {
|
type Discord struct {
|
||||||
token string
|
token string
|
||||||
defaultChannel string
|
defaultChannel string
|
||||||
conversationTracker *ConversationTracker[string]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDiscord creates a new Discord connector
|
// NewDiscord creates a new Discord connector
|
||||||
@@ -25,15 +24,15 @@ type Discord struct {
|
|||||||
// - defaultChannel: Discord channel to always answer even if not mentioned
|
// - defaultChannel: Discord channel to always answer even if not mentioned
|
||||||
func NewDiscord(config map[string]string) *Discord {
|
func NewDiscord(config map[string]string) *Discord {
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
token := config["token"]
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
if !strings.HasPrefix(token, "Bot ") {
|
||||||
|
token = "Bot " + token
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Discord{
|
return &Discord{
|
||||||
conversationTracker: NewConversationTracker[string](duration),
|
token: token,
|
||||||
token: config["token"],
|
defaultChannel: config["defaultChannel"],
|
||||||
defaultChannel: config["defaultChannel"],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +83,27 @@ func (d *Discord) Start(a *agent.Agent) {
|
|||||||
|
|
||||||
dg.StateEnabled = true
|
dg.StateEnabled = true
|
||||||
|
|
||||||
|
if d.defaultChannel != "" {
|
||||||
|
// handle new conversations
|
||||||
|
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||||
|
xlog.Debug("Subscriber(discord)", "message", ccm.Content)
|
||||||
|
|
||||||
|
// Send the message to the default channel
|
||||||
|
_, err := dg.ChannelMessageSend(d.defaultChannel, ccm.Content)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error sending message: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("discord:%s", d.defaultChannel),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Register the messageCreate func as a callback for MessageCreate events.
|
// Register the messageCreate func as a callback for MessageCreate events.
|
||||||
dg.AddHandler(d.messageCreate(a))
|
dg.AddHandler(d.messageCreate(a))
|
||||||
|
|
||||||
@@ -151,12 +171,12 @@ func (d *Discord) handleThreadMessage(a *agent.Agent, s *discordgo.Session, m *d
|
|||||||
|
|
||||||
func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *discordgo.MessageCreate) {
|
func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
|
||||||
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: m.Content,
|
Content: m.Content,
|
||||||
})
|
})
|
||||||
|
|
||||||
conv := d.conversationTracker.GetConversation(m.ChannelID)
|
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("discord:%s", m.ChannelID))
|
||||||
|
|
||||||
jobResult := a.Ask(
|
jobResult := a.Ask(
|
||||||
types.WithConversationHistory(conv),
|
types.WithConversationHistory(conv),
|
||||||
@@ -167,7 +187,7 @@ func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: jobResult.Response,
|
Content: jobResult.Response,
|
||||||
})
|
})
|
||||||
|
|||||||
457
services/connectors/email.go
Normal file
457
services/connectors/email.go
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
package connectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
htmltomarkdown "github.com/JohannesKaufmann/html-to-markdown/v2"
|
||||||
|
imap "github.com/emersion/go-imap/v2"
|
||||||
|
sasl "github.com/emersion/go-sasl"
|
||||||
|
smtp "github.com/emersion/go-smtp"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/imapclient"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/charset"
|
||||||
|
"github.com/gomarkdown/markdown"
|
||||||
|
"github.com/gomarkdown/markdown/html"
|
||||||
|
"github.com/gomarkdown/markdown/parser"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Email struct {
|
||||||
|
username string
|
||||||
|
name string
|
||||||
|
password string
|
||||||
|
email string
|
||||||
|
smtpServer string
|
||||||
|
smtpInsecure bool
|
||||||
|
imapServer string
|
||||||
|
imapInsecure bool
|
||||||
|
defaultEmail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmail(config map[string]string) *Email {
|
||||||
|
|
||||||
|
return &Email{
|
||||||
|
username: config["username"],
|
||||||
|
name: config["name"],
|
||||||
|
password: config["password"],
|
||||||
|
email: config["email"],
|
||||||
|
smtpServer: config["smtpServer"],
|
||||||
|
smtpInsecure: config["smtpInsecure"] == "true",
|
||||||
|
imapServer: config["imapServer"],
|
||||||
|
imapInsecure: config["imapInsecure"] == "true",
|
||||||
|
defaultEmail: config["defaultEmail"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmailConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "smtpServer",
|
||||||
|
Label: "SMTP Host:port",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "SMTP server host:port (e.g., smtp.gmail.com:587)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "smtpInsecure",
|
||||||
|
Label: "Insecure SMTP",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "imapServer",
|
||||||
|
Label: "IMAP Host:port",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "IMAP server host:port (e.g., imap.gmail.com:993)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "imapInsecure",
|
||||||
|
Label: "Insecure IMAP",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "username",
|
||||||
|
Label: "Username",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "Username/email address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "name",
|
||||||
|
Label: "Friendly Name",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "Friendly name of sender",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "password",
|
||||||
|
Label: "Password",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "SMTP/IMAP password or app password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "email",
|
||||||
|
Label: "From Email",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "Agent email address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "defaultEmail",
|
||||||
|
Label: "Default Recipient",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
HelpText: "Default email address to send messages to when the agent wants to initiate a conversation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) AgentResultCallback() func(state types.ActionState) {
|
||||||
|
return func(state types.ActionState) {
|
||||||
|
// Send the result to the bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
|
||||||
|
return func(state types.ActionCurrentState) bool {
|
||||||
|
// Send the reasoning to the bot
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterEmailRecipients(input string, emailToRemove string) string {
|
||||||
|
|
||||||
|
addresses := strings.Split(strings.TrimPrefix(input, "To: "), ",")
|
||||||
|
|
||||||
|
var filtered []string
|
||||||
|
for _, address := range addresses {
|
||||||
|
address = strings.TrimSpace(address)
|
||||||
|
if !strings.Contains(address, emailToRemove) {
|
||||||
|
filtered = append(filtered, address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
return strings.Join(filtered, ", ")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) sendMail(to, subject, content, replyToID, references string, emails []string, html bool) {
|
||||||
|
|
||||||
|
auth := sasl.NewPlainClient("", e.username, e.password)
|
||||||
|
|
||||||
|
contentType := "text/plain"
|
||||||
|
if html {
|
||||||
|
contentType = "text/html"
|
||||||
|
}
|
||||||
|
|
||||||
|
var replyHeaders string
|
||||||
|
if replyToID != "" {
|
||||||
|
referenceLine := strings.ReplaceAll(references+" "+replyToID, "\n", "")
|
||||||
|
replyHeaders = fmt.Sprintf("In-Reply-To: %s\r\nReferences: %s\r\n", replyToID, referenceLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build full message content
|
||||||
|
var builder strings.Builder
|
||||||
|
fmt.Fprintf(&builder, "To: %s\r\n", to)
|
||||||
|
fmt.Fprintf(&builder, "From: %s <%s>\r\n", e.name, e.email)
|
||||||
|
builder.WriteString(replyHeaders)
|
||||||
|
fmt.Fprintf(&builder, "MIME-Version: 1.0\r\nContent-Type: %s;\r\n", contentType)
|
||||||
|
fmt.Fprintf(&builder, "Subject: %s\r\n\r\n", subject)
|
||||||
|
fmt.Fprintf(&builder, "%s\r\n", content)
|
||||||
|
msg := strings.NewReader(builder.String())
|
||||||
|
|
||||||
|
if !e.smtpInsecure {
|
||||||
|
|
||||||
|
err := smtp.SendMail(e.smtpServer, auth, e.email, emails, msg)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email send err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
c, err := smtp.Dial(e.smtpServer)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email connection err: %v", err))
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
err = c.Hello("client")
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email hello err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Auth(auth)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email auth err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.SendMail(e.email, emails, msg)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email send err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imapWorker(done chan bool, e *Email, a *agent.Agent, c *imapclient.Client, startIndex uint32) {
|
||||||
|
|
||||||
|
currentIndex := startIndex
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
|
||||||
|
xlog.Info("Stopping imapWorker")
|
||||||
|
err := c.Logout().Wait()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email IMAP logout fail: %v", err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
|
selectedMbox, err := c.Select("INBOX", nil).Wait()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email IMAP mailbox err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over any new messages recieved in selected mailbox
|
||||||
|
for currentIndex < selectedMbox.NumMessages {
|
||||||
|
|
||||||
|
currentIndex++
|
||||||
|
|
||||||
|
// Download email info
|
||||||
|
seqSet := imap.SeqSetNum(currentIndex)
|
||||||
|
bodySection := &imap.FetchItemBodySection{}
|
||||||
|
fetchOptions := &imap.FetchOptions{
|
||||||
|
Flags: true,
|
||||||
|
Envelope: true,
|
||||||
|
BodySection: []*imap.FetchItemBodySection{bodySection},
|
||||||
|
}
|
||||||
|
messageBuffers, err := c.Fetch(seqSet, fetchOptions).Collect()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email IMAP fetch err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start conversation goroutine
|
||||||
|
go func(e *Email, a *agent.Agent, c *imapclient.Client, fmb *imapclient.FetchMessageBuffer) {
|
||||||
|
|
||||||
|
// Download Email contents
|
||||||
|
r := bytes.NewReader(fmb.FindBodySection(bodySection))
|
||||||
|
msg, err := message.Read(r)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email reader err: %v", err))
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(msg.Body)
|
||||||
|
|
||||||
|
xlog.Debug("New email!")
|
||||||
|
xlog.Debug(fmt.Sprintf("From: %s", msg.Header.Get("From")))
|
||||||
|
xlog.Debug(fmt.Sprintf("To: %s", msg.Header.Get("To")))
|
||||||
|
xlog.Debug(fmt.Sprintf("Subject: %s", msg.Header.Get("Subject")))
|
||||||
|
|
||||||
|
// In the event that an email account has multiple email addresses, only respond to the one configured
|
||||||
|
if !strings.Contains(msg.Header.Get("To"), e.email) {
|
||||||
|
xlog.Info(fmt.Sprintf("Email was sent to %s, but appeared in my inbox (%s). Ignoring!", msg.Header.Get("To"), e.email))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := buf.String()
|
||||||
|
contentIsHTML := false
|
||||||
|
|
||||||
|
// Convert email to markdown only if it's in HTML
|
||||||
|
prefixes := []string{"<html", "<body", "<div", "<head"}
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if strings.HasPrefix(strings.ToLower(content), prefix) {
|
||||||
|
content, err = htmltomarkdown.ConvertString(buf.String())
|
||||||
|
contentIsHTML = true
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email html => md err: %v", err))
|
||||||
|
contentIsHTML = false
|
||||||
|
content = buf.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Debug(fmt.Sprintf("Markdown:\n\n%s", content))
|
||||||
|
|
||||||
|
// Construct prompt
|
||||||
|
prompt := fmt.Sprintf("%s %s:\n\nFrom: %s\nTime: %s\nSubject: %s\n=====\n%s",
|
||||||
|
"This email thread was sent to you. You are",
|
||||||
|
e.email,
|
||||||
|
msg.Header.Get("From"),
|
||||||
|
fmb.Envelope.Date.Format(time.RFC3339),
|
||||||
|
fmb.Envelope.Subject,
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
conv := []openai.ChatCompletionMessage{}
|
||||||
|
conv = append(conv, openai.ChatCompletionMessage{Role: "user", Content: prompt})
|
||||||
|
|
||||||
|
// Send prompt to agent and wait for result
|
||||||
|
xlog.Debug(fmt.Sprintf("Starting conversation:\n\n%v", conv))
|
||||||
|
jobResult := a.Ask(types.WithConversationHistory(conv))
|
||||||
|
if jobResult.Error != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error asking agent: %v", jobResult.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send agent response to user, replying to original email.
|
||||||
|
xlog.Debug("Agent finished responding. Sending reply email to user")
|
||||||
|
|
||||||
|
// Get a list of emails to respond to ("Reply All" logic)
|
||||||
|
// This could be done through regex, but it's probably safer to rebuild explicitly
|
||||||
|
fromEmail := fmt.Sprintf("%s@%s", fmb.Envelope.From[0].Mailbox, fmb.Envelope.From[0].Host)
|
||||||
|
emails := []string{}
|
||||||
|
emails = append(emails, fromEmail)
|
||||||
|
|
||||||
|
for _, addr := range fmb.Envelope.To {
|
||||||
|
if addr.Mailbox != "" && addr.Host != "" {
|
||||||
|
email := fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host)
|
||||||
|
if email != e.email {
|
||||||
|
emails = append(emails, email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the original header, in case sender had contact names as part of the header
|
||||||
|
newToHeader := msg.Header.Get("From") + ", " + filterEmailRecipients(msg.Header.Get("To"), e.email)
|
||||||
|
|
||||||
|
// Create the body of the email
|
||||||
|
replyContent := jobResult.Response
|
||||||
|
if jobResult.Response == "" {
|
||||||
|
replyContent =
|
||||||
|
"System: I'm sorry, but it looks like the agent did not respond. " +
|
||||||
|
"This could be in error, or maybe it had nothing to say."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote the original message. This lets the agent see conversation history and is an email standard.
|
||||||
|
quoteHeader := fmt.Sprintf("\r\n\r\nOn %s, %s wrote:\n",
|
||||||
|
fmb.Envelope.Date.Format("Monday, Jan 2, 2006 at 15:04"),
|
||||||
|
fmt.Sprintf("%s <%s>", fmb.Envelope.From[0].Name, fromEmail),
|
||||||
|
)
|
||||||
|
quotedLines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||||
|
for i, line := range quotedLines {
|
||||||
|
quotedLines[i] = "> " + line
|
||||||
|
}
|
||||||
|
replyContent = replyContent + quoteHeader + strings.Join(quotedLines, "\r\n")
|
||||||
|
|
||||||
|
// If the original email was sent in HTML, reply with HTML
|
||||||
|
if contentIsHTML {
|
||||||
|
p := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock)
|
||||||
|
doc := p.Parse([]byte(replyContent))
|
||||||
|
|
||||||
|
opts := html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank | html.CompletePage}
|
||||||
|
renderer := html.NewRenderer(opts)
|
||||||
|
|
||||||
|
replyContent = string(markdown.Render(doc, renderer))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
e.sendMail(newToHeader,
|
||||||
|
fmt.Sprintf("Re: %s", msg.Header.Get("Subject")),
|
||||||
|
replyContent,
|
||||||
|
msg.Header.Get("Message-ID"),
|
||||||
|
msg.Header.Get("References"),
|
||||||
|
emails,
|
||||||
|
contentIsHTML,
|
||||||
|
)
|
||||||
|
}(e, a, c, messageBuffers[0])
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second) // Refresh inbox every n seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Email) Start(a *agent.Agent) {
|
||||||
|
go func() {
|
||||||
|
if e.defaultEmail != "" {
|
||||||
|
// handle new conversations
|
||||||
|
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||||
|
xlog.Debug("Subscriber(email)", "message", ccm.Content)
|
||||||
|
|
||||||
|
// Send the message to the default email
|
||||||
|
e.sendMail(
|
||||||
|
e.defaultEmail,
|
||||||
|
"Message from LocalAGI",
|
||||||
|
ccm.Content,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
[]string{e.defaultEmail},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("email:%s", e.defaultEmail),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Info("Email connector is now running. Press CTRL-C to exit.")
|
||||||
|
// IMAP dial
|
||||||
|
imapOpts := &imapclient.Options{WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}}
|
||||||
|
var c *imapclient.Client
|
||||||
|
var err error
|
||||||
|
if e.imapInsecure {
|
||||||
|
c, err = imapclient.DialInsecure(e.imapServer, imapOpts)
|
||||||
|
} else {
|
||||||
|
c, err = imapclient.DialTLS(e.imapServer, imapOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email IMAP dial err: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// IMAP login
|
||||||
|
err = c.Login(e.username, e.password).Wait()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email IMAP login err: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP mailbox
|
||||||
|
mailboxes, err := c.List("", "%", nil).Collect()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Email IMAP mailbox err: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Debug(fmt.Sprintf("Email IMAP mailbox count: %v", len(mailboxes)))
|
||||||
|
for _, mbox := range mailboxes {
|
||||||
|
xlog.Debug(fmt.Sprintf(" - %v", mbox.Mailbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select INBOX
|
||||||
|
selectedMbox, err := c.Select("INBOX", nil).Wait()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Cannot select INBOX mailbox! %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Debug(fmt.Sprintf("INBOX contains %v messages", selectedMbox.NumMessages))
|
||||||
|
|
||||||
|
// Start checking INBOX for new mail
|
||||||
|
imapWorkerHandle := make(chan bool)
|
||||||
|
go imapWorker(imapWorkerHandle, e, a, c, selectedMbox.NumMessages)
|
||||||
|
|
||||||
|
<-a.Context().Done()
|
||||||
|
imapWorkerHandle <- true
|
||||||
|
xlog.Info("Email connector is now stopped.")
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -15,28 +15,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IRC struct {
|
type IRC struct {
|
||||||
server string
|
server string
|
||||||
port string
|
port string
|
||||||
nickname string
|
nickname string
|
||||||
channel string
|
channel string
|
||||||
conn *irc.Connection
|
conn *irc.Connection
|
||||||
alwaysReply bool
|
alwaysReply bool
|
||||||
conversationTracker *ConversationTracker[string]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRC(config map[string]string) *IRC {
|
func NewIRC(config map[string]string) *IRC {
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
return &IRC{
|
return &IRC{
|
||||||
server: config["server"],
|
server: config["server"],
|
||||||
port: config["port"],
|
port: config["port"],
|
||||||
nickname: config["nickname"],
|
nickname: config["nickname"],
|
||||||
channel: config["channel"],
|
channel: config["channel"],
|
||||||
alwaysReply: config["alwaysReply"] == "true",
|
alwaysReply: config["alwaysReply"] == "true",
|
||||||
conversationTracker: NewConversationTracker[string](duration),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +70,56 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
i.conn.UseTLS = false
|
i.conn.UseTLS = false
|
||||||
|
|
||||||
|
if i.channel != "" {
|
||||||
|
// handle new conversations
|
||||||
|
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||||
|
xlog.Debug("Subscriber(irc)", "message", ccm.Content)
|
||||||
|
|
||||||
|
// Split the response into multiple messages if it's too long
|
||||||
|
maxLength := 400 // Safe limit for most IRC servers
|
||||||
|
response := ccm.Content
|
||||||
|
|
||||||
|
// Handle multiline responses
|
||||||
|
lines := strings.Split(response, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split long lines
|
||||||
|
for len(line) > 0 {
|
||||||
|
var chunk string
|
||||||
|
if len(line) > maxLength {
|
||||||
|
chunk = line[:maxLength]
|
||||||
|
line = line[maxLength:]
|
||||||
|
} else {
|
||||||
|
chunk = line
|
||||||
|
line = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message to the channel
|
||||||
|
i.conn.Privmsg(i.channel, chunk)
|
||||||
|
|
||||||
|
// Small delay to prevent flooding
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("irc:%s", i.channel),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
i.conn.AddCallback("001", func(e *irc.Event) {
|
i.conn.AddCallback("001", func(e *irc.Event) {
|
||||||
xlog.Info("Connected to IRC server", "server", i.server)
|
xlog.Info("Connected to IRC server", "server", i.server, "arguments", e.Arguments)
|
||||||
i.conn.Join(i.channel)
|
i.conn.Join(i.channel)
|
||||||
|
i.nickname = e.Arguments[0]
|
||||||
xlog.Info("Joined channel", "channel", i.channel)
|
xlog.Info("Joined channel", "channel", i.channel)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -114,7 +155,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
cleanedMessage := cleanUpMessage(message, i.nickname)
|
cleanedMessage := cleanUpMessage(message, i.nickname)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conv := i.conversationTracker.GetConversation(channel)
|
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("irc:%s", channel))
|
||||||
|
|
||||||
conv = append(conv,
|
conv = append(conv,
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
@@ -124,7 +165,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Update the conversation history
|
// Update the conversation history
|
||||||
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
|
||||||
Content: cleanedMessage,
|
Content: cleanedMessage,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
})
|
})
|
||||||
@@ -139,7 +180,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the conversation history
|
// Update the conversation history
|
||||||
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
})
|
})
|
||||||
@@ -207,6 +248,13 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
|
|
||||||
// Start the IRC client in a goroutine
|
// Start the IRC client in a goroutine
|
||||||
go i.conn.Loop()
|
go i.conn.Loop()
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-a.Context().Done():
|
||||||
|
i.conn.Quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IRCConfigMeta returns the metadata for IRC connector configuration fields
|
// IRCConfigMeta returns the metadata for IRC connector configuration fields
|
||||||
@@ -241,11 +289,5 @@ func IRCConfigMeta() []config.Field {
|
|||||||
Label: "Always Reply",
|
Label: "Always Reply",
|
||||||
Type: config.FieldTypeCheckbox,
|
Type: config.FieldTypeCheckbox,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "lastMessageDuration",
|
|
||||||
Label: "Last Message Duration",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
DefaultValue: "5m",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
330
services/connectors/matrix.go
Normal file
330
services/connectors/matrix.go
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
package connectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Matrix struct {
|
||||||
|
homeserverURL string
|
||||||
|
userID string
|
||||||
|
accessToken string
|
||||||
|
roomID string
|
||||||
|
roomMode bool
|
||||||
|
|
||||||
|
// To track placeholder messages
|
||||||
|
placeholders map[string]string // map[jobUUID]messageID
|
||||||
|
placeholderMutex sync.RWMutex
|
||||||
|
client *mautrix.Client
|
||||||
|
|
||||||
|
// Track active jobs for cancellation
|
||||||
|
activeJobs map[string][]*types.Job // map[roomID]bool to track if a room has active processing
|
||||||
|
activeJobsMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrixThinkingMessage = "🤔 thinking..."
|
||||||
|
|
||||||
|
func NewMatrix(config map[string]string) *Matrix {
|
||||||
|
|
||||||
|
return &Matrix{
|
||||||
|
homeserverURL: config["homeserverURL"],
|
||||||
|
userID: config["userID"],
|
||||||
|
accessToken: config["accessToken"],
|
||||||
|
roomID: config["roomID"],
|
||||||
|
roomMode: config["roomMode"] == "true",
|
||||||
|
placeholders: make(map[string]string),
|
||||||
|
activeJobs: make(map[string][]*types.Job),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) AgentResultCallback() func(state types.ActionState) {
|
||||||
|
return func(state types.ActionState) {
|
||||||
|
// Mark the job as completed when we get the final result
|
||||||
|
if state.ActionCurrentState.Job != nil && state.ActionCurrentState.Job.Metadata != nil {
|
||||||
|
if room, ok := state.ActionCurrentState.Job.Metadata["room"].(string); ok && room != "" {
|
||||||
|
m.activeJobsMutex.Lock()
|
||||||
|
delete(m.activeJobs, room)
|
||||||
|
m.activeJobsMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
|
||||||
|
return func(state types.ActionCurrentState) bool {
|
||||||
|
// Check if we have a placeholder message for this job
|
||||||
|
m.placeholderMutex.RLock()
|
||||||
|
msgID, exists := m.placeholders[state.Job.UUID]
|
||||||
|
room := ""
|
||||||
|
if state.Job.Metadata != nil {
|
||||||
|
if r, ok := state.Job.Metadata["room"].(string); ok {
|
||||||
|
room = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.placeholderMutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists || msgID == "" || room == "" || m.client == nil {
|
||||||
|
return true // Skip if we don't have a message to update
|
||||||
|
}
|
||||||
|
|
||||||
|
thought := matrixThinkingMessage + "\n\n"
|
||||||
|
if state.Reasoning != "" {
|
||||||
|
thought += "Current thought process:\n" + state.Reasoning
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the placeholder message with the current reasoning
|
||||||
|
_, err := m.client.SendText(context.Background(), id.RoomID(room), thought)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error updating reasoning message: %v", err))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancelActiveJobForRoom cancels any active job for the given room
|
||||||
|
func (m *Matrix) cancelActiveJobForRoom(roomID string) {
|
||||||
|
m.activeJobsMutex.RLock()
|
||||||
|
ctxs, exists := m.activeJobs[roomID]
|
||||||
|
m.activeJobsMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
xlog.Info(fmt.Sprintf("Cancelling active job for room: %s", roomID))
|
||||||
|
|
||||||
|
// Mark the job as inactive
|
||||||
|
m.activeJobsMutex.Lock()
|
||||||
|
for _, c := range ctxs {
|
||||||
|
c.Cancel()
|
||||||
|
}
|
||||||
|
delete(m.activeJobs, roomID)
|
||||||
|
m.activeJobsMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) handleRoomMessage(a *agent.Agent, evt *event.Event) {
|
||||||
|
if m.roomID != evt.RoomID.String() && m.roomMode { // If we have a roomID and it's not the same as the event room
|
||||||
|
// Skip messages from other rooms
|
||||||
|
xlog.Info("Skipping reply to room", "event room", evt.RoomID, "config room", m.roomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.Sender == id.UserID(m.userID) {
|
||||||
|
// Skip messages from ourselves
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if message does not mention the bot
|
||||||
|
mentioned := false
|
||||||
|
msg := evt.Content.AsMessage()
|
||||||
|
if msg.Mentions != nil {
|
||||||
|
mentioned = slices.Contains(evt.Content.AsMessage().Mentions.UserIDs, m.client.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mentioned && !m.roomMode {
|
||||||
|
xlog.Info("Skipping reply because it does not mention the bot", "mentions", evt.Content.AsMessage().Mentions.UserIDs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any active job for this room before starting a new one
|
||||||
|
m.cancelActiveJobForRoom(evt.RoomID.String())
|
||||||
|
|
||||||
|
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("matrix:%s", evt.RoomID.String()))
|
||||||
|
|
||||||
|
message := evt.Content.AsMessage().Body
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
agentOptions := []types.JobOption{
|
||||||
|
types.WithUUID(evt.ID.String()),
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||||
|
Role: "user",
|
||||||
|
Content: message,
|
||||||
|
})
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("matrix:%s", evt.RoomID.String()), currentConv[len(currentConv)-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
||||||
|
|
||||||
|
// Add room to metadata for tracking
|
||||||
|
metadata := map[string]any{
|
||||||
|
"room": evt.RoomID.String(),
|
||||||
|
}
|
||||||
|
agentOptions = append(agentOptions, types.WithMetadata(metadata))
|
||||||
|
|
||||||
|
job := types.NewJob(agentOptions...)
|
||||||
|
|
||||||
|
// Mark this room as having an active job
|
||||||
|
m.activeJobsMutex.Lock()
|
||||||
|
m.activeJobs[evt.RoomID.String()] = append(m.activeJobs[evt.RoomID.String()], job)
|
||||||
|
m.activeJobsMutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Mark job as complete
|
||||||
|
m.activeJobsMutex.Lock()
|
||||||
|
job.Cancel()
|
||||||
|
for i, j := range m.activeJobs[evt.RoomID.String()] {
|
||||||
|
if j.UUID == job.UUID {
|
||||||
|
m.activeJobs[evt.RoomID.String()] = slices.Delete(m.activeJobs[evt.RoomID.String()], i, i+1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.activeJobsMutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
res := a.Ask(
|
||||||
|
agentOptions...,
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.Response == "" {
|
||||||
|
xlog.Debug(fmt.Sprintf("Empty response from agent"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error from agent: %v", res.Error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("matrix:%s", evt.RoomID.String()), openai.ChatCompletionMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: res.Response,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send the response to the room
|
||||||
|
_, err := m.client.SendText(context.Background(), evt.RoomID, res.Response)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error sending message: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) Start(a *agent.Agent) {
|
||||||
|
client, err := mautrix.NewClient(m.homeserverURL, id.UserID(m.userID), m.accessToken)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error creating Matrix client: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Info("Matrix client created")
|
||||||
|
m.client = client
|
||||||
|
|
||||||
|
if m.roomID != "" {
|
||||||
|
// handle new conversations
|
||||||
|
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||||
|
xlog.Debug("Subscriber(matrix)", "message", ccm.Content)
|
||||||
|
_, err := m.client.SendText(context.Background(), id.RoomID(m.roomID), ccm.Content)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
||||||
|
}
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("matrix:%s", m.roomID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
syncer := client.Syncer.(*mautrix.DefaultSyncer)
|
||||||
|
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
||||||
|
xlog.Info("Received message", evt.Content.AsMessage().Body)
|
||||||
|
m.handleRoomMessage(a, evt)
|
||||||
|
})
|
||||||
|
|
||||||
|
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
|
||||||
|
if evt.GetStateKey() == client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
|
||||||
|
_, err := client.JoinRoomByID(ctx, evt.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error joining room: %v", err))
|
||||||
|
}
|
||||||
|
xlog.Info(fmt.Sprintf("Joined room: %s (%s)", evt.RoomID.String(), evt.RoomID.URI()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
syncer.OnEventType(event.EventEncrypted, func(ctx context.Context, evt *event.Event) {
|
||||||
|
xlog.Info("Received encrypted message, this does not work yet", evt.RoomID.String())
|
||||||
|
//m.handleRoomMessage(a, evt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// This prevents the agent from picking up a backlog of messages and swamping the chat with responses.
|
||||||
|
syncer.FilterJSON = &mautrix.Filter{
|
||||||
|
Room: mautrix.RoomFilter{
|
||||||
|
Timeline: mautrix.FilterPart{
|
||||||
|
Limit: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.Context().Done():
|
||||||
|
xlog.Info("Context cancelled, stopping sync loop")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
err := client.SyncWithContext(a.Context())
|
||||||
|
|
||||||
|
xlog.Info("Syncing")
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error syncing: %v", err))
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatrixConfigMeta returns the metadata for Matrix connector configuration fields
|
||||||
|
func MatrixConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "homeserverURL",
|
||||||
|
Label: "Homeserver URL",
|
||||||
|
HelpText: "e.g. http://host.docker.internal:8008",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "userID",
|
||||||
|
Label: "User ID",
|
||||||
|
HelpText: "e.g. @bot:host",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "accessToken",
|
||||||
|
Label: "Access Token",
|
||||||
|
HelpText: "Token obtained from _matrix/client/v3/login",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "roomID",
|
||||||
|
Label: "Internal Room ID",
|
||||||
|
HelpText: "The autogenerated unique identifier for a room",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "roomMode",
|
||||||
|
Label: "Room Mode",
|
||||||
|
HelpText: "Respond to all messages in the specified room",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/localoperator"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/pkg/xstrings"
|
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||||
"github.com/mudler/LocalAGI/services/actions"
|
"github.com/mudler/LocalAGI/services/actions"
|
||||||
@@ -41,27 +41,19 @@ type Slack struct {
|
|||||||
// Track active jobs for cancellation
|
// Track active jobs for cancellation
|
||||||
activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing
|
activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing
|
||||||
activeJobsMutex sync.RWMutex
|
activeJobsMutex sync.RWMutex
|
||||||
|
|
||||||
conversationTracker *ConversationTracker[string]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const thinkingMessage = ":hourglass: thinking..."
|
const thinkingMessage = ":hourglass: thinking..."
|
||||||
|
|
||||||
func NewSlack(config map[string]string) *Slack {
|
func NewSlack(config map[string]string) *Slack {
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Slack{
|
return &Slack{
|
||||||
appToken: config["appToken"],
|
appToken: config["appToken"],
|
||||||
botToken: config["botToken"],
|
botToken: config["botToken"],
|
||||||
channelID: config["channelID"],
|
channelID: config["channelID"],
|
||||||
channelMode: config["channelMode"] == "true",
|
channelMode: config["channelMode"] == "true",
|
||||||
conversationTracker: NewConversationTracker[string](duration),
|
placeholders: make(map[string]string),
|
||||||
placeholders: make(map[string]string),
|
activeJobs: make(map[string][]*types.Job),
|
||||||
activeJobs: make(map[string][]*types.Job),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,16 +131,6 @@ func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) strin
|
|||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractUserIDsFromMessage(message string) []string {
|
|
||||||
var userIDs []string
|
|
||||||
for _, part := range strings.Split(message, " ") {
|
|
||||||
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
|
|
||||||
userIDs = append(userIDs, strings.TrimPrefix(strings.TrimSuffix(part, ">"), "<@"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string {
|
func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string {
|
||||||
for _, part := range strings.Split(message, " ") {
|
for _, part := range strings.Split(message, " ") {
|
||||||
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
|
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
|
||||||
@@ -167,8 +149,38 @@ func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string
|
|||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateAttachmentsFromJobResponse(j *types.JobResult) (attachments []slack.Attachment) {
|
func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
|
||||||
for _, state := range j.State {
|
for _, state := range j.State {
|
||||||
|
// coming from the browser agent
|
||||||
|
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||||
|
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||||
|
state := historyStruct.States[len(historyStruct.States)-1]
|
||||||
|
// Decode base64 screenshot and upload to Slack
|
||||||
|
if state.Screenshot != "" {
|
||||||
|
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error decoding screenshot: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data := string(screenshotData)
|
||||||
|
// Upload the file to Slack
|
||||||
|
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
|
||||||
|
Reader: bytes.NewReader(screenshotData),
|
||||||
|
FileSize: len(data),
|
||||||
|
ThreadTimestamp: ts,
|
||||||
|
Channel: channelID,
|
||||||
|
Filename: "screenshot.png",
|
||||||
|
InitialComment: "Browser Agent Screenshot",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error(fmt.Sprintf("Error uploading screenshot: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// coming from the search action
|
// coming from the search action
|
||||||
if urls, exists := state.Metadata[actions.MetadataUrls]; exists {
|
if urls, exists := state.Metadata[actions.MetadataUrls]; exists {
|
||||||
for _, url := range xstrings.UniqueSlice(urls.([]string)) {
|
for _, url := range xstrings.UniqueSlice(urls.([]string)) {
|
||||||
@@ -248,7 +260,7 @@ func (t *Slack) handleChannelMessage(
|
|||||||
// Cancel any active job for this channel before starting a new one
|
// Cancel any active job for this channel before starting a new one
|
||||||
t.cancelActiveJobForChannel(ev.Channel)
|
t.cancelActiveJobForChannel(ev.Channel)
|
||||||
|
|
||||||
currentConv := t.conversationTracker.GetConversation(t.channelID)
|
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID))
|
||||||
|
|
||||||
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
|
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
|
||||||
|
|
||||||
@@ -292,8 +304,8 @@ func (t *Slack) handleChannelMessage(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
t.channelID, currentConv[len(currentConv)-1],
|
fmt.Sprintf("slack:%s", t.channelID), currentConv[len(currentConv)-1],
|
||||||
)
|
)
|
||||||
|
|
||||||
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
||||||
@@ -339,14 +351,14 @@ func (t *Slack) handleChannelMessage(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
t.channelID, openai.ChatCompletionMessage{
|
fmt.Sprintf("slack:%s", t.channelID), openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Debug("After adding message to conversation tracker", "conversation", t.conversationTracker.GetConversation(t.channelID))
|
xlog.Debug("After adding message to conversation tracker", "conversation", a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID)))
|
||||||
|
|
||||||
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
|
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
|
||||||
|
|
||||||
@@ -375,7 +387,7 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
|
|||||||
slack.MsgOptionEnableLinkUnfurl(),
|
slack.MsgOptionEnableLinkUnfurl(),
|
||||||
slack.MsgOptionText(message, true),
|
slack.MsgOptionText(message, true),
|
||||||
slack.MsgOptionPostMessageParameters(postMessageParams),
|
slack.MsgOptionPostMessageParameters(postMessageParams),
|
||||||
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, "")...),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
||||||
@@ -387,7 +399,7 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
|
|||||||
slack.MsgOptionEnableLinkUnfurl(),
|
slack.MsgOptionEnableLinkUnfurl(),
|
||||||
slack.MsgOptionText(res.Response, true),
|
slack.MsgOptionText(res.Response, true),
|
||||||
slack.MsgOptionPostMessageParameters(postMessageParams),
|
slack.MsgOptionPostMessageParameters(postMessageParams),
|
||||||
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, "")...),
|
||||||
// slack.MsgOptionTS(ts),
|
// slack.MsgOptionTS(ts),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -408,7 +420,7 @@ func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackeven
|
|||||||
slack.MsgOptionLinkNames(true),
|
slack.MsgOptionLinkNames(true),
|
||||||
slack.MsgOptionEnableLinkUnfurl(),
|
slack.MsgOptionEnableLinkUnfurl(),
|
||||||
slack.MsgOptionText(messages[0], true),
|
slack.MsgOptionText(messages[0], true),
|
||||||
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, msgTs)...),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
|
xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
|
||||||
@@ -435,7 +447,7 @@ func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackeven
|
|||||||
slack.MsgOptionLinkNames(true),
|
slack.MsgOptionLinkNames(true),
|
||||||
slack.MsgOptionEnableLinkUnfurl(),
|
slack.MsgOptionEnableLinkUnfurl(),
|
||||||
slack.MsgOptionText(finalResponse, true),
|
slack.MsgOptionText(finalResponse, true),
|
||||||
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, msgTs)...),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
|
xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
|
||||||
@@ -721,6 +733,13 @@ func (t *Slack) Start(a *agent.Agent) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
||||||
}
|
}
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("slack:%s", t.channelID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,11 +823,5 @@ func SlackConfigMeta() []config.Field {
|
|||||||
Label: "Always Reply",
|
Label: "Always Reply",
|
||||||
Type: config.FieldTypeCheckbox,
|
Type: config.FieldTypeCheckbox,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "lastMessageDuration",
|
|
||||||
Label: "Last Message Duration",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
DefaultValue: "5m",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,631 @@
|
|||||||
package connectors
|
package connectors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-telegram/bot"
|
"github.com/go-telegram/bot"
|
||||||
"github.com/go-telegram/bot/models"
|
"github.com/go-telegram/bot/models"
|
||||||
"github.com/mudler/LocalAGI/core/agent"
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/localoperator"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/pkg/xstrings"
|
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||||
"github.com/mudler/LocalAGI/services/actions"
|
"github.com/mudler/LocalAGI/services/actions"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const telegramThinkingMessage = "🤔 thinking..."
|
||||||
|
const telegramMaxMessageLength = 3000
|
||||||
|
|
||||||
type Telegram struct {
|
type Telegram struct {
|
||||||
Token string
|
Token string
|
||||||
bot *bot.Bot
|
bot *bot.Bot
|
||||||
agent *agent.Agent
|
agent *agent.Agent
|
||||||
|
|
||||||
currentconversation map[int64][]openai.ChatCompletionMessage
|
|
||||||
lastMessageTime map[int64]time.Time
|
|
||||||
lastMessageDuration time.Duration
|
|
||||||
|
|
||||||
admins []string
|
admins []string
|
||||||
|
|
||||||
conversationTracker *ConversationTracker[int64]
|
// To track placeholder messages
|
||||||
|
placeholders map[string]int // map[jobUUID]messageID
|
||||||
|
placeholderMutex sync.RWMutex
|
||||||
|
|
||||||
|
// Track active jobs for cancellation
|
||||||
|
activeJobs map[int64][]*types.Job // map[chatID]bool to track if a chat has active processing
|
||||||
|
activeJobsMutex sync.RWMutex
|
||||||
|
|
||||||
|
channelID string
|
||||||
|
groupMode bool
|
||||||
|
mentionOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send any text message to the bot after the bot has been started
|
// isBotMentioned checks if the bot is mentioned in the message
|
||||||
|
func (t *Telegram) isBotMentioned(message string, botUsername string) bool {
|
||||||
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
|
return strings.Contains(message, "@"+botUsername)
|
||||||
return func(state types.ActionState) {
|
|
||||||
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
|
|
||||||
Description: state.Reasoning,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
|
// handleGroupMessage handles messages in group chats
|
||||||
return func(state types.ActionCurrentState) bool {
|
func (t *Telegram) handleGroupMessage(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
||||||
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
|
xlog.Debug("Handling group message", "update", update)
|
||||||
Description: state.Reasoning,
|
if !t.groupMode {
|
||||||
})
|
xlog.Debug("Group mode is disabled, skipping group message", "chatID", update.Message.Chat.ID)
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
|
||||||
username := update.Message.From.Username
|
|
||||||
|
|
||||||
if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
|
|
||||||
xlog.Info("Unauthorized user", "username", username)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentConv := t.conversationTracker.GetConversation(update.Message.From.ID)
|
// Get bot info to check username
|
||||||
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
botInfo, err := b.GetMe(ctx)
|
||||||
Content: update.Message.Text,
|
if err != nil {
|
||||||
Role: "user",
|
xlog.Error("Error getting bot info", "error", err)
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
// Skip messages from ourselves
|
||||||
update.Message.From.ID,
|
if update.Message.From.Username == botInfo.Username {
|
||||||
openai.ChatCompletionMessage{
|
return
|
||||||
Content: update.Message.Text,
|
}
|
||||||
Role: "user",
|
|
||||||
|
// If mention-only mode is enabled, check if bot is mentioned
|
||||||
|
if t.mentionOnly && !t.isBotMentioned(update.Message.Text, botInfo.Username) {
|
||||||
|
xlog.Debug("Bot not mentioned in message, skipping", "chatID", update.Message.Chat.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any active job for this chat before starting a new one
|
||||||
|
t.cancelActiveJobForChat(update.Message.Chat.ID)
|
||||||
|
|
||||||
|
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.Chat.ID))
|
||||||
|
|
||||||
|
// Clean up the message by removing bot mentions
|
||||||
|
message := strings.ReplaceAll(update.Message.Text, "@"+botInfo.Username, "")
|
||||||
|
message = strings.TrimSpace(message)
|
||||||
|
|
||||||
|
// Send initial placeholder message
|
||||||
|
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: bot.EscapeMarkdown(telegramThinkingMessage),
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
ReplyParameters: &models.ReplyParameters{
|
||||||
|
MessageID: update.Message.ID,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending initial message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the UUID->placeholder message mapping
|
||||||
|
jobUUID := fmt.Sprintf("%d", msg.ID)
|
||||||
|
|
||||||
|
t.placeholderMutex.Lock()
|
||||||
|
t.placeholders[jobUUID] = msg.ID
|
||||||
|
t.placeholderMutex.Unlock()
|
||||||
|
|
||||||
|
// Add chat ID to metadata for tracking
|
||||||
|
metadata := map[string]interface{}{
|
||||||
|
"chatID": update.Message.Chat.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle images if present
|
||||||
|
if len(update.Message.Photo) > 0 {
|
||||||
|
// Get the largest photo
|
||||||
|
photo := update.Message.Photo[len(update.Message.Photo)-1]
|
||||||
|
|
||||||
|
// Download the photo
|
||||||
|
file, err := b.GetFile(ctx, &bot.GetFileParams{
|
||||||
|
FileID: photo.FileID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error getting file", "error", err)
|
||||||
|
} else {
|
||||||
|
// Download the file content
|
||||||
|
resp, err := http.Get(file.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error downloading file", "error", err)
|
||||||
|
} else {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
imageBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error reading image", "error", err)
|
||||||
|
} else {
|
||||||
|
// Encode to base64
|
||||||
|
imgBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||||
|
|
||||||
|
// Add to conversation as multi-content message
|
||||||
|
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||||
|
Role: "user",
|
||||||
|
MultiContent: []openai.ChatMessagePart{
|
||||||
|
{
|
||||||
|
Text: message,
|
||||||
|
Type: openai.ChatMessagePartTypeText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: openai.ChatMessagePartTypeImageURL,
|
||||||
|
ImageURL: &openai.ChatMessageImageURL{
|
||||||
|
URL: fmt.Sprintf("data:image/jpeg;base64,%s", imgBase64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||||
|
Content: message,
|
||||||
|
Role: "user",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("telegram:%d", update.Message.Chat.ID),
|
||||||
|
currentConv[len(currentConv)-1],
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Info("New message", "username", username, "conversation", currentConv)
|
// Create a new job with the conversation history and metadata
|
||||||
|
job := types.NewJob(
|
||||||
|
types.WithConversationHistory(currentConv),
|
||||||
|
types.WithUUID(jobUUID),
|
||||||
|
types.WithMetadata(metadata),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mark this chat as having an active job
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID], job)
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Mark job as complete
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
job.Cancel()
|
||||||
|
for i, j := range t.activeJobs[update.Message.Chat.ID] {
|
||||||
|
if j.UUID == job.UUID {
|
||||||
|
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID][:i], t.activeJobs[update.Message.Chat.ID][i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
|
||||||
|
// Clean up the placeholder map
|
||||||
|
t.placeholderMutex.Lock()
|
||||||
|
delete(t.placeholders, jobUUID)
|
||||||
|
t.placeholderMutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
res := a.Ask(
|
res := a.Ask(
|
||||||
types.WithConversationHistory(currentConv),
|
types.WithConversationHistory(currentConv),
|
||||||
|
types.WithUUID(jobUUID),
|
||||||
|
types.WithMetadata(metadata),
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Debug("Response", "response", res.Response)
|
|
||||||
|
|
||||||
if res.Response == "" {
|
if res.Response == "" {
|
||||||
xlog.Error("Empty response from agent")
|
xlog.Error("Empty response from agent")
|
||||||
|
_, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: "there was an internal error. try again!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating error message", "error", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
update.Message.From.ID,
|
fmt.Sprintf("telegram:%d", update.Message.Chat.ID),
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Debug("Sending message back to telegram", "response", res.Response)
|
// Handle any multimedia content in the response and collect URLs
|
||||||
|
urls, err := t.handleMultimediaContent(ctx, update.Message.Chat.ID, res)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error handling multimedia content", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, res := range res.State {
|
// Update the message with the final response
|
||||||
// coming from the search action
|
formattedResponse := formatResponseWithURLs(res.Response, urls)
|
||||||
// if urls, exists := res.Metadata[actions.MetadataUrls]; exists {
|
|
||||||
// for _, url := range uniqueStringSlice(urls.([]string)) {
|
|
||||||
|
|
||||||
// }
|
// Split the message if it's too long
|
||||||
// }
|
messages := xstrings.SplitParagraph(formattedResponse, telegramMaxMessageLength)
|
||||||
|
|
||||||
// coming from the gen image actions
|
if len(messages) == 0 {
|
||||||
if imagesUrls, exists := res.Metadata[actions.MetadataImages]; exists {
|
_, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: "there was an internal error. try again!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating error message", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the first message
|
||||||
|
_, err = b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: messages[0],
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send additional chunks as new messages
|
||||||
|
for i := 1; i < len(messages); i++ {
|
||||||
|
_, err = b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: messages[i],
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
ReplyParameters: &models.ReplyParameters{
|
||||||
|
MessageID: update.Message.ID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending additional message", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send any text message to the bot after the bot has been started
|
||||||
|
|
||||||
|
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
|
||||||
|
return func(state types.ActionState) {
|
||||||
|
// Mark the job as completed when we get the final result
|
||||||
|
if state.ActionCurrentState.Job != nil && state.ActionCurrentState.Job.Metadata != nil {
|
||||||
|
if chatID, ok := state.ActionCurrentState.Job.Metadata["chatID"].(int64); ok && chatID != 0 {
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
delete(t.activeJobs, chatID)
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
|
||||||
|
return func(state types.ActionCurrentState) bool {
|
||||||
|
// Check if we have a placeholder message for this job
|
||||||
|
t.placeholderMutex.RLock()
|
||||||
|
msgID, exists := t.placeholders[state.Job.UUID]
|
||||||
|
chatID := int64(0)
|
||||||
|
if state.Job.Metadata != nil {
|
||||||
|
if ch, ok := state.Job.Metadata["chatID"].(int64); ok {
|
||||||
|
chatID = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.placeholderMutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists || msgID == 0 || chatID == 0 || t.bot == nil {
|
||||||
|
return true // Skip if we don't have a message to update
|
||||||
|
}
|
||||||
|
|
||||||
|
thought := telegramThinkingMessage + "\n\n"
|
||||||
|
if state.Reasoning != "" {
|
||||||
|
thought += "Current thought process:\n" + state.Reasoning
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the placeholder message with the current reasoning
|
||||||
|
_, err := t.bot.EditMessageText(t.agent.Context(), &bot.EditMessageTextParams{
|
||||||
|
ChatID: chatID,
|
||||||
|
MessageID: msgID,
|
||||||
|
Text: thought,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating reasoning message", "error", err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancelActiveJobForChat cancels any active job for the given chat
|
||||||
|
func (t *Telegram) cancelActiveJobForChat(chatID int64) {
|
||||||
|
t.activeJobsMutex.RLock()
|
||||||
|
ctxs, exists := t.activeJobs[chatID]
|
||||||
|
t.activeJobsMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
xlog.Info("Cancelling active job for chat", "chatID", chatID)
|
||||||
|
|
||||||
|
// Mark the job as inactive
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
for _, c := range ctxs {
|
||||||
|
c.Cancel()
|
||||||
|
}
|
||||||
|
delete(t.activeJobs, chatID)
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendImageToTelegram downloads and sends an image to Telegram
|
||||||
|
func sendImageToTelegram(ctx context.Context, b *bot.Bot, chatID int64, url string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error downloading image: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read the entire body into memory
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading image body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send image with caption
|
||||||
|
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||||
|
ChatID: chatID,
|
||||||
|
Photo: &models.InputFileUpload{
|
||||||
|
Filename: "image.jpg",
|
||||||
|
Data: bytes.NewReader(bodyBytes),
|
||||||
|
},
|
||||||
|
Caption: "Generated image",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error sending photo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMultimediaContent processes and sends multimedia content from the agent's response
|
||||||
|
func (t *Telegram) handleMultimediaContent(ctx context.Context, chatID int64, res *types.JobResult) ([]string, error) {
|
||||||
|
var urls []string
|
||||||
|
|
||||||
|
for _, state := range res.State {
|
||||||
|
// Collect URLs from search action
|
||||||
|
if urlList, exists := state.Metadata[actions.MetadataUrls]; exists {
|
||||||
|
urls = append(urls, xstrings.UniqueSlice(urlList.([]string))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle images from gen image actions
|
||||||
|
if imagesUrls, exists := state.Metadata[actions.MetadataImages]; exists {
|
||||||
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
|
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
|
||||||
xlog.Debug("Sending photo", "url", url)
|
xlog.Debug("Sending photo", "url", url)
|
||||||
|
if err := sendImageToTelegram(ctx, t.bot, chatID, url); err != nil {
|
||||||
resp, err := http.Get(url)
|
xlog.Error("Error handling image", "error", err)
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Error downloading image", "error", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
|
|
||||||
ChatID: update.Message.Chat.ID,
|
|
||||||
Photo: models.InputFileUpload{
|
|
||||||
Filename: "image.jpg",
|
|
||||||
Data: resp.Body,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Error sending photo", "error", err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle browser agent screenshots
|
||||||
|
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||||
|
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||||
|
state := historyStruct.States[len(historyStruct.States)-1]
|
||||||
|
if state.Screenshot != "" {
|
||||||
|
// Decode base64 screenshot
|
||||||
|
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error decoding screenshot", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send screenshot with caption
|
||||||
|
_, err = t.bot.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||||
|
ChatID: chatID,
|
||||||
|
Photo: &models.InputFileUpload{
|
||||||
|
Filename: "screenshot.png",
|
||||||
|
Data: bytes.NewReader(screenshotData),
|
||||||
|
},
|
||||||
|
Caption: "Browser Agent Screenshot",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending screenshot", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
|
||||||
// ParseMode: models.ParseModeMarkdown,
|
return urls, nil
|
||||||
ChatID: update.Message.Chat.ID,
|
}
|
||||||
Text: res.Response,
|
|
||||||
|
// formatResponseWithURLs formats the response text and creates message entities for URLs
|
||||||
|
func formatResponseWithURLs(response string, urls []string) string {
|
||||||
|
finalResponse := response
|
||||||
|
if len(urls) > 0 {
|
||||||
|
finalResponse += "\n\nReferences:\n"
|
||||||
|
for i, url := range urls {
|
||||||
|
finalResponse += fmt.Sprintf("🔗 %d. %s\n", i+1, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot.EscapeMarkdown(finalResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
||||||
|
if update.Message == nil || update.Message.From == nil {
|
||||||
|
xlog.Debug("Message or user is nil", "update", update)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := update.Message.From.Username
|
||||||
|
|
||||||
|
xlog.Debug("Received message from user", "username", username, "chatID", update.Message.Chat.ID, "message", update.Message.Text)
|
||||||
|
internalError := func(err error, msg *models.Message) {
|
||||||
|
xlog.Error("Error updating final message", "error", err)
|
||||||
|
b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: "there was an internal error. try again!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
xlog.Debug("Handling message", "update", update)
|
||||||
|
// Handle group messages
|
||||||
|
if update.Message.Chat.Type == "group" || update.Message.Chat.Type == "supergroup" {
|
||||||
|
t.handleGroupMessage(ctx, b, a, update)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle private messages
|
||||||
|
if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
|
||||||
|
xlog.Info("Unauthorized user", "username", username, "admins", t.admins)
|
||||||
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: "you are not authorized to use this bot!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending unauthorized message", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any active job for this chat before starting a new one
|
||||||
|
t.cancelActiveJobForChat(update.Message.Chat.ID)
|
||||||
|
|
||||||
|
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.From.ID))
|
||||||
|
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||||
|
Content: update.Message.Text,
|
||||||
|
Role: "user",
|
||||||
|
})
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("telegram:%d", update.Message.From.ID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: update.Message.Text,
|
||||||
|
Role: "user",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send initial placeholder message
|
||||||
|
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: bot.EscapeMarkdown(telegramThinkingMessage),
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error sending message", "error", err)
|
xlog.Error("Error sending initial message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the UUID->placeholder message mapping
|
||||||
|
jobUUID := fmt.Sprintf("%d", msg.ID)
|
||||||
|
|
||||||
|
t.placeholderMutex.Lock()
|
||||||
|
t.placeholders[jobUUID] = msg.ID
|
||||||
|
t.placeholderMutex.Unlock()
|
||||||
|
|
||||||
|
// Add chat ID to metadata for tracking
|
||||||
|
metadata := map[string]interface{}{
|
||||||
|
"chatID": update.Message.Chat.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new job with the conversation history and metadata
|
||||||
|
job := types.NewJob(
|
||||||
|
types.WithConversationHistory(currentConv),
|
||||||
|
types.WithUUID(jobUUID),
|
||||||
|
types.WithMetadata(metadata),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mark this chat as having an active job
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID], job)
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Mark job as complete
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
job.Cancel()
|
||||||
|
for i, j := range t.activeJobs[update.Message.Chat.ID] {
|
||||||
|
if j.UUID == job.UUID {
|
||||||
|
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID][:i], t.activeJobs[update.Message.Chat.ID][i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
|
||||||
|
// Clean up the placeholder map
|
||||||
|
t.placeholderMutex.Lock()
|
||||||
|
delete(t.placeholders, jobUUID)
|
||||||
|
t.placeholderMutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
res := a.Ask(
|
||||||
|
types.WithConversationHistory(currentConv),
|
||||||
|
types.WithUUID(jobUUID),
|
||||||
|
types.WithMetadata(metadata),
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.Response == "" {
|
||||||
|
xlog.Error("Empty response from agent")
|
||||||
|
_, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: "there was an internal error. try again!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating error message", "error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("telegram:%d", update.Message.From.ID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: res.Response,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle any multimedia content in the response and collect URLs
|
||||||
|
urls, err := t.handleMultimediaContent(ctx, update.Message.Chat.ID, res)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error handling multimedia content", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the message with the final response
|
||||||
|
formattedResponse := formatResponseWithURLs(res.Response, urls)
|
||||||
|
|
||||||
|
// Split the message if it's too long
|
||||||
|
messages := xstrings.SplitParagraph(formattedResponse, telegramMaxMessageLength)
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
_, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: "there was an internal error. try again!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating error message", "error", err)
|
||||||
|
internalError(fmt.Errorf("error updating error message: %w", err), msg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the first message
|
||||||
|
_, err = b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: messages[0],
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error updating message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send additional chunks as new messages
|
||||||
|
for i := 1; i < len(messages); i++ {
|
||||||
|
_, err = b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: messages[i],
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending additional message", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,18 +651,42 @@ func (t *Telegram) Start(a *agent.Agent) {
|
|||||||
|
|
||||||
b, err := bot.New(t.Token, opts...)
|
b, err := bot.New(t.Token, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
xlog.Error("Error creating bot", "error", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.bot = b
|
t.bot = b
|
||||||
t.agent = a
|
t.agent = a
|
||||||
|
|
||||||
// go func() {
|
// go func() {
|
||||||
// for m := range a.ConversationChannel() {
|
// forc m := range a.ConversationChannel() {
|
||||||
// t.handleNewMessage(ctx, b, m)
|
// t.handleNewMessage(ctx, b, m)
|
||||||
// }
|
// }
|
||||||
// }()
|
// }()
|
||||||
|
|
||||||
|
if t.channelID != "" {
|
||||||
|
// handle new conversations
|
||||||
|
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||||
|
xlog.Debug("Subscriber(telegram)", "message", ccm.Content)
|
||||||
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: t.channelID,
|
||||||
|
Text: ccm.Content,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.agent.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("telegram:%s", t.channelID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
b.Start(ctx)
|
b.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,24 +696,20 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
|
|||||||
return nil, errors.New("token is required")
|
return nil, errors.New("token is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
admins := []string{}
|
admins := []string{}
|
||||||
|
|
||||||
if _, ok := config["admins"]; ok {
|
if _, ok := config["admins"]; ok && strings.Contains(config["admins"], ",") {
|
||||||
admins = append(admins, strings.Split(config["admins"], ",")...)
|
admins = append(admins, strings.Split(config["admins"], ",")...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Telegram{
|
return &Telegram{
|
||||||
Token: token,
|
Token: token,
|
||||||
lastMessageDuration: duration,
|
admins: admins,
|
||||||
admins: admins,
|
placeholders: make(map[string]int),
|
||||||
currentconversation: map[int64][]openai.ChatCompletionMessage{},
|
activeJobs: make(map[int64][]*types.Job),
|
||||||
lastMessageTime: map[int64]time.Time{},
|
channelID: config["channel_id"],
|
||||||
conversationTracker: NewConversationTracker[int64](duration),
|
groupMode: config["group_mode"] == "true",
|
||||||
|
mentionOnly: config["mention_only"] == "true",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +729,22 @@ func TelegramConfigMeta() []config.Field {
|
|||||||
HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot",
|
HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "lastMessageDuration",
|
Name: "channel_id",
|
||||||
Label: "Last Message Duration",
|
Label: "Channel ID",
|
||||||
Type: config.FieldTypeText,
|
Type: config.FieldTypeText,
|
||||||
DefaultValue: "5m",
|
HelpText: "Telegram channel ID to send messages to if the agent needs to initiate a conversation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "group_mode",
|
||||||
|
Label: "Group Mode",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
HelpText: "Enable bot to respond in group chats",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "mention_only",
|
||||||
|
Label: "Mention Only",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
HelpText: "Bot will only respond when mentioned in group chats",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user