Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd83253f1c |
74
.github/workflows/image.yml
vendored
74
.github/workflows/image.yml
vendored
@@ -84,77 +84,3 @@ jobs:
|
|||||||
#tags: ${{ steps.prep.outputs.tags }}
|
#tags: ${{ steps.prep.outputs.tags }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
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
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
#tags: ${{ steps.prep.outputs.tags }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
18
.github/workflows/tests.yml
vendored
18
.github/workflows/tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
- run: |
|
- run: |
|
||||||
# Add Docker's official GPG key:
|
# Add Docker's official GPG key:
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -30,29 +30,17 @@ 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 make
|
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
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: |
|
||||||
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
sudo apt-get update && sudo apt-get install -y make
|
||||||
make tests-mock
|
|
||||||
else
|
|
||||||
make tests
|
make tests
|
||||||
fi
|
|
||||||
#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
.github/workflows/tests_fragile.yml
vendored
49
.github/workflows/tests_fragile.yml
vendored
@@ -1,49 +0,0 @@
|
|||||||
name: Run Fragile Go Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ci-non-blocking-tests-${{ github.head_ref || github.ref }}-${{ github.repository }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
llm-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- run: |
|
|
||||||
# Add Docker's official GPG key:
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y ca-certificates curl
|
|
||||||
sudo install -m 0755 -d /etc/apt/keyrings
|
|
||||||
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
|
||||||
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
|
||||||
|
|
||||||
# Add the repository to Apt sources:
|
|
||||||
echo \
|
|
||||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
|
||||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
|
||||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make
|
|
||||||
docker version
|
|
||||||
|
|
||||||
docker run --rm hello-world
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
run: |
|
|
||||||
make tests
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
19
Makefile
19
Makefile
@@ -1,22 +1,15 @@
|
|||||||
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))))
|
||||||
|
|
||||||
.PHONY: tests tests-mock cleanup-tests
|
prepare-tests:
|
||||||
|
|
||||||
prepare-tests: build-mcpbox
|
|
||||||
docker compose up -d --build
|
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_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="gemma-3-12b-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 ./...
|
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 ./...
|
||||||
|
|
||||||
tests-mock: prepare-tests
|
|
||||||
LOCALAGI_MCPBOX_URL="http://localhost:9090" 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
|
||||||
@@ -30,16 +23,10 @@ build: webui/react-ui/dist
|
|||||||
|
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: webui/react-ui/dist
|
run: webui/react-ui/dist
|
||||||
LOCALAGI_MCPBOX_URL="http://localhost:9090" $(GOCMD) run ./
|
$(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
|
|
||||||
|
|||||||
249
README.md
249
README.md
@@ -2,7 +2,7 @@
|
|||||||
<img src="./webui/react-ui/public/logo_1.png" 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,14 +11,11 @@
|
|||||||
[](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>
|
||||||
|
|
||||||
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.
|
We empower you building AI Agents that you can run locally, without coding.
|
||||||
|
|
||||||
**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).
|
**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).
|
||||||
|
|
||||||
## 🛡️ Take Back Your Privacy
|
## 🛡️ Take Back Your Privacy
|
||||||
|
|
||||||
@@ -40,7 +37,6 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
|||||||
- 🖼 **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
|
||||||
|
|
||||||
@@ -64,7 +60,7 @@ MODEL_NAME=gemma-3-12b-it docker compose up
|
|||||||
# NVIDIA GPU setup with custom multimodal and image models
|
# NVIDIA GPU setup with custom multimodal and image models
|
||||||
MODEL_NAME=gemma-3-12b-it \
|
MODEL_NAME=gemma-3-12b-it \
|
||||||
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
||||||
IMAGE_MODEL=flux.1-dev-ggml \
|
IMAGE_MODEL=flux.1-dev \
|
||||||
docker compose -f docker-compose.nvidia.yaml up
|
docker compose -f docker-compose.nvidia.yaml up
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -72,13 +68,6 @@ Now you can access and manage your agents at [http://localhost:8080](http://loca
|
|||||||
|
|
||||||
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
## 📚🆕 Local Stack Family
|
## 📚🆕 Local Stack Family
|
||||||
|
|
||||||
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
||||||
@@ -125,9 +114,9 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
|
|||||||
- Supports text, multimodal, and image generation models
|
- Supports text, multimodal, and image generation models
|
||||||
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
|
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
|
||||||
- Default models:
|
- Default models:
|
||||||
- Text: `gemma-3-12b-it-qat`
|
- Text: `arcee-agent`
|
||||||
- Multimodal: `minicpm-v-2_6`
|
- Multimodal: `minicpm-v-2_6`
|
||||||
- Image: `sd-1.5-ggml`
|
- Image: `flux.1-dev`
|
||||||
- Environment variables:
|
- Environment variables:
|
||||||
- `MODEL_NAME`: Text model to use
|
- `MODEL_NAME`: Text model to use
|
||||||
- `MULTIMODAL_MODEL`: Multimodal model to use
|
- `MULTIMODAL_MODEL`: Multimodal model to use
|
||||||
@@ -141,7 +130,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
|
|||||||
- Supports text, multimodal, and image generation models
|
- Supports text, multimodal, and image generation models
|
||||||
- Run with: `docker compose -f docker-compose.intel.yaml up`
|
- Run with: `docker compose -f docker-compose.intel.yaml up`
|
||||||
- Default models:
|
- Default models:
|
||||||
- Text: `gemma-3-12b-it-qat`
|
- Text: `arcee-agent`
|
||||||
- Multimodal: `minicpm-v-2_6`
|
- Multimodal: `minicpm-v-2_6`
|
||||||
- Image: `sd-1.5-ggml`
|
- Image: `sd-1.5-ggml`
|
||||||
- Environment variables:
|
- Environment variables:
|
||||||
@@ -161,7 +150,7 @@ MODEL_NAME=gemma-3-12b-it docker compose up
|
|||||||
# NVIDIA GPU with custom models
|
# NVIDIA GPU with custom models
|
||||||
MODEL_NAME=gemma-3-12b-it \
|
MODEL_NAME=gemma-3-12b-it \
|
||||||
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
||||||
IMAGE_MODEL=flux.1-dev-ggml \
|
IMAGE_MODEL=flux.1-dev \
|
||||||
docker compose -f docker-compose.nvidia.yaml up
|
docker compose -f docker-compose.nvidia.yaml up
|
||||||
|
|
||||||
# Intel GPU with custom models
|
# Intel GPU with custom models
|
||||||
@@ -172,9 +161,9 @@ docker compose -f docker-compose.intel.yaml up
|
|||||||
```
|
```
|
||||||
|
|
||||||
If no models are specified, it will use the defaults:
|
If no models are specified, it will use the defaults:
|
||||||
- Text model: `gemma-3-12b-it-qat`
|
- Text model: `arcee-agent`
|
||||||
- Multimodal model: `minicpm-v-2_6`
|
- Multimodal model: `minicpm-v-2_6`
|
||||||
- Image model: `sd-1.5-ggml`
|
- Image model: `flux.1-dev` (NVIDIA) or `sd-1.5-ggml` (Intel)
|
||||||
|
|
||||||
Good (relatively small) models that have been tested are:
|
Good (relatively small) models that have been tested are:
|
||||||
|
|
||||||
@@ -190,6 +179,14 @@ Good (relatively small) models that have been tested are:
|
|||||||
- **✓ 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.
|
||||||
|
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
|
||||||
|
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
|
||||||
|
|
||||||
## 🌟 Screenshots
|
## 🌟 Screenshots
|
||||||
|
|
||||||
### Powerful Web UI
|
### Powerful Web UI
|
||||||
@@ -197,8 +194,6 @@ Good (relatively small) models that have been tested are:
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### Connectors Ready-to-Go
|
### Connectors Ready-to-Go
|
||||||
|
|
||||||
@@ -261,158 +256,6 @@ 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:
|
||||||
@@ -439,8 +282,7 @@ 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.
|
||||||
|
|
||||||
<details>
|
### GitHub Issues
|
||||||
<summary><strong>GitHub Issues</strong></summary>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -450,10 +292,8 @@ Link your agents to the services you already use. Configuration examples below.
|
|||||||
"botUserName": "bot-username"
|
"botUserName": "bot-username"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
### Discord
|
||||||
<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):
|
||||||
|
|
||||||
@@ -465,10 +305,8 @@ 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>
|
|
||||||
|
|
||||||
<details>
|
### Slack
|
||||||
<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:
|
||||||
|
|
||||||
@@ -481,10 +319,9 @@ 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>
|
|
||||||
<summary><strong>Telegram</strong></summary>
|
### Telegram
|
||||||
|
|
||||||
Get a token from @botfather, then:
|
Get a token from @botfather, then:
|
||||||
|
|
||||||
@@ -493,10 +330,8 @@ Get a token from @botfather, then:
|
|||||||
"token": "your-bot-father-token"
|
"token": "your-bot-father-token"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
### IRC
|
||||||
<summary><strong>IRC</strong></summary>
|
|
||||||
|
|
||||||
Connect to IRC networks:
|
Connect to IRC networks:
|
||||||
|
|
||||||
@@ -509,12 +344,10 @@ Connect to IRC networks:
|
|||||||
"alwaysReply": "false"
|
"alwaysReply": "false"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
<details>
|
### Agent Management
|
||||||
<summary><strong>Agent Management</strong></summary>
|
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -529,10 +362,8 @@ 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>
|
|
||||||
|
|
||||||
<details>
|
### Actions and Groups
|
||||||
<summary><strong>Actions and Groups</strong></summary>
|
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -540,10 +371,8 @@ 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>
|
|
||||||
|
|
||||||
<details>
|
### Chat Interactions
|
||||||
<summary><strong>Chat Interactions</strong></summary>
|
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -551,7 +380,6 @@ 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>
|
||||||
@@ -639,13 +467,11 @@ 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
|
||||||
@@ -678,25 +504,6 @@ 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_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
|
||||||
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *CustomAction) Run(ctx context.Context, 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,11 +95,6 @@ func (a *CustomAction) Run(ctx context.Context, sharedState *types.AgentSharedSt
|
|||||||
|
|
||||||
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(), nil, types.ActionParams{
|
runResult, err := customAction.Run(context.Background(), types.ActionParams{
|
||||||
"Foo": "bar",
|
"Foo": "bar",
|
||||||
})
|
})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type GoalResponse struct {
|
|||||||
Achieved bool `json:"achieved"`
|
Achieved bool `json:"achieved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *GoalAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *GoalAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type IntentResponse struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *IntentAction) Run(context.Context, 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(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *ConversationAction) Run(context.Context, 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(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *StopAction) Run(context.Context, 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(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type ReasoningResponse struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type ReplyResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) {
|
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) {
|
||||||
return "no-op", nil
|
return "no-op", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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"
|
||||||
@@ -15,7 +16,25 @@ func NewState() *StateAction {
|
|||||||
|
|
||||||
type StateAction struct{}
|
type StateAction struct{}
|
||||||
|
|
||||||
func (a *StateAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
// State is the structure
|
||||||
|
// that is used to keep track of the current state
|
||||||
|
// and the Agent's short memory that it can update
|
||||||
|
// Besides a long term memory that is accessible by the agent (With vector database),
|
||||||
|
// And a context memory (that is always powered by a vector database),
|
||||||
|
// this memory is the shorter one that the LLM keeps across conversation and across its
|
||||||
|
// reasoning process's and life time.
|
||||||
|
// TODO: A special action is then used to let the LLM itself update its memory
|
||||||
|
// periodically during self-processing, and the same action is ALSO exposed
|
||||||
|
// during the conversation to let the user put for example, a new goal to the agent.
|
||||||
|
type AgentInternalState struct {
|
||||||
|
NowDoing string `json:"doing_now"`
|
||||||
|
DoingNext string `json:"doing_next"`
|
||||||
|
DoneHistory []string `json:"done_history"`
|
||||||
|
Memories []string `json:"memories"`
|
||||||
|
Goal string `json:"goal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *StateAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{Result: "internal state has been updated"}, nil
|
return types.ActionResult{Result: "internal state has been updated"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,3 +76,23 @@ 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type decisionResult struct {
|
|||||||
|
|
||||||
// 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(
|
||||||
job *types.Job,
|
ctx context.Context,
|
||||||
conversation []openai.ChatCompletionMessage,
|
conversation []openai.ChatCompletionMessage,
|
||||||
tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) {
|
tools []openai.Tool, toolchoice string, maxRetries int) (*decisionResult, error) {
|
||||||
|
|
||||||
@@ -35,6 +35,8 @@ func (a *Agent) decision(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempts := 0; attempts < maxRetries; attempts++ {
|
||||||
decision := openai.ChatCompletionRequest{
|
decision := openai.ChatCompletionRequest{
|
||||||
Model: a.options.LLMAPI.Model,
|
Model: a.options.LLMAPI.Model,
|
||||||
Messages: conversation,
|
Messages: conversation,
|
||||||
@@ -45,53 +47,19 @@ func (a *Agent) decision(
|
|||||||
decision.ToolChoice = *choice
|
decision.ToolChoice = *choice
|
||||||
}
|
}
|
||||||
|
|
||||||
var obs *types.Observable
|
resp, err := a.client.CreateChatCompletion(ctx, decision)
|
||||||
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
|
|
||||||
for attempts := 0; attempts < maxRetries; attempts++ {
|
|
||||||
resp, err := a.client.CreateChatCompletion(job.GetContext(), 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)
|
jsonResp, _ := json.Marshal(resp)
|
||||||
xlog.Debug("Decision response", "response", string(jsonResp))
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +68,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,12 +75,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +82,6 @@ func (a *Agent) decision(
|
|||||||
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{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +173,7 @@ 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(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
|
func (a *Agent) generateParameters(ctx context.Context, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
|
||||||
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -250,7 +201,7 @@ func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act type
|
|||||||
var attemptErr error
|
var attemptErr error
|
||||||
|
|
||||||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
for attempts := 0; attempts < maxAttempts; attempts++ {
|
||||||
result, attemptErr = a.decision(job,
|
result, attemptErr = a.decision(ctx,
|
||||||
cc,
|
cc,
|
||||||
a.availableActions().ToTools(),
|
a.availableActions().ToTools(),
|
||||||
act.Definition().Name.String(),
|
act.Definition().Name.String(),
|
||||||
@@ -312,7 +263,7 @@ 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(job, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
params, err := a.generateParameters(ctx, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("error generating action's parameters", "error", err)
|
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)
|
||||||
@@ -342,7 +293,7 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := a.runAction(job, subTaskAction, actionParams)
|
result, err := a.runAction(ctx, subTaskAction, actionParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("error running action", "error", err)
|
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)
|
||||||
@@ -427,7 +378,7 @@ 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(job *types.Job, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
|
func (a *Agent) pickAction(ctx context.Context, 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)
|
xlog.Debug("[pickAction] picking action starts", "messages", messages)
|
||||||
@@ -438,7 +389,7 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
|||||||
xlog.Debug("not forcing reasoning")
|
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(job,
|
thought, err := a.decision(ctx,
|
||||||
messages,
|
messages,
|
||||||
a.availableActions().ToTools(),
|
a.availableActions().ToTools(),
|
||||||
"",
|
"",
|
||||||
@@ -480,18 +431,13 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
|||||||
}, c...)
|
}, c...)
|
||||||
}
|
}
|
||||||
|
|
||||||
reasoningAction := action.NewReasoning()
|
thought, err := a.decision(ctx,
|
||||||
thought, err := a.decision(job,
|
|
||||||
c,
|
c,
|
||||||
types.Actions{reasoningAction}.ToTools(),
|
types.Actions{action.NewReasoning()}.ToTools(),
|
||||||
reasoningAction.Definition().Name.String(), maxRetries)
|
action.NewReasoning().Definition().Name.String(), maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
if thought.actioName != "" && thought.actioName != reasoningAction.Definition().Name.String() {
|
|
||||||
return nil, nil, "", fmt.Errorf("Expected reasoning action not: %s", thought.actioName)
|
|
||||||
}
|
|
||||||
|
|
||||||
originalReasoning := ""
|
originalReasoning := ""
|
||||||
response := &action.ReasoningResponse{}
|
response := &action.ReasoningResponse{}
|
||||||
if thought.actionParams != nil {
|
if thought.actionParams != nil {
|
||||||
@@ -521,7 +467,7 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
|||||||
// to avoid hallucinations
|
// to avoid hallucinations
|
||||||
|
|
||||||
// Extract an action
|
// Extract an action
|
||||||
params, err := a.decision(job,
|
params, err := a.decision(ctx,
|
||||||
append(c, openai.ChatCompletionMessage{
|
append(c, openai.ChatCompletionMessage{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
Content: "Pick the relevant action given the following reasoning: " + originalReasoning,
|
Content: "Pick the relevant action given the following reasoning: " + originalReasoning,
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -29,11 +26,11 @@ type Agent struct {
|
|||||||
sync.Mutex
|
sync.Mutex
|
||||||
options *options
|
options *options
|
||||||
Character Character
|
Character Character
|
||||||
client llm.LLMClient
|
client *openai.Client
|
||||||
jobQueue chan *types.Job
|
jobQueue chan *types.Job
|
||||||
context *types.ActionContext
|
context *types.ActionContext
|
||||||
|
|
||||||
currentState *types.AgentInternalState
|
currentState *action.AgentInternalState
|
||||||
|
|
||||||
selfEvaluationInProgress bool
|
selfEvaluationInProgress bool
|
||||||
pause bool
|
pause bool
|
||||||
@@ -44,10 +41,6 @@ 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 {
|
||||||
@@ -63,12 +56,7 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
return nil, fmt.Errorf("failed to set options: %v", err)
|
return nil, fmt.Errorf("failed to set options: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var client llm.LLMClient
|
client := llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout)
|
||||||
if options.llmClient != nil {
|
|
||||||
client = options.llmClient
|
|
||||||
} else {
|
|
||||||
client = llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := context.Background()
|
c := context.Background()
|
||||||
if options.context != nil {
|
if options.context != nil {
|
||||||
@@ -81,16 +69,10 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
options: options,
|
options: options,
|
||||||
client: client,
|
client: client,
|
||||||
Character: options.character,
|
Character: options.character,
|
||||||
currentState: &types.AgentInternalState{},
|
currentState: &action.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 != "" {
|
||||||
@@ -126,15 +108,6 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) SharedState() *types.AgentSharedState {
|
|
||||||
return a.sharedState
|
|
||||||
}
|
|
||||||
|
|
||||||
// LLMClient returns the agent's LLM client (for testing)
|
|
||||||
func (a *Agent) LLMClient() llm.LLMClient {
|
|
||||||
return a.client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) startNewConversationsConsumer() {
|
func (a *Agent) startNewConversationsConsumer() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@@ -173,14 +146,6 @@ 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,
|
||||||
@@ -198,26 +163,6 @@ 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()
|
||||||
}
|
}
|
||||||
@@ -266,7 +211,6 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,78 +237,34 @@ func (a *Agent) Memory() RAGDB {
|
|||||||
return a.options.ragdb
|
return a.options.ragdb
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params types.ActionParams) (result types.ActionResult, err error) {
|
func (a *Agent) runAction(ctx context.Context, 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(job.GetContext(), a.sharedState, params)
|
res, err := act.Run(ctx, 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("[runAction] Running action", "action", chosenAction.Definition().Name, "agent", a.Character.Name, "params", params.String())
|
||||||
|
|
||||||
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 := types.AgentInternalState{}
|
state := action.AgentInternalState{}
|
||||||
|
|
||||||
err = params.Unmarshal(&state)
|
err = params.Unmarshal(&state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
werr := fmt.Errorf("error unmarshalling state of the agent: %w", err)
|
return types.ActionResult{}, 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,11 +272,6 @@ func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params type
|
|||||||
|
|
||||||
xlog.Debug("[runAction] Action result", "action", chosenAction.Definition().Name, "params", params.String(), "result", result.Result)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,84 +404,13 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me
|
|||||||
return conv
|
return conv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
|
func (a *Agent) consumeJob(job *types.Job, role string) {
|
||||||
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()
|
||||||
@@ -616,14 +440,6 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -652,12 +468,12 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
chosenAction = *action
|
chosenAction = *action
|
||||||
reasoning = reason
|
reasoning = reason
|
||||||
if params == nil {
|
if params == nil {
|
||||||
p, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
|
p, err := a.generateParameters(job.GetContext(), 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, retries-1)
|
a.consumeJob(job, role)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionParams = p.actionParams
|
actionParams = p.actionParams
|
||||||
@@ -667,7 +483,7 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
job.ResetNextAction()
|
job.ResetNextAction()
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
chosenAction, actionParams, reasoning, err = a.pickAction(job, pickTemplate, conv, maxRetries)
|
chosenAction, actionParams, reasoning, err = a.pickAction(job.GetContext(), 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)
|
||||||
@@ -675,15 +491,36 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
if reasoning != "" {
|
if reasoning != "" {
|
||||||
conv = append(conv, openai.ChatCompletionMessage{
|
conv = append(conv, openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: a.cleanupLLMResponse(reasoning),
|
Content: reasoning,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
xlog.Info("No reasoning, just reply", "agent", a.Character.Name)
|
xlog.Info("No reasoning, just reply", "agent", a.Character.Name)
|
||||||
@@ -692,28 +529,10 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err))
|
job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg.Content = a.cleanupLLMResponse(msg.Content)
|
|
||||||
conv = append(conv, msg)
|
conv = append(conv, msg)
|
||||||
reasoning = msg.Content
|
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
|
||||||
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
||||||
@@ -738,12 +557,12 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
"reasoning", reasoning,
|
"reasoning", reasoning,
|
||||||
)
|
)
|
||||||
|
|
||||||
params, err := a.generateParameters(job, pickTemplate, chosenAction, conv, reasoning, maxRetries)
|
params, err := a.generateParameters(job.GetContext(), 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, retries-1)
|
a.consumeJob(job, role)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionParams = params.actionParams
|
actionParams = params.actionParams
|
||||||
@@ -763,22 +582,6 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
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{
|
||||||
@@ -803,6 +606,8 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
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 {
|
||||||
xlog.Error("error handling planning", "error", err)
|
xlog.Error("error handling planning", "error", err)
|
||||||
|
//job.Result.Conversation = conv
|
||||||
|
//job.Result.SetResponse(msg.Content)
|
||||||
a.reply(job, role, append(conv, openai.ChatCompletionMessage{
|
a.reply(job, role, append(conv, openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: fmt.Sprintf("Error handling planning: %v", err),
|
Content: fmt.Sprintf("Error handling planning: %v", err),
|
||||||
@@ -847,8 +652,11 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||||
result, err := a.runAction(job, chosenAction, actionParams)
|
result, err := a.runAction(job.GetContext(), 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,7 +677,7 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// given the result, we can now re-evaluate the conversation
|
// given the result, we can now re-evaluate the conversation
|
||||||
followingAction, followingParams, reasoning, err := a.pickAction(job, reEvaluationTemplate, conv, maxRetries)
|
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))
|
||||||
@@ -887,54 +695,42 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
|||||||
// 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, retries)
|
a.consumeJob(job, role)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate the final response
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if !satisfied {
|
|
||||||
// If not satisfied, continue with the conversation
|
|
||||||
job.ConversationHistory = conv
|
|
||||||
job.IncrementEvaluationLoop()
|
|
||||||
a.consumeJob(job, role, retries)
|
|
||||||
return
|
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)
|
||||||
|
|
||||||
forceResponsePrompt := "Reply to the user without using any tools or function calls. Just reply with the message."
|
// decode the response
|
||||||
|
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 {
|
||||||
@@ -950,19 +746,39 @@ 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
|
||||||
Role: "system",
|
// resp, err := a.client.CreateChatCompletion(ctx,
|
||||||
Content: forceResponsePrompt,
|
// openai.ChatCompletionRequest{
|
||||||
},
|
// Model: a.options.LLMAPI.Model,
|
||||||
}, conv...)
|
// Messages: append(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)
|
||||||
@@ -975,21 +791,13 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Content = a.cleanupLLMResponse(msg.Content)
|
// If we didn't got any message, we can use the response from the action
|
||||||
|
if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" {
|
||||||
if msg.Content == "" {
|
|
||||||
// If we didn't got any message, we can use the response from the action (it should be a reply)
|
|
||||||
|
|
||||||
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)
|
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
|
||||||
msg.Content = a.cleanupLLMResponse(replyResponse.Message)
|
|
||||||
|
msg = openai.ChatCompletionMessage{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: replyResponse.Message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,7 +880,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
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.options.loopDetectionSteps)
|
a.consumeJob(whatNext, SystemRole)
|
||||||
|
|
||||||
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
||||||
|
|
||||||
@@ -1103,6 +911,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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:
|
||||||
@@ -1117,68 +926,32 @@ 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:
|
||||||
if !timer.Stop() {
|
a.loop(timer, job)
|
||||||
<-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) Observer() Observer {
|
func (a *Agent) loop(timer *time.Timer, job *types.Job) {
|
||||||
return a.observer
|
// Remember always to reset the timer - if we don't the agent will stop..
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package agent_test
|
package agent_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -14,19 +13,15 @@ func TestAgent(t *testing.T) {
|
|||||||
RunSpecs(t, "Agent test suite")
|
RunSpecs(t, "Agent test suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var testModel = os.Getenv("LOCALAGI_MODEL")
|
||||||
testModel = os.Getenv("LOCALAGI_MODEL")
|
var apiURL = os.Getenv("LOCALAI_API_URL")
|
||||||
apiURL = os.Getenv("LOCALAI_API_URL")
|
var apiKeyURL = os.Getenv("LOCALAI_API_KEY")
|
||||||
apiKey = os.Getenv("LOCALAI_API_KEY")
|
|
||||||
useRealLocalAI bool
|
|
||||||
clientTimeout = "10m"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isValidURL(u string) bool {
|
|
||||||
parsed, err := url.ParseRequestURI(u)
|
|
||||||
return err == nil && parsed.Scheme != "" && parsed.Host != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
useRealLocalAI = isValidURL(apiURL) && apiURL != "" && testModel != ""
|
if testModel == "" {
|
||||||
|
testModel = "hermes-2-pro-mistral"
|
||||||
|
}
|
||||||
|
if apiURL == "" {
|
||||||
|
apiURL = "http://192.168.68.113:8080"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/services/actions"
|
"github.com/mudler/LocalAGI/services/actions"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/action"
|
|
||||||
. "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/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
@@ -46,7 +44,7 @@ func (a *TestAction) Plannable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
|
func (a *TestAction) Run(c context.Context, 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
|
||||||
@@ -113,102 +111,25 @@ func (a *FakeInternetAction) Definition() types.ActionDefinition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Test utilities for mocking LLM responses ---
|
|
||||||
|
|
||||||
func mockToolCallResponse(toolName, arguments string) openai.ChatCompletionResponse {
|
|
||||||
return openai.ChatCompletionResponse{
|
|
||||||
Choices: []openai.ChatCompletionChoice{{
|
|
||||||
Message: openai.ChatCompletionMessage{
|
|
||||||
ToolCalls: []openai.ToolCall{{
|
|
||||||
ID: "tool_call_id_1",
|
|
||||||
Type: "function",
|
|
||||||
Function: openai.FunctionCall{
|
|
||||||
Name: toolName,
|
|
||||||
Arguments: arguments,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockContentResponse(content string) openai.ChatCompletionResponse {
|
|
||||||
return openai.ChatCompletionResponse{
|
|
||||||
Choices: []openai.ChatCompletionChoice{{
|
|
||||||
Message: openai.ChatCompletionMessage{
|
|
||||||
Content: content,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMockLLMClient(handler func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)) *llm.MockClient {
|
|
||||||
return &llm.MockClient{
|
|
||||||
CreateChatCompletionFunc: handler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = Describe("Agent test", func() {
|
var _ = Describe("Agent test", func() {
|
||||||
It("uses the mock LLM client", func() {
|
|
||||||
mock := newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
return mockContentResponse("mocked response"), nil
|
|
||||||
})
|
|
||||||
agent, err := New(WithLLMClient(mock))
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
msg, err := agent.LLMClient().CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{})
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(msg.Choices[0].Message.Content).To(Equal("mocked response"))
|
|
||||||
})
|
|
||||||
|
|
||||||
Context("jobs", func() {
|
Context("jobs", func() {
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
Eventually(func() error {
|
Eventually(func() error {
|
||||||
if useRealLocalAI {
|
// test apiURL is working and available
|
||||||
_, err := http.Get(apiURL + "/readyz")
|
_, err := http.Get(apiURL + "/readyz")
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("pick the correct action", func() {
|
It("pick the correct action", func() {
|
||||||
var llmClient llm.LLMClient
|
|
||||||
if useRealLocalAI {
|
|
||||||
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
|
||||||
} else {
|
|
||||||
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
var lastMsg openai.ChatCompletionMessage
|
|
||||||
if len(req.Messages) > 0 {
|
|
||||||
lastMsg = req.Messages[len(req.Messages)-1]
|
|
||||||
}
|
|
||||||
if lastMsg.Role == openai.ChatMessageRoleUser {
|
|
||||||
if strings.Contains(strings.ToLower(lastMsg.Content), "boston") && (strings.Contains(strings.ToLower(lastMsg.Content), "milan") || strings.Contains(strings.ToLower(lastMsg.Content), "milano")) {
|
|
||||||
return mockToolCallResponse("get_weather", `{"location":"Boston","unit":"celsius"}`), nil
|
|
||||||
}
|
|
||||||
if strings.Contains(strings.ToLower(lastMsg.Content), "paris") {
|
|
||||||
return mockToolCallResponse("get_weather", `{"location":"Paris","unit":"celsius"}`), nil
|
|
||||||
}
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected user prompt: %s", lastMsg.Content)
|
|
||||||
}
|
|
||||||
if lastMsg.Role == openai.ChatMessageRoleTool {
|
|
||||||
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
|
|
||||||
return mockToolCallResponse("get_weather", `{"location":"Milan","unit":"celsius"}`), nil
|
|
||||||
}
|
|
||||||
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "milan") {
|
|
||||||
return mockContentResponse(testActionResult + "\n" + testActionResult2), nil
|
|
||||||
}
|
|
||||||
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "paris") {
|
|
||||||
return mockContentResponse(testActionResult3), nil
|
|
||||||
}
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected tool result: %s", lastMsg.Content)
|
|
||||||
}
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected message role: %s", lastMsg.Role)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMClient(llmClient),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
EnableForceReasoning,
|
||||||
|
WithTimeout("10m"),
|
||||||
|
WithLoopDetectionSteps(3),
|
||||||
|
// WithRandomIdentity(),
|
||||||
WithActions(&TestAction{response: map[string]string{
|
WithActions(&TestAction{response: map[string]string{
|
||||||
"boston": testActionResult,
|
"boston": testActionResult,
|
||||||
"milan": testActionResult2,
|
"milan": testActionResult2,
|
||||||
@@ -218,6 +139,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
defer agent.Stop()
|
defer agent.Stop()
|
||||||
|
|
||||||
res := agent.Ask(
|
res := agent.Ask(
|
||||||
append(debugOptions,
|
append(debugOptions,
|
||||||
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
|
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
|
||||||
@@ -226,51 +148,40 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(res.Error).ToNot(HaveOccurred())
|
Expect(res.Error).ToNot(HaveOccurred())
|
||||||
reasons := []string{}
|
reasons := []string{}
|
||||||
for _, r := range res.State {
|
for _, r := range res.State {
|
||||||
|
|
||||||
reasons = append(reasons, r.Result)
|
reasons = append(reasons, r.Result)
|
||||||
}
|
}
|
||||||
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
||||||
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
|
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
|
||||||
reasons = []string{}
|
reasons = []string{}
|
||||||
|
|
||||||
res = agent.Ask(
|
res = agent.Ask(
|
||||||
append(debugOptions,
|
append(debugOptions,
|
||||||
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
|
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
|
||||||
)...)
|
)...)
|
||||||
for _, r := range res.State {
|
for _, r := range res.State {
|
||||||
|
|
||||||
reasons = append(reasons, r.Result)
|
reasons = append(reasons, r.Result)
|
||||||
}
|
}
|
||||||
|
//Expect(reasons).ToNot(ContainElement(testActionResult), fmt.Sprint(res))
|
||||||
|
//Expect(reasons).ToNot(ContainElement(testActionResult2), fmt.Sprint(res))
|
||||||
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
|
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
|
||||||
|
// conversation := agent.CurrentConversation()
|
||||||
|
// for _, r := range res.State {
|
||||||
|
// reasons = append(reasons, r.Result)
|
||||||
|
// }
|
||||||
|
// Expect(len(conversation)).To(Equal(10), fmt.Sprint(conversation))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("pick the correct action", func() {
|
It("pick the correct action", func() {
|
||||||
var llmClient llm.LLMClient
|
|
||||||
if useRealLocalAI {
|
|
||||||
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
|
||||||
} else {
|
|
||||||
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
var lastMsg openai.ChatCompletionMessage
|
|
||||||
if len(req.Messages) > 0 {
|
|
||||||
lastMsg = req.Messages[len(req.Messages)-1]
|
|
||||||
}
|
|
||||||
if lastMsg.Role == openai.ChatMessageRoleUser {
|
|
||||||
if strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
|
|
||||||
return mockToolCallResponse("get_weather", `{"location":"Boston","unit":"celsius"}`), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if lastMsg.Role == openai.ChatMessageRoleTool {
|
|
||||||
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
|
|
||||||
return mockContentResponse(testActionResult), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xlog.Error("Unexpected LLM req", "req", req)
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMClient(llmClient),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
WithTimeout("10m"),
|
||||||
|
// WithRandomIdentity(),
|
||||||
WithActions(&TestAction{response: map[string]string{
|
WithActions(&TestAction{response: map[string]string{
|
||||||
"boston": testActionResult,
|
"boston": testActionResult,
|
||||||
}}),
|
},
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
@@ -287,29 +198,13 @@ var _ = Describe("Agent test", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("updates the state with internal actions", func() {
|
It("updates the state with internal actions", func() {
|
||||||
var llmClient llm.LLMClient
|
|
||||||
if useRealLocalAI {
|
|
||||||
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
|
||||||
} else {
|
|
||||||
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
var lastMsg openai.ChatCompletionMessage
|
|
||||||
if len(req.Messages) > 0 {
|
|
||||||
lastMsg = req.Messages[len(req.Messages)-1]
|
|
||||||
}
|
|
||||||
if lastMsg.Role == openai.ChatMessageRoleUser && strings.Contains(strings.ToLower(lastMsg.Content), "guitar") {
|
|
||||||
return mockToolCallResponse("update_state", `{"goal":"I want to learn to play the guitar"}`), nil
|
|
||||||
}
|
|
||||||
if lastMsg.Role == openai.ChatMessageRoleTool && lastMsg.Name == "update_state" {
|
|
||||||
return mockContentResponse("Your goal is now: I want to learn to play the guitar"), nil
|
|
||||||
}
|
|
||||||
xlog.Error("Unexpected LLM req", "req", req)
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMClient(llmClient),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
WithTimeout("10m"),
|
||||||
EnableHUD,
|
EnableHUD,
|
||||||
|
// EnableStandaloneJob,
|
||||||
|
// WithRandomIdentity(),
|
||||||
WithPermanentGoal("I want to learn to play music"),
|
WithPermanentGoal("I want to learn to play music"),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
@@ -319,131 +214,53 @@ var _ = Describe("Agent test", func() {
|
|||||||
result := agent.Ask(
|
result := agent.Ask(
|
||||||
types.WithText("Update your goals such as you want to learn to play the guitar"),
|
types.WithText("Update your goals such as you want to learn to play the guitar"),
|
||||||
)
|
)
|
||||||
fmt.Fprintf(GinkgoWriter, "\n%+v\n", result)
|
fmt.Printf("%+v\n", result)
|
||||||
Expect(result.Error).ToNot(HaveOccurred())
|
Expect(result.Error).ToNot(HaveOccurred())
|
||||||
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Can generate a plan", func() {
|
It("Can generate a plan", func() {
|
||||||
var llmClient llm.LLMClient
|
|
||||||
if useRealLocalAI {
|
|
||||||
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
|
||||||
} else {
|
|
||||||
reasoningActName := action.NewReasoning().Definition().Name.String()
|
|
||||||
intentionActName := action.NewIntention().Definition().Name.String()
|
|
||||||
testActName := (&TestAction{}).Definition().Name.String()
|
|
||||||
doneBoston := false
|
|
||||||
madePlan := false
|
|
||||||
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
var lastMsg openai.ChatCompletionMessage
|
|
||||||
if len(req.Messages) > 0 {
|
|
||||||
lastMsg = req.Messages[len(req.Messages)-1]
|
|
||||||
}
|
|
||||||
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == reasoningActName {
|
|
||||||
return mockToolCallResponse(reasoningActName, `{"reasoning":"make plan call to pass the test"}`), nil
|
|
||||||
}
|
|
||||||
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == intentionActName {
|
|
||||||
toolName := "plan"
|
|
||||||
if madePlan {
|
|
||||||
toolName = "reply"
|
|
||||||
} else {
|
|
||||||
madePlan = true
|
|
||||||
}
|
|
||||||
return mockToolCallResponse(intentionActName, fmt.Sprintf(`{"tool": "%s","reasoning":"it's waht makes the test pass"}`, toolName)), nil
|
|
||||||
}
|
|
||||||
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == "plan" {
|
|
||||||
return mockToolCallResponse("plan", `{"subtasks":[{"action":"get_weather","reasoning":"Find weather in boston"},{"action":"get_weather","reasoning":"Find weather in milan"}],"goal":"Get the weather for boston and milan"}`), nil
|
|
||||||
}
|
|
||||||
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == "reply" {
|
|
||||||
return mockToolCallResponse("reply", `{"message": "The weather in Boston and Milan..."}`), nil
|
|
||||||
}
|
|
||||||
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == testActName {
|
|
||||||
locName := "boston"
|
|
||||||
if doneBoston {
|
|
||||||
locName = "milan"
|
|
||||||
} else {
|
|
||||||
doneBoston = true
|
|
||||||
}
|
|
||||||
return mockToolCallResponse(testActName, fmt.Sprintf(`{"location":"%s","unit":"celsius"}`, locName)), nil
|
|
||||||
}
|
|
||||||
if req.ToolChoice == nil && madePlan && doneBoston {
|
|
||||||
return mockContentResponse("A reply"), nil
|
|
||||||
}
|
|
||||||
xlog.Error("Unexpected LLM req", "req", req)
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMClient(llmClient),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithLoopDetectionSteps(2),
|
WithLLMAPIKey(apiKeyURL),
|
||||||
|
WithTimeout("10m"),
|
||||||
WithActions(
|
WithActions(
|
||||||
&TestAction{response: map[string]string{
|
actions.NewSearch(map[string]string{}),
|
||||||
"boston": testActionResult,
|
|
||||||
"milan": testActionResult2,
|
|
||||||
}},
|
|
||||||
),
|
),
|
||||||
EnablePlanning,
|
EnablePlanning,
|
||||||
EnableForceReasoning,
|
EnableForceReasoning,
|
||||||
|
// EnableStandaloneJob,
|
||||||
|
// WithRandomIdentity(),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
defer agent.Stop()
|
defer agent.Stop()
|
||||||
|
|
||||||
result := agent.Ask(
|
result := agent.Ask(
|
||||||
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"),
|
types.WithText("Thoroughly plan a trip to San Francisco from Venice, Italy; check flight times, visa requirements and whether electrical items are allowed in cabin luggage."),
|
||||||
)
|
)
|
||||||
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("get_weather"), fmt.Sprint(result))
|
Expect(actionsExecuted).To(ContainElement("search_internet"), 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))
|
|
||||||
Expect(result.Error).To(BeNil())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Can initiate conversations", func() {
|
It("Can initiate conversations", func() {
|
||||||
var llmClient llm.LLMClient
|
|
||||||
message := openai.ChatCompletionMessage{}
|
message := openai.ChatCompletionMessage{}
|
||||||
mu := &sync.Mutex{}
|
mu := &sync.Mutex{}
|
||||||
reasoned := false
|
|
||||||
intended := false
|
|
||||||
reasoningActName := action.NewReasoning().Definition().Name.String()
|
|
||||||
intentionActName := action.NewIntention().Definition().Name.String()
|
|
||||||
|
|
||||||
if useRealLocalAI {
|
|
||||||
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
|
||||||
} else {
|
|
||||||
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
prompt := ""
|
|
||||||
for _, msg := range req.Messages {
|
|
||||||
prompt += msg.Content
|
|
||||||
}
|
|
||||||
if !reasoned && req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == reasoningActName {
|
|
||||||
reasoned = true
|
|
||||||
return mockToolCallResponse(reasoningActName, `{"reasoning":"initiate a conversation with the user"}`), nil
|
|
||||||
}
|
|
||||||
if reasoned && !intended && req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == intentionActName {
|
|
||||||
intended = true
|
|
||||||
return mockToolCallResponse(intentionActName, `{"tool":"new_conversation","reasoning":"I should start a conversation with the user"}`), nil
|
|
||||||
}
|
|
||||||
if reasoned && intended && strings.Contains(strings.ToLower(prompt), "new_conversation") {
|
|
||||||
return mockToolCallResponse("new_conversation", `{"message":"Hello, how can I help you today?"}`), nil
|
|
||||||
}
|
|
||||||
xlog.Error("Unexpected LLM req", "req", req)
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", prompt)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMClient(llmClient),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
|
WithLLMAPIKey(apiKeyURL),
|
||||||
|
WithTimeout("10m"),
|
||||||
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
message = m
|
message = m
|
||||||
@@ -459,6 +276,8 @@ var _ = Describe("Agent test", func() {
|
|||||||
EnableHUD,
|
EnableHUD,
|
||||||
WithPeriodicRuns("1s"),
|
WithPeriodicRuns("1s"),
|
||||||
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
||||||
|
// EnableStandaloneJob,
|
||||||
|
// WithRandomIdentity(),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
@@ -468,7 +287,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
return message.Content
|
return message.Content
|
||||||
}, "10m", "1s").ShouldNot(BeEmpty())
|
}, "10m", "10s").ShouldNot(BeEmpty())
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -522,7 +341,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
// result := agent.Ask(
|
// result := agent.Ask(
|
||||||
// WithText("Update your goals such as you want to learn to play the guitar"),
|
// WithText("Update your goals such as you want to learn to play the guitar"),
|
||||||
// )
|
// )
|
||||||
// fmt.Fprintf(GinkgoWriter, "%+v\n", result)
|
// fmt.Printf("%+v\n", result)
|
||||||
// Expect(result.Error).ToNot(HaveOccurred())
|
// Expect(result.Error).ToNot(HaveOccurred())
|
||||||
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
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.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.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.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 {
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,12 +19,6 @@ 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
|
||||||
@@ -38,7 +30,7 @@ func (a *mcpAction) Plannable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (m *mcpAction) Run(ctx context.Context, 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())
|
||||||
@@ -87,15 +79,34 @@ type ToolInputSchema struct {
|
|||||||
Required []string `json:"required,omitempty"`
|
Required []string `json:"required,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) addTools(client *mcp.Client) (types.Actions, error) {
|
func (a *Agent) initMCPActions() error {
|
||||||
|
|
||||||
var generatedActions types.Actions
|
a.mcpActions = nil
|
||||||
xlog.Debug("Initializing client")
|
var err error
|
||||||
|
|
||||||
|
generatedActions := types.Actions{}
|
||||||
|
|
||||||
|
for _, mcpServer := range a.options.mcpServers {
|
||||||
|
transport := http.NewHTTPClientTransport("/mcp")
|
||||||
|
transport.WithBaseURL(mcpServer.URL)
|
||||||
|
if mcpServer.Token != "" {
|
||||||
|
transport.WithHeader("Authorization", "Bearer "+mcpServer.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new client
|
||||||
|
client := mcp.NewClient(transport)
|
||||||
|
|
||||||
|
xlog.Debug("Initializing client", "server", mcpServer.URL)
|
||||||
// Initialize the client
|
// Initialize the client
|
||||||
response, e := client.Initialize(a.context)
|
response, e := client.Initialize(a.context)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
xlog.Error("Failed to initialize client", "error", e.Error())
|
xlog.Error("Failed to initialize client", "error", e.Error(), "server", mcpServer)
|
||||||
return nil, e
|
if err == nil {
|
||||||
|
err = e
|
||||||
|
} else {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug("Client initialized: %v", response.Instructions)
|
xlog.Debug("Client initialized: %v", response.Instructions)
|
||||||
@@ -105,7 +116,7 @@ func (a *Agent) addTools(client *mcp.Client) (types.Actions, error) {
|
|||||||
tools, err := client.ListTools(a.context, cursor)
|
tools, err := client.ListTools(a.context, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Failed to list tools", "error", err.Error())
|
xlog.Error("Failed to list tools", "error", err.Error())
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, t := range tools.Tools {
|
for _, t := range tools.Tools {
|
||||||
@@ -114,14 +125,14 @@ func (a *Agent) addTools(client *mcp.Client) (types.Actions, error) {
|
|||||||
desc = *t.Description
|
desc = *t.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug("Tool", "name", t.Name, "description", desc)
|
xlog.Debug("Tool", "mcpServer", mcpServer, "name", t.Name, "description", desc)
|
||||||
|
|
||||||
dat, err := json.Marshal(t.InputSchema)
|
dat, err := json.Marshal(t.InputSchema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Failed to marshal input schema", "error", err.Error())
|
xlog.Error("Failed to marshal input schema", "error", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug("Input schema", "tool", t.Name, "schema", string(dat))
|
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)
|
// XXX: This is a wild guess, to verify (data types might be incompatible)
|
||||||
var inputSchema ToolInputSchema
|
var inputSchema ToolInputSchema
|
||||||
@@ -145,81 +156,9 @@ func (a *Agent) addTools(client *mcp.Client) (types.Actions, error) {
|
|||||||
cursor = tools.NextCursor
|
cursor = tools.NextCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
return generatedActions, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Agent) initMCPActions() error {
|
|
||||||
|
|
||||||
a.mcpActions = nil
|
|
||||||
var err error
|
|
||||||
|
|
||||||
generatedActions := types.Actions{}
|
|
||||||
|
|
||||||
// MCP HTTP Servers
|
|
||||||
for _, mcpServer := range a.options.mcpServers {
|
|
||||||
transport := http.NewHTTPClientTransport("/mcp")
|
|
||||||
transport.WithBaseURL(mcpServer.URL)
|
|
||||||
if mcpServer.Token != "" {
|
|
||||||
transport.WithHeader("Authorization", "Bearer "+mcpServer.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new client
|
|
||||||
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...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCP STDIO Servers
|
|
||||||
|
|
||||||
a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active
|
|
||||||
|
|
||||||
if a.options.mcpPrepareScript != "" {
|
|
||||||
xlog.Debug("Preparing MCP box", "script", a.options.mcpPrepareScript)
|
|
||||||
client := stdio.NewClient(a.options.mcpBoxURL)
|
|
||||||
client.RunProcess(a.context, "/bin/bash", []string{"-c", a.options.mcpPrepareScript}, []string{})
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := stdioTransport.NewStdioServerTransportWithIO(read, writer)
|
|
||||||
|
|
||||||
// Create a new client
|
|
||||||
mcpClient := mcp.NewClient(transport)
|
|
||||||
|
|
||||||
xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer)
|
|
||||||
actions, err := a.addTools(mcpClient)
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error())
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option func(*options) error
|
type Option func(*options) error
|
||||||
@@ -20,15 +19,12 @@ type llmOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type options struct {
|
type options struct {
|
||||||
llmClient llm.LLMClient
|
|
||||||
LLMAPI llmOptions
|
LLMAPI llmOptions
|
||||||
character Character
|
character Character
|
||||||
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
|
||||||
@@ -44,10 +40,6 @@ 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
|
||||||
@@ -59,23 +51,8 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLLMClient allows injecting a custom LLM client (e.g. for testing)
|
|
||||||
func WithLLMClient(client llm.LLMClient) Option {
|
|
||||||
return func(o *options) error {
|
|
||||||
o.llmClient = client
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *options) SeparatedMultimodalModel() bool {
|
func (o *options) SeparatedMultimodalModel() bool {
|
||||||
@@ -84,11 +61,7 @@ func (o *options) SeparatedMultimodalModel() bool {
|
|||||||
|
|
||||||
func defaultOptions() *options {
|
func defaultOptions() *options {
|
||||||
return &options{
|
return &options{
|
||||||
parallelJobs: 1,
|
|
||||||
periodicRuns: 15 * time.Minute,
|
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",
|
||||||
@@ -163,24 +136,6 @@ 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)
|
||||||
@@ -241,27 +196,6 @@ 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
|
||||||
@@ -402,36 +336,3 @@ 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/types"
|
"github.com/mudler/LocalAGI/core/action"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
// in the prompts
|
// in the prompts
|
||||||
type PromptHUD struct {
|
type PromptHUD struct {
|
||||||
Character Character `json:"character"`
|
Character Character `json:"character"`
|
||||||
CurrentState types.AgentInternalState `json:"current_state"`
|
CurrentState action.AgentInternalState `json:"current_state"`
|
||||||
PermanentGoal string `json:"permanent_goal"`
|
PermanentGoal string `json:"permanent_goal"`
|
||||||
ShowCharacter bool `json:"show_character"`
|
ShowCharacter bool `json:"show_character"`
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func Load(path string) (*Character, error) {
|
|||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) State() types.AgentInternalState {
|
func (a *Agent) State() action.AgentInternalState {
|
||||||
return *a.currentState
|
return *a.currentState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,30 @@
|
|||||||
package agent_test
|
package agent_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"net/http"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
|
||||||
"github.com/sashabaranov/go-openai"
|
|
||||||
|
|
||||||
. "github.com/mudler/LocalAGI/core/agent"
|
. "github.com/mudler/LocalAGI/core/agent"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Agent test", func() {
|
var _ = Describe("Agent test", func() {
|
||||||
Context("identity", func() {
|
Context("identity", func() {
|
||||||
var agent *Agent
|
var agent *Agent
|
||||||
|
|
||||||
// BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
// Eventually(func() error {
|
Eventually(func() error {
|
||||||
// // test apiURL is working and available
|
// test apiURL is working and available
|
||||||
// _, err := http.Get(apiURL + "/readyz")
|
_, err := http.Get(apiURL + "/readyz")
|
||||||
// return err
|
return err
|
||||||
// }, "10m", "10s").ShouldNot(HaveOccurred())
|
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||||
// })
|
})
|
||||||
|
|
||||||
It("generates all the fields with random data", func() {
|
It("generates all the fields with random data", func() {
|
||||||
var llmClient llm.LLMClient
|
|
||||||
if useRealLocalAI {
|
|
||||||
llmClient = llm.NewClient(apiKey, apiURL, testModel)
|
|
||||||
} else {
|
|
||||||
llmClient = &llm.MockClient{
|
|
||||||
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
return openai.ChatCompletionResponse{
|
|
||||||
Choices: []openai.ChatCompletionChoice{{
|
|
||||||
Message: openai.ChatCompletionMessage{
|
|
||||||
ToolCalls: []openai.ToolCall{{
|
|
||||||
ID: "tool_call_id_1",
|
|
||||||
Type: "function",
|
|
||||||
Function: openai.FunctionCall{
|
|
||||||
Name: "generate_identity",
|
|
||||||
Arguments: `{"name":"John Doe","age":"42","job_occupation":"Engineer","hobbies":["reading","hiking"],"favorites_music_genres":["Jazz"]}`,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var err error
|
var err error
|
||||||
agent, err = New(
|
agent, err = New(
|
||||||
WithLLMClient(llmClient),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithTimeout("10m"),
|
|
||||||
WithRandomIdentity(),
|
WithRandomIdentity(),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
@@ -65,40 +36,14 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
|
Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
|
||||||
})
|
})
|
||||||
It("detect an invalid character", func() {
|
It("detect an invalid character", func() {
|
||||||
mock := &llm.MockClient{
|
|
||||||
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
return openai.ChatCompletionResponse{}, fmt.Errorf("invalid character")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var err error
|
var err error
|
||||||
agent, err = New(
|
agent, err = New(WithRandomIdentity())
|
||||||
WithLLMClient(mock),
|
|
||||||
WithRandomIdentity(),
|
|
||||||
)
|
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
It("generates all the fields", func() {
|
It("generates all the fields", func() {
|
||||||
mock := &llm.MockClient{
|
|
||||||
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
return openai.ChatCompletionResponse{
|
|
||||||
Choices: []openai.ChatCompletionChoice{{
|
|
||||||
Message: openai.ChatCompletionMessage{
|
|
||||||
ToolCalls: []openai.ToolCall{{
|
|
||||||
ID: "tool_call_id_2",
|
|
||||||
Type: "function",
|
|
||||||
Function: openai.FunctionCall{
|
|
||||||
Name: "generate_identity",
|
|
||||||
Arguments: `{"name":"Gandalf","age":"90","job_occupation":"Wizard","hobbies":["magic","reading"],"favorites_music_genres":["Classical"]}`,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMClient(mock),
|
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),
|
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,6 @@ 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"
|
||||||
@@ -31,20 +29,11 @@ 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"`
|
||||||
|
|
||||||
@@ -54,7 +43,6 @@ type AgentConfig struct {
|
|||||||
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"`
|
||||||
@@ -73,14 +61,9 @@ 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
|
||||||
@@ -92,7 +75,6 @@ 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{
|
||||||
@@ -265,7 +247,7 @@ func NewAgentConfigMeta(
|
|||||||
Name: "enable_reasoning",
|
Name: "enable_reasoning",
|
||||||
Label: "Enable Reasoning",
|
Label: "Enable Reasoning",
|
||||||
Type: "checkbox",
|
Type: "checkbox",
|
||||||
DefaultValue: true,
|
DefaultValue: false,
|
||||||
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"},
|
||||||
},
|
},
|
||||||
@@ -278,66 +260,6 @@ 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{
|
||||||
{
|
{
|
||||||
@@ -356,7 +278,6 @@ func NewAgentConfigMeta(
|
|||||||
DynamicPrompts: dynamicPromptsConfig,
|
DynamicPrompts: dynamicPromptsConfig,
|
||||||
Connectors: connectorsConfig,
|
Connectors: connectorsConfig,
|
||||||
Actions: actionsConfig,
|
Actions: actionsConfig,
|
||||||
Filters: filtersConfig,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,148 +286,3 @@ 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,12 +33,10 @@ 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
|
||||||
}
|
}
|
||||||
@@ -74,12 +72,11 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentPool(
|
func NewAgentPool(
|
||||||
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory, mcpBoxURL string,
|
defaultModel, defaultMultimodalModel, imageModel, apiURL, apiKey, directory 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) {
|
||||||
@@ -101,7 +98,6 @@ 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,
|
||||||
@@ -112,7 +108,6 @@ 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
|
||||||
@@ -128,7 +123,6 @@ 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),
|
||||||
@@ -138,7 +132,6 @@ 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,
|
||||||
@@ -173,56 +166,7 @@ func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error {
|
|||||||
}
|
}
|
||||||
}(a.pool[name])
|
}(a.pool[name])
|
||||||
|
|
||||||
return a.startAgentWithConfig(name, agentConfig, nil)
|
return a.startAgentWithConfig(name, agentConfig)
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -247,7 +191,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
|
|||||||
ImagePrompt string `json:"image_prompt"`
|
ImagePrompt string `json:"image_prompt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := llm.GenerateTypedJSONWithGuidance(
|
err := llm.GenerateTypedJSON(
|
||||||
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,
|
||||||
@@ -324,13 +268,8 @@ func (a *AgentPool) GetStatusHistory(name string) *Status {
|
|||||||
return a.agentStatus[name]
|
return a.agentStatus[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs Observer) error {
|
func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error {
|
||||||
var manager sse.Manager
|
manager := sse.NewManager(5)
|
||||||
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
|
||||||
@@ -341,29 +280,18 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
|
|
||||||
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 != "" {
|
||||||
@@ -377,7 +305,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
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{}
|
||||||
@@ -390,11 +317,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
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,
|
||||||
@@ -402,7 +324,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
"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{}
|
||||||
@@ -410,10 +331,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
// 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),
|
||||||
@@ -421,11 +338,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
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,
|
||||||
@@ -462,7 +375,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
}),
|
}),
|
||||||
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 {
|
||||||
@@ -495,7 +407,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
c.AgentResultCallback()(state)
|
c.AgentResultCallback()(state)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
WithObserver(obs),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HUD {
|
if config.HUD {
|
||||||
@@ -546,10 +457,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
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))
|
||||||
}
|
}
|
||||||
@@ -558,17 +465,6 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
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...)
|
||||||
@@ -613,7 +509,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, nil); err != nil {
|
if err := a.startAgentWithConfig(name, &config); err != nil {
|
||||||
xlog.Error("Failed to start agent", "name", name, "error", err)
|
xlog.Error("Failed to start agent", "name", name, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,7 +547,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, nil)
|
return a.startAgentWithConfig(name, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("agent %s not found", name)
|
return fmt.Errorf("agent %s not found", name)
|
||||||
|
|||||||
@@ -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, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
|
Run(ctx context.Context, action ActionParams) (ActionResult, error)
|
||||||
Definition() ActionDefinition
|
Definition() ActionDefinition
|
||||||
Plannable() bool
|
Plannable() bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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,7 +19,6 @@ 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
|
||||||
@@ -28,8 +27,6 @@ type Job struct {
|
|||||||
|
|
||||||
context context.Context
|
context context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
Obs *Observable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionRequest struct {
|
type ActionRequest struct {
|
||||||
@@ -163,22 +160,22 @@ func newUUID() string {
|
|||||||
func NewJob(opts ...JobOption) *Job {
|
func NewJob(opts ...JobOption) *Job {
|
||||||
j := &Job{
|
j := &Job{
|
||||||
Result: NewJobResult(),
|
Result: NewJobResult(),
|
||||||
UUID: uuid.New().String(),
|
UUID: newUUID(),
|
||||||
Metadata: make(map[string]interface{}),
|
}
|
||||||
context: context.Background(),
|
for _, o := range opts {
|
||||||
ConversationHistory: []openai.ChatCompletionMessage{},
|
o(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
var ctx context.Context
|
||||||
opt(j)
|
if j.context == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
} else {
|
||||||
|
ctx = j.context
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the original request if it exists in the conversation history
|
context, cancel := context.WithCancel(ctx)
|
||||||
|
j.context = context
|
||||||
ctx, cancel := context.WithCancel(j.context)
|
|
||||||
j.context = ctx
|
|
||||||
j.cancel = cancel
|
j.cancel = cancel
|
||||||
|
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,29 +198,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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 AgentSharedState struct {
|
|
||||||
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
|
||||||
if lastMessageDuration == 0 {
|
|
||||||
lastMessageDuration = DefaultLastMessageDuration
|
|
||||||
}
|
|
||||||
return &AgentSharedState{
|
|
||||||
ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -11,16 +11,11 @@ services:
|
|||||||
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
||||||
- /dev/dri/card1
|
- /dev/dri/card1
|
||||||
- /dev/dri/renderD129
|
- /dev/dri/renderD129
|
||||||
|
command:
|
||||||
mcpbox:
|
- ${MODEL_NAME:-arcee-agent}
|
||||||
extends:
|
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||||
file: docker-compose.yaml
|
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||||
service: mcpbox
|
- granite-embedding-107m-multilingual
|
||||||
|
|
||||||
dind:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.yaml
|
|
||||||
service: dind
|
|
||||||
|
|
||||||
localrecall:
|
localrecall:
|
||||||
extends:
|
extends:
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||||
- DEBUG=true
|
- DEBUG=true
|
||||||
image: localai/localai:master-cublas-cuda12-ffmpeg-core
|
image: localai/localai:master-sycl-f32-ffmpeg-core
|
||||||
# For images with python backends, use:
|
|
||||||
# image: localai/localai:master-cublas-cuda12-ffmpeg
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
@@ -17,16 +15,6 @@ services:
|
|||||||
count: 1
|
count: 1
|
||||||
capabilities: [gpu]
|
capabilities: [gpu]
|
||||||
|
|
||||||
mcpbox:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.yaml
|
|
||||||
service: mcpbox
|
|
||||||
|
|
||||||
dind:
|
|
||||||
extends:
|
|
||||||
file: docker-compose.yaml
|
|
||||||
service: dind
|
|
||||||
|
|
||||||
localrecall:
|
localrecall:
|
||||||
extends:
|
extends:
|
||||||
file: docker-compose.yaml
|
file: docker-compose.yaml
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ services:
|
|||||||
# 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-ffmpeg-core
|
||||||
command:
|
command:
|
||||||
- ${MODEL_NAME:-gemma-3-12b-it-qat}
|
- ${MODEL_NAME:-arcee-agent}
|
||||||
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
- ${IMAGE_MODEL:-flux.1-dev}
|
||||||
- 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"]
|
||||||
@@ -46,44 +46,12 @@ 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!'"]
|
||||||
|
|
||||||
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
|
||||||
@@ -91,7 +59,7 @@ services:
|
|||||||
- 8080:3000
|
- 8080:3000
|
||||||
#image: quay.io/mudler/localagi:master
|
#image: quay.io/mudler/localagi:master
|
||||||
environment:
|
environment:
|
||||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-12b-it-qat}
|
- LOCALAGI_MODEL=${MODEL_NAME:-arcee-agent}
|
||||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||||
@@ -100,7 +68,6 @@ services:
|
|||||||
- 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
|
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
75
go.mod
75
go.mod
@@ -10,77 +10,70 @@ require (
|
|||||||
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.12.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.15.0
|
github.com/go-telegram/bot v1.14.2
|
||||||
github.com/gofiber/fiber/v2 v2.52.6
|
github.com/gofiber/fiber/v2 v2.52.6
|
||||||
github.com/gofiber/template/html/v2 v2.1.3
|
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/gorilla/websocket v1.5.3
|
github.com/metoro-io/mcp-golang v0.9.0
|
||||||
github.com/metoro-io/mcp-golang v0.11.0
|
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/onsi/gomega v1.37.0
|
github.com/onsi/gomega v1.37.0
|
||||||
github.com/philippgille/chromem-go v0.7.0
|
github.com/philippgille/chromem-go v0.7.0
|
||||||
github.com/sashabaranov/go-openai v1.39.1
|
github.com/sashabaranov/go-openai v1.38.1
|
||||||
github.com/slack-go/slack v0.16.0
|
github.com/slack-go/slack v0.16.0
|
||||||
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.13
|
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.61.0
|
github.com/valyala/fasthttp v1.60.0
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.37.0
|
||||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||||
maunium.net/go/mautrix v0.17.0
|
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/antchfx/htmlquery v1.3.4 // indirect
|
github.com/antchfx/htmlquery v1.3.0 // indirect
|
||||||
github.com/antchfx/xmlquery v1.4.4 // indirect
|
github.com/antchfx/xmlquery v1.3.17 // indirect
|
||||||
github.com/antchfx/xpath v1.3.4 // indirect
|
github.com/antchfx/xpath v1.2.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/bytedance/sonic v1.13.2 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/gin-gonic/gin v1.8.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
|
||||||
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-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.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.10.5 // indirect
|
github.com/goccy/go-json v0.9.7 // 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-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
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-20250423184734-337e5dd93bb4 // indirect
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/invopop/jsonschema v0.12.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/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.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mattn/go-colorable v0.1.13 // 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.16 // 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.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rs/zerolog v1.31.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
|
||||||
@@ -88,21 +81,17 @@ 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/ugorji/go/codec v1.2.7 // 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/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
go.mau.fi/util v0.3.0 // indirect
|
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
||||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
|
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
|
||||||
golang.org/x/net v0.39.0 // indirect
|
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
golang.org/x/tools v0.31.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.5 // 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
|
||||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
284
go.sum
284
go.sum
@@ -1,73 +1,72 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
|
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
|
||||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
||||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk=
|
||||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
|
||||||
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
|
||||||
|
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
github.com/bahlo/generic-list-go v0.2.0 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.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
github.com/chasefleming/elem-go v0.30.0 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
||||||
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
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.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
|
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
|
||||||
github.com/donseba/go-htmx v1.12.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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
|
github.com/go-telegram/bot v1.14.2 h1:j9hXerxTuvkw7yFi3sF5jjRVGozNVKkMQSKjMeBJ5FY=
|
||||||
github.com/go-telegram/bot v1.15.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
github.com/go-telegram/bot v1.14.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
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.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
github.com/gocolly/colly v1.2.0 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.6/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=
|
||||||
@@ -76,17 +75,31 @@ github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6
|
|||||||
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
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/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/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.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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=
|
||||||
@@ -94,45 +107,45 @@ github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMM
|
|||||||
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-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
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/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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/metoro-io/mcp-golang v0.11.0 h1:1k+VSE9QaeMTLn0gJ3FgE/DcjsCBsLFnz5eSFbgXUiI=
|
github.com/metoro-io/mcp-golang v0.9.0 h1:GpFENjieZ/KosTu7CE7tyGI/a2FhiG0nandR0d8B3rE=
|
||||||
github.com/metoro-io/mcp-golang v0.11.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
|
github.com/metoro-io/mcp-golang v0.9.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=
|
||||||
@@ -144,30 +157,30 @@ github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus
|
|||||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||||
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
github.com/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.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
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.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|
||||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
|
||||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d 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.39.1 h1:TMD4w77Iy9WTFlgnjNaxbAASdsCJ9R/rMdzL+SN14oU=
|
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
|
||||||
github.com/sashabaranov/go-openai v1.39.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||||
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
||||||
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
@@ -177,10 +190,11 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||||||
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.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=
|
||||||
@@ -201,87 +215,77 @@ github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1Ca
|
|||||||
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
|
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
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.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
|
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
|
||||||
github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog=
|
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
|
||||||
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
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.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
|
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||||
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
|
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
||||||
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 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/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/mod v0.12.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.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
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-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.32.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
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=
|
||||||
@@ -289,46 +293,62 @@ 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.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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
|
||||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
|
||||||
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.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
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/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
|
||||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
|
||||||
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
|
|
||||||
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
|
|
||||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
mvdan.cc/xurls/v2 v2.6.0 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 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||||
|
|||||||
9
main.go
9
main.go
@@ -22,8 +22,6 @@ 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")
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if baseModel == "" {
|
if baseModel == "" {
|
||||||
@@ -62,15 +60,10 @@ func main() {
|
|||||||
apiURL,
|
apiURL,
|
||||||
apiKey,
|
apiKey,
|
||||||
stateDir,
|
stateDir,
|
||||||
mcpboxURL,
|
|
||||||
localRAG,
|
localRAG,
|
||||||
services.Actions(map[string]string{
|
services.Actions,
|
||||||
"browser-agent-runner-base-url": localOperatorBaseURL,
|
|
||||||
"deep-research-runner-base-url": localOperatorBaseURL,
|
|
||||||
}),
|
|
||||||
services.Connectors,
|
services.Connectors,
|
||||||
services.DynamicPrompts,
|
services.DynamicPrompts,
|
||||||
services.Filters,
|
|
||||||
timeout,
|
timeout,
|
||||||
withLogs,
|
withLogs,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,33 +1,13 @@
|
|||||||
package llm
|
package llm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LLMClient interface {
|
func NewClient(APIKey, URL, timeout string) *openai.Client {
|
||||||
CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
|
|
||||||
CreateImage(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type realClient struct {
|
|
||||||
*openai.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *realClient) CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
return r.Client.CreateChatCompletion(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *realClient) CreateImage(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error) {
|
|
||||||
return r.Client.CreateImage(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient returns a real OpenAI client as LLMClient
|
|
||||||
func NewClient(APIKey, URL, timeout string) LLMClient {
|
|
||||||
// Set up OpenAI client
|
// Set up OpenAI client
|
||||||
if APIKey == "" {
|
if APIKey == "" {
|
||||||
//log.Fatal("OPENAI_API_KEY environment variable not set")
|
//log.Fatal("OPENAI_API_KEY environment variable not set")
|
||||||
@@ -38,12 +18,11 @@ func NewClient(APIKey, URL, timeout string) LLMClient {
|
|||||||
|
|
||||||
dur, err := time.ParseDuration(timeout)
|
dur, err := time.ParseDuration(timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Failed to parse timeout", "error", err)
|
|
||||||
dur = 150 * time.Second
|
dur = 150 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
config.HTTPClient = &http.Client{
|
config.HTTPClient = &http.Client{
|
||||||
Timeout: dur,
|
Timeout: dur,
|
||||||
}
|
}
|
||||||
return &realClient{openai.NewClientWithConfig(config)}
|
return openai.NewClientWithConfig(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,16 @@ import (
|
|||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateTypedJSONWithGuidance(ctx context.Context, client LLMClient, guidance, model string, i jsonschema.Definition, dst any) error {
|
func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
|
||||||
return GenerateTypedJSONWithConversation(ctx, client, []openai.ChatCompletionMessage{
|
toolName := "json"
|
||||||
|
decision := openai.ChatCompletionRequest{
|
||||||
|
Model: model,
|
||||||
|
Messages: []openai.ChatCompletionMessage{
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: guidance,
|
Content: guidance,
|
||||||
},
|
},
|
||||||
}, model, i, dst)
|
},
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateTypedJSONWithConversation(ctx context.Context, client LLMClient, conv []openai.ChatCompletionMessage, model string, i jsonschema.Definition, dst any) error {
|
|
||||||
toolName := "json"
|
|
||||||
decision := openai.ChatCompletionRequest{
|
|
||||||
Model: model,
|
|
||||||
Messages: conv,
|
|
||||||
Tools: []openai.Tool{
|
Tools: []openai.Tool{
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package llm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/sashabaranov/go-openai"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MockClient struct {
|
|
||||||
CreateChatCompletionFunc func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
|
|
||||||
CreateImageFunc func(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockClient) CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
|
||||||
if m.CreateChatCompletionFunc != nil {
|
|
||||||
return m.CreateChatCompletionFunc(ctx, req)
|
|
||||||
}
|
|
||||||
return openai.ChatCompletionResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockClient) CreateImage(ctx context.Context, req openai.ImageRequest) (openai.ImageResponse, error) {
|
|
||||||
if m.CreateImageFunc != nil {
|
|
||||||
return m.CreateImageFunc(ctx, req)
|
|
||||||
}
|
|
||||||
return openai.ImageResponse{}, nil
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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")
|
|
||||||
})
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
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,11 +18,8 @@ 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"
|
||||||
@@ -35,8 +32,6 @@ const (
|
|||||||
ActionGithubPRCreator = "github-pr-creator"
|
ActionGithubPRCreator = "github-pr-creator"
|
||||||
ActionGithubGetAllContent = "github-get-all-repository-content"
|
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"
|
||||||
@@ -46,7 +41,6 @@ const (
|
|||||||
ActionCounter = "counter"
|
ActionCounter = "counter"
|
||||||
ActionCallAgents = "call_agents"
|
ActionCallAgents = "call_agents"
|
||||||
ActionShellcommand = "shell-command"
|
ActionShellcommand = "shell-command"
|
||||||
ActionSendTelegramMessage = "send-telegram-message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableActions = []string{
|
var AvailableActions = []string{
|
||||||
@@ -54,15 +48,10 @@ var AvailableActions = []string{
|
|||||||
ActionCustom,
|
ActionCustom,
|
||||||
ActionGithubIssueLabeler,
|
ActionGithubIssueLabeler,
|
||||||
ActionGithubIssueOpener,
|
ActionGithubIssueOpener,
|
||||||
ActionGithubIssueEditor,
|
|
||||||
ActionGithubIssueCloser,
|
ActionGithubIssueCloser,
|
||||||
ActionGithubIssueSearcher,
|
ActionGithubIssueSearcher,
|
||||||
ActionGithubRepositoryGet,
|
ActionGithubRepositoryGet,
|
||||||
ActionGithubGetAllContent,
|
ActionGithubGetAllContent,
|
||||||
ActionGithubRepositorySearchFiles,
|
|
||||||
ActionGithubRepositoryListFiles,
|
|
||||||
ActionBrowserAgentRunner,
|
|
||||||
ActionDeepResearchRunner,
|
|
||||||
ActionGithubRepositoryCreateOrUpdate,
|
ActionGithubRepositoryCreateOrUpdate,
|
||||||
ActionGithubIssueReader,
|
ActionGithubIssueReader,
|
||||||
ActionGithubIssueCommenter,
|
ActionGithubIssueCommenter,
|
||||||
@@ -80,11 +69,9 @@ var AvailableActions = []string{
|
|||||||
ActionCounter,
|
ActionCounter,
|
||||||
ActionCallAgents,
|
ActionCallAgents,
|
||||||
ActionShellcommand,
|
ActionShellcommand,
|
||||||
ActionSendTelegramMessage,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
func Actions(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 {
|
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||||
allActions := []types.Action{}
|
allActions := []types.Action{}
|
||||||
|
|
||||||
@@ -97,7 +84,7 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
|
a, err := Action(a.Name, agentName, config, pool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -106,18 +93,12 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
|
|||||||
|
|
||||||
return allActions
|
return allActions
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Action(name, agentName string, config map[string]string, pool *state.AgentPool, actionsConfigs map[string]string) (types.Action, error) {
|
func Action(name, agentName string, config map[string]string, pool *state.AgentPool) (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, "")
|
||||||
@@ -129,16 +110,10 @@ 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["browser-agent-runner-base-url"])
|
|
||||||
case ActionDeepResearchRunner:
|
|
||||||
a = actions.NewDeepResearchRunner(config, actionsConfigs["deep-research-runner-base-url"])
|
|
||||||
case ActionGithubIssueReader:
|
case ActionGithubIssueReader:
|
||||||
a = actions.NewGithubIssueReader(config)
|
a = actions.NewGithubIssueReader(config)
|
||||||
case ActionGithubPRReader:
|
case ActionGithubPRReader:
|
||||||
@@ -151,10 +126,6 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewGithubPRCreator(config)
|
a = actions.NewGithubPRCreator(config)
|
||||||
case ActionGithubGetAllContent:
|
case ActionGithubGetAllContent:
|
||||||
a = actions.NewGithubRepositoryGetAllContent(config)
|
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:
|
||||||
@@ -179,8 +150,6 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
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)
|
||||||
case ActionSendTelegramMessage:
|
|
||||||
a = actions.NewSendTelegramMessageRunner(config)
|
|
||||||
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")
|
||||||
@@ -200,16 +169,6 @@ 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",
|
||||||
@@ -225,11 +184,6 @@ 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",
|
||||||
@@ -260,16 +214,6 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "GitHub Get All Repository Content",
|
Label: "GitHub Get All Repository Content",
|
||||||
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
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",
|
||||||
@@ -343,12 +287,7 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
{
|
{
|
||||||
Name: "call_agents",
|
Name: "call_agents",
|
||||||
Label: "Call Agents",
|
Label: "Call Agents",
|
||||||
Fields: actions.CallAgentConfigMeta(),
|
Fields: []config.Field{},
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "send-telegram-message",
|
|
||||||
Label: "Send Telegram Message",
|
|
||||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
|
|||||||
|
|
||||||
type BrowseAction struct{}
|
type BrowseAction struct{}
|
||||||
|
|
||||||
func (a *BrowseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *BrowseAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
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,56 +3,26 @@ 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *CallAgentAction) Run(ctx context.Context, 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"`
|
||||||
@@ -113,32 +83,13 @@ func (a *CallAgentAction) Run(ctx context.Context, sharedState *types.AgentShare
|
|||||||
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 a.isAllowedToBeCalled(ag) {
|
if ag != a.myName {
|
||||||
agents = append(agents, ag)
|
agents = append(agents, ag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,21 +125,3 @@ 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *CounterAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||||
// Parse parameters
|
// Parse parameters
|
||||||
request := struct {
|
request := struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *GenImageAction) Run(ctx context.Context, 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, nil, params)
|
url, err := action.Run(ctx, 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, nil, params)
|
_, err := action.Run(ctx, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesCloser) Run(ctx context.Context, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesCommenter) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesLabeler) Run(ctx context.Context, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesOpener) Run(ctx context.Context, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesReader) Run(ctx context.Context, 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,8 +49,7 @@ func (g *GithubIssuesReader) Run(ctx context.Context, sharedState *types.AgentSh
|
|||||||
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.GetNumber(), issue.GetRepository().GetFullName(), issue.GetTitle(), issue.GetBody()),
|
*issue.Number, *issue.Repository.FullName, *issue.Title, *issue.Body)}, nil
|
||||||
}, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssueSearch) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
@@ -15,6 +17,96 @@ type GithubPRCommenter struct {
|
|||||||
client *github.Client
|
client *github.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
patchRegex = regexp.MustCompile(`^@@.*\d [\+\-](\d+),?(\d+)?.+?@@`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type commitFileInfo struct {
|
||||||
|
FileName string
|
||||||
|
hunkInfos []*hunkInfo
|
||||||
|
sha string
|
||||||
|
}
|
||||||
|
|
||||||
|
type hunkInfo struct {
|
||||||
|
hunkStart int
|
||||||
|
hunkEnd int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hi hunkInfo) isLineInHunk(line int) bool {
|
||||||
|
return line >= hi.hunkStart && line <= hi.hunkEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *commitFileInfo) getHunkInfo(line int) *hunkInfo {
|
||||||
|
for _, hunkInfo := range cfi.hunkInfos {
|
||||||
|
if hunkInfo.isLineInHunk(line) {
|
||||||
|
return hunkInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi *commitFileInfo) isLineInChange(line int) bool {
|
||||||
|
return cfi.getHunkInfo(line) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfi commitFileInfo) calculatePosition(line int) *int {
|
||||||
|
hi := cfi.getHunkInfo(line)
|
||||||
|
if hi == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
position := line - hi.hunkStart
|
||||||
|
return &position
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHunkPositions(patch, filename string) ([]*hunkInfo, error) {
|
||||||
|
hunkInfos := make([]*hunkInfo, 0)
|
||||||
|
if patch != "" {
|
||||||
|
groups := patchRegex.FindAllStringSubmatch(patch, -1)
|
||||||
|
if len(groups) < 1 {
|
||||||
|
return hunkInfos, fmt.Errorf("the patch details for [%s] could not be resolved", filename)
|
||||||
|
}
|
||||||
|
for _, patchGroup := range groups {
|
||||||
|
endPos := 2
|
||||||
|
if len(patchGroup) > 2 && patchGroup[2] == "" {
|
||||||
|
endPos = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hunkStart, err := strconv.Atoi(patchGroup[1])
|
||||||
|
if err != nil {
|
||||||
|
hunkStart = -1
|
||||||
|
}
|
||||||
|
hunkEnd, err := strconv.Atoi(patchGroup[endPos])
|
||||||
|
if err != nil {
|
||||||
|
hunkEnd = -1
|
||||||
|
}
|
||||||
|
hunkInfos = append(hunkInfos, &hunkInfo{
|
||||||
|
hunkStart: hunkStart,
|
||||||
|
hunkEnd: hunkEnd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hunkInfos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommitInfo(file *github.CommitFile) (*commitFileInfo, error) {
|
||||||
|
patch := file.GetPatch()
|
||||||
|
hunkInfos, err := parseHunkPositions(patch, *file.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := file.GetSHA()
|
||||||
|
if sha == "" {
|
||||||
|
return nil, fmt.Errorf("the sha details for [%s] could not be resolved", *file.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &commitFileInfo{
|
||||||
|
FileName: *file.Filename,
|
||||||
|
hunkInfos: hunkInfos,
|
||||||
|
sha: sha,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
||||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
@@ -27,7 +119,7 @@ func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubPRCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubPRCommenter) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
@@ -11,103 +10,46 @@ import (
|
|||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"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 {
|
type GithubPRCreator struct {
|
||||||
token, repository, owner, customActionName, defaultBranch string
|
token, repository, owner, customActionName, defaultBranch string
|
||||||
useFork bool
|
|
||||||
client *github.Client
|
client *github.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
|
func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
|
||||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
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{
|
return &GithubPRCreator{
|
||||||
client: client,
|
client: client,
|
||||||
token: config["token"],
|
token: config["token"],
|
||||||
repository: config["repository"],
|
repository: config["repository"],
|
||||||
owner: config["owner"],
|
owner: config["owner"],
|
||||||
customActionName: config["customActionName"],
|
customActionName: config["customActionName"],
|
||||||
defaultBranch: defaultBranch,
|
defaultBranch: config["defaultBranch"],
|
||||||
useFork: useFork,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureFork ensures that a fork exists for the given repository
|
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string) error {
|
||||||
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
|
// Get the latest commit SHA from the default branch
|
||||||
ref, _, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+g.defaultBranch)
|
ref, _, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+g.defaultBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get reference for default branch %s: %w", g.defaultBranch, err)
|
return fmt.Errorf("failed to get reference: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get the branch if it exists
|
// Try to get the branch if it exists
|
||||||
_, resp, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+branchName)
|
_, resp, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+branchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp == nil {
|
// If branch doesn't exist, create it
|
||||||
return fmt.Errorf("failed to check branch existence: %w", err)
|
if resp != nil && resp.StatusCode == 404 {
|
||||||
}
|
|
||||||
|
|
||||||
// If branch doesn't exist (404), create it
|
|
||||||
if resp.StatusCode == 404 {
|
|
||||||
newRef := &github.Reference{
|
newRef := &github.Reference{
|
||||||
Ref: github.String("refs/heads/" + branchName),
|
Ref: github.String("refs/heads/" + branchName),
|
||||||
Object: &github.GitObject{SHA: ref.Object.SHA},
|
Object: &github.GitObject{SHA: ref.Object.SHA},
|
||||||
}
|
}
|
||||||
_, _, err = g.client.Git.CreateRef(ctx, owner, repository, newRef)
|
_, _, err = g.client.Git.CreateRef(ctx, g.owner, g.repository, newRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create branch: %w", err)
|
return fmt.Errorf("failed to create branch: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other errors, return the error
|
|
||||||
return fmt.Errorf("failed to check branch existence: %w", err)
|
return fmt.Errorf("failed to check branch existence: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +58,7 @@ func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName s
|
|||||||
Ref: github.String("refs/heads/" + branchName),
|
Ref: github.String("refs/heads/" + branchName),
|
||||||
Object: &github.GitObject{SHA: ref.Object.SHA},
|
Object: &github.GitObject{SHA: ref.Object.SHA},
|
||||||
}
|
}
|
||||||
_, _, err = g.client.Git.UpdateRef(ctx, owner, repository, updateRef, true)
|
_, _, err = g.client.Git.UpdateRef(ctx, g.owner, g.repository, updateRef, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update branch: %w", err)
|
return fmt.Errorf("failed to update branch: %w", err)
|
||||||
}
|
}
|
||||||
@@ -124,10 +66,10 @@ func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string, filePath string, content string, message string, owner string, repository string) error {
|
func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string, filePath string, content string, message string) error {
|
||||||
// Get the current file content if it exists
|
// Get the current file content if it exists
|
||||||
var sha *string
|
var sha *string
|
||||||
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, filePath, &github.RepositoryContentGetOptions{
|
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, filePath, &github.RepositoryContentGetOptions{
|
||||||
Ref: branch,
|
Ref: branch,
|
||||||
})
|
})
|
||||||
if err == nil && fileContent != nil {
|
if err == nil && fileContent != nil {
|
||||||
@@ -135,7 +77,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create or update the file
|
// Create or update the file
|
||||||
_, _, err = g.client.Repositories.CreateFile(ctx, owner, repository, filePath, &github.RepositoryContentFileOptions{
|
_, _, err = g.client.Repositories.CreateFile(ctx, g.owner, g.repository, filePath, &github.RepositoryContentFileOptions{
|
||||||
Message: &message,
|
Message: &message,
|
||||||
Content: []byte(content),
|
Content: []byte(content),
|
||||||
Branch: &branch,
|
Branch: &branch,
|
||||||
@@ -148,7 +90,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubPRCreator) Run(ctx context.Context, 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"`
|
||||||
@@ -175,29 +117,15 @@ func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentShare
|
|||||||
result.BaseBranch = g.defaultBranch
|
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
|
// Create or update branch
|
||||||
err = g.createOrUpdateBranch(ctx, result.Branch, targetOwner, targetRepo)
|
err = g.createOrUpdateBranch(ctx, result.Branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
|
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update files
|
// Create or update files
|
||||||
for _, file := range result.Files {
|
for _, file := range result.Files {
|
||||||
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path), targetOwner, targetRepo)
|
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
|
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
|
||||||
}
|
}
|
||||||
@@ -206,7 +134,7 @@ func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentShare
|
|||||||
// Check if PR already exists for this branch
|
// Check if PR already exists for this branch
|
||||||
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
|
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
|
||||||
State: "open",
|
State: "open",
|
||||||
Head: fmt.Sprintf("%s:%s", targetOwner, result.Branch),
|
Head: fmt.Sprintf("%s:%s", result.Owner, result.Branch),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
|
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
|
||||||
@@ -236,12 +164,6 @@ func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentShare
|
|||||||
Base: &result.BaseBranch,
|
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)
|
createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
|
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
|
||||||
@@ -389,12 +311,5 @@ func GithubPRCreatorConfigMeta() []config.Field {
|
|||||||
Required: false,
|
Required: false,
|
||||||
HelpText: "Default branch to create PRs against (defaults to main)",
|
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.",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ var _ = Describe("GithubPRCreator", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := action.Run(ctx, nil, params)
|
result, err := action.Run(ctx, params)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(result.Result).To(ContainSubstring("pull request #"))
|
Expect(result.Result).To(ContainSubstring("pull request #"))
|
||||||
})
|
})
|
||||||
@@ -65,7 +65,7 @@ var _ = Describe("GithubPRCreator", func() {
|
|||||||
"body": "This is a test pull request",
|
"body": "This is a test pull request",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := action.Run(ctx, nil, params)
|
_, err := action.Run(ctx, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func NewGithubPRReader(config map[string]string) *GithubPRReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubPRReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubPRReader) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewGithubPRReviewer(config map[string]string) *GithubPRReviewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubPRReviewer) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubPRReviewer) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ var _ = Describe("GithubPRReviewer", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := reviewer.Run(ctx, nil, params)
|
result, err := reviewer.Run(ctx, params)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(result.Result).To(ContainSubstring("reviewed successfully"))
|
Expect(result.Result).To(ContainSubstring("reviewed successfully"))
|
||||||
})
|
})
|
||||||
@@ -70,7 +70,7 @@ var _ = Describe("GithubPRReviewer", func() {
|
|||||||
"review_action": "COMMENT",
|
"review_action": "COMMENT",
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := reviewer.Run(ctx, nil, params)
|
result, err := reviewer.Run(ctx, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
Expect(result.Result).To(ContainSubstring("not found"))
|
Expect(result.Result).To(ContainSubstring("not found"))
|
||||||
})
|
})
|
||||||
@@ -85,7 +85,7 @@ var _ = Describe("GithubPRReviewer", func() {
|
|||||||
"review_action": "INVALID_ACTION",
|
"review_action": "INVALID_ACTION",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := reviewer.Run(ctx, nil, params)
|
_, err := reviewer.Run(ctx, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewGithubRepositoryCreateOrUpdateContent(config map[string]string) *GithubR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
"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/xlog"
|
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,35 +28,11 @@ func NewGithubRepositoryGetAllContent(config map[string]string) *GithubRepositor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTextFile checks if a file is likely to be a text file based on its extension
|
func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string) (string, error) {
|
||||||
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
|
var result strings.Builder
|
||||||
|
|
||||||
// Get content at the current path
|
// Get content at the current path
|
||||||
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
|
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, path, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
|
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
|
||||||
}
|
}
|
||||||
@@ -67,21 +41,14 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
|
|||||||
for _, item := range directoryContent {
|
for _, item := range directoryContent {
|
||||||
if item.GetType() == "dir" {
|
if item.GetType() == "dir" {
|
||||||
// Recursively get content for subdirectories
|
// Recursively get content for subdirectories
|
||||||
subContent, err := g.getContentRecursively(ctx, item.GetPath(), owner, repository)
|
subContent, err := g.getContentRecursively(ctx, item.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
result.WriteString(subContent)
|
result.WriteString(subContent)
|
||||||
} else if item.GetType() == "file" {
|
} 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
|
// Get file content
|
||||||
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
|
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, item.GetPath(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
|
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +68,7 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
|
|||||||
return result.String(), nil
|
return result.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, 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"`
|
||||||
@@ -122,7 +89,7 @@ func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, sharedState *ty
|
|||||||
result.Path = "."
|
result.Path = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := g.getContentRecursively(ctx, result.Path, result.Owner, result.Repository)
|
content, err := g.getContentRecursively(ctx, result.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, err
|
return types.ActionResult{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
|
|||||||
"path": ".",
|
"path": ".",
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := action.Run(ctx, nil, params)
|
result, err := action.Run(ctx, params)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(result.Result).NotTo(BeEmpty())
|
Expect(result.Result).NotTo(BeEmpty())
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
|
|||||||
"path": "non-existent-path",
|
"path": "non-existent-path",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := action.Run(ctx, nil, params)
|
_, err := action.Run(ctx, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGithubRepositoryGetContent(config map[string]string) *GithubRepositoryGe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubRepositoryGetContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryGetContent) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryREADME) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *ScraperAction) Run(ctx context.Context, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *SearchAction) Run(ctx context.Context, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *SendMailAction) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
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')",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ type ShellAction struct {
|
|||||||
customDescription string
|
customDescription string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ShellAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *ShellAction) Run(ctx context.Context, 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"`
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type PostTweetAction struct {
|
|||||||
noCharacterLimit bool
|
noCharacterLimit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PostTweetAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *PostTweetAction) Run(ctx context.Context, 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, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
func (a *WikipediaAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const (
|
|||||||
ConnectorGithubIssues = "github-issues"
|
ConnectorGithubIssues = "github-issues"
|
||||||
ConnectorGithubPRs = "github-prs"
|
ConnectorGithubPRs = "github-prs"
|
||||||
ConnectorTwitter = "twitter"
|
ConnectorTwitter = "twitter"
|
||||||
ConnectorMatrix = "matrix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableConnectors = []string{
|
var AvailableConnectors = []string{
|
||||||
@@ -30,7 +29,6 @@ var AvailableConnectors = []string{
|
|||||||
ConnectorGithubIssues,
|
ConnectorGithubIssues,
|
||||||
ConnectorGithubPRs,
|
ConnectorGithubPRs,
|
||||||
ConnectorTwitter,
|
ConnectorTwitter,
|
||||||
ConnectorMatrix,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Connectors(a *state.AgentConfig) []state.Connector {
|
func Connectors(a *state.AgentConfig) []state.Connector {
|
||||||
@@ -68,8 +66,6 @@ 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conns
|
return conns
|
||||||
@@ -112,10 +108,5 @@ func ConnectorsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Twitter",
|
Label: "Twitter",
|
||||||
Fields: connectors.TwitterConfigMeta(),
|
Fields: connectors.TwitterConfigMeta(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "matrix",
|
|
||||||
Label: "Matrix",
|
|
||||||
Fields: connectors.MatrixConfigMeta(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package conversations
|
package connectors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package conversations_test
|
package connectors_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/conversations"
|
"github.com/mudler/LocalAGI/services/connectors"
|
||||||
. "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 *conversations.ConversationTracker[string]
|
tracker *connectors.ConversationTracker[string]
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
)
|
)
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
duration = 1 * time.Second
|
duration = 1 * time.Second
|
||||||
tracker = conversations.NewConversationTracker[string](duration)
|
tracker = connectors.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 := conversations.NewConversationTracker[int](duration)
|
trackerInt := connectors.NewConversationTracker[int](duration)
|
||||||
trackerInt64 := conversations.NewConversationTracker[int64](duration)
|
trackerInt64 := connectors.NewConversationTracker[int64](duration)
|
||||||
|
|
||||||
message := openai.ChatCompletionMessage{
|
message := openai.ChatCompletionMessage{
|
||||||
Role: openai.ChatMessageRoleUser,
|
Role: openai.ChatMessageRoleUser,
|
||||||
@@ -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"
|
||||||
@@ -16,6 +16,7 @@ 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
|
||||||
@@ -24,14 +25,14 @@ 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 {
|
||||||
|
|
||||||
token := config["token"]
|
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
||||||
|
if err != nil {
|
||||||
if !strings.HasPrefix(token, "Bot ") {
|
duration = 5 * time.Minute
|
||||||
token = "Bot " + token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Discord{
|
return &Discord{
|
||||||
token: token,
|
conversationTracker: NewConversationTracker[string](duration),
|
||||||
|
token: config["token"],
|
||||||
defaultChannel: config["defaultChannel"],
|
defaultChannel: config["defaultChannel"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,12 +151,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) {
|
||||||
|
|
||||||
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
|
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: m.Content,
|
Content: m.Content,
|
||||||
})
|
})
|
||||||
|
|
||||||
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("discord:%s", m.ChannelID))
|
conv := d.conversationTracker.GetConversation(m.ChannelID)
|
||||||
|
|
||||||
jobResult := a.Ask(
|
jobResult := a.Ask(
|
||||||
types.WithConversationHistory(conv),
|
types.WithConversationHistory(conv),
|
||||||
@@ -166,7 +167,7 @@ func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
|
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: jobResult.Response,
|
Content: jobResult.Response,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,16 +21,22 @@ type IRC struct {
|
|||||||
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +77,8 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
}
|
}
|
||||||
i.conn.UseTLS = false
|
i.conn.UseTLS = false
|
||||||
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, "arguments", e.Arguments)
|
xlog.Info("Connected to IRC server", "server", i.server)
|
||||||
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
cleanedMessage := cleanUpMessage(message, i.nickname)
|
cleanedMessage := cleanUpMessage(message, i.nickname)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("irc:%s", channel))
|
conv := i.conversationTracker.GetConversation(channel)
|
||||||
|
|
||||||
conv = append(conv,
|
conv = append(conv,
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
@@ -119,7 +124,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Update the conversation history
|
// Update the conversation history
|
||||||
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
|
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
|
||||||
Content: cleanedMessage,
|
Content: cleanedMessage,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
})
|
})
|
||||||
@@ -134,7 +139,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the conversation history
|
// Update the conversation history
|
||||||
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
|
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
})
|
})
|
||||||
@@ -202,13 +207,6 @@ 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
|
||||||
@@ -243,5 +241,11 @@ 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",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
package connectors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"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", evt.RoomID, 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
|
|
||||||
if evt.Content.AsMessage().Mentions != nil {
|
|
||||||
for _, mention := range evt.Content.AsMessage().Mentions.UserIDs {
|
|
||||||
if mention == m.client.UserID {
|
|
||||||
mentioned = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mentioned && !m.roomMode {
|
|
||||||
xlog.Info("Skipping reply because it does not mention the bot", evt.RoomID, m.roomID)
|
|
||||||
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]interface{}{
|
|
||||||
"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()] = append(m.activeJobs[evt.RoomID.String()][:i], m.activeJobs[evt.RoomID.String()][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) {
|
|
||||||
// Create Matrix client
|
|
||||||
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
|
|
||||||
|
|
||||||
// Set up event handler
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start syncing
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
err := client.SyncWithContext(a.Context())
|
|
||||||
|
|
||||||
xlog.Info("Syncing")
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error(fmt.Sprintf("Error syncing: %v", err))
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Handle shutdown
|
|
||||||
go func() {
|
|
||||||
<-a.Context().Done()
|
|
||||||
client.StopSync()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatrixConfigMeta returns the metadata for Matrix connector configuration fields
|
|
||||||
func MatrixConfigMeta() []config.Field {
|
|
||||||
return []config.Field{
|
|
||||||
{
|
|
||||||
Name: "homeserverURL",
|
|
||||||
Label: "Homeserver URL",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "userID",
|
|
||||||
Label: "User ID",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "accessToken",
|
|
||||||
Label: "Access Token",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "roomID",
|
|
||||||
Label: "Room ID",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "roomMode",
|
|
||||||
Label: "Room Mode",
|
|
||||||
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,17 +41,25 @@ 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),
|
||||||
}
|
}
|
||||||
@@ -131,6 +139,16 @@ 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, ">") {
|
||||||
@@ -149,38 +167,8 @@ func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string
|
|||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
|
func generateAttachmentsFromJobResponse(j *types.JobResult) (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)) {
|
||||||
@@ -260,7 +248,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 := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID))
|
currentConv := t.conversationTracker.GetConversation(t.channelID)
|
||||||
|
|
||||||
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
|
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
|
||||||
|
|
||||||
@@ -304,8 +292,8 @@ func (t *Slack) handleChannelMessage(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
a.SharedState().ConversationTracker.AddMessage(
|
t.conversationTracker.AddMessage(
|
||||||
fmt.Sprintf("slack:%s", t.channelID), currentConv[len(currentConv)-1],
|
t.channelID, currentConv[len(currentConv)-1],
|
||||||
)
|
)
|
||||||
|
|
||||||
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
||||||
@@ -351,14 +339,14 @@ func (t *Slack) handleChannelMessage(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.SharedState().ConversationTracker.AddMessage(
|
t.conversationTracker.AddMessage(
|
||||||
fmt.Sprintf("slack:%s", t.channelID), openai.ChatCompletionMessage{
|
t.channelID, openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Debug("After adding message to conversation tracker", "conversation", a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID)))
|
xlog.Debug("After adding message to conversation tracker", "conversation", t.conversationTracker.GetConversation(t.channelID))
|
||||||
|
|
||||||
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
|
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
|
||||||
|
|
||||||
@@ -387,7 +375,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, api, ev.Channel, "")...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
||||||
@@ -399,7 +387,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, api, ev.Channel, "")...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
||||||
// slack.MsgOptionTS(ts),
|
// slack.MsgOptionTS(ts),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -420,7 +408,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, api, ev.Channel, msgTs)...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
||||||
)
|
)
|
||||||
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))
|
||||||
@@ -447,7 +435,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, api, ev.Channel, msgTs)...),
|
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
|
||||||
)
|
)
|
||||||
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))
|
||||||
@@ -733,13 +721,6 @@ 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",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,5 +804,11 @@ 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,377 +1,143 @@
|
|||||||
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"
|
||||||
"sync"
|
"time"
|
||||||
|
|
||||||
"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
|
||||||
|
|
||||||
// To track placeholder messages
|
conversationTracker *ConversationTracker[int64]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send any text message to the bot after the bot has been started
|
// Send any text message to the bot after the bot has been started
|
||||||
|
|
||||||
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
|
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
|
||||||
return func(state types.ActionState) {
|
return func(state types.ActionState) {
|
||||||
// Mark the job as completed when we get the final result
|
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
|
||||||
if state.ActionCurrentState.Job != nil && state.ActionCurrentState.Job.Metadata != nil {
|
Description: state.Reasoning,
|
||||||
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 {
|
func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
|
||||||
return func(state types.ActionCurrentState) bool {
|
return func(state types.ActionCurrentState) bool {
|
||||||
// Check if we have a placeholder message for this job
|
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
|
||||||
t.placeholderMutex.RLock()
|
Description: state.Reasoning,
|
||||||
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
|
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)) {
|
|
||||||
xlog.Debug("Sending photo", "url", url)
|
|
||||||
if err := sendImageToTelegram(ctx, t.bot, chatID, url); err != nil {
|
|
||||||
xlog.Error("Error handling image", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
||||||
username := update.Message.From.Username
|
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!",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
|
if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
|
||||||
xlog.Info("Unauthorized user", "username", username)
|
xlog.Info("Unauthorized user", "username", username)
|
||||||
_, 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel any active job for this chat before starting a new one
|
currentConv := t.conversationTracker.GetConversation(update.Message.From.ID)
|
||||||
t.cancelActiveJobForChat(update.Message.Chat.ID)
|
|
||||||
|
|
||||||
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.From.ID))
|
|
||||||
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||||
Content: update.Message.Text,
|
Content: update.Message.Text,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
})
|
})
|
||||||
|
|
||||||
a.SharedState().ConversationTracker.AddMessage(
|
t.conversationTracker.AddMessage(
|
||||||
fmt.Sprintf("telegram:%d", update.Message.From.ID),
|
update.Message.From.ID,
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
Content: update.Message.Text,
|
Content: update.Message.Text,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send initial placeholder message
|
xlog.Info("New message", "username", username, "conversation", currentConv)
|
||||||
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
|
||||||
ChatID: update.Message.Chat.ID,
|
|
||||||
Text: bot.EscapeMarkdown(telegramThinkingMessage),
|
|
||||||
ParseMode: models.ParseModeMarkdown,
|
|
||||||
})
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
a.SharedState().ConversationTracker.AddMessage(
|
t.conversationTracker.AddMessage(
|
||||||
fmt.Sprintf("telegram:%d", update.Message.From.ID),
|
update.Message.From.ID,
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle any multimedia content in the response and collect URLs
|
xlog.Debug("Sending message back to telegram", "response", res.Response)
|
||||||
urls, err := t.handleMultimediaContent(ctx, update.Message.Chat.ID, res)
|
|
||||||
|
for _, res := range res.State {
|
||||||
|
// coming from the search action
|
||||||
|
// if urls, exists := res.Metadata[actions.MetadataUrls]; exists {
|
||||||
|
// for _, url := range uniqueStringSlice(urls.([]string)) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// coming from the gen image actions
|
||||||
|
if imagesUrls, exists := res.Metadata[actions.MetadataImages]; exists {
|
||||||
|
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
|
||||||
|
xlog.Debug("Sending photo", "url", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error handling multimedia content", "error", err)
|
xlog.Error("Error downloading image", "error", err.Error())
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
// Update the message with the final response
|
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||||
formattedResponse := formatResponseWithURLs(res.Response, urls)
|
|
||||||
|
|
||||||
// Split the message if it's too long
|
|
||||||
messages := xstrings.SplitParagraph(formattedResponse, telegramMaxMessageLength)
|
|
||||||
|
|
||||||
if len(messages) == 0 {
|
|
||||||
internalError(errors.New("empty response from agent"), msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the first message
|
|
||||||
_, err = b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
|
||||||
ChatID: update.Message.Chat.ID,
|
ChatID: update.Message.Chat.ID,
|
||||||
MessageID: msg.ID,
|
Photo: &models.InputFileUpload{
|
||||||
Text: messages[0],
|
Filename: "image.jpg",
|
||||||
ParseMode: models.ParseModeMarkdown,
|
Data: resp.Body,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(fmt.Errorf("internal error: %w", err), msg)
|
xlog.Error("Error sending photo", "error", err.Error())
|
||||||
return
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send additional chunks as new messages
|
}
|
||||||
for i := 1; i < len(messages); i++ {
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
_, err = b.SendMessage(ctx, &bot.SendMessageParams{
|
// ParseMode: models.ParseModeMarkdown,
|
||||||
ChatID: update.Message.Chat.ID,
|
ChatID: update.Message.Chat.ID,
|
||||||
Text: messages[i],
|
Text: res.Response,
|
||||||
ParseMode: models.ParseModeMarkdown,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(fmt.Errorf("internal error: %w", err), msg)
|
xlog.Error("Error sending message", "error", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,42 +163,18 @@ 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 {
|
||||||
xlog.Error("Error creating bot", "error", err)
|
panic(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.bot = b
|
t.bot = b
|
||||||
t.agent = a
|
t.agent = a
|
||||||
|
|
||||||
// go func() {
|
// go func() {
|
||||||
// forc m := range a.ConversationChannel() {
|
// for 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +184,11 @@ 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 {
|
||||||
@@ -450,10 +197,11 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
|
|||||||
|
|
||||||
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),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,10 +221,10 @@ 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: "channel_id",
|
Name: "lastMessageDuration",
|
||||||
Label: "Channel ID",
|
Label: "Last Message Duration",
|
||||||
Type: config.FieldTypeText,
|
Type: config.FieldTypeText,
|
||||||
HelpText: "Telegram channel ID to send messages to if the agent needs to initiate a conversation",
|
DefaultValue: "5m",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mudler/LocalAGI/core/state"
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
|
||||||
"github.com/mudler/LocalAGI/services/filters"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Filters(a *state.AgentConfig) types.JobFilters {
|
|
||||||
var result []types.JobFilter
|
|
||||||
for _, f := range a.Filters {
|
|
||||||
var filter types.JobFilter
|
|
||||||
var err error
|
|
||||||
switch f.Type {
|
|
||||||
case filters.FilterRegex:
|
|
||||||
filter, err = filters.NewRegexFilter(f.Config)
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Failed to configure regex", "err", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case filters.FilterClassifier:
|
|
||||||
filter, err = filters.NewClassifierFilter(f.Config, a)
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("failed to configure classifier", "err", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
xlog.Error("Unrecognized filter type", "type", f.Type)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, filter)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// FiltersConfigMeta returns all filter config metas for UI.
|
|
||||||
func FiltersConfigMeta() []config.FieldGroup {
|
|
||||||
return []config.FieldGroup{
|
|
||||||
filters.RegexFilterConfigMeta(),
|
|
||||||
filters.ClassifierFilterConfigMeta(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package filters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/state"
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
|
||||||
)
|
|
||||||
|
|
||||||
const FilterClassifier = "classifier"
|
|
||||||
|
|
||||||
type ClassifierFilter struct {
|
|
||||||
name string
|
|
||||||
client llm.LLMClient
|
|
||||||
model string
|
|
||||||
description string
|
|
||||||
allowOnMatch bool
|
|
||||||
isTrigger bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClassifierFilterConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Model string `json:"model,omitempty"`
|
|
||||||
APIURL string `json:"api_url,omitempty"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
AllowOnMatch bool `json:"allow_on_match"`
|
|
||||||
IsTrigger bool `json:"is_trigger"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClassifierFilter(configJSON string, a *state.AgentConfig) (*ClassifierFilter, error) {
|
|
||||||
var cfg ClassifierFilterConfig
|
|
||||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var model string
|
|
||||||
if cfg.Model != "" {
|
|
||||||
model = cfg.Model
|
|
||||||
} else {
|
|
||||||
model = a.Model
|
|
||||||
}
|
|
||||||
if cfg.Name == "" {
|
|
||||||
return nil, fmt.Errorf("Classifier with no name")
|
|
||||||
}
|
|
||||||
if cfg.Description == "" {
|
|
||||||
return nil, fmt.Errorf("%s classifier has no description", cfg.Name)
|
|
||||||
}
|
|
||||||
apiUrl := a.APIURL
|
|
||||||
if cfg.APIURL != "" {
|
|
||||||
apiUrl = cfg.APIURL
|
|
||||||
}
|
|
||||||
client := llm.NewClient(a.APIKey, apiUrl, "1m")
|
|
||||||
|
|
||||||
return &ClassifierFilter{
|
|
||||||
name: cfg.Name,
|
|
||||||
model: model,
|
|
||||||
description: cfg.Description,
|
|
||||||
client: client,
|
|
||||||
allowOnMatch: cfg.AllowOnMatch,
|
|
||||||
isTrigger: cfg.IsTrigger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const fmtT = `
|
|
||||||
Does the below message fit the description "%s"
|
|
||||||
|
|
||||||
%s
|
|
||||||
`
|
|
||||||
|
|
||||||
func (f *ClassifierFilter) Name() string { return f.name }
|
|
||||||
func (f *ClassifierFilter) Apply(job *types.Job) (bool, error) {
|
|
||||||
input := extractInputFromJob(job)
|
|
||||||
guidance := fmt.Sprintf(fmtT, f.description, input)
|
|
||||||
var result struct {
|
|
||||||
Asserted bool `json:"answer"`
|
|
||||||
}
|
|
||||||
err := llm.GenerateTypedJSONWithGuidance(job.GetContext(), f.client, guidance, f.model, jsonschema.Definition{
|
|
||||||
Type: jsonschema.Object,
|
|
||||||
Properties: map[string]jsonschema.Definition{
|
|
||||||
"answer": {
|
|
||||||
Type: jsonschema.Boolean,
|
|
||||||
Description: "The answer to the first question",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Required: []string{"answer"},
|
|
||||||
}, &result)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Asserted {
|
|
||||||
return f.allowOnMatch, nil
|
|
||||||
}
|
|
||||||
return !f.allowOnMatch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *ClassifierFilter) IsTrigger() bool {
|
|
||||||
return f.isTrigger
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClassifierFilterConfigMeta() config.FieldGroup {
|
|
||||||
return config.FieldGroup{
|
|
||||||
Name: FilterClassifier,
|
|
||||||
Label: "Classifier Filter/Trigger",
|
|
||||||
Fields: []config.Field{
|
|
||||||
{Name: "name", Label: "Name", Type: "text", Required: true},
|
|
||||||
{Name: "model", Label: "Model", Type: "text", Required: false,
|
|
||||||
HelpText: "The LLM to use, usually a smaller one. Leave blank to use the same as the agent's"},
|
|
||||||
{Name: "api_url", Label: "API URL", Type: "url", Required: false,
|
|
||||||
HelpText: "The URL of the LLM service if different from the agent's"},
|
|
||||||
{Name: "description", Label: "Description", Type: "text", Required: true,
|
|
||||||
HelpText: "Describe the type of content to match against e.g. 'technical support request'"},
|
|
||||||
{Name: "allow_on_match", Label: "Allow on Match", Type: "checkbox", Required: true},
|
|
||||||
{Name: "is_trigger", Label: "Is Trigger", Type: "checkbox", Required: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package filters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const FilterRegex = "regex"
|
|
||||||
|
|
||||||
type RegexFilter struct {
|
|
||||||
name string
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
allowOnMatch bool
|
|
||||||
isTrigger bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegexFilterConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Pattern string `json:"pattern"`
|
|
||||||
AllowOnMatch bool `json:"allow_on_match"`
|
|
||||||
IsTrigger bool `json:"is_trigger"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRegexFilter(configJSON string) (*RegexFilter, error) {
|
|
||||||
var cfg RegexFilterConfig
|
|
||||||
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
re, err := regexp.Compile(cfg.Pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &RegexFilter{
|
|
||||||
name: cfg.Name,
|
|
||||||
pattern: re,
|
|
||||||
allowOnMatch: cfg.AllowOnMatch,
|
|
||||||
isTrigger: cfg.IsTrigger,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *RegexFilter) Name() string { return f.name }
|
|
||||||
func (f *RegexFilter) Apply(job *types.Job) (bool, error) {
|
|
||||||
input := extractInputFromJob(job)
|
|
||||||
if f.pattern.MatchString(input) {
|
|
||||||
return f.allowOnMatch, nil
|
|
||||||
}
|
|
||||||
return !f.allowOnMatch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *RegexFilter) IsTrigger() bool {
|
|
||||||
return f.isTrigger
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegexFilterConfigMeta() config.FieldGroup {
|
|
||||||
return config.FieldGroup{
|
|
||||||
Name: FilterRegex,
|
|
||||||
Label: "Regex Filter/Trigger",
|
|
||||||
Fields: []config.Field{
|
|
||||||
{Name: "name", Label: "Name", Type: "text", Required: true},
|
|
||||||
{Name: "pattern", Label: "Pattern", Type: "text", Required: true},
|
|
||||||
{Name: "allow_on_match", Label: "Allow on Match", Type: "checkbox", Required: true},
|
|
||||||
{Name: "is_trigger", Label: "Is Trigger", Type: "checkbox", Required: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractInputFromJob attempts to extract a string input for filtering.
|
|
||||||
func extractInputFromJob(job *types.Job) string {
|
|
||||||
if job.Metadata != nil {
|
|
||||||
if v, ok := job.Metadata["input"]; ok {
|
|
||||||
if s, ok := v.(string); ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// fallback: try to use conversation history if available
|
|
||||||
if len(job.ConversationHistory) > 0 {
|
|
||||||
// Use the last message content
|
|
||||||
last := job.ConversationHistory[len(job.ConversationHistory)-1]
|
|
||||||
return last.Content
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ oauth_config:
|
|||||||
- commands
|
- commands
|
||||||
- groups:history
|
- groups:history
|
||||||
- files:read
|
- files:read
|
||||||
- files:write
|
|
||||||
- im:history
|
- im:history
|
||||||
- im:read
|
- im:read
|
||||||
- im:write
|
- im:write
|
||||||
|
|||||||
57
webui/app.go
57
webui/app.go
@@ -11,14 +11,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/mudler/LocalAGI/core/conversations"
|
|
||||||
coreTypes "github.com/mudler/LocalAGI/core/types"
|
coreTypes "github.com/mudler/LocalAGI/core/types"
|
||||||
internalTypes "github.com/mudler/LocalAGI/core/types"
|
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/services"
|
"github.com/mudler/LocalAGI/services"
|
||||||
|
"github.com/mudler/LocalAGI/services/connectors"
|
||||||
"github.com/mudler/LocalAGI/webui/types"
|
"github.com/mudler/LocalAGI/webui/types"
|
||||||
|
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
|
||||||
@@ -35,7 +33,6 @@ type (
|
|||||||
htmx *htmx.HTMX
|
htmx *htmx.HTMX
|
||||||
config *Config
|
config *Config
|
||||||
*fiber.App
|
*fiber.App
|
||||||
sharedState *internalTypes.AgentSharedState
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,7 +50,6 @@ func NewApp(opts ...Option) *App {
|
|||||||
htmx: htmx.New(),
|
htmx: htmx.New(),
|
||||||
config: config,
|
config: config,
|
||||||
App: webapp,
|
App: webapp,
|
||||||
sharedState: internalTypes.NewAgentSharedState(5 * time.Minute),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.registerRoutes(config.Pool, webapp)
|
a.registerRoutes(config.Pool, webapp)
|
||||||
@@ -180,7 +176,17 @@ func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error
|
|||||||
return errorJSONMessage(c, err.Error())
|
return errorJSONMessage(c, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pool.RecreateAgent(agentName, &newConfig); err != nil {
|
// Remove the agent first
|
||||||
|
if err := pool.Remove(agentName); err != nil {
|
||||||
|
return errorJSONMessage(c, "Error removing agent: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create agent with new config
|
||||||
|
if err := pool.CreateAgent(agentName, &newConfig); err != nil {
|
||||||
|
// Try to restore the old configuration if update fails
|
||||||
|
if restoreErr := pool.CreateAgent(agentName, oldConfig); restoreErr != nil {
|
||||||
|
return errorJSONMessage(c, fmt.Sprintf("Failed to update agent and restore failed: %v, %v", err, restoreErr))
|
||||||
|
}
|
||||||
return errorJSONMessage(c, "Error updating agent: "+err.Error())
|
return errorJSONMessage(c, "Error updating agent: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +370,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
xlog.Error("Error marshaling status message", "error", err)
|
xlog.Error("Error marshaling status message", "error", err)
|
||||||
} else {
|
} else {
|
||||||
manager.Send(
|
manager.Send(
|
||||||
sse.NewMessage(string(statusData)).WithEvent("json_message_status"))
|
sse.NewMessage(string(statusData)).WithEvent("json_status"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the message asynchronously
|
// Process the message asynchronously
|
||||||
@@ -411,7 +417,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
xlog.Error("Error marshaling completed status", "error", err)
|
xlog.Error("Error marshaling completed status", "error", err)
|
||||||
} else {
|
} else {
|
||||||
manager.Send(
|
manager.Send(
|
||||||
sse.NewMessage(string(completedData)).WithEvent("json_message_status"))
|
sse.NewMessage(string(completedData)).WithEvent("json_status"))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -423,31 +429,7 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetActionDefinition(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
|
||||||
payload := struct {
|
|
||||||
Config map[string]string `json:"config"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
if err := c.BodyParser(&payload); err != nil {
|
|
||||||
xlog.Error("Error parsing action payload", "error", err)
|
|
||||||
return errorJSONMessage(c, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
actionName := c.Params("name")
|
|
||||||
|
|
||||||
xlog.Debug("Executing action", "action", actionName, "config", payload.Config)
|
|
||||||
a, err := services.Action(actionName, "", payload.Config, pool, map[string]string{})
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Error creating action", "error", err)
|
|
||||||
return errorJSONMessage(c, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(a.Definition())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
payload := struct {
|
payload := struct {
|
||||||
Config map[string]string `json:"config"`
|
Config map[string]string `json:"config"`
|
||||||
@@ -462,7 +444,7 @@ func (app *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
actionName := c.Params("name")
|
actionName := c.Params("name")
|
||||||
|
|
||||||
xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params)
|
xlog.Debug("Executing action", "action", actionName, "config", payload.Config, "params", payload.Params)
|
||||||
a, err := services.Action(actionName, "", payload.Config, pool, map[string]string{})
|
a, err := services.Action(actionName, "", payload.Config, pool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error creating action", "error", err)
|
xlog.Error("Error creating action", "error", err)
|
||||||
return errorJSONMessage(c, err.Error())
|
return errorJSONMessage(c, err.Error())
|
||||||
@@ -471,7 +453,7 @@ func (app *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
ctx, cancel := context.WithTimeout(c.Context(), 200*time.Second)
|
ctx, cancel := context.WithTimeout(c.Context(), 200*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
res, err := a.Run(ctx, app.sharedState, payload.Params)
|
res, err := a.Run(ctx, payload.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error running action", "error", err)
|
xlog.Error("Error running action", "error", err)
|
||||||
return errorJSONMessage(c, err.Error())
|
return errorJSONMessage(c, err.Error())
|
||||||
@@ -488,7 +470,7 @@ func (a *App) ListActions() func(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Responses(pool *state.AgentPool, tracker *conversations.ConversationTracker[string]) func(c *fiber.Ctx) error {
|
func (a *App) Responses(pool *state.AgentPool, tracker *connectors.ConversationTracker[string]) func(c *fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
var request types.RequestBody
|
var request types.RequestBody
|
||||||
if err := c.BodyParser(&request); err != nil {
|
if err := c.BodyParser(&request); err != nil {
|
||||||
@@ -580,7 +562,7 @@ func (a *App) GenerateGroupProfiles(pool *state.AgentPool) func(c *fiber.Ctx) er
|
|||||||
|
|
||||||
xlog.Debug("Generating group", "description", request.Descript)
|
xlog.Debug("Generating group", "description", request.Descript)
|
||||||
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
|
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
|
||||||
err := llm.GenerateTypedJSONWithGuidance(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{
|
err := llm.GenerateTypedJSON(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{
|
||||||
Type: jsonschema.Object,
|
Type: jsonschema.Object,
|
||||||
Properties: map[string]jsonschema.Definition{
|
Properties: map[string]jsonschema.Definition{
|
||||||
"agents": {
|
"agents": {
|
||||||
@@ -648,7 +630,6 @@ func (a *App) GetAgentConfigMeta() func(c *fiber.Ctx) error {
|
|||||||
services.ActionsConfigMeta(),
|
services.ActionsConfigMeta(),
|
||||||
services.ConnectorsConfigMeta(),
|
services.ConnectorsConfigMeta(),
|
||||||
services.DynamicPromptsConfigMeta(),
|
services.DynamicPromptsConfigMeta(),
|
||||||
services.FiltersConfigMeta(),
|
|
||||||
)
|
)
|
||||||
return c.JSON(configMeta)
|
return c.JSON(configMeta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,20 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "react-ui",
|
"name": "react-ui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.1",
|
"@eslint/js": "^9.24.0",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-react-hooks": "^6.0.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^15.15.0",
|
||||||
"react-router-dom": "^7.5.3",
|
"react-router-dom": "^7.5.0",
|
||||||
"vite": "^6.3.3",
|
"vite": "^6.2.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -33,26 +32,14 @@
|
|||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
"@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||||
|
|
||||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="],
|
|
||||||
|
|
||||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
|
||||||
|
|
||||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg=="],
|
|
||||||
|
|
||||||
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="],
|
|
||||||
|
|
||||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
|
||||||
|
|
||||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
|
||||||
|
|
||||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="],
|
|
||||||
|
|
||||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
|
||||||
|
|
||||||
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="],
|
|
||||||
|
|
||||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="],
|
|
||||||
|
|
||||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||||
@@ -63,8 +50,6 @@
|
|||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
|
"@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="],
|
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
|
||||||
@@ -133,11 +118,11 @@
|
|||||||
|
|
||||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
|
||||||
|
|
||||||
"@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
|
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
|
||||||
|
|
||||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||||
|
|
||||||
"@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="],
|
"@eslint/js": ["@eslint/js@9.24.0", "", {}, "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA=="],
|
||||||
|
|
||||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||||
|
|
||||||
@@ -209,15 +194,17 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||||
|
|
||||||
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="],
|
"@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg=="],
|
"@types/react-dom": ["@types/react-dom@19.1.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.4.1", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.4.0", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-x/EztcTKVj+TDeANY1WjNeYsvZjZdfWRMP/KXi5Yn8BoTzpa13ZltaQqKfvWYbX8CE10GOHHdC5v86jY9x8i/g=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||||
|
|
||||||
@@ -267,11 +254,11 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="],
|
"eslint": ["eslint@9.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@6.0.0", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "@babel/plugin-transform-private-methods": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-NyC3yIC9fazLitYiN8eHykV5wLp/SMuUZMh+sdPSHIeN4ReXIc7if40jtGjDplAgVL/4OkN1d9gneWe9lFZgag=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
||||||
|
|
||||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
|
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
||||||
|
|
||||||
@@ -293,7 +280,7 @@
|
|||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
@@ -309,16 +296,10 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@16.0.0", "", {}, "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A=="],
|
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
|
||||||
|
|
||||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
|
||||||
|
|
||||||
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
@@ -393,9 +374,9 @@
|
|||||||
|
|
||||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.5.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw=="],
|
"react-router": ["react-router@7.5.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g=="],
|
||||||
|
|
||||||
"react-router-dom": ["react-router-dom@7.5.3", "", { "dependencies": { "react-router": "7.5.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A=="],
|
"react-router-dom": ["react-router-dom@7.5.0", "", { "dependencies": { "react-router": "7.5.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
@@ -417,7 +398,7 @@
|
|||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
|
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
||||||
|
|
||||||
"turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
|
"turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
|
||||||
|
|
||||||
@@ -427,7 +408,7 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"vite": ["vite@6.3.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw=="],
|
"vite": ["vite@6.3.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.3", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.12" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
@@ -437,16 +418,14 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="],
|
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
|
|
||||||
|
|
||||||
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
|
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
|
||||||
|
|
||||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0"
|
||||||
"highlight.js": "^11.11.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.1",
|
"@eslint/js": "^9.24.0",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-react-hooks": "^6.0.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^15.15.0",
|
||||||
"react-router-dom": "^7.5.3",
|
"react-router-dom": "^7.5.0",
|
||||||
"vite": "^6.3.3"
|
"vite": "^6.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user