Compare commits

..

48 Commits

Author SHA1 Message Date
Richard Palethorpe
5698d0b832 chore(tests): Mock LLM in tests for PRs
This saves time when testing on CPU which is the only sensible thing
to do on GitHub CI for PRs. For releases or once the commit is merged
we could use an external runner with GPU or just wait.

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-12 13:51:45 +01:00
Ettore Di Giacinto
c23e655f44 feat(agent): shared state, allow to track conversations globally (#148)
* feat(agent): shared state, allow to track conversations globally

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Cleanup

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* track conversations initiated by the bot

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-11 22:23:01 +02:00
Ettore Di Giacinto
2b07dd79ec feat(telegram): add action to send telegram message (#147)
Signed-off-by: mudler <mudler@localai.io>
2025-05-11 19:17:20 +02:00
Ettore Di Giacinto
864bf8b94c fix(telegram): split long messages (#146)
Signed-off-by: mudler <mudler@localai.io>
2025-05-11 19:01:25 +02:00
mudler
60c53c8f3e Update README 2025-05-11 18:43:17 +02:00
mudler
cc0f5cbdcc Update README 2025-05-11 18:41:43 +02:00
mudler
8d527d6a09 Update README 2025-05-11 18:40:16 +02:00
Ettore Di Giacinto
e431bc234b feat(evaluation): add deep evaluation mechanism (#145)
* feat(evaluation): add deep evaluation mechanism

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* consider whole conversation when evaluating

Signed-off-by: mudler <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-05-11 18:31:04 +02:00
Ettore Di Giacinto
289edb67a6 feat(call_agents): allow to specify whitelist and blacklist agents (#144)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-10 22:27:34 +02:00
Ettore Di Giacinto
324124e002 Update README.md 2025-05-07 22:23:31 +02:00
Ettore Di Giacinto
2bacac687f Update README.md 2025-05-07 22:23:04 +02:00
Ettore Di Giacinto
8b504a5e1e Update README.md 2025-05-07 22:22:41 +02:00
Martin Richardson
0e4e60cc15 fix(playground): convert fieldType number to string for json unmarshalling 2025-05-07 20:56:22 +01:00
Ettore Di Giacinto
fb1ab70650 fix(telegram): upload of images (#143)
* fix(telegram): upload of images

Signed-off-by: mudler <mudler@localai.io>

* Parse markdown

Signed-off-by: mudler <mudler@localai.io>

---------

Signed-off-by: mudler <mudler@localai.io>
2025-05-07 20:10:51 +02:00
Ettore Di Giacinto
94f4d350c9 feat(telegram): handle correctly generated multimedia and links (#141)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-06 22:38:33 +02:00
Ettore Di Giacinto
cc3fdecfc9 feat(telegram): show thought process (#140)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-06 22:24:11 +02:00
dependabot[bot]
c92acd670e chore(deps-dev): bump @types/react-dom from 19.1.2 to 19.1.3 in /webui/react-ui (#135)
chore(deps-dev): bump @types/react-dom in /webui/react-ui

Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 19.1.2 to 19.1.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-version: 19.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 10:12:31 +02:00
dependabot[bot]
0464a5b344 chore(deps): bump github.com/sashabaranov/go-openai from 1.39.0 to 1.39.1 (#134)
chore(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.39.0 to 1.39.1.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.39.0...v1.39.1)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.39.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 10:12:07 +02:00
dependabot[bot]
8bdb575bb2 chore(deps): bump github.com/go-telegram/bot from 1.14.2 to 1.15.0 (#132)
Bumps [github.com/go-telegram/bot](https://github.com/go-telegram/bot) from 1.14.2 to 1.15.0.
- [Release notes](https://github.com/go-telegram/bot/releases)
- [Changelog](https://github.com/go-telegram/bot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-telegram/bot/compare/v1.14.2...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/go-telegram/bot
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 10:11:43 +02:00
Richard Palethorpe
f2c3b9dbdb feat(filters): Add configurable filters for incoming jobs
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-05-06 09:08:14 +01:00
Ettore Di Giacinto
02c6b5ad4e fix(reply): force replying without using tools (#131)
Signed-off-by: mudler <mudler@localai.io>
2025-05-05 18:33:53 +02:00
Ettore Di Giacinto
5e5224da25 fix(github): skip binary files
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-02 22:34:37 +02:00
Ettore Di Giacinto
c529f880d3 feat(github): add action to list and search files in a repository (#110)
Signed-off-by: mudler <mudler@localai.io>
2025-05-02 14:49:01 +02:00
Ettore Di Giacinto
18eb40ec14 fix(actions): make sure to initialize a config (#109)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-01 22:24:11 +02:00
Ettore Di Giacinto
904765591c fix(github/issue-editor): also update the title 2025-05-01 22:15:19 +02:00
dependabot[bot]
f726d3c3e5 chore(deps): bump github.com/sashabaranov/go-openai from 1.38.2 to 1.38.3 (#93)
chore(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.38.2 to 1.38.3.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.38.2...v1.38.3)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 22:08:08 +02:00
Ettore Di Giacinto
62ce629bf1 feat(github): add issue editor (#106)
* feat(github): add issue editor

Signed-off-by: mudler <mudler@localai.io>

* Small changes

---------

Signed-off-by: mudler <mudler@localai.io>
2025-05-01 22:07:41 +02:00
dependabot[bot]
9c555bd99f chore(deps): bump ubuntu from 22.04 to 24.04 (#107)
Bumps ubuntu from 22.04 to 24.04.

---
updated-dependencies:
- dependency-name: ubuntu
  dependency-version: '24.04'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 22:00:07 +02:00
Ettore Di Giacinto
5981109730 feat(github): add option to create PR from forks (#105)
* feat(github): add option to create PR from forks

Signed-off-by: mudler <mudler@localai.io>

* extend delays in waiting forking from github

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: mudler <mudler@localai.io>
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-05-01 21:59:28 +02:00
Ettore Di Giacinto
087a5fbe0f feat(connectors): add support for Matrix (#82)
* feat(connectors): add support for Matrix

Signed-off-by: mudler <mudler@localai.io>

* make it functional

Signed-off-by: mudler <mudler@localai.io>

---------

Signed-off-by: mudler <mudler@localai.io>
2025-05-01 20:10:19 +02:00
Ettore Di Giacinto
67cb5937e7 fix: cleanup responses also when not picking any tool (#102)
* fix: process response also when no action is picked

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* chore: rename method to be more meaningful

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-30 22:15:34 +02:00
Richard Palethorpe
8abf5512a4 fix(core): Add recursive loop detection and move loop detection (#101)
* fix(core): Add recursive loop detection and move loop detection

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(ci): Free up space after installation to avoid out of space error

Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-30 13:51:43 +00:00
mudler
45dd74d27c fix: multiline
Signed-off-by: mudler <mudler@localai.io>
2025-04-30 11:44:26 +02:00
Ettore Di Giacinto
1109b0a533 feat: add option to strip thinking from output (#100)
Signed-off-by: mudler <mudler@localai.io>
2025-04-30 11:05:39 +02:00
Richard Palethorpe
bd1b06f326 chore: Update all deps (#95)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-29 17:10:44 +02:00
Ettore Di Giacinto
7406db5882 feat: specify timeout (#97)
Signed-off-by: mudler <mudler@localai.io>
2025-04-29 17:10:11 +02:00
mudler
a1efa07b24 fix: set default timeout 2025-04-29 10:51:00 +02:00
Ettore Di Giacinto
29f7644577 feat: add deep research action (#91)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-04-29 08:46:55 +02:00
Ettore Di Giacinto
f3884c0244 chore(defaults): Enable reasoning by default (#89) 2025-04-27 17:42:53 +02:00
Richard Palethorpe
6516af6c34 Update README with videos
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-26 11:00:11 +01:00
Richard Palethorpe
77680c6fee feat(ui): Add summary details of each observable
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-26 11:00:11 +01:00
mudler
5faa599321 chore(mcpbox): add wget and curl
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 18:19:57 +02:00
mudler
6209ededff fix(ci): add DEBIAN_FRONTEND
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 18:11:13 +02:00
Richard Palethorpe
f6b6d5246c feat(ui): Action playground config and parameter forms
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-25 16:36:19 +01:00
Ettore Di Giacinto
b81624bfc2 chore(mcpbox): use ubuntu:24.04 as base (#86)
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 17:06:50 +02:00
Ettore Di Giacinto
c1844f7230 chore(mcpbox): use dind (#85)
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 17:05:56 +02:00
Richard Palethorpe
15efd2d527 fix(docker): Add mcpbox server to extended compose files (#84)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2025-04-25 17:04:06 +02:00
Ettore Di Giacinto
5e3bc0f89b fix(discord): automatically add 'Bot' prefix to token if missing (#83)
Signed-off-by: mudler <mudler@localai.io>
2025-04-25 16:20:29 +02:00
102 changed files with 4337 additions and 889 deletions

View File

@@ -30,17 +30,29 @@ jobs:
$(. /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
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: |
sudo apt-get update && sudo apt-get install -y make
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
make tests-mock
else
make tests
fi
#sudo mv coverage/coverage.txt coverage.txt
#sudo chmod 777 coverage.txt

49
.github/workflows/tests_fragile.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
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

View File

@@ -20,10 +20,12 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
# Final stage
FROM alpine:3.19
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies
RUN apk add --no-cache ca-certificates tzdata docker
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

View File

@@ -3,6 +3,8 @@ IMAGE_NAME?=webui
MCPBOX_IMAGE_NAME?=mcpbox
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
.PHONY: tests tests-mock cleanup-tests
prepare-tests: build-mcpbox
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)
@@ -13,6 +15,9 @@ cleanup-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 ./...
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:
$(MAKE) run KBDISABLEINDEX=true

226
README.md
View File

@@ -11,6 +11,9 @@
[![GitHub stars](https://img.shields.io/github/stars/mudler/LocalAGI)](https://github.com/mudler/LocalAGI/stargazers)
[![GitHub issues](https://img.shields.io/github/issues/mudler/LocalAGI)](https://github.com/mudler/LocalAGI/issues)
Try on [![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/LocalAGI_bot)
</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.
@@ -69,6 +72,13 @@ 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
## Videos
[![Creating a basic agent](https://img.youtube.com/vi/HtVwIxW3ePg/mqdefault.jpg)](https://youtu.be/HtVwIxW3ePg)
[![Agent Observability](https://img.youtube.com/vi/v82rswGJt_M/mqdefault.jpg)](https://youtu.be/v82rswGJt_M)
[![Filters and Triggers](https://youtu.be/d_we-AYksSw/mqdefault.jpg)](https://youtu.be/d_we-AYksSw)
## 📚🆕 Local Stack Family
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
@@ -180,14 +190,6 @@ Good (relatively small) models that have been tested are:
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
## 🌐 The Local Ecosystem
LocalAGI is part of the powerful Local family of privacy-focused AI tools:
- [**LocalAI**](https://github.com/mudler/LocalAI): Run Large Language Models locally.
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
## 🌟 Screenshots
### Powerful Web UI
@@ -259,6 +261,158 @@ go build -o localagi
./localagi
```
### Using as a Library
LocalAGI can be used as a Go library to programmatically create and manage AI agents. Let's start with a simple example of creating a single agent:
<details>
<summary><strong>Basic Usage: Single Agent</strong></summary>
```go
import (
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
)
// Create a new agent with basic configuration
agent, err := agent.New(
agent.WithModel("gpt-4"),
agent.WithLLMAPIURL("http://localhost:8080"),
agent.WithLLMAPIKey("your-api-key"),
agent.WithSystemPrompt("You are a helpful assistant."),
agent.WithCharacter(agent.Character{
Name: "my-agent",
}),
agent.WithActions(
// Add your custom actions here
),
agent.WithStateFile("./state/my-agent.state.json"),
agent.WithCharacterFile("./state/my-agent.character.json"),
agent.WithTimeout("10m"),
agent.EnableKnowledgeBase(),
agent.EnableReasoning(),
)
if err != nil {
log.Fatal(err)
}
// Start the agent
go func() {
if err := agent.Run(); err != nil {
log.Printf("Agent stopped: %v", err)
}
}()
// Stop the agent when done
agent.Stop()
```
This basic example shows how to:
- Create a single agent with essential configuration
- Set up the agent's model and API connection
- Configure basic features like knowledge base and reasoning
- Start and stop the agent
</details>
<details>
<summary><strong>Advanced Usage: Agent Pools</strong></summary>
For managing multiple agents, you can use the AgentPool system:
```go
import (
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
)
// Create a new agent pool
pool, err := state.NewAgentPool(
"default-model", // default model name
"default-multimodal-model", // default multimodal model
"image-model", // image generation model
"http://localhost:8080", // API URL
"your-api-key", // API key
"./state", // state directory
"", // 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
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
@@ -285,7 +439,8 @@ cd ../.. && go run main.go
Link your agents to the services you already use. Configuration examples below.
### GitHub Issues
<details>
<summary><strong>GitHub Issues</strong></summary>
```json
{
@@ -295,8 +450,10 @@ Link your agents to the services you already use. Configuration examples below.
"botUserName": "bot-username"
}
```
</details>
### Discord
<details>
<summary><strong>Discord</strong></summary>
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
@@ -308,8 +465,10 @@ After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/dis
```
> Don't forget to enable "Message Content Intent" in Bot(tab) settings!
> Enable " Message Content Intent " in the Bot tab!
</details>
### Slack
<details>
<summary><strong>Slack</strong></summary>
Use the included `slack.yaml` manifest to create your app, then configure:
@@ -322,9 +481,10 @@ Use the included `slack.yaml` manifest to create your app, then configure:
- Create Oauth token bot token from "OAuth & Permissions" -> "OAuth Tokens for Your Workspace"
- Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
</details>
### Telegram
<details>
<summary><strong>Telegram</strong></summary>
Get a token from @botfather, then:
@@ -333,8 +493,10 @@ Get a token from @botfather, then:
"token": "your-bot-father-token"
}
```
</details>
### IRC
<details>
<summary><strong>IRC</strong></summary>
Connect to IRC networks:
@@ -347,10 +509,12 @@ Connect to IRC networks:
"alwaysReply": "false"
}
```
</details>
## REST API
### Agent Management
<details>
<summary><strong>Agent Management</strong></summary>
| Endpoint | Method | Description | Example |
|----------|--------|-------------|---------|
@@ -365,8 +529,10 @@ Connect to IRC networks:
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
| `/settings/import` | POST | Import agent config | [Example](#import-agent) |
</details>
### Actions and Groups
<details>
<summary><strong>Actions and Groups</strong></summary>
| Endpoint | Method | Description | Example |
|----------|--------|-------------|---------|
@@ -374,8 +540,10 @@ Connect to IRC networks:
| `/api/action/:name/run` | POST | Execute an action | |
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
| `/api/agent/group/create` | POST | Create a new agent group | |
</details>
### Chat Interactions
<details>
<summary><strong>Chat Interactions</strong></summary>
| Endpoint | Method | Description | Example |
|----------|--------|-------------|---------|
@@ -383,6 +551,7 @@ Connect to IRC networks:
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
| `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) |
| `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) |
</details>
<details>
<summary><strong>Curl Examples</strong></summary>
@@ -470,11 +639,13 @@ curl -X POST "http://localhost:3000/api/notify/my-agent" \
curl -N -X GET "http://localhost:3000/api/sse/my-agent"
```
Note: For proper SSE handling, you should use a client that supports SSE natively.
</details>
### Agent Configuration Reference
<details>
<summary><strong>Configuration Structure</strong></summary>
The agent configuration defines how an agent behaves and what capabilities it has. You can view the available configuration options and their descriptions by using the metadata endpoint:
```bash
@@ -507,6 +678,25 @@ Here's an example of the agent configuration structure:
"summary_long_term_memory": false
}
```
</details>
<details>
<summary><strong>Environment Configuration</strong></summary>
LocalAGI supports environment configurations. Note that these environment variables needs to be specified in the localagi container in the docker-compose file to have effect.
| Variable | What It Does |
|----------|--------------|
| `LOCALAGI_MODEL` | Your go-to model |
| `LOCALAGI_MULTIMODAL_MODEL` | Optional model for multimodal capabilities |
| `LOCALAGI_LLM_API_URL` | OpenAI-compatible API server URL |
| `LOCALAGI_LLM_API_KEY` | API authentication |
| `LOCALAGI_TIMEOUT` | Request timeout settings |
| `LOCALAGI_STATE_DIR` | Where state gets stored |
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
</details>
## LICENSE

View File

@@ -81,7 +81,7 @@ func (a *CustomAction) Plannable() bool {
return true
}
func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *CustomAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
if err != nil {
return types.ActionResult{}, err
@@ -95,6 +95,11 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
func (a *CustomAction) Definition() types.ActionDefinition {
if a.i == nil {
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
return types.ActionDefinition{}
}
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
if err != nil {
xlog.Error("Error getting custom action definition", "error", err)

View File

@@ -76,7 +76,7 @@ return []string{"foo"}
Description: "A test action",
}))
runResult, err := customAction.Run(context.Background(), types.ActionParams{
runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
"Foo": "bar",
})
Expect(err).ToNot(HaveOccurred())

View File

@@ -21,7 +21,7 @@ type GoalResponse struct {
Achieved bool `json:"achieved"`
}
func (a *GoalAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *GoalAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}

View File

@@ -22,7 +22,7 @@ type IntentResponse struct {
Reasoning string `json:"reasoning"`
}
func (a *IntentAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}

View File

@@ -19,7 +19,7 @@ type ConversationActionResponse struct {
Message string `json:"message"`
}
func (a *ConversationAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *ConversationAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}

View File

@@ -16,7 +16,7 @@ func NewStop() *StopAction {
type StopAction struct{}
func (a *StopAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *StopAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}

View File

@@ -30,7 +30,7 @@ type PlanSubtask struct {
Reasoning string `json:"reasoning"`
}
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *PlanAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}

View File

@@ -20,7 +20,7 @@ type ReasoningResponse struct {
Reasoning string `json:"reasoning"`
}
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{}, nil
}

View File

@@ -22,7 +22,7 @@ type ReplyResponse struct {
Message string `json:"message"`
}
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) {
func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) {
return "no-op", nil
}

View File

@@ -15,7 +15,7 @@ func NewState() *StateAction {
type StateAction struct{}
func (a *StateAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
func (a *StateAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
return types.ActionResult{Result: "internal state has been updated"}, nil
}

View File

@@ -480,13 +480,18 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
}, c...)
}
reasoningAction := action.NewReasoning()
thought, err := a.decision(job,
c,
types.Actions{action.NewReasoning()}.ToTools(),
action.NewReasoning().Definition().Name.String(), maxRetries)
types.Actions{reasoningAction}.ToTools(),
reasoningAction.Definition().Name.String(), maxRetries)
if err != nil {
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 := ""
response := &action.ReasoningResponse{}
if thought.actionParams != nil {

View File

@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"sync"
"time"
@@ -27,7 +29,7 @@ type Agent struct {
sync.Mutex
options *options
Character Character
client *openai.Client
client llm.LLMClient
jobQueue chan *types.Job
context *types.ActionContext
@@ -44,6 +46,8 @@ type Agent struct {
newMessagesSubscribers []func(openai.ChatCompletionMessage)
observer Observer
sharedState *types.AgentSharedState
}
type RAGDB interface {
@@ -59,7 +63,12 @@ func New(opts ...Option) (*Agent, error) {
return nil, fmt.Errorf("failed to set options: %v", err)
}
client := llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout)
var client llm.LLMClient
if options.llmClient != nil {
client = options.llmClient
} else {
client = llm.NewClient(options.LLMAPI.APIKey, options.LLMAPI.APIURL, options.timeout)
}
c := context.Background()
if options.context != nil {
@@ -76,6 +85,7 @@ func New(opts ...Option) (*Agent, error) {
context: types.NewActionContext(ctx, cancel),
newConversations: make(chan openai.ChatCompletionMessage),
newMessagesSubscribers: options.newConversationsSubscribers,
sharedState: types.NewAgentSharedState(options.lastMessageDuration),
}
// Initialize observer if provided
@@ -116,6 +126,15 @@ func New(opts ...Option) (*Agent, error) {
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() {
go func() {
for {
@@ -180,6 +199,12 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
}()
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,
@@ -286,7 +311,7 @@ func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params type
for _, act := range a.availableActions() {
if act.Definition().Name == chosenAction.Definition().Name {
res, err := act.Run(job.GetContext(), params)
res, err := act.Run(job.GetContext(), a.sharedState, params)
if err != nil {
if obs != nil {
obs.Completion = &types.Completion{
@@ -484,13 +509,84 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me
return conv
}
func (a *Agent) consumeJob(job *types.Job, role string) {
func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
hasTriggers := false
triggeredBy := ""
failedBy := ""
if job.DoneFilter {
return true, nil
}
job.DoneFilter = true
if len(a.options.jobFilters) < 1 {
xlog.Debug("No filters")
return true, nil
}
for _, filter := range a.options.jobFilters {
name := filter.Name()
if triggeredBy != "" && filter.IsTrigger() {
continue
}
ok, err = filter.Apply(job)
if err != nil {
xlog.Error("Error in job filter", "filter", name, "error", err)
failedBy = name
break
}
if filter.IsTrigger() {
hasTriggers = true
if ok {
triggeredBy = name
xlog.Info("Job triggered by filter", "filter", name)
}
} else if !ok {
failedBy = name
xlog.Info("Job failed filter", "filter", name)
break
} else {
xlog.Debug("Job passed filter", "filter", name)
}
}
if a.Observer() != nil {
obs := a.Observer().NewObservable()
obs.Name = "filter"
obs.Icon = "shield"
obs.ParentID = job.Obs.ID
if err == nil {
obs.Completion = &types.Completion{
FilterResult: &types.FilterResult{
HasTriggers: hasTriggers,
TriggeredBy: triggeredBy,
FailedBy: failedBy,
},
}
} else {
obs.Completion = &types.Completion{
Error: err.Error(),
}
}
a.Observer().Update(*obs)
}
return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil
}
func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
if err := job.GetContext().Err(); err != nil {
job.Result.Finish(fmt.Errorf("expired"))
return
}
if retries < 1 {
job.Result.Finish(fmt.Errorf("Exceeded recursive retries"))
return
}
a.Lock()
paused := a.pause
a.Unlock()
@@ -520,6 +616,14 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
}
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)
// RAG
@@ -553,7 +657,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
xlog.Error("Error generating parameters, trying again", "error", err)
// try again
job.SetNextAction(&chosenAction, nil, reasoning)
a.consumeJob(job, role)
a.consumeJob(job, role, retries-1)
return
}
actionParams = p.actionParams
@@ -571,36 +675,15 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
}
}
// check if the agent is looping over the same action
// if so, we need to stop it
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
count := map[string]int{}
for i := len(job.GetPastActions()) - 1; i >= 0; i-- {
pastAction := job.GetPastActions()[i]
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
pastAction.Params.String() == actionParams.String() {
count[chosenAction.Definition().Name.String()]++
}
}
if count[chosenAction.Definition().Name.String()] > a.options.loopDetectionSteps {
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
return
}
}
//xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning)
if chosenAction == nil {
// If 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.
//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)
if reasoning != "" {
conv = append(conv, openai.ChatCompletionMessage{
Role: "assistant",
Content: reasoning,
Content: a.cleanupLLMResponse(reasoning),
})
} else {
xlog.Info("No reasoning, just reply", "agent", a.Character.Name)
@@ -609,10 +692,28 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err))
return
}
msg.Content = a.cleanupLLMResponse(msg.Content)
conv = append(conv, msg)
reasoning = msg.Content
}
var satisfied bool
var err error
// Evaluate the response
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
if err != nil {
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
return
}
if !satisfied {
// If not satisfied, continue with the conversation
job.ConversationHistory = conv
job.IncrementEvaluationLoop()
a.consumeJob(job, role, retries)
return
}
xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv))
job.Result.Conversation = conv
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
@@ -642,7 +743,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
xlog.Error("Error generating parameters, trying again", "error", err)
// try again
job.SetNextAction(&chosenAction, nil, reasoning)
a.consumeJob(job, role)
a.consumeJob(job, role, retries-1)
return
}
actionParams = params.actionParams
@@ -662,6 +763,22 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
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)
if !job.Callback(types.ActionCurrentState{
@@ -686,8 +803,6 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv)
if err != nil {
xlog.Error("error handling planning", "error", err)
//job.Result.Conversation = conv
//job.Result.SetResponse(msg.Content)
a.reply(job, role, append(conv, openai.ChatCompletionMessage{
Role: "assistant",
Content: fmt.Sprintf("Error handling planning: %v", err),
@@ -734,9 +849,6 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
result, err := a.runAction(job, chosenAction, actionParams)
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)
}
@@ -775,42 +887,54 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
// The agent decided to do another action
// call ourselves again
job.SetNextAction(&followingAction, &followingParams, reasoning)
a.consumeJob(job, role)
a.consumeJob(job, role, retries)
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
}
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) {
job.Result.Conversation = conv
// At this point can only be a reply action
xlog.Info("Computing reply", "agent", a.Character.Name)
// 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
// }
// }
forceResponsePrompt := "Reply to the user without using any tools or function calls. Just reply with the message."
// If we have a hud, display it when answering normally
if a.options.enableHUD {
@@ -826,39 +950,19 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
Role: "system",
Content: prompt,
},
{
Role: "system",
Content: forceResponsePrompt,
},
}, conv...)
}
}
// Generate a human-readable response
// resp, err := a.client.CreateChatCompletion(ctx,
// openai.ChatCompletionRequest{
// Model: a.options.LLMAPI.Model,
// 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
} else {
conv = append([]openai.ChatCompletionMessage{
{
Role: "system",
Content: forceResponsePrompt,
},
}, conv...)
}
xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name)
@@ -871,13 +975,21 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
return
}
// If we didn't got any message, we can use the response from the action
if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" {
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
msg.Content = a.cleanupLLMResponse(msg.Content)
msg = openai.ChatCompletionMessage{
Role: "assistant",
Content: replyResponse.Message,
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)
msg.Content = a.cleanupLLMResponse(replyResponse.Message)
}
}
@@ -960,7 +1072,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
types.WithReasoningCallback(a.options.reasoningCallback),
types.WithResultCallback(a.options.resultCallback),
)
a.consumeJob(whatNext, SystemRole)
a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps)
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
@@ -1044,7 +1156,7 @@ func (a *Agent) run(timer *time.Timer) error {
<-timer.C
}
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
a.consumeJob(job, UserRole)
a.consumeJob(job, UserRole, a.options.loopDetectionSteps)
timer.Reset(a.options.periodicRuns)
case <-a.context.Done():
// Agent has been canceled, return error

View File

@@ -1,6 +1,7 @@
package agent_test
import (
"net/url"
"os"
"testing"
@@ -13,15 +14,19 @@ func TestAgent(t *testing.T) {
RunSpecs(t, "Agent test suite")
}
var testModel = os.Getenv("LOCALAGI_MODEL")
var apiURL = os.Getenv("LOCALAI_API_URL")
var apiKeyURL = os.Getenv("LOCALAI_API_KEY")
var (
testModel = os.Getenv("LOCALAGI_MODEL")
apiURL = os.Getenv("LOCALAI_API_URL")
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() {
if testModel == "" {
testModel = "hermes-2-pro-mistral"
}
if apiURL == "" {
apiURL = "http://192.168.68.113:8080"
}
useRealLocalAI = isValidURL(apiURL) && apiURL != "" && testModel != ""
}

View File

@@ -7,9 +7,11 @@ import (
"strings"
"sync"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/services/actions"
"github.com/mudler/LocalAGI/core/action"
. "github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
. "github.com/onsi/ginkgo/v2"
@@ -44,7 +46,7 @@ func (a *TestAction) Plannable() bool {
return true
}
func (a *TestAction) Run(c context.Context, p types.ActionParams) (types.ActionResult, error) {
func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
for k, r := range a.response {
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
return types.ActionResult{Result: r}, nil
@@ -111,25 +113,102 @@ 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() {
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() {
BeforeEach(func() {
Eventually(func() error {
// test apiURL is working and available
if useRealLocalAI {
_, err := http.Get(apiURL + "/readyz")
return err
}
return nil
}, "10m", "10s").ShouldNot(HaveOccurred())
})
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(
WithLLMAPIURL(apiURL),
WithLLMClient(llmClient),
WithModel(testModel),
EnableForceReasoning,
WithTimeout("10m"),
WithLoopDetectionSteps(3),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{
"boston": testActionResult,
"milan": testActionResult2,
@@ -139,7 +218,6 @@ var _ = Describe("Agent test", func() {
Expect(err).ToNot(HaveOccurred())
go agent.Run()
defer agent.Stop()
res := agent.Ask(
append(debugOptions,
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
@@ -148,40 +226,51 @@ var _ = Describe("Agent test", func() {
Expect(res.Error).ToNot(HaveOccurred())
reasons := []string{}
for _, r := range res.State {
reasons = append(reasons, r.Result)
}
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
reasons = []string{}
res = agent.Ask(
append(debugOptions,
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
)...)
for _, r := range res.State {
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))
// 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() {
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(
WithLLMAPIURL(apiURL),
WithLLMClient(llmClient),
WithModel(testModel),
WithTimeout("10m"),
// WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{
"boston": testActionResult,
},
}),
}}),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
@@ -198,13 +287,29 @@ var _ = Describe("Agent test", 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(
WithLLMAPIURL(apiURL),
WithLLMClient(llmClient),
WithModel(testModel),
WithTimeout("10m"),
EnableHUD,
// EnableStandaloneJob,
// WithRandomIdentity(),
WithPermanentGoal("I want to learn to play music"),
)
Expect(err).ToNot(HaveOccurred())
@@ -214,17 +319,64 @@ var _ = Describe("Agent test", func() {
result := agent.Ask(
types.WithText("Update your goals such as you want to learn to play the guitar"),
)
fmt.Printf("%+v\n", result)
fmt.Fprintf(GinkgoWriter, "\n%+v\n", result)
Expect(result.Error).ToNot(HaveOccurred())
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
})
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(
WithLLMAPIURL(apiURL),
WithLLMClient(llmClient),
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithLoopDetectionSteps(2),
WithActions(
&TestAction{response: map[string]string{
"boston": testActionResult,
@@ -233,8 +385,6 @@ var _ = Describe("Agent test", func() {
),
EnablePlanning,
EnableForceReasoning,
// EnableStandaloneJob,
// WithRandomIdentity(),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
@@ -256,17 +406,44 @@ var _ = Describe("Agent test", func() {
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() {
var llmClient llm.LLMClient
message := openai.ChatCompletionMessage{}
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(
WithLLMAPIURL(apiURL),
WithLLMClient(llmClient),
WithModel(testModel),
WithLLMAPIKey(apiKeyURL),
WithTimeout("10m"),
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
mu.Lock()
message = m
@@ -282,8 +459,6 @@ var _ = Describe("Agent test", func() {
EnableHUD,
WithPeriodicRuns("1s"),
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
// EnableStandaloneJob,
// WithRandomIdentity(),
)
Expect(err).ToNot(HaveOccurred())
go agent.Run()
@@ -293,7 +468,7 @@ var _ = Describe("Agent test", func() {
mu.Lock()
defer mu.Unlock()
return message.Content
}, "10m", "10s").ShouldNot(BeEmpty())
}, "10m", "1s").ShouldNot(BeEmpty())
})
/*
@@ -347,7 +522,7 @@ var _ = Describe("Agent test", func() {
// result := agent.Ask(
// WithText("Update your goals such as you want to learn to play the guitar"),
// )
// fmt.Printf("%+v\n", result)
// fmt.Fprintf(GinkgoWriter, "%+v\n", result)
// Expect(result.Error).ToNot(HaveOccurred())
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
})

162
core/agent/evaluation.go Normal file
View File

@@ -0,0 +1,162 @@
package agent
import (
"fmt"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
type EvaluationResult struct {
Satisfied bool `json:"satisfied"`
Gaps []string `json:"gaps"`
Reasoning string `json:"reasoning"`
}
type GoalExtraction struct {
Goal string `json:"goal"`
Constraints []string `json:"constraints"`
Context string `json:"context"`
}
func (a *Agent) extractGoal(job *types.Job, conv []openai.ChatCompletionMessage) (*GoalExtraction, error) {
// Create the goal extraction schema
schema := jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"goal": {
Type: jsonschema.String,
Description: "The main goal or request from the user",
},
"constraints": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.String,
},
Description: "Any constraints or requirements specified by the user",
},
"context": {
Type: jsonschema.String,
Description: "Additional context that might be relevant for understanding the goal",
},
},
Required: []string{"goal", "constraints", "context"},
}
// Create the goal extraction prompt
prompt := `Analyze the conversation and extract the user's main goal, any constraints, and relevant context.
Consider the entire conversation history to understand the complete context and requirements.
Focus on identifying the primary objective and any specific requirements or limitations mentioned.`
var result GoalExtraction
err := llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
append(
[]openai.ChatCompletionMessage{
{
Role: "system",
Content: prompt,
},
},
conv...), a.options.LLMAPI.Model, schema, &result)
if err != nil {
return nil, fmt.Errorf("error extracting goal: %w", err)
}
return &result, nil
}
func (a *Agent) evaluateJob(job *types.Job, conv []openai.ChatCompletionMessage) (*EvaluationResult, error) {
if !a.options.enableEvaluation {
return &EvaluationResult{Satisfied: true}, nil
}
// Extract the goal first
goal, err := a.extractGoal(job, conv)
if err != nil {
return nil, fmt.Errorf("error extracting goal: %w", err)
}
// Create the evaluation schema
schema := jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"satisfied": {
Type: jsonschema.Boolean,
},
"gaps": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.String,
},
},
"reasoning": {
Type: jsonschema.String,
},
},
Required: []string{"satisfied", "gaps", "reasoning"},
}
// Create the evaluation prompt
prompt := fmt.Sprintf(`Evaluate if the assistant has satisfied the user's request. Consider:
1. The identified goal: %s
2. Constraints and requirements: %v
3. Context: %s
4. The conversation history
5. Any gaps or missing information
6. Whether the response fully addresses the user's needs
Provide a detailed evaluation with specific gaps if any are found.`,
goal.Goal,
goal.Constraints,
goal.Context)
var result EvaluationResult
err = llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
append(
[]openai.ChatCompletionMessage{
{
Role: "system",
Content: prompt,
},
},
conv...),
a.options.LLMAPI.Model, schema, &result)
if err != nil {
return nil, fmt.Errorf("error generating evaluation: %w", err)
}
return &result, nil
}
func (a *Agent) handleEvaluation(job *types.Job, conv []openai.ChatCompletionMessage, currentLoop int) (bool, []openai.ChatCompletionMessage, error) {
if !a.options.enableEvaluation || currentLoop >= a.options.maxEvaluationLoops {
return true, conv, nil
}
result, err := a.evaluateJob(job, conv)
if err != nil {
return false, conv, err
}
if result.Satisfied {
return true, conv, nil
}
// If there are gaps, we need to address them
if len(result.Gaps) > 0 {
// Add the evaluation result to the conversation
conv = append(conv, openai.ChatCompletionMessage{
Role: "system",
Content: fmt.Sprintf("Evaluation found gaps that need to be addressed:\n%s\nReasoning: %s",
result.Gaps, result.Reasoning),
})
xlog.Debug("Evaluation found gaps, incrementing loop count", "loop", currentLoop+1)
return false, conv, nil
}
return true, conv, nil
}

View File

@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
guidance = "Generate a random character for roleplaying."
}
err := llm.GenerateTypedJSON(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
err := llm.GenerateTypedJSONWithGuidance(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
a.Character = a.options.character
if err != nil {

View File

@@ -38,7 +38,7 @@ func (a *mcpAction) Plannable() bool {
return true
}
func (m *mcpAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
if err != nil {
xlog.Error("Failed to call tool", "error", err.Error())

View File

@@ -7,6 +7,7 @@ import (
"github.com/mudler/LocalAGI/core/types"
"github.com/sashabaranov/go-openai"
"github.com/mudler/LocalAGI/pkg/llm"
)
type Option func(*options) error
@@ -19,12 +20,15 @@ type llmOptions struct {
}
type options struct {
llmClient llm.LLMClient
LLMAPI llmOptions
character Character
randomIdentityGuidance string
randomIdentity bool
userActions types.Actions
jobFilters types.JobFilters
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
stripThinkingTags bool
canStopItself bool
initiateConversations bool
@@ -40,6 +44,10 @@ type options struct {
kbResults int
ragdb RAGDB
// Evaluation settings
maxEvaluationLoops int
enableEvaluation bool
prompts []DynamicPrompt
systemPrompt string
@@ -58,6 +66,16 @@ type options struct {
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 {
@@ -68,6 +86,9 @@ func defaultOptions() *options {
return &options{
parallelJobs: 1,
periodicRuns: 15 * time.Minute,
loopDetectionSteps: 10,
maxEvaluationLoops: 2,
enableEvaluation: false,
LLMAPI: llmOptions{
APIURL: "http://localhost:8080",
Model: "gpt-4",
@@ -142,6 +163,17 @@ 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
@@ -371,9 +403,35 @@ func WithActions(actions ...types.Action) Option {
}
}
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
}
}

View File

@@ -1,29 +1,57 @@
package agent_test
import (
"net/http"
"context"
"fmt"
"github.com/mudler/LocalAGI/pkg/llm"
"github.com/sashabaranov/go-openai"
. "github.com/mudler/LocalAGI/core/agent"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Agent test", func() {
Context("identity", func() {
var agent *Agent
BeforeEach(func() {
Eventually(func() error {
// test apiURL is working and available
_, err := http.Get(apiURL + "/readyz")
return err
}, "10m", "10s").ShouldNot(HaveOccurred())
})
// BeforeEach(func() {
// Eventually(func() error {
// // test apiURL is working and available
// _, err := http.Get(apiURL + "/readyz")
// return err
// }, "10m", "10s").ShouldNot(HaveOccurred())
// })
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
agent, err = New(
WithLLMAPIURL(apiURL),
WithLLMClient(llmClient),
WithModel(testModel),
WithTimeout("10m"),
WithRandomIdentity(),
@@ -37,14 +65,40 @@ var _ = Describe("Agent test", func() {
Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
})
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
agent, err = New(WithRandomIdentity())
agent, err = New(
WithLLMClient(mock),
WithRandomIdentity(),
)
Expect(err).To(HaveOccurred())
})
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
agent, err := New(
WithLLMClient(mock),
WithLLMAPIURL(apiURL),
WithModel(testModel),
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),

View File

@@ -0,0 +1,13 @@
package conversations_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestConversations(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Conversations test suite")
}

View File

@@ -1,4 +1,4 @@
package connectors
package conversations
import (
"fmt"

View File

@@ -1,9 +1,9 @@
package connectors_test
package conversations_test
import (
"time"
"github.com/mudler/LocalAGI/services/connectors"
"github.com/mudler/LocalAGI/core/conversations"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sashabaranov/go-openai"
@@ -11,13 +11,13 @@ import (
var _ = Describe("ConversationTracker", func() {
var (
tracker *connectors.ConversationTracker[string]
tracker *conversations.ConversationTracker[string]
duration time.Duration
)
BeforeEach(func() {
duration = 1 * time.Second
tracker = connectors.NewConversationTracker[string](duration)
tracker = conversations.NewConversationTracker[string](duration)
})
It("should initialize with empty conversations", func() {
@@ -81,8 +81,8 @@ var _ = Describe("ConversationTracker", func() {
})
It("should handle different key types", func() {
trackerInt := connectors.NewConversationTracker[int](duration)
trackerInt64 := connectors.NewConversationTracker[int64](duration)
trackerInt := conversations.NewConversationTracker[int](duration)
trackerInt64 := conversations.NewConversationTracker[int64](duration)
message := openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser,

View File

@@ -31,6 +31,11 @@ func (d DynamicPromptsConfig) ToMap() map[string]string {
return config
}
type FiltersConfig struct {
Type string `json:"type"`
Config string `json:"config"`
}
type AgentConfig struct {
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
Actions []ActionsConfig `json:"actions" form:"actions"`
@@ -39,6 +44,7 @@ type AgentConfig struct {
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"`
@@ -48,6 +54,7 @@ type AgentConfig struct {
APIKey string `json:"api_key" form:"api_key"`
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
Name string `json:"name" form:"name"`
HUD bool `json:"hud" form:"hud"`
@@ -67,9 +74,13 @@ type AgentConfig struct {
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"`
EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"`
MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"`
}
type AgentConfigMeta struct {
Filters []config.FieldGroup
Fields []config.Field
Connectors []config.FieldGroup
Actions []config.FieldGroup
@@ -81,6 +92,7 @@ func NewAgentConfigMeta(
actionsConfig []config.FieldGroup,
connectorsConfig []config.FieldGroup,
dynamicPromptsConfig []config.FieldGroup,
filtersConfig []config.FieldGroup,
) AgentConfigMeta {
return AgentConfigMeta{
Fields: []config.Field{
@@ -253,7 +265,7 @@ func NewAgentConfigMeta(
Name: "enable_reasoning",
Label: "Enable Reasoning",
Type: "checkbox",
DefaultValue: false,
DefaultValue: true,
HelpText: "Enable agent to explain its reasoning process",
Tags: config.Tags{Section: "AdvancedSettings"},
},
@@ -292,6 +304,40 @@ func NewAgentConfigMeta(
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{
{
@@ -310,6 +356,7 @@ func NewAgentConfigMeta(
DynamicPrompts: dynamicPromptsConfig,
Connectors: connectorsConfig,
Actions: actionsConfig,
Filters: filtersConfig,
}
}

View File

@@ -38,6 +38,7 @@ type AgentPool struct {
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
connectors func(*AgentConfig) []Connector
dynamicPrompt func(*AgentConfig) []DynamicPrompt
filters func(*AgentConfig) types.JobFilters
timeout string
conversationLogs string
}
@@ -78,6 +79,7 @@ func NewAgentPool(
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
connectors func(*AgentConfig) []Connector,
promptBlocks func(*AgentConfig) []DynamicPrompt,
filters func(*AgentConfig) types.JobFilters,
timeout string,
withLogs bool,
) (*AgentPool, error) {
@@ -110,6 +112,7 @@ func NewAgentPool(
connectors: connectors,
availableActions: availableActions,
dynamicPrompt: promptBlocks,
filters: filters,
timeout: timeout,
conversationLogs: conversationPath,
}, nil
@@ -135,6 +138,7 @@ func NewAgentPool(
connectors: connectors,
localRAGAPI: LocalRAGAPI,
dynamicPrompt: promptBlocks,
filters: filters,
availableActions: availableActions,
timeout: timeout,
conversationLogs: conversationPath,
@@ -243,7 +247,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
ImagePrompt string `json:"image_prompt"`
}
err := llm.GenerateTypedJSON(
err := llm.GenerateTypedJSONWithGuidance(
context.Background(),
llm.NewClient(APIKey, APIURL, "10m"),
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
@@ -337,6 +341,8 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
if config.Model != "" {
model = config.Model
} else {
config.Model = model
}
if config.MCPBoxURL != "" {
@@ -347,12 +353,17 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
config.PeriodicRuns = "10m"
}
// XXX: Why do we update the pool config from an Agent's config?
if config.APIURL != "" {
a.apiURL = config.APIURL
} else {
config.APIURL = a.apiURL
}
if config.APIKey != "" {
a.apiKey = config.APIKey
} else {
config.APIKey = a.apiKey
}
if config.LocalRAGURL != "" {
@@ -366,6 +377,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
connectors := a.connectors(config)
promptBlocks := a.dynamicPrompt(config)
actions := a.availableActions(config)(ctx, a)
filters := a.filters(config)
stateFile, characterFile := a.stateFiles(name)
actionsLog := []string{}
@@ -378,6 +390,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
}
filtersLog := []string{}
for _, filter := range filters {
filtersLog = append(filtersLog, filter.Name())
}
xlog.Info(
"Creating agent",
"name", name,
@@ -385,6 +402,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
"api_url", a.apiURL,
"actions", actionsLog,
"connectors", connectorLog,
"filters", filtersLog,
)
// dynamicPrompts := []map[string]string{}
@@ -406,6 +424,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
WithMCPSTDIOServers(config.MCPSTDIOServers...),
WithMCPBoxURL(a.mcpBoxURL),
WithPrompts(promptBlocks...),
WithJobFilters(filters...),
WithMCPPrepareScript(config.MCPPrepareScript),
// WithDynamicPrompts(dynamicPrompts...),
WithCharacter(Character{
@@ -443,6 +462,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
}),
WithSystemPrompt(config.SystemPrompt),
WithMultimodalModel(multimodalModel),
WithLastMessageDuration(config.LastMessageDuration),
WithAgentResultCallback(func(state types.ActionState) {
a.Lock()
if _, ok := a.agentStatus[name]; !ok {
@@ -526,6 +546,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
opts = append(opts, EnableForceReasoning)
}
if config.StripThinkingTags {
opts = append(opts, EnableStripThinkingTags)
}
if config.KnowledgeBaseResults > 0 {
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
}
@@ -538,6 +562,13 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
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)
agent, err := New(opts...)

View File

@@ -88,7 +88,7 @@ func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
// Actions is something the agent can do
type Action interface {
Run(ctx context.Context, action ActionParams) (ActionResult, error)
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
Definition() ActionDefinition
Plannable() bool
}

15
core/types/filters.go Normal file
View File

@@ -0,0 +1,15 @@
package types
type JobFilter interface {
Name() string
Apply(job *Job) (bool, error)
IsTrigger() bool
}
type JobFilters []JobFilter
type FilterResult struct {
HasTriggers bool `json:"has_triggers"`
TriggeredBy string `json:"triggered_by,omitempty"`
FailedBy string `json:"failed_by,omitempty"`
}

View File

@@ -19,6 +19,7 @@ type Job struct {
ConversationHistory []openai.ChatCompletionMessage
UUID string
Metadata map[string]interface{}
DoneFilter bool
pastActions []*ActionRequest
nextAction *Action
@@ -162,22 +163,22 @@ func newUUID() string {
func NewJob(opts ...JobOption) *Job {
j := &Job{
Result: NewJobResult(),
UUID: newUUID(),
}
for _, o := range opts {
o(j)
UUID: uuid.New().String(),
Metadata: make(map[string]interface{}),
context: context.Background(),
ConversationHistory: []openai.ChatCompletionMessage{},
}
var ctx context.Context
if j.context == nil {
ctx = context.Background()
} else {
ctx = j.context
for _, opt := range opts {
opt(j)
}
context, cancel := context.WithCancel(ctx)
j.context = context
// Store the original request if it exists in the conversation history
ctx, cancel := context.WithCancel(j.context)
j.context = ctx
j.cancel = cancel
return j
}
@@ -206,3 +207,23 @@ func WithObservable(obs *Observable) JobOption {
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
}

View File

@@ -6,6 +6,7 @@ import (
)
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"`
@@ -23,7 +24,8 @@ type Completion struct {
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"`
AgentState *AgentInternalState `json:"agent_state,omitempty"`
FilterResult *FilterResult `json:"filter_result,omitempty"`
}
type Observable struct {

View File

@@ -1,6 +1,11 @@
package types
import "fmt"
import (
"fmt"
"time"
"github.com/mudler/LocalAGI/core/conversations"
)
// State is the structure
// that is used to keep track of the current state
@@ -20,6 +25,23 @@ type AgentInternalState struct {
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

View File

@@ -17,6 +17,11 @@ services:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
service: dind
localrecall:
extends:
file: docker-compose.yaml

View File

@@ -22,6 +22,11 @@ services:
file: docker-compose.yaml
service: mcpbox
dind:
extends:
file: docker-compose.yaml
service: dind
localrecall:
extends:
file: docker-compose.yaml

View File

@@ -54,14 +54,28 @@ services:
- "8080"
volumes:
- ./volumes/mcpbox:/app/data
# share docker socket if you want it to be able to run docker commands
- /var/run/docker.sock:/var/run/docker.sock
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:
depends_on:
localai:

73
go.mod
View File

@@ -10,70 +10,77 @@ require (
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
github.com/donseba/go-htmx v1.12.0
github.com/eritikass/githubmarkdownconvertergo v0.1.10
github.com/go-telegram/bot v1.14.2
github.com/go-telegram/bot v1.15.0
github.com/gofiber/fiber/v2 v2.52.6
github.com/gofiber/template/html/v2 v2.1.3
github.com/google/go-github/v69 v69.2.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/metoro-io/mcp-golang v0.11.0
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/philippgille/chromem-go v0.7.0
github.com/sashabaranov/go-openai v1.38.2
github.com/sashabaranov/go-openai v1.39.1
github.com/slack-go/slack v0.16.0
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
github.com/tmc/langchaingo v0.1.13
github.com/traefik/yaegi v0.16.1
github.com/valyala/fasthttp v1.60.0
github.com/valyala/fasthttp v1.61.0
golang.org/x/crypto v0.37.0
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
maunium.net/go/mautrix v0.17.0
mvdan.cc/xurls/v2 v2.6.0
)
require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/antchfx/htmlquery v1.3.0 // indirect
github.com/antchfx/xmlquery v1.3.17 // indirect
github.com/antchfx/xpath v1.2.4 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/antchfx/htmlquery v1.3.4 // indirect
github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.3.4 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.8.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // 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-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gocolly/colly v1.2.0 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
@@ -81,17 +88,21 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
go.mau.fi/util v0.3.0 // indirect
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/arch v0.16.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/text v0.24.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)

280
go.sum
View File

@@ -1,72 +1,73 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk=
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
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/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
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/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/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/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.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-telegram/bot v1.14.2 h1:j9hXerxTuvkw7yFi3sF5jjRVGozNVKkMQSKjMeBJ5FY=
github.com/go-telegram/bot v1.14.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
github.com/go-telegram/bot v1.15.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/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/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
@@ -75,31 +76,17 @@ 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/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
@@ -107,38 +94,38 @@ 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/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
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/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -157,30 +144,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/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
github.com/pkg/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sashabaranov/go-openai v1.38.2 h1:akrssjj+6DY3lWuDwHv6cBvJ8Z+FZDM9XEaaYFt0Auo=
github.com/sashabaranov/go-openai v1.38.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.39.1 h1:TMD4w77Iy9WTFlgnjNaxbAASdsCJ9R/rMdzL+SN14oU=
github.com/sashabaranov/go-openai v1.39.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/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@@ -190,11 +177,10 @@ 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/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
@@ -215,77 +201,87 @@ github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1Ca
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/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog=
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-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.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/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -293,62 +289,46 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.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/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@@ -66,9 +66,11 @@ func main() {
localRAG,
services.Actions(map[string]string{
"browser-agent-runner-base-url": localOperatorBaseURL,
"deep-research-runner-base-url": localOperatorBaseURL,
}),
services.Connectors,
services.DynamicPrompts,
services.Filters,
timeout,
withLogs,
)

View File

@@ -1,13 +1,33 @@
package llm
import (
"context"
"net/http"
"time"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai"
)
func NewClient(APIKey, URL, timeout string) *openai.Client {
type LLMClient interface {
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
if APIKey == "" {
//log.Fatal("OPENAI_API_KEY environment variable not set")
@@ -18,11 +38,12 @@ func NewClient(APIKey, URL, timeout string) *openai.Client {
dur, err := time.ParseDuration(timeout)
if err != nil {
xlog.Error("Failed to parse timeout", "error", err)
dur = 150 * time.Second
}
config.HTTPClient = &http.Client{
Timeout: dur,
}
return openai.NewClientWithConfig(config)
return &realClient{openai.NewClientWithConfig(config)}
}

View File

@@ -10,16 +10,20 @@ import (
"github.com/sashabaranov/go-openai/jsonschema"
)
func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
toolName := "json"
decision := openai.ChatCompletionRequest{
Model: model,
Messages: []openai.ChatCompletionMessage{
func GenerateTypedJSONWithGuidance(ctx context.Context, client LLMClient, guidance, model string, i jsonschema.Definition, dst any) error {
return GenerateTypedJSONWithConversation(ctx, client, []openai.ChatCompletionMessage{
{
Role: "user",
Content: guidance,
},
},
}, model, i, dst)
}
func GenerateTypedJSONWithConversation(ctx context.Context, client LLMClient, conv []openai.ChatCompletionMessage, model string, i jsonschema.Definition, dst any) error {
toolName := "json"
decision := openai.ChatCompletionRequest{
Model: model,
Messages: conv,
Tools: []openai.Tool{
{

25
pkg/llm/mock_client.go Normal file
View File

@@ -0,0 +1,25 @@
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
}

View File

@@ -4,69 +4,146 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// Client represents a client for interacting with the LocalOperator API
type Client struct {
baseURL string
httpClient *http.Client
}
// NewClient creates a new API client
func NewClient(baseURL string) *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{},
httpClient: &http.Client{
Timeout: defaultTimeout,
},
}
}
// AgentRequest represents the request body for running an agent
type AgentRequest struct {
Goal string `json:"goal"`
MaxAttempts int `json:"max_attempts,omitempty"`
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
}
// StateDescription represents a single state in the agent's history
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"` // MIME type of the screenshot (e.g., "image/png")
ScreenshotMimeType string `json:"screenshot_mime_type"`
}
// StateHistory represents the complete history of states during agent execution
type StateHistory struct {
States []StateDescription `json:"states"`
}
// RunAgent sends a request to run an agent with the given goal
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
type DesktopStateDescription struct {
ScreenContent string `json:"screen_content"`
ScreenshotPath string `json:"screenshot_path"`
}
resp, err := c.httpClient.Post(
fmt.Sprintf("%s/api/browser/run", c.baseURL),
"application/json",
bytes.NewBuffer(body),
)
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 nil, fmt.Errorf("failed to send request: %w", err)
return "", fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
var state StateHistory
if err := json.NewDecoder(resp.Body).Decode(&state); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
return resp.Status, nil
}
return &state, 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
}

View File

@@ -19,8 +19,10 @@ const (
ActionSearch = "search"
ActionCustom = "custom"
ActionBrowserAgentRunner = "browser-agent-runner"
ActionDeepResearchRunner = "deep-research-runner"
ActionGithubIssueLabeler = "github-issue-labeler"
ActionGithubIssueOpener = "github-issue-opener"
ActionGithubIssueEditor = "github-issue-editor"
ActionGithubIssueCloser = "github-issue-closer"
ActionGithubIssueSearcher = "github-issue-searcher"
ActionGithubRepositoryGet = "github-repository-get-content"
@@ -33,6 +35,8 @@ const (
ActionGithubPRCreator = "github-pr-creator"
ActionGithubGetAllContent = "github-get-all-repository-content"
ActionGithubREADME = "github-readme"
ActionGithubRepositorySearchFiles = "github-repository-search-files"
ActionGithubRepositoryListFiles = "github-repository-list-files"
ActionScraper = "scraper"
ActionWikipedia = "wikipedia"
ActionBrowse = "browse"
@@ -42,6 +46,7 @@ const (
ActionCounter = "counter"
ActionCallAgents = "call_agents"
ActionShellcommand = "shell-command"
ActionSendTelegramMessage = "send-telegram-message"
)
var AvailableActions = []string{
@@ -49,11 +54,15 @@ var AvailableActions = []string{
ActionCustom,
ActionGithubIssueLabeler,
ActionGithubIssueOpener,
ActionGithubIssueEditor,
ActionGithubIssueCloser,
ActionGithubIssueSearcher,
ActionGithubRepositoryGet,
ActionGithubGetAllContent,
ActionGithubRepositorySearchFiles,
ActionGithubRepositoryListFiles,
ActionBrowserAgentRunner,
ActionDeepResearchRunner,
ActionGithubRepositoryCreateOrUpdate,
ActionGithubIssueReader,
ActionGithubIssueCommenter,
@@ -71,6 +80,7 @@ var AvailableActions = []string{
ActionCounter,
ActionCallAgents,
ActionShellcommand,
ActionSendTelegramMessage,
}
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
@@ -104,6 +114,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
var a types.Action
var err error
if config == nil {
config = map[string]string{}
}
switch name {
case ActionCustom:
a, err = action.NewCustom(config, "")
@@ -115,12 +129,16 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubIssueLabeler(config)
case ActionGithubIssueOpener:
a = actions.NewGithubIssueOpener(config)
case ActionGithubIssueEditor:
a = actions.NewGithubIssueEditor(config)
case ActionGithubIssueCloser:
a = actions.NewGithubIssueCloser(config)
case ActionGithubIssueSearcher:
a = actions.NewGithubIssueSearch(config)
case ActionBrowserAgentRunner:
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
case ActionDeepResearchRunner:
a = actions.NewDeepResearchRunner(config, actionsConfigs["deep-research-runner-base-url"])
case ActionGithubIssueReader:
a = actions.NewGithubIssueReader(config)
case ActionGithubPRReader:
@@ -133,6 +151,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubPRCreator(config)
case ActionGithubGetAllContent:
a = actions.NewGithubRepositoryGetAllContent(config)
case ActionGithubRepositorySearchFiles:
a = actions.NewGithubRepositorySearchFiles(config)
case ActionGithubRepositoryListFiles:
a = actions.NewGithubRepositoryListFiles(config)
case ActionGithubIssueCommenter:
a = actions.NewGithubIssueCommenter(config)
case ActionGithubRepositoryGet:
@@ -157,6 +179,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
case ActionShellcommand:
a = actions.NewShell(config)
case ActionSendTelegramMessage:
a = actions.NewSendTelegramMessageRunner(config)
default:
xlog.Error("Action not found", "name", name)
return nil, fmt.Errorf("Action not found")
@@ -181,6 +205,11 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "Browser Agent Runner",
Fields: actions.BrowserAgentRunnerConfigMeta(),
},
{
Name: "deep-research-runner",
Label: "Deep Research Runner",
Fields: actions.DeepResearchRunnerConfigMeta(),
},
{
Name: "generate_image",
Label: "Generate Image",
@@ -196,6 +225,11 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "GitHub Issue Opener",
Fields: actions.GithubIssueOpenerConfigMeta(),
},
{
Name: "github-issue-editor",
Label: "GitHub Issue Editor",
Fields: actions.GithubIssueEditorConfigMeta(),
},
{
Name: "github-issue-closer",
Label: "GitHub Issue Closer",
@@ -226,6 +260,16 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "GitHub Get All Repository Content",
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
},
{
Name: "github-repository-search-files",
Label: "GitHub Repository Search Files",
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
},
{
Name: "github-repository-list-files",
Label: "GitHub Repository List Files",
Fields: actions.GithubRepositoryListFilesConfigMeta(),
},
{
Name: "github-repository-create-or-update-content",
Label: "GitHub Repository Create/Update Content",
@@ -299,7 +343,12 @@ func ActionsConfigMeta() []config.FieldGroup {
{
Name: "call_agents",
Label: "Call Agents",
Fields: []config.Field{},
Fields: actions.CallAgentConfigMeta(),
},
{
Name: "send-telegram-message",
Label: "Send Telegram Message",
Fields: actions.SendTelegramMessageConfigMeta(),
},
}
}

View File

@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
type BrowseAction struct{}
func (a *BrowseAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *BrowseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
URL string `json:"url"`
}{}

View File

@@ -3,6 +3,7 @@ package actions
import (
"context"
"fmt"
"time"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
@@ -24,7 +25,18 @@ func NewBrowserAgentRunner(config map[string]string, defaultURL string) *Browser
config["baseURL"] = defaultURL
}
client := api.NewClient(config["baseURL"])
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,
@@ -33,7 +45,7 @@ func NewBrowserAgentRunner(config map[string]string, defaultURL string) *Browser
}
}
func (b *BrowserAgentRunner) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
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 {
@@ -117,5 +129,12 @@ func BrowserAgentRunnerConfigMeta() []config.Field {
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.",
},
}
}

View File

@@ -3,26 +3,56 @@ package actions
import (
"context"
"fmt"
"slices"
"strings"
"github.com/mudler/LocalAGI/core/state"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
func trimList(list []string) []string {
for i, v := range list {
list[i] = strings.TrimSpace(v)
}
return list
}
func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction {
whitelist := []string{}
blacklist := []string{}
if v, ok := config["whitelist"]; ok {
if strings.Contains(v, ",") {
whitelist = trimList(strings.Split(v, ","))
} else {
whitelist = []string{v}
}
}
if v, ok := config["blacklist"]; ok {
if strings.Contains(v, ",") {
blacklist = trimList(strings.Split(v, ","))
} else {
blacklist = []string{v}
}
}
return &CallAgentAction{
pool: pool,
myName: agentName,
whitelist: whitelist,
blacklist: blacklist,
}
}
type CallAgentAction struct {
pool *state.AgentPoolInternalAPI
myName string
whitelist []string
blacklist []string
}
func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *CallAgentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
AgentName string `json:"agent_name"`
Message string `json:"message"`
@@ -83,13 +113,32 @@ func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (t
return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil
}
func (a *CallAgentAction) isAllowedToBeCalled(agentName string) bool {
if agentName == a.myName {
return false
}
if len(a.whitelist) > 0 && len(a.blacklist) > 0 {
return slices.Contains(a.whitelist, agentName) && !slices.Contains(a.blacklist, agentName)
}
if len(a.whitelist) > 0 {
return slices.Contains(a.whitelist, agentName)
}
if len(a.blacklist) > 0 {
return !slices.Contains(a.blacklist, agentName)
}
return true
}
func (a *CallAgentAction) Definition() types.ActionDefinition {
allAgents := a.pool.AllAgents()
agents := []string{}
for _, ag := range allAgents {
if ag != a.myName {
if a.isAllowedToBeCalled(ag) {
agents = append(agents, ag)
}
}
@@ -125,3 +174,21 @@ func (a *CallAgentAction) Definition() types.ActionDefinition {
func (a *CallAgentAction) Plannable() bool {
return true
}
func CallAgentConfigMeta() []config.Field {
return []config.Field{
{
Name: "whitelist",
Label: "Whitelist",
Type: config.FieldTypeText,
Required: false,
HelpText: "Comma-separated list of agent names to call. If not specified, all agents are allowed.",
},
{
Name: "blacklist",
Label: "Blacklist",
Type: config.FieldTypeText,
HelpText: "Comma-separated list of agent names to exclude from the call. If not specified, all agents are allowed.",
},
}
}

View File

@@ -24,7 +24,7 @@ func NewCounter(config map[string]string) *CounterAction {
}
// Run executes the counter action
func (a *CounterAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *CounterAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
// Parse parameters
request := struct {
Name string `json:"name"`

View File

@@ -0,0 +1,148 @@
package actions
import (
"context"
"fmt"
"time"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
api "github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataDeepResearchResult = "deep_research_result"
)
type DeepResearchRunner struct {
baseURL, customActionName string
client *api.Client
}
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
if config["baseURL"] == "" {
config["baseURL"] = defaultURL
}
timeout := "15m"
if config["timeout"] != "" {
timeout = config["timeout"]
}
duration, err := time.ParseDuration(timeout)
if err != nil {
// If parsing fails, use default 15 minutes
duration = 15 * time.Minute
}
client := api.NewClient(config["baseURL"], duration)
return &DeepResearchRunner{
client: client,
baseURL: config["baseURL"],
customActionName: config["customActionName"],
}
}
func (d *DeepResearchRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := api.DeepResearchRequest{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
req := api.DeepResearchRequest{
Topic: result.Topic,
MaxCycles: result.MaxCycles,
MaxNoActionAttempts: result.MaxNoActionAttempts,
MaxResults: result.MaxResults,
}
researchResult, err := d.client.RunDeepResearch(req)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
}
// Format the research result into a readable string
var resultStr string
resultStr += "Deep research result\n"
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
if len(researchResult.Sources) > 0 {
resultStr += "Sources:\n"
for _, source := range researchResult.Sources {
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
}
}
return types.ActionResult{
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
}, nil
}
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
actionName := "run_deep_research"
if d.customActionName != "" {
actionName = d.customActionName
}
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"topic": {
Type: jsonschema.String,
Description: "The topic to research",
},
"max_cycles": {
Type: jsonschema.Number,
Description: "Maximum number of research cycles to perform (optional)",
},
"max_no_action_attempts": {
Type: jsonschema.Number,
Description: "Maximum number of attempts without taking an action (optional)",
},
"max_results": {
Type: jsonschema.Number,
Description: "Maximum number of results to collect (optional)",
},
},
Required: []string{"topic"},
}
}
func (d *DeepResearchRunner) Plannable() bool {
return true
}
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
func DeepResearchRunnerConfigMeta() []config.Field {
return []config.Field{
{
Name: "baseURL",
Label: "Base URL",
Type: config.FieldTypeText,
Required: false,
HelpText: "Base URL of the LocalOperator API",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
{
Name: "timeout",
Label: "Client Timeout",
Type: config.FieldTypeText,
Required: false,
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
},
}
}

View File

@@ -29,7 +29,7 @@ type GenImageAction struct {
imageModel string
}
func (a *GenImageAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *GenImageAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Prompt string `json:"prompt"`
Size string `json:"size"`

View File

@@ -42,7 +42,7 @@ var _ = Describe("GenImageAction", func() {
"size": "256x256",
}
url, err := action.Run(ctx, params)
url, err := action.Run(ctx, nil, params)
Expect(err).ToNot(HaveOccurred())
Expect(url).ToNot(BeEmpty())
})
@@ -52,7 +52,7 @@ var _ = Describe("GenImageAction", func() {
"size": "256x256",
}
_, err := action.Run(ctx, params)
_, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})

View File

@@ -26,7 +26,7 @@ func NewGithubIssueCloser(config map[string]string) *GithubIssuesCloser {
}
}
func (g *GithubIssuesCloser) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubIssuesCloser) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -27,7 +27,7 @@ func NewGithubIssueCommenter(config map[string]string) *GithubIssuesCommenter {
}
}
func (g *GithubIssuesCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubIssuesCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -0,0 +1,151 @@
package actions
import (
"context"
"fmt"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubIssueEditor struct {
token, repository, owner, customActionName string
client *github.Client
}
func NewGithubIssueEditor(config map[string]string) *GithubIssueEditor {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubIssueEditor{
client: client,
token: config["token"],
customActionName: config["customActionName"],
repository: config["repository"],
owner: config["owner"],
}
}
func (g *GithubIssueEditor) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
Description string `json:"description"`
Title string `json:"title"`
IssueNumber int `json:"issue_number"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, err
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
_, _, err = g.client.Issues.Edit(ctx, result.Owner, result.Repository, result.IssueNumber,
&github.IssueRequest{
Body: &result.Description,
Title: &result.Title,
})
resultString := fmt.Sprintf("Updated issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
if err != nil {
resultString = fmt.Sprintf("Error updating issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
}
return types.ActionResult{Result: resultString}, err
}
func (g *GithubIssueEditor) Definition() types.ActionDefinition {
actionName := "edit_github_issue"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "Edit the title and description of a Github issue in a repository. Use this action after reading the issue"
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"issue_number": {
Type: jsonschema.Number,
Description: "The number of the issue to edit.",
},
"title": {
Type: jsonschema.String,
Description: "The new title for the issue.",
},
"description": {
Type: jsonschema.String,
Description: "The new description for the issue.",
},
},
Required: []string{"issue_number", "title", "description"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"issue_number": {
Type: jsonschema.Number,
Description: "The number of the issue to edit.",
},
"repository": {
Type: jsonschema.String,
Description: "The repository containing the issue.",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository.",
},
"title": {
Type: jsonschema.String,
Description: "The new title for the issue.",
},
"description": {
Type: jsonschema.String,
Description: "The new description for the issue.",
},
},
Required: []string{"issue_number", "repository", "owner", "title", "description"},
}
}
func (a *GithubIssueEditor) Plannable() bool {
return true
}
// GithubIssueEditorConfigMeta returns the metadata for GitHub Issue Editor action configuration fields
func GithubIssueEditorConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -38,7 +38,7 @@ func NewGithubIssueLabeler(config map[string]string) *GithubIssuesLabeler {
}
}
func (g *GithubIssuesLabeler) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubIssuesLabeler) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -27,7 +27,7 @@ func NewGithubIssueOpener(config map[string]string) *GithubIssuesOpener {
}
}
func (g *GithubIssuesOpener) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubIssuesOpener) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Title string `json:"title"`
Body string `json:"text"`

View File

@@ -27,7 +27,7 @@ func NewGithubIssueReader(config map[string]string) *GithubIssuesReader {
}
}
func (g *GithubIssuesReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubIssuesReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -28,7 +28,7 @@ func NewGithubIssueSearch(config map[string]string) *GithubIssueSearch {
}
}
func (g *GithubIssueSearch) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubIssueSearch) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Query string `json:"query"`
Repository string `json:"repository"`

View File

@@ -3,8 +3,6 @@ package actions
import (
"context"
"fmt"
"regexp"
"strconv"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
@@ -17,96 +15,6 @@ type GithubPRCommenter struct {
client *github.Client
}
var (
patchRegex = regexp.MustCompile(`^@@.*\d [\+\-](\d+),?(\d+)?.+?@@`)
)
type commitFileInfo struct {
FileName string
hunkInfos []*hunkInfo
sha string
}
type hunkInfo struct {
hunkStart int
hunkEnd int
}
func (hi hunkInfo) isLineInHunk(line int) bool {
return line >= hi.hunkStart && line <= hi.hunkEnd
}
func (cfi *commitFileInfo) getHunkInfo(line int) *hunkInfo {
for _, hunkInfo := range cfi.hunkInfos {
if hunkInfo.isLineInHunk(line) {
return hunkInfo
}
}
return nil
}
func (cfi *commitFileInfo) isLineInChange(line int) bool {
return cfi.getHunkInfo(line) != nil
}
func (cfi commitFileInfo) calculatePosition(line int) *int {
hi := cfi.getHunkInfo(line)
if hi == nil {
return nil
}
position := line - hi.hunkStart
return &position
}
func parseHunkPositions(patch, filename string) ([]*hunkInfo, error) {
hunkInfos := make([]*hunkInfo, 0)
if patch != "" {
groups := patchRegex.FindAllStringSubmatch(patch, -1)
if len(groups) < 1 {
return hunkInfos, fmt.Errorf("the patch details for [%s] could not be resolved", filename)
}
for _, patchGroup := range groups {
endPos := 2
if len(patchGroup) > 2 && patchGroup[2] == "" {
endPos = 1
}
hunkStart, err := strconv.Atoi(patchGroup[1])
if err != nil {
hunkStart = -1
}
hunkEnd, err := strconv.Atoi(patchGroup[endPos])
if err != nil {
hunkEnd = -1
}
hunkInfos = append(hunkInfos, &hunkInfo{
hunkStart: hunkStart,
hunkEnd: hunkEnd,
})
}
}
return hunkInfos, nil
}
func getCommitInfo(file *github.CommitFile) (*commitFileInfo, error) {
patch := file.GetPatch()
hunkInfos, err := parseHunkPositions(patch, *file.Filename)
if err != nil {
return nil, err
}
sha := file.GetSHA()
if sha == "" {
return nil, fmt.Errorf("the sha details for [%s] could not be resolved", *file.Filename)
}
return &commitFileInfo{
FileName: *file.Filename,
hunkInfos: hunkInfos,
sha: sha,
}, nil
}
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
client := github.NewClient(nil).WithAuthToken(config["token"])
@@ -119,7 +27,7 @@ func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
}
}
func (g *GithubPRCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubPRCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -3,6 +3,7 @@ package actions
import (
"context"
"fmt"
"time"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
@@ -10,8 +11,16 @@ import (
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
// forkCreationRetries is the number of times to retry checking if a fork is ready
forkCreationRetries = 30
// forkCreationRetryDelay is the duration to wait between fork creation checks
forkCreationRetryDelay = 5 * time.Second
)
type GithubPRCreator struct {
token, repository, owner, customActionName, defaultBranch string
useFork bool
client *github.Client
}
@@ -23,6 +32,8 @@ func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
defaultBranch = "main" // Default to "main" if not specified
}
useFork := config["useFork"] == "true"
return &GithubPRCreator{
client: client,
token: config["token"],
@@ -30,9 +41,45 @@ func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
owner: config["owner"],
customActionName: config["customActionName"],
defaultBranch: defaultBranch,
useFork: useFork,
}
}
// ensureFork ensures that a fork exists for the given repository
func (g *GithubPRCreator) ensureFork(ctx context.Context, owner, repo string) (string, error) {
// First check if we already have a fork
user, _, err := g.client.Users.Get(ctx, "")
if err != nil {
return "", fmt.Errorf("failed to get current user: %w", err)
}
forkOwner := user.GetLogin()
// Check if fork already exists
_, _, err = g.client.Repositories.Get(ctx, forkOwner, repo)
if err == nil {
// Fork already exists
return forkOwner, nil
}
// Create fork
_, _, err = g.client.Repositories.CreateFork(ctx, owner, repo, &github.RepositoryCreateForkOptions{})
if err != nil {
return "", fmt.Errorf("failed to create fork: %w", err)
}
// Wait for fork to be ready
for i := 0; i < forkCreationRetries; i++ {
_, _, err = g.client.Repositories.Get(ctx, forkOwner, repo)
if err == nil {
return forkOwner, nil
}
// Sleep for a bit before retrying
time.Sleep(forkCreationRetryDelay)
}
return "", fmt.Errorf("fork creation timed out")
}
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string, owner string, repository string) error {
// Get the latest commit SHA from the default branch
ref, _, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+g.defaultBranch)
@@ -101,7 +148,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
return nil
}
func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubPRCreator) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
@@ -128,15 +175,29 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
result.BaseBranch = g.defaultBranch
}
var targetOwner, targetRepo string
if g.useFork {
// Ensure we have a fork and get the fork owner
forkOwner, err := g.ensureFork(ctx, result.Owner, result.Repository)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to ensure fork: %w", err)
}
targetOwner = forkOwner
targetRepo = result.Repository
} else {
targetOwner = result.Owner
targetRepo = result.Repository
}
// Create or update branch
err = g.createOrUpdateBranch(ctx, result.Branch, result.Owner, result.Repository)
err = g.createOrUpdateBranch(ctx, result.Branch, targetOwner, targetRepo)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
}
// Create or update files
for _, file := range result.Files {
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path), result.Owner, result.Repository)
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path), targetOwner, targetRepo)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
}
@@ -145,7 +206,7 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
// Check if PR already exists for this branch
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
State: "open",
Head: fmt.Sprintf("%s:%s", result.Owner, result.Branch),
Head: fmt.Sprintf("%s:%s", targetOwner, result.Branch),
})
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
@@ -175,6 +236,12 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
Base: &result.BaseBranch,
}
// If using a fork, we need to specify the full head reference
if g.useFork {
head := fmt.Sprintf("%s:%s", targetOwner, result.Branch)
newPR.Head = &head
}
createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
@@ -322,5 +389,12 @@ func GithubPRCreatorConfigMeta() []config.Field {
Required: false,
HelpText: "Default branch to create PRs against (defaults to main)",
},
{
Name: "useFork",
Label: "Use Fork",
Type: config.FieldTypeCheckbox,
Required: false,
HelpText: "Whether to create PRs from a fork (useful when you don't have write access to the repository). Set to 'true' to enable.",
},
}
}

View File

@@ -54,7 +54,7 @@ var _ = Describe("GithubPRCreator", func() {
},
}
result, err := action.Run(ctx, params)
result, err := action.Run(ctx, nil, params)
Expect(err).NotTo(HaveOccurred())
Expect(result.Result).To(ContainSubstring("pull request #"))
})
@@ -65,7 +65,7 @@ var _ = Describe("GithubPRCreator", func() {
"body": "This is a test pull request",
}
_, err := action.Run(ctx, params)
_, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})

View File

@@ -34,7 +34,7 @@ func NewGithubPRReader(config map[string]string) *GithubPRReader {
}
}
func (g *GithubPRReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubPRReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -30,7 +30,7 @@ func NewGithubPRReviewer(config map[string]string) *GithubPRReviewer {
}
}
func (g *GithubPRReviewer) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubPRReviewer) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -58,7 +58,7 @@ var _ = Describe("GithubPRReviewer", func() {
},
}
result, err := reviewer.Run(ctx, params)
result, err := reviewer.Run(ctx, nil, params)
Expect(err).NotTo(HaveOccurred())
Expect(result.Result).To(ContainSubstring("reviewed successfully"))
})
@@ -70,7 +70,7 @@ var _ = Describe("GithubPRReviewer", func() {
"review_action": "COMMENT",
}
result, err := reviewer.Run(ctx, params)
result, err := reviewer.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
Expect(result.Result).To(ContainSubstring("not found"))
})
@@ -85,7 +85,7 @@ var _ = Describe("GithubPRReviewer", func() {
"review_action": "INVALID_ACTION",
}
_, err := reviewer.Run(ctx, params)
_, err := reviewer.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})

View File

@@ -30,7 +30,7 @@ func NewGithubRepositoryCreateOrUpdateContent(config map[string]string) *GithubR
}
}
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Path string `json:"path"`
Repository string `json:"repository"`

View File

@@ -3,11 +3,13 @@ package actions
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/sashabaranov/go-openai/jsonschema"
)
@@ -28,6 +30,30 @@ func NewGithubRepositoryGetAllContent(config map[string]string) *GithubRepositor
}
}
// isTextFile checks if a file is likely to be a text file based on its extension
func isTextFile(path string) bool {
// List of common text/code file extensions
textExtensions := map[string]bool{
".txt": true, ".md": true, ".go": true, ".py": true, ".js": true,
".ts": true, ".jsx": true, ".tsx": true, ".html": true, ".css": true,
".json": true, ".yaml": true, ".yml": true, ".xml": true, ".sql": true,
".sh": true, ".bash": true, ".zsh": true, ".rb": true, ".php": true,
".java": true, ".c": true, ".cpp": true, ".h": true, ".hpp": true,
".rs": true, ".swift": true, ".kt": true, ".scala": true, ".lua": true,
".pl": true, ".r": true, ".m": true, ".mm": true, ".f": true,
".f90": true, ".f95": true, ".f03": true, ".f08": true, ".f15": true,
".hs": true, ".lhs": true, ".erl": true, ".hrl": true, ".ex": true,
".exs": true, ".eex": true, ".leex": true, ".heex": true, ".config": true,
".toml": true, ".ini": true, ".conf": true, ".env": true, ".gitignore": true,
".dockerignore": true, ".editorconfig": true, ".prettierrc": true, ".eslintrc": true,
".babelrc": true, ".npmrc": true, ".yarnrc": true, ".lock": true,
}
// Get the file extension
ext := strings.ToLower(filepath.Ext(path))
return textExtensions[ext]
}
func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string, owner string, repository string) (string, error) {
var result strings.Builder
@@ -47,6 +73,13 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
}
result.WriteString(subContent)
} else if item.GetType() == "file" {
// Skip binary/image files
if !isTextFile(item.GetPath()) {
xlog.Warn("Skipping non-text file: ", "file", item.GetPath())
result.WriteString(fmt.Sprintf("Skipping non-text file: %s\n", item.GetPath()))
continue
}
// Get file content
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
if err != nil {
@@ -68,7 +101,7 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
return result.String(), nil
}
func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -45,7 +45,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
"path": ".",
}
result, err := action.Run(ctx, params)
result, err := action.Run(ctx, nil, params)
Expect(err).NotTo(HaveOccurred())
Expect(result.Result).NotTo(BeEmpty())
@@ -64,7 +64,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
"path": "non-existent-path",
}
_, err := action.Run(ctx, params)
_, err := action.Run(ctx, nil, params)
Expect(err).To(HaveOccurred())
})
})

View File

@@ -27,7 +27,7 @@ func NewGithubRepositoryGetContent(config map[string]string) *GithubRepositoryGe
}
}
func (g *GithubRepositoryGetContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubRepositoryGetContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Path string `json:"path"`
Repository string `json:"repository"`

View File

@@ -0,0 +1,163 @@
package actions
import (
"context"
"fmt"
"strings"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubRepositoryListFiles struct {
token, repository, owner, customActionName string
client *github.Client
}
func NewGithubRepositoryListFiles(config map[string]string) *GithubRepositoryListFiles {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubRepositoryListFiles{
client: client,
token: config["token"],
repository: config["repository"],
owner: config["owner"],
customActionName: config["customActionName"],
}
}
func (g *GithubRepositoryListFiles) listFilesRecursively(ctx context.Context, path string, owner string, repository string) ([]string, error) {
var files []string
// Get content at the current path
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
if err != nil {
return nil, fmt.Errorf("error getting content at path %s: %w", path, err)
}
// Process each item in the directory
for _, item := range directoryContent {
if item.GetType() == "dir" {
// Recursively list files in subdirectories
subFiles, err := g.listFilesRecursively(ctx, item.GetPath(), owner, repository)
if err != nil {
return nil, err
}
files = append(files, subFiles...)
} else if item.GetType() == "file" {
// Add file path to the list
files = append(files, item.GetPath())
}
}
return files, nil
}
func (g *GithubRepositoryListFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
Path string `json:"path,omitempty"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
// Start from root if no path specified
if result.Path == "" {
result.Path = "."
}
files, err := g.listFilesRecursively(ctx, result.Path, result.Owner, result.Repository)
if err != nil {
return types.ActionResult{}, err
}
// Join all file paths with newlines for better readability
content := strings.Join(files, "\n")
return types.ActionResult{Result: content}, nil
}
func (g *GithubRepositoryListFiles) Definition() types.ActionDefinition {
actionName := "list_github_repository_files"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "List all files in a GitHub repository"
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start listing from (defaults to repository root)",
},
},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start listing from (defaults to repository root)",
},
"repository": {
Type: jsonschema.String,
Description: "The repository to list files from",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository",
},
},
Required: []string{"repository", "owner"},
}
}
func (a *GithubRepositoryListFiles) Plannable() bool {
return true
}
// GithubRepositoryListFilesConfigMeta returns the metadata for GitHub Repository List Files action configuration fields
func GithubRepositoryListFilesConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -25,7 +25,7 @@ func NewGithubRepositoryREADME(config map[string]string) *GithubRepositoryREADME
}
}
func (g *GithubRepositoryREADME) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (g *GithubRepositoryREADME) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`

View File

@@ -0,0 +1,187 @@
package actions
import (
"context"
"fmt"
"strings"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubRepositorySearchFiles struct {
token, repository, owner, customActionName string
client *github.Client
}
func NewGithubRepositorySearchFiles(config map[string]string) *GithubRepositorySearchFiles {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubRepositorySearchFiles{
client: client,
token: config["token"],
repository: config["repository"],
owner: config["owner"],
customActionName: config["customActionName"],
}
}
func (g *GithubRepositorySearchFiles) searchFilesRecursively(ctx context.Context, path string, owner string, repository string, searchPattern string) (string, error) {
var result strings.Builder
// Get content at the current path
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
if err != nil {
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
}
// Process each item in the directory
for _, item := range directoryContent {
if item.GetType() == "dir" {
// Recursively search in subdirectories
subContent, err := g.searchFilesRecursively(ctx, item.GetPath(), owner, repository, searchPattern)
if err != nil {
return "", err
}
result.WriteString(subContent)
} else if item.GetType() == "file" {
// Check if file name matches the search pattern
if strings.Contains(strings.ToLower(item.GetName()), strings.ToLower(searchPattern)) {
// Get file content
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
if err != nil {
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
}
content, err := fileContent.GetContent()
if err != nil {
return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err)
}
// Add file content to result with clear markers
result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath()))
result.WriteString(content)
result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath()))
}
}
}
return result.String(), nil
}
func (g *GithubRepositorySearchFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
Path string `json:"path,omitempty"`
SearchPattern string `json:"searchPattern"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
// Start from root if no path specified
if result.Path == "" {
result.Path = "."
}
content, err := g.searchFilesRecursively(ctx, result.Path, result.Owner, result.Repository, result.SearchPattern)
if err != nil {
return types.ActionResult{}, err
}
return types.ActionResult{Result: content}, nil
}
func (g *GithubRepositorySearchFiles) Definition() types.ActionDefinition {
actionName := "search_github_repository_files"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "Search for files in a GitHub repository and return their content"
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start searching from (defaults to repository root)",
},
"searchPattern": {
Type: jsonschema.String,
Description: "Pattern to search for in file names (case-insensitive)",
},
},
Required: []string{"searchPattern"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"path": {
Type: jsonschema.String,
Description: "Optional path to start searching from (defaults to repository root)",
},
"repository": {
Type: jsonschema.String,
Description: "The repository to search in",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository",
},
"searchPattern": {
Type: jsonschema.String,
Description: "Pattern to search for in file names (case-insensitive)",
},
},
Required: []string{"repository", "owner", "searchPattern"},
}
}
func (a *GithubRepositorySearchFiles) Plannable() bool {
return true
}
// GithubRepositorySearchFilesConfigMeta returns the metadata for GitHub Repository Search Files action configuration fields
func GithubRepositorySearchFilesConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -16,7 +16,7 @@ func NewScraper(config map[string]string) *ScraperAction {
type ScraperAction struct{}
func (a *ScraperAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *ScraperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
URL string `json:"url"`
}{}

View File

@@ -35,7 +35,7 @@ func NewSearch(config map[string]string) *SearchAction {
type SearchAction struct{ results int }
func (a *SearchAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *SearchAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Query string `json:"query"`
}{}

View File

@@ -28,7 +28,7 @@ type SendMailAction struct {
smtpPort string
}
func (a *SendMailAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *SendMailAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Message string `json:"message"`
To string `json:"to"`

View File

@@ -0,0 +1,195 @@
package actions
import (
"context"
"fmt"
"strconv"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/xstrings"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataTelegramMessageSent = "telegram_message_sent"
telegramMaxMessageLength = 3000
)
type SendTelegramMessageRunner struct {
token string
chatID int64
bot *bot.Bot
customName string
customDescription string
}
func NewSendTelegramMessageRunner(config map[string]string) *SendTelegramMessageRunner {
token := config["token"]
if token == "" {
return nil
}
// Parse chat ID from config if present
var chatID int64
if configChatID := config["chat_id"]; configChatID != "" {
var err error
chatID, err = strconv.ParseInt(configChatID, 10, 64)
if err != nil {
return nil
}
}
b, err := bot.New(token)
if err != nil {
return nil
}
return &SendTelegramMessageRunner{
token: token,
chatID: chatID,
bot: b,
customName: config["custom_name"],
customDescription: config["custom_description"],
}
}
type TelegramMessageParams struct {
ChatID int64 `json:"chat_id"`
Message string `json:"message"`
}
func (s *SendTelegramMessageRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
var messageParams TelegramMessageParams
err := params.Unmarshal(&messageParams)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if s.chatID != 0 {
messageParams.ChatID = s.chatID
}
if messageParams.ChatID == 0 {
return types.ActionResult{}, fmt.Errorf("chat_id is required either in config or parameters")
}
if messageParams.Message == "" {
return types.ActionResult{}, fmt.Errorf("message is required")
}
// Split the message if it's too long
messages := xstrings.SplitParagraph(messageParams.Message, telegramMaxMessageLength)
if len(messages) == 0 {
return types.ActionResult{}, fmt.Errorf("empty message after splitting")
}
// Send each message part
for i, msg := range messages {
_, err = s.bot.SendMessage(ctx, &bot.SendMessageParams{
ChatID: messageParams.ChatID,
Text: msg,
ParseMode: models.ParseModeMarkdown,
})
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to send telegram message part %d: %w", i+1, err)
}
}
sharedState.ConversationTracker.AddMessage(fmt.Sprintf("telegram:%d", messageParams.ChatID), openai.ChatCompletionMessage{
Content: messageParams.Message,
Role: "assistant",
})
return types.ActionResult{
Result: fmt.Sprintf("Message sent successfully to chat ID %d in %d parts", messageParams.ChatID, len(messages)),
Metadata: map[string]interface{}{
MetadataTelegramMessageSent: true,
},
}, nil
}
func (s *SendTelegramMessageRunner) Definition() types.ActionDefinition {
customName := "send_telegram_message"
if s.customName != "" {
customName = s.customName
}
customDescription := "Send a message to a Telegram user or group"
if s.customDescription != "" {
customDescription = s.customDescription
}
if s.chatID != 0 {
return types.ActionDefinition{
Name: types.ActionDefinitionName(customName),
Description: customDescription,
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message to send",
},
},
Required: []string{"message"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(customName),
Description: customDescription,
Properties: map[string]jsonschema.Definition{
"chat_id": {
Type: jsonschema.Number,
Description: "The Telegram chat ID to send the message to (optional if configured in config)",
},
"message": {
Type: jsonschema.String,
Description: "The message to send",
},
},
Required: []string{"message", "chat_id"},
}
}
func (s *SendTelegramMessageRunner) Plannable() bool {
return true
}
// SendTelegramMessageConfigMeta returns the metadata for Send Telegram Message action configuration fields
func SendTelegramMessageConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "Telegram Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "Telegram bot token for sending messages",
},
{
Name: "chat_id",
Label: "Default Chat ID",
Type: config.FieldTypeText,
Required: false,
HelpText: "Default Telegram chat ID to send messages to (can be overridden in parameters)",
},
{
Name: "custom_name",
Label: "Custom Name",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom name for the action (optional, defaults to 'send_telegram_message')",
},
{
Name: "custom_description",
Label: "Custom Description",
Type: config.FieldTypeText,
Required: false,
HelpText: "Custom description for the action (optional, defaults to 'Send a message to a Telegram user or group')",
},
}
}

View File

@@ -28,7 +28,7 @@ type ShellAction struct {
customDescription string
}
func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *ShellAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Command string `json:"command"`
Host string `json:"host"`

View File

@@ -22,7 +22,7 @@ type PostTweetAction struct {
noCharacterLimit bool
}
func (a *PostTweetAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *PostTweetAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Text string `json:"text"`
}{}

View File

@@ -15,7 +15,7 @@ func NewWikipedia(config map[string]string) *WikipediaAction {
type WikipediaAction struct{}
func (a *WikipediaAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
func (a *WikipediaAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Query string `json:"query"`
}{}

View File

@@ -19,6 +19,7 @@ const (
ConnectorGithubIssues = "github-issues"
ConnectorGithubPRs = "github-prs"
ConnectorTwitter = "twitter"
ConnectorMatrix = "matrix"
)
var AvailableConnectors = []string{
@@ -29,6 +30,7 @@ var AvailableConnectors = []string{
ConnectorGithubIssues,
ConnectorGithubPRs,
ConnectorTwitter,
ConnectorMatrix,
}
func Connectors(a *state.AgentConfig) []state.Connector {
@@ -66,6 +68,8 @@ func Connectors(a *state.AgentConfig) []state.Connector {
continue
}
conns = append(conns, cc)
case ConnectorMatrix:
conns = append(conns, connectors.NewMatrix(config))
}
}
return conns
@@ -108,5 +112,10 @@ func ConnectorsConfigMeta() []config.FieldGroup {
Label: "Twitter",
Fields: connectors.TwitterConfigMeta(),
},
{
Name: "matrix",
Label: "Matrix",
Fields: connectors.MatrixConfigMeta(),
},
}
}

View File

@@ -2,8 +2,8 @@ package connectors
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/bwmarrin/discordgo"
"github.com/mudler/LocalAGI/core/agent"
@@ -16,7 +16,6 @@ import (
type Discord struct {
token string
defaultChannel string
conversationTracker *ConversationTracker[string]
}
// NewDiscord creates a new Discord connector
@@ -25,14 +24,14 @@ type Discord struct {
// - defaultChannel: Discord channel to always answer even if not mentioned
func NewDiscord(config map[string]string) *Discord {
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
token := config["token"]
if !strings.HasPrefix(token, "Bot ") {
token = "Bot " + token
}
return &Discord{
conversationTracker: NewConversationTracker[string](duration),
token: config["token"],
token: token,
defaultChannel: config["defaultChannel"],
}
}
@@ -151,12 +150,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) {
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
Role: "user",
Content: m.Content,
})
conv := d.conversationTracker.GetConversation(m.ChannelID)
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("discord:%s", m.ChannelID))
jobResult := a.Ask(
types.WithConversationHistory(conv),
@@ -167,7 +166,7 @@ func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *
return
}
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
Role: "assistant",
Content: jobResult.Response,
})

View File

@@ -21,22 +21,16 @@ type IRC struct {
channel string
conn *irc.Connection
alwaysReply bool
conversationTracker *ConversationTracker[string]
}
func NewIRC(config map[string]string) *IRC {
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
}
return &IRC{
server: config["server"],
port: config["port"],
nickname: config["nickname"],
channel: config["channel"],
alwaysReply: config["alwaysReply"] == "true",
conversationTracker: NewConversationTracker[string](duration),
}
}
@@ -115,7 +109,7 @@ func (i *IRC) Start(a *agent.Agent) {
cleanedMessage := cleanUpMessage(message, i.nickname)
go func() {
conv := i.conversationTracker.GetConversation(channel)
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("irc:%s", channel))
conv = append(conv,
openai.ChatCompletionMessage{
@@ -125,7 +119,7 @@ func (i *IRC) Start(a *agent.Agent) {
)
// Update the conversation history
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
Content: cleanedMessage,
Role: "user",
})
@@ -140,7 +134,7 @@ func (i *IRC) Start(a *agent.Agent) {
}
// Update the conversation history
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
Content: res.Response,
Role: "assistant",
})
@@ -249,11 +243,5 @@ func IRCConfigMeta() []config.Field {
Label: "Always Reply",
Type: config.FieldTypeCheckbox,
},
{
Name: "lastMessageDuration",
Label: "Last Message Duration",
Type: config.FieldTypeText,
DefaultValue: "5m",
},
}
}

View File

@@ -0,0 +1,304 @@
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,
},
}
}

View File

@@ -8,7 +8,6 @@ import (
"os"
"strings"
"sync"
"time"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/localoperator"
@@ -42,25 +41,17 @@ type Slack struct {
// Track active jobs for cancellation
activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing
activeJobsMutex sync.RWMutex
conversationTracker *ConversationTracker[string]
}
const thinkingMessage = ":hourglass: thinking..."
func NewSlack(config map[string]string) *Slack {
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
}
return &Slack{
appToken: config["appToken"],
botToken: config["botToken"],
channelID: config["channelID"],
channelMode: config["channelMode"] == "true",
conversationTracker: NewConversationTracker[string](duration),
placeholders: make(map[string]string),
activeJobs: make(map[string][]*types.Job),
}
@@ -140,16 +131,6 @@ func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) strin
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 {
for _, part := range strings.Split(message, " ") {
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
@@ -279,7 +260,7 @@ func (t *Slack) handleChannelMessage(
// Cancel any active job for this channel before starting a new one
t.cancelActiveJobForChannel(ev.Channel)
currentConv := t.conversationTracker.GetConversation(t.channelID)
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID))
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
@@ -323,8 +304,8 @@ func (t *Slack) handleChannelMessage(
})
}
t.conversationTracker.AddMessage(
t.channelID, currentConv[len(currentConv)-1],
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("slack:%s", t.channelID), currentConv[len(currentConv)-1],
)
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
@@ -370,14 +351,14 @@ func (t *Slack) handleChannelMessage(
return
}
t.conversationTracker.AddMessage(
t.channelID, openai.ChatCompletionMessage{
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("slack:%s", t.channelID), openai.ChatCompletionMessage{
Role: "assistant",
Content: res.Response,
},
)
xlog.Debug("After adding message to conversation tracker", "conversation", t.conversationTracker.GetConversation(t.channelID))
xlog.Debug("After adding message to conversation tracker", "conversation", a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID)))
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
@@ -752,6 +733,13 @@ func (t *Slack) Start(a *agent.Agent) {
if err != nil {
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",
},
)
})
}
@@ -835,11 +823,5 @@ func SlackConfigMeta() []config.Field {
Label: "Always Reply",
Type: config.FieldTypeCheckbox,
},
{
Name: "lastMessageDuration",
Label: "Last Message Duration",
Type: config.FieldTypeText,
DefaultValue: "5m",
},
}
}

View File

@@ -1,143 +1,377 @@
package connectors
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"slices"
"strings"
"time"
"sync"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/mudler/LocalAGI/core/agent"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/pkg/xstrings"
"github.com/mudler/LocalAGI/services/actions"
"github.com/sashabaranov/go-openai"
)
const telegramThinkingMessage = "🤔 thinking..."
const telegramMaxMessageLength = 3000
type Telegram struct {
Token string
bot *bot.Bot
agent *agent.Agent
currentconversation map[int64][]openai.ChatCompletionMessage
lastMessageTime map[int64]time.Time
lastMessageDuration time.Duration
admins []string
conversationTracker *ConversationTracker[int64]
// To track placeholder messages
placeholders map[string]int // map[jobUUID]messageID
placeholderMutex sync.RWMutex
// Track active jobs for cancellation
activeJobs map[int64][]*types.Job // map[chatID]bool to track if a chat has active processing
activeJobsMutex sync.RWMutex
channelID string
}
// Send any text message to the bot after the bot has been started
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
return func(state types.ActionState) {
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
Description: state.Reasoning,
})
// Mark the job as completed when we get the final result
if state.ActionCurrentState.Job != nil && state.ActionCurrentState.Job.Metadata != nil {
if chatID, ok := state.ActionCurrentState.Job.Metadata["chatID"].(int64); ok && chatID != 0 {
t.activeJobsMutex.Lock()
delete(t.activeJobs, chatID)
t.activeJobsMutex.Unlock()
}
}
}
}
func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
return func(state types.ActionCurrentState) bool {
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
Description: state.Reasoning,
// Check if we have a placeholder message for this job
t.placeholderMutex.RLock()
msgID, exists := t.placeholders[state.Job.UUID]
chatID := int64(0)
if state.Job.Metadata != nil {
if ch, ok := state.Job.Metadata["chatID"].(int64); ok {
chatID = ch
}
}
t.placeholderMutex.RUnlock()
if !exists || msgID == 0 || chatID == 0 || t.bot == nil {
return true // Skip if we don't have a message to update
}
thought := telegramThinkingMessage + "\n\n"
if state.Reasoning != "" {
thought += "Current thought process:\n" + state.Reasoning
}
// Update the placeholder message with the current reasoning
_, err := t.bot.EditMessageText(t.agent.Context(), &bot.EditMessageTextParams{
ChatID: chatID,
MessageID: msgID,
Text: thought,
})
if err != nil {
xlog.Error("Error updating reasoning message", "error", err)
}
return true
}
}
// cancelActiveJobForChat cancels any active job for the given chat
func (t *Telegram) cancelActiveJobForChat(chatID int64) {
t.activeJobsMutex.RLock()
ctxs, exists := t.activeJobs[chatID]
t.activeJobsMutex.RUnlock()
if exists {
xlog.Info("Cancelling active job for chat", "chatID", chatID)
// Mark the job as inactive
t.activeJobsMutex.Lock()
for _, c := range ctxs {
c.Cancel()
}
delete(t.activeJobs, chatID)
t.activeJobsMutex.Unlock()
}
}
// sendImageToTelegram downloads and sends an image to Telegram
func sendImageToTelegram(ctx context.Context, b *bot.Bot, chatID int64, url string) error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("error downloading image: %w", err)
}
defer resp.Body.Close()
// Read the entire body into memory
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error reading image body: %w", err)
}
// Send image with caption
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
ChatID: chatID,
Photo: &models.InputFileUpload{
Filename: "image.jpg",
Data: bytes.NewReader(bodyBytes),
},
Caption: "Generated image",
})
if err != nil {
return fmt.Errorf("error sending photo: %w", err)
}
return nil
}
// handleMultimediaContent processes and sends multimedia content from the agent's response
func (t *Telegram) handleMultimediaContent(ctx context.Context, chatID int64, res *types.JobResult) ([]string, error) {
var urls []string
for _, state := range res.State {
// Collect URLs from search action
if urlList, exists := state.Metadata[actions.MetadataUrls]; exists {
urls = append(urls, xstrings.UniqueSlice(urlList.([]string))...)
}
// Handle images from gen image actions
if imagesUrls, exists := state.Metadata[actions.MetadataImages]; exists {
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
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) {
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) {
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
}
currentConv := t.conversationTracker.GetConversation(update.Message.From.ID)
// Cancel any active job for this chat before starting a new one
t.cancelActiveJobForChat(update.Message.Chat.ID)
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.From.ID))
currentConv = append(currentConv, openai.ChatCompletionMessage{
Content: update.Message.Text,
Role: "user",
})
t.conversationTracker.AddMessage(
update.Message.From.ID,
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("telegram:%d", update.Message.From.ID),
openai.ChatCompletionMessage{
Content: update.Message.Text,
Role: "user",
},
)
xlog.Info("New message", "username", username, "conversation", currentConv)
res := a.Ask(
types.WithConversationHistory(currentConv),
)
xlog.Debug("Response", "response", res.Response)
if res.Response == "" {
xlog.Error("Empty response from agent")
// Send initial placeholder message
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: update.Message.Chat.ID,
Text: bot.EscapeMarkdown(telegramThinkingMessage),
ParseMode: models.ParseModeMarkdown,
})
if err != nil {
xlog.Error("Error sending initial message", "error", err)
return
}
t.conversationTracker.AddMessage(
update.Message.From.ID,
// Store the UUID->placeholder message mapping
jobUUID := fmt.Sprintf("%d", msg.ID)
t.placeholderMutex.Lock()
t.placeholders[jobUUID] = msg.ID
t.placeholderMutex.Unlock()
// Add chat ID to metadata for tracking
metadata := map[string]interface{}{
"chatID": update.Message.Chat.ID,
}
// Create a new job with the conversation history and metadata
job := types.NewJob(
types.WithConversationHistory(currentConv),
types.WithUUID(jobUUID),
types.WithMetadata(metadata),
)
// Mark this chat as having an active job
t.activeJobsMutex.Lock()
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID], job)
t.activeJobsMutex.Unlock()
defer func() {
// Mark job as complete
t.activeJobsMutex.Lock()
job.Cancel()
for i, j := range t.activeJobs[update.Message.Chat.ID] {
if j.UUID == job.UUID {
t.activeJobs[update.Message.Chat.ID] = append(t.activeJobs[update.Message.Chat.ID][:i], t.activeJobs[update.Message.Chat.ID][i+1:]...)
break
}
}
t.activeJobsMutex.Unlock()
// Clean up the placeholder map
t.placeholderMutex.Lock()
delete(t.placeholders, jobUUID)
t.placeholderMutex.Unlock()
}()
res := a.Ask(
types.WithConversationHistory(currentConv),
types.WithUUID(jobUUID),
types.WithMetadata(metadata),
)
if res.Response == "" {
xlog.Error("Empty response from agent")
_, err := b.EditMessageText(ctx, &bot.EditMessageTextParams{
ChatID: update.Message.Chat.ID,
MessageID: msg.ID,
Text: "there was an internal error. try again!",
})
if err != nil {
xlog.Error("Error updating error message", "error", err)
}
return
}
a.SharedState().ConversationTracker.AddMessage(
fmt.Sprintf("telegram:%d", update.Message.From.ID),
openai.ChatCompletionMessage{
Content: res.Response,
Role: "assistant",
},
)
xlog.Debug("Sending message back to telegram", "response", res.Response)
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)
// Handle any multimedia content in the response and collect URLs
urls, err := t.handleMultimediaContent(ctx, update.Message.Chat.ID, res)
if err != nil {
xlog.Error("Error downloading image", "error", err.Error())
continue
xlog.Error("Error handling multimedia content", "error", err)
}
defer resp.Body.Close()
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
// Update the message with the final response
formattedResponse := formatResponseWithURLs(res.Response, urls)
// Split the message if it's too long
messages := xstrings.SplitParagraph(formattedResponse, telegramMaxMessageLength)
if len(messages) == 0 {
internalError(errors.New("empty response from agent"), msg)
return
}
// Update the first message
_, err = b.EditMessageText(ctx, &bot.EditMessageTextParams{
ChatID: update.Message.Chat.ID,
Photo: &models.InputFileUpload{
Filename: "image.jpg",
Data: resp.Body,
},
MessageID: msg.ID,
Text: messages[0],
ParseMode: models.ParseModeMarkdown,
})
if err != nil {
xlog.Error("Error sending photo", "error", err.Error())
}
}
internalError(fmt.Errorf("internal error: %w", err), msg)
return
}
}
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
// ParseMode: models.ParseModeMarkdown,
// Send additional chunks as new messages
for i := 1; i < len(messages); i++ {
_, err = b.SendMessage(ctx, &bot.SendMessageParams{
ChatID: update.Message.Chat.ID,
Text: res.Response,
Text: messages[i],
ParseMode: models.ParseModeMarkdown,
})
if err != nil {
xlog.Error("Error sending message", "error", err)
internalError(fmt.Errorf("internal error: %w", err), msg)
}
}
}
@@ -163,18 +397,42 @@ func (t *Telegram) Start(a *agent.Agent) {
b, err := bot.New(t.Token, opts...)
if err != nil {
panic(err)
xlog.Error("Error creating bot", "error", err)
return
}
t.bot = b
t.agent = a
// go func() {
// for m := range a.ConversationChannel() {
// forc m := range a.ConversationChannel() {
// 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)
}
@@ -184,11 +442,6 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
return nil, errors.New("token is required")
}
duration, err := time.ParseDuration(config["lastMessageDuration"])
if err != nil {
duration = 5 * time.Minute
}
admins := []string{}
if _, ok := config["admins"]; ok {
@@ -197,11 +450,10 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
return &Telegram{
Token: token,
lastMessageDuration: duration,
admins: admins,
currentconversation: map[int64][]openai.ChatCompletionMessage{},
lastMessageTime: map[int64]time.Time{},
conversationTracker: NewConversationTracker[int64](duration),
placeholders: make(map[string]int),
activeJobs: make(map[int64][]*types.Job),
channelID: config["channel_id"],
}, nil
}
@@ -221,10 +473,10 @@ func TelegramConfigMeta() []config.Field {
HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot",
},
{
Name: "lastMessageDuration",
Label: "Last Message Duration",
Name: "channel_id",
Label: "Channel ID",
Type: config.FieldTypeText,
DefaultValue: "5m",
HelpText: "Telegram channel ID to send messages to if the agent needs to initiate a conversation",
},
}
}

44
services/filters.go Normal file
View File

@@ -0,0 +1,44 @@
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(),
}
}

View File

@@ -0,0 +1,120 @@
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},
},
}
}

86
services/filters/regex.go Normal file
View File

@@ -0,0 +1,86 @@
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 ""
}

View File

@@ -11,12 +11,14 @@ import (
"time"
"github.com/google/uuid"
"github.com/mudler/LocalAGI/core/conversations"
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/xlog"
"github.com/mudler/LocalAGI/services"
"github.com/mudler/LocalAGI/services/connectors"
"github.com/mudler/LocalAGI/webui/types"
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
@@ -33,6 +35,7 @@ type (
htmx *htmx.HTMX
config *Config
*fiber.App
sharedState *internalTypes.AgentSharedState
}
)
@@ -50,6 +53,7 @@ func NewApp(opts ...Option) *App {
htmx: htmx.New(),
config: config,
App: webapp,
sharedState: internalTypes.NewAgentSharedState(5 * time.Minute),
}
a.registerRoutes(config.Pool, webapp)
@@ -419,7 +423,31 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
}
}
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
func (a *App) GetActionDefinition(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 {
payload := struct {
Config map[string]string `json:"config"`
@@ -443,7 +471,7 @@ func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.Context(), 200*time.Second)
defer cancel()
res, err := a.Run(ctx, payload.Params)
res, err := a.Run(ctx, app.sharedState, payload.Params)
if err != nil {
xlog.Error("Error running action", "error", err)
return errorJSONMessage(c, err.Error())
@@ -460,7 +488,7 @@ func (a *App) ListActions() func(c *fiber.Ctx) error {
}
}
func (a *App) Responses(pool *state.AgentPool, tracker *connectors.ConversationTracker[string]) func(c *fiber.Ctx) error {
func (a *App) Responses(pool *state.AgentPool, tracker *conversations.ConversationTracker[string]) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
var request types.RequestBody
if err := c.BodyParser(&request); err != nil {
@@ -552,7 +580,7 @@ func (a *App) GenerateGroupProfiles(pool *state.AgentPool) func(c *fiber.Ctx) er
xlog.Debug("Generating group", "description", request.Descript)
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
err := llm.GenerateTypedJSON(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{
err := llm.GenerateTypedJSONWithGuidance(c.Context(), client, request.Descript, a.config.LLMModel, jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"agents": {
@@ -620,6 +648,7 @@ func (a *App) GetAgentConfigMeta() func(c *fiber.Ctx) error {
services.ActionsConfigMeta(),
services.ConnectorsConfigMeta(),
services.DynamicPromptsConfigMeta(),
services.FiltersConfigMeta(),
)
return c.JSON(configMeta)
}

View File

@@ -9,16 +9,16 @@
"react-dom": "^19.1.0",
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@eslint/js": "^9.25.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.0",
"eslint": "^9.24.0",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.1",
"eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0",
"react-router-dom": "^7.5.1",
"vite": "^6.3.2",
"react-router-dom": "^7.5.3",
"vite": "^6.3.3",
},
},
},
@@ -133,11 +133,11 @@
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
"@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
"@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.24.0", "", {}, "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA=="],
"@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
@@ -215,9 +215,9 @@
"@types/react": ["@types/react@19.1.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw=="],
"@types/react-dom": ["@types/react-dom@19.1.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw=="],
"@types/react-dom": ["@types/react-dom@19.1.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg=="],
"@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=="],
"@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=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
@@ -267,11 +267,11 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"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": ["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-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-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="],
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
@@ -293,7 +293,7 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -393,9 +393,9 @@
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-router": ["react-router@7.5.1", "", { "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-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA=="],
"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-dom": ["react-router-dom@7.5.1", "", { "dependencies": { "react-router": "7.5.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA=="],
"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=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@@ -417,7 +417,7 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
@@ -427,7 +427,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@6.3.2", "", { "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-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg=="],
"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=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -447,8 +447,6 @@
"@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=="],
}
}

View File

@@ -15,15 +15,15 @@
"highlight.js": "^11.11.1"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@eslint/js": "^9.25.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.0",
"eslint": "^9.24.0",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.1",
"eslint-plugin-react-hooks": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0",
"react-router-dom": "^7.5.1",
"vite": "^6.3.2"
"react-router-dom": "^7.5.3",
"vite": "^6.3.3"
}
}

View File

@@ -11,6 +11,7 @@ import ModelSettingsSection from './agent-form-sections/ModelSettingsSection';
import PromptsGoalsSection from './agent-form-sections/PromptsGoalsSection';
import AdvancedSettingsSection from './agent-form-sections/AdvancedSettingsSection';
import ExportSection from './agent-form-sections/ExportSection';
import FiltersSection from './agent-form-sections/FiltersSection';
const AgentForm = ({
isEdit = false,
@@ -189,6 +190,13 @@ const AgentForm = ({
<i className="fas fa-plug"></i>
Connectors
</li>
<li
className={`wizard-nav-item ${activeSection === 'filters-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('filters-section')}
>
<i className="fas fa-shield"></i>
Filters &amp; Triggers
</li>
<li
className={`wizard-nav-item ${activeSection === 'actions-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('actions-section')}
@@ -255,6 +263,10 @@ const AgentForm = ({
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorChange={handleConnectorChange} metadata={metadata} />
</div>
<div style={{ display: activeSection === 'filters-section' ? 'block' : 'none' }}>
<FiltersSection formData={formData} setFormData={setFormData} metadata={metadata} />
</div>
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
<ActionsSection formData={formData} setFormData={setFormData} metadata={metadata} />
</div>
@@ -306,6 +318,10 @@ const AgentForm = ({
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorChange={handleConnectorChange} metadata={metadata} />
</div>
<div style={{ display: activeSection === 'filters-section' ? 'block' : 'none' }}>
<FiltersSection formData={formData} setFormData={setFormData} metadata={metadata} />
</div>
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
<ActionsSection formData={formData} setFormData={setFormData} metadata={metadata} />
</div>

View File

@@ -0,0 +1,103 @@
import React, { useState } from 'react';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import 'highlight.js/styles/monokai.css';
hljs.registerLanguage('json', json);
export default function CollapsibleRawSections({ container }) {
const [showCreation, setShowCreation] = useState(false);
const [showProgress, setShowProgress] = useState(false);
const [showCompletion, setShowCompletion] = useState(false);
const [copied, setCopied] = useState({ creation: false, progress: false, completion: false });
const handleCopy = (section, data) => {
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
setCopied(prev => ({ ...prev, [section]: true }));
setTimeout(() => setCopied(prev => ({ ...prev, [section]: false })), 1200);
};
return (
<div>
{/* Creation Section */}
{container.creation && (
<div>
<h4 style={{ display: 'flex', alignItems: 'center' }}>
<span
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', flex: 1 }}
onClick={() => setShowCreation(v => !v)}
>
<i className={`fas fa-chevron-${showCreation ? 'down' : 'right'}`} style={{ marginRight: 6 }} />
Creation
</span>
<button
title="Copy Creation JSON"
onClick={e => { e.stopPropagation(); handleCopy('creation', container.creation); }}
style={{ marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: '#ccc' }}
>
{copied.creation ? <span style={{ color: '#6f6' }}>Copied!</span> : <i className="fas fa-copy" />}
</button>
</h4>
{showCreation && (
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.creation || {}, null, 2), { language: 'json' }).value }} />
</code></pre>
)}
</div>
)}
{/* Progress Section */}
{container.progress && container.progress.length > 0 && (
<div>
<h4 style={{ display: 'flex', alignItems: 'center' }}>
<span
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', flex: 1 }}
onClick={() => setShowProgress(v => !v)}
>
<i className={`fas fa-chevron-${showProgress ? 'down' : 'right'}`} style={{ marginRight: 6 }} />
Progress
</span>
<button
title="Copy Progress JSON"
onClick={e => { e.stopPropagation(); handleCopy('progress', container.progress); }}
style={{ marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: '#ccc' }}
>
{copied.progress ? <span style={{ color: '#6f6' }}>Copied!</span> : <i className="fas fa-copy" />}
</button>
</h4>
{showProgress && (
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.progress || {}, null, 2), { language: 'json' }).value }} />
</code></pre>
)}
</div>
)}
{/* Completion Section */}
{container.completion && (
<div>
<h4 style={{ display: 'flex', alignItems: 'center' }}>
<span
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', flex: 1 }}
onClick={() => setShowCompletion(v => !v)}
>
<i className={`fas fa-chevron-${showCompletion ? 'down' : 'right'}`} style={{ marginRight: 6 }} />
Completion
</span>
<button
title="Copy Completion JSON"
onClick={e => { e.stopPropagation(); handleCopy('completion', container.completion); }}
style={{ marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: '#ccc' }}
>
{copied.completion ? <span style={{ color: '#6f6' }}>Copied!</span> : <i className="fas fa-copy" />}
</button>
</h4>
{showCompletion && (
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.completion || {}, null, 2), { language: 'json' }).value }} />
</code></pre>
)}
</div>
)}
</div>
);
}

View File

@@ -13,6 +13,7 @@ import FormFieldDefinition from './common/FormFieldDefinition';
* @param {String} props.itemType - Type of items being configured ('action', 'connector', etc.)
* @param {String} props.typeField - The field name that determines the item's type (e.g., 'name' for actions, 'type' for connectors)
* @param {String} props.addButtonText - Text for the add button
* @param {String} props.saveAllFieldsAsString - Whether to save all fields as string or the appropriate JSON type
*/
const ConfigForm = ({
items = [],
@@ -22,7 +23,8 @@ const ConfigForm = ({
onAdd,
itemType = 'item',
typeField = 'type',
addButtonText = 'Add Item'
addButtonText = 'Add Item',
saveAllFieldsAsString = true,
}) => {
// Generate options from fieldGroups
const typeOptions = [
@@ -62,8 +64,10 @@ const ConfigForm = ({
const item = items[index];
const config = parseConfig(item);
if (type === 'checkbox')
config[key] = checked ? 'true' : 'false';
if (type === 'number' && !saveAllFieldsAsString)
config[key] = Number(value);
else if (type === 'checkbox')
config[key] = saveAllFieldsAsString ? (checked ? 'true' : 'false') : checked;
else
config[key] = value;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import ConfigForm from './ConfigForm';
/**
* FilterForm component for configuring an filter
* Renders filter configuration forms based on field group metadata
*/
const FilterForm = ({ filters = [], onChange, onRemove, onAdd, fieldGroups = [] }) => {
const handleFilterChange = (index, updatedFilter) => {
onChange(index, updatedFilter);
};
return (
<ConfigForm
items={filters}
fieldGroups={fieldGroups}
onChange={handleFilterChange}
onRemove={onRemove}
onAdd={onAdd}
itemType="filter"
addButtonText="Add Filter"
saveAllFieldsAsString={false}
/>
);
};
export default FilterForm;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import FilterForm from '../FilterForm';
/**
* FiltersSection component for the agent form
*/
const FiltersSection = ({ formData, setFormData, metadata }) => {
// Handle filter change
const handleFilterChange = (index, updatedFilter) => {
const updatedFilters = [...(formData.filters || [])];
updatedFilters[index] = updatedFilter;
setFormData({
...formData,
filters: updatedFilters
});
};
// Handle filter removal
const handleFilterRemove = (index) => {
const updatedFilters = [...(formData.filters || [])].filter((_, i) => i !== index);
setFormData({
...formData,
filters: updatedFilters
});
};
// Handle adding an filter
const handleAddFilter = () => {
setFormData({
...formData,
filters: [
...(formData.filters || []),
{ name: '', config: '{}' }
]
});
};
return (
<div className="filters-section">
<h3>Filters</h3>
<p className="text-muted">
Jobs received by the agent must pass all filters and at least one trigger (if any are specified)
</p>
<FilterForm
filters={formData.filters || []}
onChange={handleFilterChange}
onRemove={handleFilterRemove}
onAdd={handleAddFilter}
fieldGroups={metadata?.filters || []}
/>
</div>
);
};
export default FiltersSection;

View File

@@ -1,6 +1,11 @@
import { useState, useEffect } from 'react';
import { useOutletContext, useNavigate } from 'react-router-dom';
import { actionApi } from '../utils/api';
import { actionApi, agentApi } from '../utils/api';
import FormFieldDefinition from '../components/common/FormFieldDefinition';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import 'highlight.js/styles/monokai.css';
hljs.registerLanguage('json', json);
function ActionsPlayground() {
const { showToast } = useOutletContext();
@@ -12,6 +17,10 @@ function ActionsPlayground() {
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [loadingActions, setLoadingActions] = useState(true);
const [actionMetadata, setActionMetadata] = useState(null);
const [agentMetadata, setAgentMetadata] = useState(null);
const [configFields, setConfigFields] = useState([]);
const [paramFields, setParamFields] = useState([]);
// Update document title
useEffect(() => {
@@ -36,21 +45,106 @@ function ActionsPlayground() {
};
fetchActions();
}, [showToast]);
}, []);
// Fetch agent metadata on mount
useEffect(() => {
const fetchAgentMetadata = async () => {
try {
const metadata = await agentApi.getAgentConfigMetadata();
setAgentMetadata(metadata);
} catch (err) {
console.error('Error fetching agent metadata:', err);
showToast('Failed to load agent metadata', 'error');
}
};
fetchAgentMetadata();
}, []);
// Fetch action definition when action is selected or config changes
useEffect(() => {
if (!selectedAction) return;
const fetchActionDefinition = async () => {
try {
// Get config fields from agent metadata
const actionMeta = agentMetadata?.actions?.find(action => action.name === selectedAction);
const configFields = actionMeta?.fields || [];
console.debug('Config fields:', configFields);
setConfigFields(configFields);
// Parse current config to pass to action definition
let currentConfig = {};
try {
currentConfig = JSON.parse(configJson);
} catch (err) {
console.error('Error parsing current config:', err);
}
// Get parameter fields from action definition
const paramFields = await actionApi.getActionDefinition(selectedAction, currentConfig);
console.debug('Parameter fields:', paramFields);
setParamFields(paramFields);
// Reset JSON to match the new fields
setConfigJson(JSON.stringify(currentConfig, null, 2));
setParamsJson(JSON.stringify({}, null, 2));
setResult(null);
} catch (err) {
console.error('Error fetching action definition:', err);
showToast('Failed to load action definition', 'error');
}
};
fetchActionDefinition();
}, [selectedAction, agentMetadata]);
// Handle action selection
const handleActionChange = (e) => {
setSelectedAction(e.target.value);
setConfigJson('{}');
setParamsJson('{}');
setResult(null);
};
// Handle JSON input changes
const handleConfigChange = (e) => {
setConfigJson(e.target.value);
// Helper to generate onChange handlers for form fields
const makeFieldChangeHandler = (fields, updateFn) => (e) => {
let value;
if (e && e.target) {
const fieldName = e.target.name;
const fieldDef = fields.find(f => f.name === fieldName);
const fieldType = fieldDef ? fieldDef.type : undefined;
if (fieldType === 'checkbox') {
value = e.target.checked;
} else if (fieldType === 'number') {
value = e.target.value === '' ? '' : String(e.target.value);
} else {
value = e.target.value;
}
updateFn(fieldName, value);
}
};
const handleParamsChange = (e) => {
setParamsJson(e.target.value);
// Handle form field changes
const handleConfigChange = (field, value) => {
try {
const config = JSON.parse(configJson);
config[field] = value;
setConfigJson(JSON.stringify(config, null, 2));
} catch (err) {
console.error('Error updating config:', err);
}
};
const handleParamsChange = (field, value) => {
try {
const params = JSON.parse(paramsJson);
params[field] = value;
setParamsJson(JSON.stringify(params, null, 2));
} catch (err) {
console.error('Error updating params:', err);
}
};
// Execute the selected action
@@ -135,34 +229,31 @@ function ActionsPlayground() {
{selectedAction && (
<div className="section-box">
<h2>Action Configuration</h2>
<form onSubmit={handleExecuteAction}>
<div className="form-group mb-6">
<label htmlFor="config-json">Configuration (JSON):</label>
<textarea
id="config-json"
value={configJson}
onChange={handleConfigChange}
className="form-control"
rows="5"
placeholder='{"key": "value"}'
{configFields.length > 0 && (
<>
<h2>Configuration</h2>
<FormFieldDefinition
fields={configFields}
values={JSON.parse(configJson)}
onChange={makeFieldChangeHandler(configFields, handleConfigChange)}
idPrefix="config_"
/>
<p className="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
</div>
</>
)}
<div className="form-group mb-6">
<label htmlFor="params-json">Parameters (JSON):</label>
<textarea
id="params-json"
value={paramsJson}
onChange={handleParamsChange}
className="form-control"
rows="5"
placeholder='{"key": "value"}'
{paramFields.length > 0 && (
<>
<h2>Parameters</h2>
<FormFieldDefinition
fields={paramFields}
values={JSON.parse(paramsJson)}
onChange={makeFieldChangeHandler(paramFields, handleParamsChange)}
idPrefix="param_"
/>
<p className="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
</div>
</>
)}
<div className="flex justify-end">
<button
@@ -194,9 +285,9 @@ function ActionsPlayground() {
backgroundColor: 'rgba(30, 30, 30, 0.7)'
}}>
{typeof result === 'object' ? (
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{JSON.stringify(result, null, 2)}
</pre>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(result, null, 2), { language: 'json' }).value }}></div>
</code></pre>
) : (
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{result}

View File

@@ -1,4 +1,200 @@
import { useState, useEffect } from 'react';
import CollapsibleRawSections from '../components/CollapsibleRawSections';
function ObservableSummary({ observable }) {
// --- CREATION SUMMARIES ---
const creation = observable?.creation || {};
// ChatCompletionRequest summary
let creationChatMsg = '';
// Prefer chat_completion_message if present (for jobs/top-level containers)
if (creation?.chat_completion_message && creation.chat_completion_message.content) {
creationChatMsg = creation.chat_completion_message.content;
} else {
const messages = creation?.chat_completion_request?.messages;
if (Array.isArray(messages) && messages.length > 0) {
const lastMsg = messages[messages.length - 1];
creationChatMsg = lastMsg?.content || '';
}
}
// FunctionDefinition summary
let creationFunctionDef = '';
if (creation?.function_definition?.name) {
creationFunctionDef = `Function: ${creation.function_definition.name}`;
}
// FunctionParams summary
let creationFunctionParams = '';
if (creation?.function_params && Object.keys(creation.function_params).length > 0) {
creationFunctionParams = `Params: ${JSON.stringify(creation.function_params)}`;
}
// --- COMPLETION SUMMARIES ---
const completion = observable?.completion || {};
// ChatCompletionResponse summary
let completionChatMsg = '';
const chatCompletion = completion?.chat_completion_response;
if (
chatCompletion &&
Array.isArray(chatCompletion.choices) &&
chatCompletion.choices.length > 0
) {
const lastChoice = chatCompletion.choices[chatCompletion.choices.length - 1];
// Prefer tool_call summary if present
let toolCallSummary = '';
const toolCalls = lastChoice?.message?.tool_calls;
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
toolCallSummary = toolCalls.map(tc => {
let args = '';
// For OpenAI-style, arguments are in tc.function.arguments, function name in tc.function.name
if (tc.function && tc.function.arguments) {
try {
args = typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments);
} catch (e) {
args = '[Unserializable arguments]';
}
}
const toolName = tc.function?.name || tc.name || 'unknown';
return `Tool call: ${toolName}(${args})`;
}).join('\n');
}
completionChatMsg = lastChoice?.message?.content || '';
// Attach toolCallSummary to completionChatMsg for rendering
if (toolCallSummary) {
completionChatMsg = { toolCallSummary, message: completionChatMsg };
}
// Else, it's just a string
}
// Conversation summary
let completionConversation = '';
if (Array.isArray(completion?.conversation) && completion.conversation.length > 0) {
const lastConv = completion.conversation[completion.conversation.length - 1];
completionConversation = lastConv?.content ? `${lastConv.content}` : '';
}
// ActionResult summary
let completionActionResult = '';
if (completion?.action_result) {
completionActionResult = `Action Result: ${String(completion.action_result).slice(0, 100)}`;
}
// AgentState summary
let completionAgentState = '';
if (completion?.agent_state) {
completionAgentState = `Agent State: ${JSON.stringify(completion.agent_state)}`;
}
// Error summary
let completionError = '';
if (completion?.error) {
completionError = `Error: ${completion.error}`;
}
let completionFilter = '';
if (completion?.filter_result) {
if (completion.filter_result?.has_triggers && !completion.filter_result?.triggered_by) {
completionFilter = 'Failed to match any triggers';
} else if (completion.filter_result?.triggered_by) {
completionFilter = `Triggered by ${completion.filter_result.triggered_by}`;
}
if (completion?.filter_result?.failed_by)
completionFilter = `${completionFilter ? completionFilter + ', ' : ''}Failed by ${completion.filter_result.failed_by}`;
}
// Only show if any summary is present
if (!creationChatMsg && !creationFunctionDef && !creationFunctionParams &&
!completionChatMsg && !completionConversation && !completionActionResult &&
!completionAgentState && !completionError && !completionFilter) {
return null;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, margin: '2px 0 0 0' }}>
{/* CREATION */}
{creationChatMsg && (
<div title={creationChatMsg} style={{ display: 'flex', alignItems: 'center', color: '#cfc', fontSize: 14 }}>
<i className="fas fa-comment-dots" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{creationChatMsg}</span>
</div>
)}
{creationFunctionDef && (
<div title={creationFunctionDef} style={{ display: 'flex', alignItems: 'center', color: '#cfc', fontSize: 14 }}>
<i className="fas fa-code" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{creationFunctionDef}</span>
</div>
)}
{creationFunctionParams && (
<div title={creationFunctionParams} style={{ display: 'flex', alignItems: 'center', color: '#fc9', fontSize: 14 }}>
<i className="fas fa-sliders-h" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{creationFunctionParams}</span>
</div>
)}
{/* COMPLETION */}
{/* COMPLETION: Tool call summary if present */}
{completionChatMsg && typeof completionChatMsg === 'object' && completionChatMsg.toolCallSummary && (
<div
title={completionChatMsg.toolCallSummary}
style={{
display: 'flex',
alignItems: 'center',
color: '#ffd966', // Distinct color for tool calls
fontSize: 14,
marginTop: 2,
whiteSpace: 'pre-line',
wordBreak: 'break-all',
}}
>
<i className="fas fa-tools" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ whiteSpace: 'pre-line', display: 'block' }}>{completionChatMsg.toolCallSummary}</span>
</div>
)}
{/* COMPLETION: Message content if present */}
{completionChatMsg && ((typeof completionChatMsg === 'object' && completionChatMsg.message) || typeof completionChatMsg === 'string') && (
<div
title={typeof completionChatMsg === 'object' ? completionChatMsg.message : completionChatMsg}
style={{
display: 'flex',
alignItems: 'center',
color: '#8fc7ff',
fontSize: 14,
marginTop: 2,
}}
>
<i className="fas fa-robot" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{typeof completionChatMsg === 'object' ? completionChatMsg.message : completionChatMsg}</span>
</div>
)}
{completionConversation && (
<div title={completionConversation} style={{ display: 'flex', alignItems: 'center', color: '#b8e2ff', fontSize: 14 }}>
<i className="fas fa-comments" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionConversation}</span>
</div>
)}
{completionActionResult && (
<div title={completionActionResult} style={{ display: 'flex', alignItems: 'center', color: '#ffd700', fontSize: 14 }}>
<i className="fas fa-bolt" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionActionResult}</span>
</div>
)}
{completionAgentState && (
<div title={completionAgentState} style={{ display: 'flex', alignItems: 'center', color: '#ffb8b8', fontSize: 14 }}>
<i className="fas fa-brain" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionAgentState}</span>
</div>
)}
{completionError && (
<div title={completionError} style={{ display: 'flex', alignItems: 'center', color: '#f66', fontSize: 14 }}>
<i className="fas fa-exclamation-triangle" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionError}</span>
</div>
)}
{completionFilter && (
<div title={completionFilter} style={{ display: 'flex', alignItems: 'center', color: '#ffd7', fontSize: 14 }}>
<i className="fas fa-shield-alt" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionFilter}</span>
</div>
)}
</div>
);
}
import { useParams, Link } from 'react-router-dom';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
@@ -7,7 +203,7 @@ import 'highlight.js/styles/monokai.css';
hljs.registerLanguage('json', json);
function AgentStatus() {
const [showStatus, setShowStatus] = useState(true);
const [showStatus, setShowStatus] = useState(false);
const { name } = useParams();
const [statusData, setStatusData] = useState(null);
const [loading, setLoading] = useState(true);
@@ -94,17 +290,16 @@ function AgentStatus() {
setObservableMap(prevMap => {
const prev = prevMap[data.id] || {};
const updated = {
...prev,
...data,
creation: data.creation,
progress: data.progress,
completion: data.completion,
...prev,
};
// Events can be received out of order
if (data.creation)
updated.creation = data.creation;
if (data.completion)
updated.completion = data.completion;
if ((data.progress?.length ?? 0) > (prev.progress?.length ?? 0))
updated.progress = data.progress;
if (data.parent_id && !prevMap[data.parent_id])
prevMap[data.parent_id] = {
id: data.parent_id,
@@ -252,11 +447,16 @@ function AgentStatus() {
setExpandedCards(new Map(expandedCards).set(container.id, newExpanded));
}}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', maxWidth: '90%' }}>
<i className={`fas fa-${container.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
<span style={{ width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
<span>
<span className='stat-label'>{container.name}</span>#<span className='stat-label'>{container.id}</span>
</span>
<ObservableSummary observable={container} />
</div>
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<i
@@ -279,17 +479,22 @@ function AgentStatus() {
const isExpanded = expandedCards.get(childKey);
return (
<div key={`${container.id}-child-${child.id}`} className='card' style={{ background: '#222', marginBottom: '0.5em' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'hand', maxWidth: '100%' }}
onClick={() => {
const newExpanded = !expandedCards.get(childKey);
setExpandedCards(new Map(expandedCards).set(childKey, newExpanded));
}}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div style={{ display: 'flex', maxWidth: '90%', gap: '10px', alignItems: 'center' }}>
<i className={`fas fa-${child.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
<span style={{ width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
<span>
<span className='stat-label'>{child.name}</span>#<span className='stat-label'>{child.id}</span>
</span>
<ObservableSummary observable={child} />
</div>
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<i
@@ -303,60 +508,14 @@ function AgentStatus() {
</div>
</div>
<div style={{ display: isExpanded ? 'block' : 'none' }}>
{child.creation && (
<div>
<h5>Creation:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.creation || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{child.progress && child.progress.length > 0 && (
<div>
<h5>Progress:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.progress || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{child.completion && (
<div>
<h5>Completion:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.completion || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
<CollapsibleRawSections container={child} />
</div>
</div>
);
})}
</div>
)}
{container.creation && (
<div>
<h4>Creation:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.creation || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{container.progress && container.progress.length > 0 && (
<div>
<h4>Progress:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.progress || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{container.completion && (
<div>
<h4>Completion:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.completion || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
<CollapsibleRawSections container={container} />
</div>
</div>
</div>

View File

@@ -24,6 +24,50 @@ const buildUrl = (endpoint) => {
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
};
// Helper function to convert ActionDefinition to FormFieldDefinition format
const convertActionDefinitionToFields = (definition) => {
if (!definition || !definition.Properties) {
return [];
}
const fields = [];
const required = definition.Required || [];
console.debug('Action definition:', definition);
Object.entries(definition.Properties).forEach(([name, property]) => {
const field = {
name,
label: name.charAt(0).toUpperCase() + name.slice(1),
type: 'text', // Default to text, we'll enhance this later
required: required.includes(name),
helpText: property.Description || '',
defaultValue: property.Default,
};
if (property.enum && property.enum.length > 0) {
field.type = 'select';
field.options = property.enum;
} else {
switch (property.type) {
case 'integer':
field.type = 'number';
field.min = property.Minimum;
field.max = property.Maximum;
break;
case 'boolean':
field.type = 'checkbox';
break;
}
// TODO: Handle Object and Array types which require nested fields
}
fields.push(field);
});
return fields;
};
// Agent-related API calls
export const agentApi = {
// Get list of all agents
@@ -77,6 +121,7 @@ export const agentApi = {
groupedMetadata.actions = metadata.Actions;
}
groupedMetadata.dynamicPrompts = metadata.DynamicPrompts;
groupedMetadata.filters = metadata.Filters;
return groupedMetadata;
}
@@ -216,6 +261,17 @@ export const actionApi = {
return handleResponse(response);
},
// Get action definition
getActionDefinition: async (name, config = {}) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.actionDefinition(name)), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(config),
});
const definition = await handleResponse(response);
return convertActionDefinitionToFields(definition);
},
// Execute an action for an agent
executeAction: async (name, actionData) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {

View File

@@ -43,6 +43,7 @@ export const API_CONFIG = {
// Action endpoints
listActions: '/api/actions',
actionDefinition: (name) => `/api/action/${name}/definition`,
executeAction: (name) => `/api/action/${name}/run`,
// Status endpoint

Some files were not shown because too many files have changed in this diff Show More