Compare commits
48 Commits
fix-mcp-co
...
mock-llm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5698d0b832 | ||
|
|
c23e655f44 | ||
|
|
2b07dd79ec | ||
|
|
864bf8b94c | ||
|
|
60c53c8f3e | ||
|
|
cc0f5cbdcc | ||
|
|
8d527d6a09 | ||
|
|
e431bc234b | ||
|
|
289edb67a6 | ||
|
|
324124e002 | ||
|
|
2bacac687f | ||
|
|
8b504a5e1e | ||
|
|
0e4e60cc15 | ||
|
|
fb1ab70650 | ||
|
|
94f4d350c9 | ||
|
|
cc3fdecfc9 | ||
|
|
c92acd670e | ||
|
|
0464a5b344 | ||
|
|
8bdb575bb2 | ||
|
|
f2c3b9dbdb | ||
|
|
02c6b5ad4e | ||
|
|
5e5224da25 | ||
|
|
c529f880d3 | ||
|
|
18eb40ec14 | ||
|
|
904765591c | ||
|
|
f726d3c3e5 | ||
|
|
62ce629bf1 | ||
|
|
9c555bd99f | ||
|
|
5981109730 | ||
|
|
087a5fbe0f | ||
|
|
67cb5937e7 | ||
|
|
8abf5512a4 | ||
|
|
45dd74d27c | ||
|
|
1109b0a533 | ||
|
|
bd1b06f326 | ||
|
|
7406db5882 | ||
|
|
a1efa07b24 | ||
|
|
29f7644577 | ||
|
|
f3884c0244 | ||
|
|
6516af6c34 | ||
|
|
77680c6fee | ||
|
|
5faa599321 | ||
|
|
6209ededff | ||
|
|
f6b6d5246c | ||
|
|
b81624bfc2 | ||
|
|
c1844f7230 | ||
|
|
15efd2d527 | ||
|
|
5e3bc0f89b |
18
.github/workflows/tests.yml
vendored
18
.github/workflows/tests.yml
vendored
@@ -30,17 +30,29 @@ jobs:
|
|||||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin make
|
||||||
docker version
|
docker version
|
||||||
|
|
||||||
docker run --rm hello-world
|
docker run --rm hello-world
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.17.0'
|
go-version: '>=1.17.0'
|
||||||
|
- name: Free up disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo apt-get clean
|
||||||
|
docker system prune -af || true
|
||||||
|
df -h
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y make
|
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
||||||
make tests
|
make tests-mock
|
||||||
|
else
|
||||||
|
make tests
|
||||||
|
fi
|
||||||
#sudo mv coverage/coverage.txt coverage.txt
|
#sudo mv coverage/coverage.txt coverage.txt
|
||||||
#sudo chmod 777 coverage.txt
|
#sudo chmod 777 coverage.txt
|
||||||
|
|
||||||
|
|||||||
49
.github/workflows/tests_fragile.yml
vendored
Normal file
49
.github/workflows/tests_fragile.yml
vendored
Normal 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
|
||||||
@@ -20,10 +20,12 @@ COPY . .
|
|||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
|
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM alpine:3.19
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install runtime dependencies
|
# 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
|
# Create non-root user
|
||||||
#RUN adduser -D -g '' appuser
|
#RUN adduser -D -g '' appuser
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -3,6 +3,8 @@ IMAGE_NAME?=webui
|
|||||||
MCPBOX_IMAGE_NAME?=mcpbox
|
MCPBOX_IMAGE_NAME?=mcpbox
|
||||||
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
|
||||||
|
.PHONY: tests tests-mock cleanup-tests
|
||||||
|
|
||||||
prepare-tests: build-mcpbox
|
prepare-tests: build-mcpbox
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
docker run -d -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 --rm -ti $(MCPBOX_IMAGE_NAME)
|
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
|
tests: prepare-tests
|
||||||
LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="gemma-3-12b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
|
LOCALAGI_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:
|
run-nokb:
|
||||||
$(MAKE) run KBDISABLEINDEX=true
|
$(MAKE) run KBDISABLEINDEX=true
|
||||||
|
|
||||||
@@ -37,4 +42,4 @@ build-mcpbox:
|
|||||||
docker build -t $(MCPBOX_IMAGE_NAME) -f Dockerfile.mcpbox .
|
docker build -t $(MCPBOX_IMAGE_NAME) -f Dockerfile.mcpbox .
|
||||||
|
|
||||||
run-mcpbox:
|
run-mcpbox:
|
||||||
docker run -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 -ti mcpbox
|
docker run -v /var/run/docker.sock:/var/run/docker.sock --privileged -p 9090:8080 -ti mcpbox
|
||||||
|
|||||||
226
README.md
226
README.md
@@ -11,6 +11,9 @@
|
|||||||
[](https://github.com/mudler/LocalAGI/stargazers)
|
[](https://github.com/mudler/LocalAGI/stargazers)
|
||||||
[](https://github.com/mudler/LocalAGI/issues)
|
[](https://github.com/mudler/LocalAGI/issues)
|
||||||
|
|
||||||
|
|
||||||
|
Try on [](https://t.me/LocalAGI_bot)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Create customizable AI assistants, automations, chat bots and agents that run 100% locally. No need for agentic Python libraries or cloud service keys, just bring your GPU (or even just CPU) and a web browser.
|
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
|
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
|
||||||
|
|
||||||
|
## Videos
|
||||||
|
|
||||||
|
[](https://youtu.be/HtVwIxW3ePg)
|
||||||
|
[](https://youtu.be/v82rswGJt_M)
|
||||||
|
[](https://youtu.be/d_we-AYksSw)
|
||||||
|
|
||||||
|
|
||||||
## 📚🆕 Local Stack Family
|
## 📚🆕 Local Stack Family
|
||||||
|
|
||||||
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
||||||
@@ -180,14 +190,6 @@ Good (relatively small) models that have been tested are:
|
|||||||
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
|
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
|
||||||
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
|
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
|
||||||
|
|
||||||
## 🌐 The Local Ecosystem
|
|
||||||
|
|
||||||
LocalAGI is part of the powerful Local family of privacy-focused AI tools:
|
|
||||||
|
|
||||||
- [**LocalAI**](https://github.com/mudler/LocalAI): Run Large Language Models locally.
|
|
||||||
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
|
|
||||||
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
|
|
||||||
|
|
||||||
## 🌟 Screenshots
|
## 🌟 Screenshots
|
||||||
|
|
||||||
### Powerful Web UI
|
### Powerful Web UI
|
||||||
@@ -259,6 +261,158 @@ go build -o localagi
|
|||||||
./localagi
|
./localagi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using as a Library
|
||||||
|
|
||||||
|
LocalAGI can be used as a Go library to programmatically create and manage AI agents. Let's start with a simple example of creating a single agent:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Basic Usage: Single Agent</strong></summary>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a new agent with basic configuration
|
||||||
|
agent, err := agent.New(
|
||||||
|
agent.WithModel("gpt-4"),
|
||||||
|
agent.WithLLMAPIURL("http://localhost:8080"),
|
||||||
|
agent.WithLLMAPIKey("your-api-key"),
|
||||||
|
agent.WithSystemPrompt("You are a helpful assistant."),
|
||||||
|
agent.WithCharacter(agent.Character{
|
||||||
|
Name: "my-agent",
|
||||||
|
}),
|
||||||
|
agent.WithActions(
|
||||||
|
// Add your custom actions here
|
||||||
|
),
|
||||||
|
agent.WithStateFile("./state/my-agent.state.json"),
|
||||||
|
agent.WithCharacterFile("./state/my-agent.character.json"),
|
||||||
|
agent.WithTimeout("10m"),
|
||||||
|
agent.EnableKnowledgeBase(),
|
||||||
|
agent.EnableReasoning(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the agent
|
||||||
|
go func() {
|
||||||
|
if err := agent.Run(); err != nil {
|
||||||
|
log.Printf("Agent stopped: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Stop the agent when done
|
||||||
|
agent.Stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
This basic example shows how to:
|
||||||
|
- Create a single agent with essential configuration
|
||||||
|
- Set up the agent's model and API connection
|
||||||
|
- Configure basic features like knowledge base and reasoning
|
||||||
|
- Start and stop the agent
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Advanced Usage: Agent Pools</strong></summary>
|
||||||
|
|
||||||
|
For managing multiple agents, you can use the AgentPool system:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/mudler/LocalAGI/core/state"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a new agent pool
|
||||||
|
pool, err := state.NewAgentPool(
|
||||||
|
"default-model", // default model name
|
||||||
|
"default-multimodal-model", // default multimodal model
|
||||||
|
"image-model", // image generation model
|
||||||
|
"http://localhost:8080", // API URL
|
||||||
|
"your-api-key", // API key
|
||||||
|
"./state", // state directory
|
||||||
|
"", // MCP box URL (optional)
|
||||||
|
"http://localhost:8081", // LocalRAG API URL
|
||||||
|
func(config *AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action {
|
||||||
|
// Define available actions for agents
|
||||||
|
return func(ctx context.Context, pool *AgentPool) []types.Action {
|
||||||
|
return []types.Action{
|
||||||
|
// Add your custom actions here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(config *AgentConfig) []Connector {
|
||||||
|
// Define connectors for agents
|
||||||
|
return []Connector{
|
||||||
|
// Add your custom connectors here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(config *AgentConfig) []DynamicPrompt {
|
||||||
|
// Define dynamic prompts for agents
|
||||||
|
return []DynamicPrompt{
|
||||||
|
// Add your custom prompts here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(config *AgentConfig) types.JobFilters {
|
||||||
|
// Define job filters for agents
|
||||||
|
return types.JobFilters{
|
||||||
|
// Add your custom filters here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"10m", // timeout
|
||||||
|
true, // enable conversation logs
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a new agent in the pool
|
||||||
|
agentConfig := &AgentConfig{
|
||||||
|
Name: "my-agent",
|
||||||
|
Model: "gpt-4",
|
||||||
|
SystemPrompt: "You are a helpful assistant.",
|
||||||
|
EnableKnowledgeBase: true,
|
||||||
|
EnableReasoning: true,
|
||||||
|
// Add more configuration options as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pool.CreateAgent("my-agent", agentConfig)
|
||||||
|
|
||||||
|
// Start all agents
|
||||||
|
err = pool.StartAll()
|
||||||
|
|
||||||
|
// Get agent status
|
||||||
|
status := pool.GetStatusHistory("my-agent")
|
||||||
|
|
||||||
|
// Stop an agent
|
||||||
|
pool.Stop("my-agent")
|
||||||
|
|
||||||
|
// Remove an agent
|
||||||
|
err = pool.Remove("my-agent")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Available Features</strong></summary>
|
||||||
|
|
||||||
|
Key features available through the library:
|
||||||
|
|
||||||
|
- **Single Agent Management**: Create and manage individual agents with basic configuration
|
||||||
|
- **Agent Pool Management**: Create, start, stop, and remove multiple agents
|
||||||
|
- **Configuration**: Customize agent behavior through AgentConfig
|
||||||
|
- **Actions**: Define custom actions for agents to perform
|
||||||
|
- **Connectors**: Add custom connectors for external services
|
||||||
|
- **Dynamic Prompts**: Create dynamic prompt templates
|
||||||
|
- **Job Filters**: Implement custom job filtering logic
|
||||||
|
- **Status Tracking**: Monitor agent status and history
|
||||||
|
- **State Persistence**: Automatic state saving and loading
|
||||||
|
|
||||||
|
For more details about available configuration options and features, refer to the [Agent Configuration Reference](#agent-configuration-reference) section.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
|
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
|
||||||
@@ -285,7 +439,8 @@ cd ../.. && go run main.go
|
|||||||
|
|
||||||
Link your agents to the services you already use. Configuration examples below.
|
Link your agents to the services you already use. Configuration examples below.
|
||||||
|
|
||||||
### GitHub Issues
|
<details>
|
||||||
|
<summary><strong>GitHub Issues</strong></summary>
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -295,8 +450,10 @@ Link your agents to the services you already use. Configuration examples below.
|
|||||||
"botUserName": "bot-username"
|
"botUserName": "bot-username"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### Discord
|
<details>
|
||||||
|
<summary><strong>Discord</strong></summary>
|
||||||
|
|
||||||
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
|
After [creating your Discord bot](https://discordpy.readthedocs.io/en/stable/discord.html):
|
||||||
|
|
||||||
@@ -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!
|
> Don't forget to enable "Message Content Intent" in Bot(tab) settings!
|
||||||
> Enable " Message Content Intent " in the Bot tab!
|
> Enable " Message Content Intent " in the Bot tab!
|
||||||
|
</details>
|
||||||
|
|
||||||
### Slack
|
<details>
|
||||||
|
<summary><strong>Slack</strong></summary>
|
||||||
|
|
||||||
Use the included `slack.yaml` manifest to create your app, then configure:
|
Use the included `slack.yaml` manifest to create your app, then configure:
|
||||||
|
|
||||||
@@ -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 Oauth token bot token from "OAuth & Permissions" -> "OAuth Tokens for Your Workspace"
|
||||||
- Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
|
- Create App level token (from "Basic Information" -> "App-Level Tokens" ( scope connections:writeRoute authorizations:read ))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
### Telegram
|
<summary><strong>Telegram</strong></summary>
|
||||||
|
|
||||||
Get a token from @botfather, then:
|
Get a token from @botfather, then:
|
||||||
|
|
||||||
@@ -333,8 +493,10 @@ Get a token from @botfather, then:
|
|||||||
"token": "your-bot-father-token"
|
"token": "your-bot-father-token"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
### IRC
|
<details>
|
||||||
|
<summary><strong>IRC</strong></summary>
|
||||||
|
|
||||||
Connect to IRC networks:
|
Connect to IRC networks:
|
||||||
|
|
||||||
@@ -347,10 +509,12 @@ Connect to IRC networks:
|
|||||||
"alwaysReply": "false"
|
"alwaysReply": "false"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
### Agent Management
|
<details>
|
||||||
|
<summary><strong>Agent Management</strong></summary>
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -365,8 +529,10 @@ Connect to IRC networks:
|
|||||||
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
|
| `/api/meta/agent/config` | GET | Get agent configuration metadata | |
|
||||||
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
|
| `/settings/export/:name` | GET | Export agent config | [Example](#export-agent) |
|
||||||
| `/settings/import` | POST | Import agent config | [Example](#import-agent) |
|
| `/settings/import` | POST | Import agent config | [Example](#import-agent) |
|
||||||
|
</details>
|
||||||
|
|
||||||
### Actions and Groups
|
<details>
|
||||||
|
<summary><strong>Actions and Groups</strong></summary>
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -374,8 +540,10 @@ Connect to IRC networks:
|
|||||||
| `/api/action/:name/run` | POST | Execute an action | |
|
| `/api/action/:name/run` | POST | Execute an action | |
|
||||||
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
|
| `/api/agent/group/generateProfiles` | POST | Generate group profiles | |
|
||||||
| `/api/agent/group/create` | POST | Create a new agent group | |
|
| `/api/agent/group/create` | POST | Create a new agent group | |
|
||||||
|
</details>
|
||||||
|
|
||||||
### Chat Interactions
|
<details>
|
||||||
|
<summary><strong>Chat Interactions</strong></summary>
|
||||||
|
|
||||||
| Endpoint | Method | Description | Example |
|
| Endpoint | Method | Description | Example |
|
||||||
|----------|--------|-------------|---------|
|
|----------|--------|-------------|---------|
|
||||||
@@ -383,6 +551,7 @@ Connect to IRC networks:
|
|||||||
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
|
| `/api/notify/:name` | POST | Send notification to agent | [Example](#notify-agent) |
|
||||||
| `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) |
|
| `/api/sse/:name` | GET | Real-time agent event stream | [Example](#agent-sse-stream) |
|
||||||
| `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) |
|
| `/v1/responses` | POST | Send message & get response | [OpenAI's Responses](https://platform.openai.com/docs/api-reference/responses/create) |
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Curl Examples</strong></summary>
|
<summary><strong>Curl Examples</strong></summary>
|
||||||
@@ -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"
|
curl -N -X GET "http://localhost:3000/api/sse/my-agent"
|
||||||
```
|
```
|
||||||
Note: For proper SSE handling, you should use a client that supports SSE natively.
|
Note: For proper SSE handling, you should use a client that supports SSE natively.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Agent Configuration Reference
|
### Agent Configuration Reference
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Configuration Structure</strong></summary>
|
||||||
|
|
||||||
The agent configuration defines how an agent behaves and what capabilities it has. You can view the available configuration options and their descriptions by using the metadata endpoint:
|
The agent configuration defines how an agent behaves and what capabilities it has. You can view the available configuration options and their descriptions by using the metadata endpoint:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -507,6 +678,25 @@ Here's an example of the agent configuration structure:
|
|||||||
"summary_long_term_memory": false
|
"summary_long_term_memory": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Environment Configuration</strong></summary>
|
||||||
|
|
||||||
|
LocalAGI supports environment configurations. Note that these environment variables needs to be specified in the localagi container in the docker-compose file to have effect.
|
||||||
|
|
||||||
|
| Variable | What It Does |
|
||||||
|
|----------|--------------|
|
||||||
|
| `LOCALAGI_MODEL` | Your go-to model |
|
||||||
|
| `LOCALAGI_MULTIMODAL_MODEL` | Optional model for multimodal capabilities |
|
||||||
|
| `LOCALAGI_LLM_API_URL` | OpenAI-compatible API server URL |
|
||||||
|
| `LOCALAGI_LLM_API_KEY` | API authentication |
|
||||||
|
| `LOCALAGI_TIMEOUT` | Request timeout settings |
|
||||||
|
| `LOCALAGI_STATE_DIR` | Where state gets stored |
|
||||||
|
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
|
||||||
|
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||||
|
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||||
|
</details>
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (a *CustomAction) Plannable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *CustomAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
|
v, err := a.i.Eval(fmt.Sprintf("%s.Run", a.config["name"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, err
|
return types.ActionResult{}, err
|
||||||
@@ -95,6 +95,11 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
|
|||||||
|
|
||||||
func (a *CustomAction) Definition() types.ActionDefinition {
|
func (a *CustomAction) Definition() types.ActionDefinition {
|
||||||
|
|
||||||
|
if a.i == nil {
|
||||||
|
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
|
||||||
|
return types.ActionDefinition{}
|
||||||
|
}
|
||||||
|
|
||||||
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error getting custom action definition", "error", err)
|
xlog.Error("Error getting custom action definition", "error", err)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ return []string{"foo"}
|
|||||||
Description: "A test action",
|
Description: "A test action",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
runResult, err := customAction.Run(context.Background(), types.ActionParams{
|
runResult, err := customAction.Run(context.Background(), nil, types.ActionParams{
|
||||||
"Foo": "bar",
|
"Foo": "bar",
|
||||||
})
|
})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type GoalResponse struct {
|
|||||||
Achieved bool `json:"achieved"`
|
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
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type IntentResponse struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *IntentAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *IntentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type ConversationActionResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ConversationAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *ConversationAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func NewStop() *StopAction {
|
|||||||
|
|
||||||
type StopAction struct{}
|
type StopAction struct{}
|
||||||
|
|
||||||
func (a *StopAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *StopAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type PlanSubtask struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PlanAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *PlanAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type ReasoningResponse struct {
|
|||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ReasoningAction) Run(context.Context, types.ActionParams) (types.ActionResult, error) {
|
func (a *ReasoningAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
return types.ActionResult{}, nil
|
return types.ActionResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type ReplyResponse struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ReplyAction) Run(context.Context, types.ActionParams) (string, error) {
|
func (a *ReplyAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (string, error) {
|
||||||
return "no-op", nil
|
return "no-op", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func NewState() *StateAction {
|
|||||||
|
|
||||||
type StateAction struct{}
|
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
|
return types.ActionResult{Result: "internal state has been updated"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -480,13 +480,18 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
|||||||
}, c...)
|
}, c...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reasoningAction := action.NewReasoning()
|
||||||
thought, err := a.decision(job,
|
thought, err := a.decision(job,
|
||||||
c,
|
c,
|
||||||
types.Actions{action.NewReasoning()}.ToTools(),
|
types.Actions{reasoningAction}.ToTools(),
|
||||||
action.NewReasoning().Definition().Name.String(), maxRetries)
|
reasoningAction.Definition().Name.String(), maxRetries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
|
if thought.actioName != "" && thought.actioName != reasoningAction.Definition().Name.String() {
|
||||||
|
return nil, nil, "", fmt.Errorf("Expected reasoning action not: %s", thought.actioName)
|
||||||
|
}
|
||||||
|
|
||||||
originalReasoning := ""
|
originalReasoning := ""
|
||||||
response := &action.ReasoningResponse{}
|
response := &action.ReasoningResponse{}
|
||||||
if thought.actionParams != nil {
|
if thought.actionParams != nil {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ type Agent struct {
|
|||||||
sync.Mutex
|
sync.Mutex
|
||||||
options *options
|
options *options
|
||||||
Character Character
|
Character Character
|
||||||
client *openai.Client
|
client llm.LLMClient
|
||||||
jobQueue chan *types.Job
|
jobQueue chan *types.Job
|
||||||
context *types.ActionContext
|
context *types.ActionContext
|
||||||
|
|
||||||
@@ -44,6 +46,8 @@ type Agent struct {
|
|||||||
newMessagesSubscribers []func(openai.ChatCompletionMessage)
|
newMessagesSubscribers []func(openai.ChatCompletionMessage)
|
||||||
|
|
||||||
observer Observer
|
observer Observer
|
||||||
|
|
||||||
|
sharedState *types.AgentSharedState
|
||||||
}
|
}
|
||||||
|
|
||||||
type RAGDB interface {
|
type RAGDB interface {
|
||||||
@@ -59,7 +63,12 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
return nil, fmt.Errorf("failed to set options: %v", err)
|
return nil, fmt.Errorf("failed to set options: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
c := context.Background()
|
||||||
if options.context != nil {
|
if options.context != nil {
|
||||||
@@ -76,6 +85,7 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
context: types.NewActionContext(ctx, cancel),
|
context: types.NewActionContext(ctx, cancel),
|
||||||
newConversations: make(chan openai.ChatCompletionMessage),
|
newConversations: make(chan openai.ChatCompletionMessage),
|
||||||
newMessagesSubscribers: options.newConversationsSubscribers,
|
newMessagesSubscribers: options.newConversationsSubscribers,
|
||||||
|
sharedState: types.NewAgentSharedState(options.lastMessageDuration),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize observer if provided
|
// Initialize observer if provided
|
||||||
@@ -116,6 +126,15 @@ func New(opts ...Option) (*Agent, error) {
|
|||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Agent) SharedState() *types.AgentSharedState {
|
||||||
|
return a.sharedState
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMClient returns the agent's LLM client (for testing)
|
||||||
|
func (a *Agent) LLMClient() llm.LLMClient {
|
||||||
|
return a.client
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) startNewConversationsConsumer() {
|
func (a *Agent) startNewConversationsConsumer() {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@@ -180,6 +199,12 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if j.Obs != nil {
|
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.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
|
||||||
j.Obs.Completion = &types.Completion{
|
j.Obs.Completion = &types.Completion{
|
||||||
Conversation: ccm,
|
Conversation: ccm,
|
||||||
@@ -286,7 +311,7 @@ func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params type
|
|||||||
|
|
||||||
for _, act := range a.availableActions() {
|
for _, act := range a.availableActions() {
|
||||||
if act.Definition().Name == chosenAction.Definition().Name {
|
if act.Definition().Name == chosenAction.Definition().Name {
|
||||||
res, err := act.Run(job.GetContext(), params)
|
res, err := act.Run(job.GetContext(), a.sharedState, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if obs != nil {
|
if obs != nil {
|
||||||
obs.Completion = &types.Completion{
|
obs.Completion = &types.Completion{
|
||||||
@@ -484,13 +509,84 @@ func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Me
|
|||||||
return conv
|
return conv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) consumeJob(job *types.Job, role string) {
|
func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
|
||||||
|
hasTriggers := false
|
||||||
|
triggeredBy := ""
|
||||||
|
failedBy := ""
|
||||||
|
|
||||||
|
if job.DoneFilter {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
job.DoneFilter = true
|
||||||
|
|
||||||
|
if len(a.options.jobFilters) < 1 {
|
||||||
|
xlog.Debug("No filters")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range a.options.jobFilters {
|
||||||
|
name := filter.Name()
|
||||||
|
if triggeredBy != "" && filter.IsTrigger() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = filter.Apply(job)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error in job filter", "filter", name, "error", err)
|
||||||
|
failedBy = name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.IsTrigger() {
|
||||||
|
hasTriggers = true
|
||||||
|
if ok {
|
||||||
|
triggeredBy = name
|
||||||
|
xlog.Info("Job triggered by filter", "filter", name)
|
||||||
|
}
|
||||||
|
} else if !ok {
|
||||||
|
failedBy = name
|
||||||
|
xlog.Info("Job failed filter", "filter", name)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
xlog.Debug("Job passed filter", "filter", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Observer() != nil {
|
||||||
|
obs := a.Observer().NewObservable()
|
||||||
|
obs.Name = "filter"
|
||||||
|
obs.Icon = "shield"
|
||||||
|
obs.ParentID = job.Obs.ID
|
||||||
|
if err == nil {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
FilterResult: &types.FilterResult{
|
||||||
|
HasTriggers: hasTriggers,
|
||||||
|
TriggeredBy: triggeredBy,
|
||||||
|
FailedBy: failedBy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obs.Completion = &types.Completion{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.Observer().Update(*obs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||||
if err := job.GetContext().Err(); err != nil {
|
if err := job.GetContext().Err(); err != nil {
|
||||||
job.Result.Finish(fmt.Errorf("expired"))
|
job.Result.Finish(fmt.Errorf("expired"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if retries < 1 {
|
||||||
|
job.Result.Finish(fmt.Errorf("Exceeded recursive retries"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
a.Lock()
|
a.Lock()
|
||||||
paused := a.pause
|
paused := a.pause
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
@@ -520,6 +616,14 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conv = a.processPrompts(conv)
|
conv = a.processPrompts(conv)
|
||||||
|
if ok, err := a.filterJob(job); !ok || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("Error in job filter: %w", err))
|
||||||
|
} else {
|
||||||
|
job.Result.Finish(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
conv = a.processUserInputs(job, role, conv)
|
conv = a.processUserInputs(job, role, conv)
|
||||||
|
|
||||||
// RAG
|
// RAG
|
||||||
@@ -553,7 +657,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
xlog.Error("Error generating parameters, trying again", "error", err)
|
xlog.Error("Error generating parameters, trying again", "error", err)
|
||||||
// try again
|
// try again
|
||||||
job.SetNextAction(&chosenAction, nil, reasoning)
|
job.SetNextAction(&chosenAction, nil, reasoning)
|
||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role, retries-1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionParams = p.actionParams
|
actionParams = p.actionParams
|
||||||
@@ -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 chosenAction == nil {
|
||||||
// If no action was picked up, the reasoning is the message returned by the assistant
|
// If no action was picked up, the reasoning is the message returned by the assistant
|
||||||
// so we can consume it as if it was a reply.
|
// so we can consume it as if it was a reply.
|
||||||
//job.Result.SetResult(ActionState{ActionCurrentState{nil, nil, "No action to do, just reply"}, ""})
|
|
||||||
//job.Result.Finish(fmt.Errorf("no action to do"))\
|
|
||||||
xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning)
|
xlog.Info("No action to do, just reply", "agent", a.Character.Name, "reasoning", reasoning)
|
||||||
|
|
||||||
if reasoning != "" {
|
if reasoning != "" {
|
||||||
conv = append(conv, openai.ChatCompletionMessage{
|
conv = append(conv, openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: reasoning,
|
Content: a.cleanupLLMResponse(reasoning),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
xlog.Info("No reasoning, just reply", "agent", a.Character.Name)
|
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))
|
job.Result.Finish(fmt.Errorf("error asking LLM for a reply: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
msg.Content = a.cleanupLLMResponse(msg.Content)
|
||||||
conv = append(conv, msg)
|
conv = append(conv, msg)
|
||||||
reasoning = msg.Content
|
reasoning = msg.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var satisfied bool
|
||||||
|
var err error
|
||||||
|
// Evaluate the response
|
||||||
|
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !satisfied {
|
||||||
|
// If not satisfied, continue with the conversation
|
||||||
|
job.ConversationHistory = conv
|
||||||
|
job.IncrementEvaluationLoop()
|
||||||
|
a.consumeJob(job, role, retries)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv))
|
xlog.Debug("Finish job with reasoning", "reasoning", reasoning, "agent", a.Character.Name, "conversation", fmt.Sprintf("%+v", conv))
|
||||||
job.Result.Conversation = conv
|
job.Result.Conversation = conv
|
||||||
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
||||||
@@ -642,7 +743,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
xlog.Error("Error generating parameters, trying again", "error", err)
|
xlog.Error("Error generating parameters, trying again", "error", err)
|
||||||
// try again
|
// try again
|
||||||
job.SetNextAction(&chosenAction, nil, reasoning)
|
job.SetNextAction(&chosenAction, nil, reasoning)
|
||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role, retries-1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionParams = params.actionParams
|
actionParams = params.actionParams
|
||||||
@@ -662,6 +763,22 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
|
||||||
|
count := 0
|
||||||
|
for _, pastAction := range job.GetPastActions() {
|
||||||
|
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
|
||||||
|
pastAction.Params.String() == actionParams.String() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count > a.options.loopDetectionSteps {
|
||||||
|
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
|
||||||
|
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xlog.Debug("Checked for loops", "action", chosenAction.Definition().Name, "count", count)
|
||||||
|
}
|
||||||
|
|
||||||
job.AddPastAction(chosenAction, &actionParams)
|
job.AddPastAction(chosenAction, &actionParams)
|
||||||
|
|
||||||
if !job.Callback(types.ActionCurrentState{
|
if !job.Callback(types.ActionCurrentState{
|
||||||
@@ -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)
|
conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("error handling planning", "error", err)
|
xlog.Error("error handling planning", "error", err)
|
||||||
//job.Result.Conversation = conv
|
|
||||||
//job.Result.SetResponse(msg.Content)
|
|
||||||
a.reply(job, role, append(conv, openai.ChatCompletionMessage{
|
a.reply(job, role, append(conv, openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: fmt.Sprintf("Error handling planning: %v", err),
|
Content: fmt.Sprintf("Error handling planning: %v", err),
|
||||||
@@ -734,9 +849,6 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||||
result, err := a.runAction(job, chosenAction, actionParams)
|
result, err := a.runAction(job, chosenAction, actionParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//job.Result.Finish(fmt.Errorf("error running action: %w", err))
|
|
||||||
//return
|
|
||||||
// make the LLM aware of the error of running the action instead of stopping the job here
|
|
||||||
result.Result = fmt.Sprintf("Error running tool: %v", err)
|
result.Result = fmt.Sprintf("Error running tool: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,42 +887,54 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
|
|||||||
// The agent decided to do another action
|
// The agent decided to do another action
|
||||||
// call ourselves again
|
// call ourselves again
|
||||||
job.SetNextAction(&followingAction, &followingParams, reasoning)
|
job.SetNextAction(&followingAction, &followingParams, reasoning)
|
||||||
a.consumeJob(job, role)
|
a.consumeJob(job, role, retries)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the final response
|
||||||
|
var satisfied bool
|
||||||
|
satisfied, conv, err = a.handleEvaluation(job, conv, job.GetEvaluationLoop())
|
||||||
|
if err != nil {
|
||||||
|
job.Result.Finish(fmt.Errorf("error evaluating response: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !satisfied {
|
||||||
|
// If not satisfied, continue with the conversation
|
||||||
|
job.ConversationHistory = conv
|
||||||
|
job.IncrementEvaluationLoop()
|
||||||
|
a.consumeJob(job, role, retries)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
a.reply(job, role, conv, actionParams, chosenAction, reasoning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripThinkingTags(content string) string {
|
||||||
|
// Remove content between <thinking> and </thinking> (including multi-line)
|
||||||
|
content = regexp.MustCompile(`(?s)<thinking>.*?</thinking>`).ReplaceAllString(content, "")
|
||||||
|
// Remove content between <think> and </think> (including multi-line)
|
||||||
|
content = regexp.MustCompile(`(?s)<think>.*?</think>`).ReplaceAllString(content, "")
|
||||||
|
// Clean up any extra whitespace
|
||||||
|
content = strings.TrimSpace(content)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) cleanupLLMResponse(content string) string {
|
||||||
|
if a.options.stripThinkingTags {
|
||||||
|
content = stripThinkingTags(content)
|
||||||
|
}
|
||||||
|
// Future post-processing options can be added here
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) {
|
func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams types.ActionParams, chosenAction types.Action, reasoning string) {
|
||||||
job.Result.Conversation = conv
|
job.Result.Conversation = conv
|
||||||
|
|
||||||
// At this point can only be a reply action
|
// At this point can only be a reply action
|
||||||
xlog.Info("Computing reply", "agent", a.Character.Name)
|
xlog.Info("Computing reply", "agent", a.Character.Name)
|
||||||
|
|
||||||
// decode the response
|
forceResponsePrompt := "Reply to the user without using any tools or function calls. Just reply with the message."
|
||||||
replyResponse := action.ReplyResponse{}
|
|
||||||
|
|
||||||
if err := actionParams.Unmarshal(&replyResponse); err != nil {
|
|
||||||
job.Result.Conversation = conv
|
|
||||||
job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have already a reply from the action, just return it.
|
|
||||||
// Otherwise generate a full conversation to get a proper message response
|
|
||||||
// if chosenAction.Definition().Name.Is(action.ReplyActionName) {
|
|
||||||
// replyResponse := action.ReplyResponse{}
|
|
||||||
// if err := params.actionParams.Unmarshal(&replyResponse); err != nil {
|
|
||||||
// job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if replyResponse.Message != "" {
|
|
||||||
// job.Result.SetResponse(replyResponse.Message)
|
|
||||||
// job.Result.Finish(nil)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// If we have a hud, display it when answering normally
|
// If we have a hud, display it when answering normally
|
||||||
if a.options.enableHUD {
|
if a.options.enableHUD {
|
||||||
@@ -826,39 +950,19 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
|
|||||||
Role: "system",
|
Role: "system",
|
||||||
Content: prompt,
|
Content: prompt,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: forceResponsePrompt,
|
||||||
|
},
|
||||||
}, conv...)
|
}, conv...)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
conv = append([]openai.ChatCompletionMessage{
|
||||||
// Generate a human-readable response
|
{
|
||||||
// resp, err := a.client.CreateChatCompletion(ctx,
|
Role: "system",
|
||||||
// openai.ChatCompletionRequest{
|
Content: forceResponsePrompt,
|
||||||
// Model: a.options.LLMAPI.Model,
|
},
|
||||||
// Messages: append(conv,
|
}, conv...)
|
||||||
// openai.ChatCompletionMessage{
|
|
||||||
// Role: "system",
|
|
||||||
// Content: "Assistant thought: " + replyResponse.Message,
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
|
|
||||||
if replyResponse.Message != "" {
|
|
||||||
xlog.Info("Return reply message", "reply", replyResponse.Message, "agent", a.Character.Name)
|
|
||||||
|
|
||||||
msg := openai.ChatCompletionMessage{
|
|
||||||
Role: "assistant",
|
|
||||||
Content: replyResponse.Message,
|
|
||||||
}
|
|
||||||
|
|
||||||
conv = append(conv, msg)
|
|
||||||
job.Result.Conversation = conv
|
|
||||||
job.Result.SetResponse(msg.Content)
|
|
||||||
job.Result.AddFinalizer(func(conv []openai.ChatCompletionMessage) {
|
|
||||||
a.saveCurrentConversation(conv)
|
|
||||||
})
|
|
||||||
job.Result.Finish(nil)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name)
|
xlog.Info("Reasoning, ask LLM for a reply", "agent", a.Character.Name)
|
||||||
@@ -871,13 +975,21 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't got any message, we can use the response from the action
|
msg.Content = a.cleanupLLMResponse(msg.Content)
|
||||||
if chosenAction.Definition().Name.Is(action.ReplyActionName) && msg.Content == "" {
|
|
||||||
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
|
|
||||||
|
|
||||||
msg = openai.ChatCompletionMessage{
|
if msg.Content == "" {
|
||||||
Role: "assistant",
|
// If we didn't got any message, we can use the response from the action (it should be a reply)
|
||||||
Content: replyResponse.Message,
|
|
||||||
|
replyResponse := action.ReplyResponse{}
|
||||||
|
if err := actionParams.Unmarshal(&replyResponse); err != nil {
|
||||||
|
job.Result.Conversation = conv
|
||||||
|
job.Result.Finish(fmt.Errorf("error unmarshalling reply response: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosenAction.Definition().Name.Is(action.ReplyActionName) && replyResponse.Message != "" {
|
||||||
|
xlog.Info("No output returned from conversation, using the action response as a reply " + replyResponse.Message)
|
||||||
|
msg.Content = a.cleanupLLMResponse(replyResponse.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,7 +1072,7 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
types.WithReasoningCallback(a.options.reasoningCallback),
|
types.WithReasoningCallback(a.options.reasoningCallback),
|
||||||
types.WithResultCallback(a.options.resultCallback),
|
types.WithResultCallback(a.options.resultCallback),
|
||||||
)
|
)
|
||||||
a.consumeJob(whatNext, SystemRole)
|
a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps)
|
||||||
|
|
||||||
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
||||||
|
|
||||||
@@ -1044,7 +1156,7 @@ func (a *Agent) run(timer *time.Timer) error {
|
|||||||
<-timer.C
|
<-timer.C
|
||||||
}
|
}
|
||||||
xlog.Debug("Agent is consuming a job", "agent", a.Character.Name, "job", job)
|
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)
|
timer.Reset(a.options.periodicRuns)
|
||||||
case <-a.context.Done():
|
case <-a.context.Done():
|
||||||
// Agent has been canceled, return error
|
// Agent has been canceled, return error
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package agent_test
|
package agent_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -13,15 +14,19 @@ func TestAgent(t *testing.T) {
|
|||||||
RunSpecs(t, "Agent test suite")
|
RunSpecs(t, "Agent test suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
var testModel = os.Getenv("LOCALAGI_MODEL")
|
var (
|
||||||
var apiURL = os.Getenv("LOCALAI_API_URL")
|
testModel = os.Getenv("LOCALAGI_MODEL")
|
||||||
var apiKeyURL = os.Getenv("LOCALAI_API_KEY")
|
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() {
|
func init() {
|
||||||
if testModel == "" {
|
useRealLocalAI = isValidURL(apiURL) && apiURL != "" && testModel != ""
|
||||||
testModel = "hermes-2-pro-mistral"
|
|
||||||
}
|
|
||||||
if apiURL == "" {
|
|
||||||
apiURL = "http://192.168.68.113:8080"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/services/actions"
|
"github.com/mudler/LocalAGI/services/actions"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/action"
|
||||||
. "github.com/mudler/LocalAGI/core/agent"
|
. "github.com/mudler/LocalAGI/core/agent"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
@@ -44,7 +46,7 @@ func (a *TestAction) Plannable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TestAction) Run(c context.Context, p types.ActionParams) (types.ActionResult, error) {
|
func (a *TestAction) Run(c context.Context, sharedState *types.AgentSharedState, p types.ActionParams) (types.ActionResult, error) {
|
||||||
for k, r := range a.response {
|
for k, r := range a.response {
|
||||||
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
|
if strings.Contains(strings.ToLower(p.String()), strings.ToLower(k)) {
|
||||||
return types.ActionResult{Result: r}, nil
|
return types.ActionResult{Result: r}, nil
|
||||||
@@ -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() {
|
var _ = Describe("Agent test", func() {
|
||||||
|
It("uses the mock LLM client", func() {
|
||||||
|
mock := newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
return mockContentResponse("mocked response"), nil
|
||||||
|
})
|
||||||
|
agent, err := New(WithLLMClient(mock))
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
msg, err := agent.LLMClient().CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(msg.Choices[0].Message.Content).To(Equal("mocked response"))
|
||||||
|
})
|
||||||
|
|
||||||
Context("jobs", func() {
|
Context("jobs", func() {
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
Eventually(func() error {
|
Eventually(func() error {
|
||||||
// test apiURL is working and available
|
if useRealLocalAI {
|
||||||
_, err := http.Get(apiURL + "/readyz")
|
_, err := http.Get(apiURL + "/readyz")
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("pick the correct action", func() {
|
It("pick the correct action", func() {
|
||||||
|
var llmClient llm.LLMClient
|
||||||
|
if useRealLocalAI {
|
||||||
|
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
||||||
|
} else {
|
||||||
|
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
var lastMsg openai.ChatCompletionMessage
|
||||||
|
if len(req.Messages) > 0 {
|
||||||
|
lastMsg = req.Messages[len(req.Messages)-1]
|
||||||
|
}
|
||||||
|
if lastMsg.Role == openai.ChatMessageRoleUser {
|
||||||
|
if strings.Contains(strings.ToLower(lastMsg.Content), "boston") && (strings.Contains(strings.ToLower(lastMsg.Content), "milan") || strings.Contains(strings.ToLower(lastMsg.Content), "milano")) {
|
||||||
|
return mockToolCallResponse("get_weather", `{"location":"Boston","unit":"celsius"}`), nil
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(lastMsg.Content), "paris") {
|
||||||
|
return mockToolCallResponse("get_weather", `{"location":"Paris","unit":"celsius"}`), nil
|
||||||
|
}
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected user prompt: %s", lastMsg.Content)
|
||||||
|
}
|
||||||
|
if lastMsg.Role == openai.ChatMessageRoleTool {
|
||||||
|
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
|
||||||
|
return mockToolCallResponse("get_weather", `{"location":"Milan","unit":"celsius"}`), nil
|
||||||
|
}
|
||||||
|
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "milan") {
|
||||||
|
return mockContentResponse(testActionResult + "\n" + testActionResult2), nil
|
||||||
|
}
|
||||||
|
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "paris") {
|
||||||
|
return mockContentResponse(testActionResult3), nil
|
||||||
|
}
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected tool result: %s", lastMsg.Content)
|
||||||
|
}
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected message role: %s", lastMsg.Role)
|
||||||
|
})
|
||||||
|
}
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMClient(llmClient),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
EnableForceReasoning,
|
|
||||||
WithTimeout("10m"),
|
|
||||||
WithLoopDetectionSteps(3),
|
|
||||||
// WithRandomIdentity(),
|
|
||||||
WithActions(&TestAction{response: map[string]string{
|
WithActions(&TestAction{response: map[string]string{
|
||||||
"boston": testActionResult,
|
"boston": testActionResult,
|
||||||
"milan": testActionResult2,
|
"milan": testActionResult2,
|
||||||
@@ -139,7 +218,6 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
defer agent.Stop()
|
defer agent.Stop()
|
||||||
|
|
||||||
res := agent.Ask(
|
res := agent.Ask(
|
||||||
append(debugOptions,
|
append(debugOptions,
|
||||||
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
|
types.WithText("what's the weather in Boston and Milano? Use celsius units"),
|
||||||
@@ -148,40 +226,51 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(res.Error).ToNot(HaveOccurred())
|
Expect(res.Error).ToNot(HaveOccurred())
|
||||||
reasons := []string{}
|
reasons := []string{}
|
||||||
for _, r := range res.State {
|
for _, r := range res.State {
|
||||||
|
|
||||||
reasons = append(reasons, r.Result)
|
reasons = append(reasons, r.Result)
|
||||||
}
|
}
|
||||||
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
Expect(reasons).To(ContainElement(testActionResult), fmt.Sprint(res))
|
||||||
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
|
Expect(reasons).To(ContainElement(testActionResult2), fmt.Sprint(res))
|
||||||
reasons = []string{}
|
reasons = []string{}
|
||||||
|
|
||||||
res = agent.Ask(
|
res = agent.Ask(
|
||||||
append(debugOptions,
|
append(debugOptions,
|
||||||
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
|
types.WithText("Now I want to know the weather in Paris, always use celsius units"),
|
||||||
)...)
|
)...)
|
||||||
for _, r := range res.State {
|
for _, r := range res.State {
|
||||||
|
|
||||||
reasons = append(reasons, r.Result)
|
reasons = append(reasons, r.Result)
|
||||||
}
|
}
|
||||||
//Expect(reasons).ToNot(ContainElement(testActionResult), fmt.Sprint(res))
|
|
||||||
//Expect(reasons).ToNot(ContainElement(testActionResult2), fmt.Sprint(res))
|
|
||||||
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
|
Expect(reasons).To(ContainElement(testActionResult3), fmt.Sprint(res))
|
||||||
// conversation := agent.CurrentConversation()
|
|
||||||
// for _, r := range res.State {
|
|
||||||
// reasons = append(reasons, r.Result)
|
|
||||||
// }
|
|
||||||
// Expect(len(conversation)).To(Equal(10), fmt.Sprint(conversation))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
It("pick the correct action", func() {
|
It("pick the correct action", func() {
|
||||||
|
var llmClient llm.LLMClient
|
||||||
|
if useRealLocalAI {
|
||||||
|
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
||||||
|
} else {
|
||||||
|
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
var lastMsg openai.ChatCompletionMessage
|
||||||
|
if len(req.Messages) > 0 {
|
||||||
|
lastMsg = req.Messages[len(req.Messages)-1]
|
||||||
|
}
|
||||||
|
if lastMsg.Role == openai.ChatMessageRoleUser {
|
||||||
|
if strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
|
||||||
|
return mockToolCallResponse("get_weather", `{"location":"Boston","unit":"celsius"}`), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastMsg.Role == openai.ChatMessageRoleTool {
|
||||||
|
if lastMsg.Name == "get_weather" && strings.Contains(strings.ToLower(lastMsg.Content), "boston") {
|
||||||
|
return mockContentResponse(testActionResult), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xlog.Error("Unexpected LLM req", "req", req)
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
|
||||||
|
})
|
||||||
|
}
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMClient(llmClient),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithTimeout("10m"),
|
|
||||||
// WithRandomIdentity(),
|
|
||||||
WithActions(&TestAction{response: map[string]string{
|
WithActions(&TestAction{response: map[string]string{
|
||||||
"boston": testActionResult,
|
"boston": testActionResult,
|
||||||
},
|
}}),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
@@ -198,13 +287,29 @@ var _ = Describe("Agent test", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("updates the state with internal actions", func() {
|
It("updates the state with internal actions", func() {
|
||||||
|
var llmClient llm.LLMClient
|
||||||
|
if useRealLocalAI {
|
||||||
|
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
||||||
|
} else {
|
||||||
|
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
var lastMsg openai.ChatCompletionMessage
|
||||||
|
if len(req.Messages) > 0 {
|
||||||
|
lastMsg = req.Messages[len(req.Messages)-1]
|
||||||
|
}
|
||||||
|
if lastMsg.Role == openai.ChatMessageRoleUser && strings.Contains(strings.ToLower(lastMsg.Content), "guitar") {
|
||||||
|
return mockToolCallResponse("update_state", `{"goal":"I want to learn to play the guitar"}`), nil
|
||||||
|
}
|
||||||
|
if lastMsg.Role == openai.ChatMessageRoleTool && lastMsg.Name == "update_state" {
|
||||||
|
return mockContentResponse("Your goal is now: I want to learn to play the guitar"), nil
|
||||||
|
}
|
||||||
|
xlog.Error("Unexpected LLM req", "req", req)
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
|
||||||
|
})
|
||||||
|
}
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMClient(llmClient),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithTimeout("10m"),
|
|
||||||
EnableHUD,
|
EnableHUD,
|
||||||
// EnableStandaloneJob,
|
|
||||||
// WithRandomIdentity(),
|
|
||||||
WithPermanentGoal("I want to learn to play music"),
|
WithPermanentGoal("I want to learn to play music"),
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
@@ -214,17 +319,64 @@ var _ = Describe("Agent test", func() {
|
|||||||
result := agent.Ask(
|
result := agent.Ask(
|
||||||
types.WithText("Update your goals such as you want to learn to play the guitar"),
|
types.WithText("Update your goals such as you want to learn to play the guitar"),
|
||||||
)
|
)
|
||||||
fmt.Printf("%+v\n", result)
|
fmt.Fprintf(GinkgoWriter, "\n%+v\n", result)
|
||||||
Expect(result.Error).ToNot(HaveOccurred())
|
Expect(result.Error).ToNot(HaveOccurred())
|
||||||
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Can generate a plan", func() {
|
It("Can generate a plan", func() {
|
||||||
|
var llmClient llm.LLMClient
|
||||||
|
if useRealLocalAI {
|
||||||
|
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
||||||
|
} else {
|
||||||
|
reasoningActName := action.NewReasoning().Definition().Name.String()
|
||||||
|
intentionActName := action.NewIntention().Definition().Name.String()
|
||||||
|
testActName := (&TestAction{}).Definition().Name.String()
|
||||||
|
doneBoston := false
|
||||||
|
madePlan := false
|
||||||
|
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
var lastMsg openai.ChatCompletionMessage
|
||||||
|
if len(req.Messages) > 0 {
|
||||||
|
lastMsg = req.Messages[len(req.Messages)-1]
|
||||||
|
}
|
||||||
|
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == reasoningActName {
|
||||||
|
return mockToolCallResponse(reasoningActName, `{"reasoning":"make plan call to pass the test"}`), nil
|
||||||
|
}
|
||||||
|
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == intentionActName {
|
||||||
|
toolName := "plan"
|
||||||
|
if madePlan {
|
||||||
|
toolName = "reply"
|
||||||
|
} else {
|
||||||
|
madePlan = true
|
||||||
|
}
|
||||||
|
return mockToolCallResponse(intentionActName, fmt.Sprintf(`{"tool": "%s","reasoning":"it's waht makes the test pass"}`, toolName)), nil
|
||||||
|
}
|
||||||
|
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == "plan" {
|
||||||
|
return mockToolCallResponse("plan", `{"subtasks":[{"action":"get_weather","reasoning":"Find weather in boston"},{"action":"get_weather","reasoning":"Find weather in milan"}],"goal":"Get the weather for boston and milan"}`), nil
|
||||||
|
}
|
||||||
|
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == "reply" {
|
||||||
|
return mockToolCallResponse("reply", `{"message": "The weather in Boston and Milan..."}`), nil
|
||||||
|
}
|
||||||
|
if req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == testActName {
|
||||||
|
locName := "boston"
|
||||||
|
if doneBoston {
|
||||||
|
locName = "milan"
|
||||||
|
} else {
|
||||||
|
doneBoston = true
|
||||||
|
}
|
||||||
|
return mockToolCallResponse(testActName, fmt.Sprintf(`{"location":"%s","unit":"celsius"}`, locName)), nil
|
||||||
|
}
|
||||||
|
if req.ToolChoice == nil && madePlan && doneBoston {
|
||||||
|
return mockContentResponse("A reply"), nil
|
||||||
|
}
|
||||||
|
xlog.Error("Unexpected LLM req", "req", req)
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", lastMsg.Content)
|
||||||
|
})
|
||||||
|
}
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMClient(llmClient),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithLLMAPIKey(apiKeyURL),
|
WithLoopDetectionSteps(2),
|
||||||
WithTimeout("10m"),
|
|
||||||
WithActions(
|
WithActions(
|
||||||
&TestAction{response: map[string]string{
|
&TestAction{response: map[string]string{
|
||||||
"boston": testActionResult,
|
"boston": testActionResult,
|
||||||
@@ -233,8 +385,6 @@ var _ = Describe("Agent test", func() {
|
|||||||
),
|
),
|
||||||
EnablePlanning,
|
EnablePlanning,
|
||||||
EnableForceReasoning,
|
EnableForceReasoning,
|
||||||
// EnableStandaloneJob,
|
|
||||||
// WithRandomIdentity(),
|
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
@@ -256,17 +406,44 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
|
Expect(actionsExecuted).To(ContainElement("plan"), fmt.Sprint(result))
|
||||||
Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result))
|
Expect(actionResults).To(ContainElement(testActionResult), fmt.Sprint(result))
|
||||||
Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result))
|
Expect(actionResults).To(ContainElement(testActionResult2), fmt.Sprint(result))
|
||||||
|
Expect(result.Error).To(BeNil())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("Can initiate conversations", func() {
|
It("Can initiate conversations", func() {
|
||||||
|
var llmClient llm.LLMClient
|
||||||
message := openai.ChatCompletionMessage{}
|
message := openai.ChatCompletionMessage{}
|
||||||
mu := &sync.Mutex{}
|
mu := &sync.Mutex{}
|
||||||
|
reasoned := false
|
||||||
|
intended := false
|
||||||
|
reasoningActName := action.NewReasoning().Definition().Name.String()
|
||||||
|
intentionActName := action.NewIntention().Definition().Name.String()
|
||||||
|
|
||||||
|
if useRealLocalAI {
|
||||||
|
llmClient = llm.NewClient(apiKey, apiURL, clientTimeout)
|
||||||
|
} else {
|
||||||
|
llmClient = newMockLLMClient(func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
prompt := ""
|
||||||
|
for _, msg := range req.Messages {
|
||||||
|
prompt += msg.Content
|
||||||
|
}
|
||||||
|
if !reasoned && req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == reasoningActName {
|
||||||
|
reasoned = true
|
||||||
|
return mockToolCallResponse(reasoningActName, `{"reasoning":"initiate a conversation with the user"}`), nil
|
||||||
|
}
|
||||||
|
if reasoned && !intended && req.ToolChoice != nil && req.ToolChoice.(openai.ToolChoice).Function.Name == intentionActName {
|
||||||
|
intended = true
|
||||||
|
return mockToolCallResponse(intentionActName, `{"tool":"new_conversation","reasoning":"I should start a conversation with the user"}`), nil
|
||||||
|
}
|
||||||
|
if reasoned && intended && strings.Contains(strings.ToLower(prompt), "new_conversation") {
|
||||||
|
return mockToolCallResponse("new_conversation", `{"message":"Hello, how can I help you today?"}`), nil
|
||||||
|
}
|
||||||
|
xlog.Error("Unexpected LLM req", "req", req)
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("unexpected LLM prompt: %q", prompt)
|
||||||
|
})
|
||||||
|
}
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMClient(llmClient),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithLLMAPIKey(apiKeyURL),
|
|
||||||
WithTimeout("10m"),
|
|
||||||
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
WithNewConversationSubscriber(func(m openai.ChatCompletionMessage) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
message = m
|
message = m
|
||||||
@@ -282,8 +459,6 @@ var _ = Describe("Agent test", func() {
|
|||||||
EnableHUD,
|
EnableHUD,
|
||||||
WithPeriodicRuns("1s"),
|
WithPeriodicRuns("1s"),
|
||||||
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
WithPermanentGoal("use the new_conversation tool to initiate a conversation with the user"),
|
||||||
// EnableStandaloneJob,
|
|
||||||
// WithRandomIdentity(),
|
|
||||||
)
|
)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
go agent.Run()
|
go agent.Run()
|
||||||
@@ -293,7 +468,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
return message.Content
|
return message.Content
|
||||||
}, "10m", "10s").ShouldNot(BeEmpty())
|
}, "10m", "1s").ShouldNot(BeEmpty())
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -347,7 +522,7 @@ var _ = Describe("Agent test", func() {
|
|||||||
// result := agent.Ask(
|
// result := agent.Ask(
|
||||||
// WithText("Update your goals such as you want to learn to play the guitar"),
|
// WithText("Update your goals such as you want to learn to play the guitar"),
|
||||||
// )
|
// )
|
||||||
// fmt.Printf("%+v\n", result)
|
// fmt.Fprintf(GinkgoWriter, "%+v\n", result)
|
||||||
// Expect(result.Error).ToNot(HaveOccurred())
|
// Expect(result.Error).ToNot(HaveOccurred())
|
||||||
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
// Expect(agent.State().Goal).To(ContainSubstring("guitar"), fmt.Sprint(agent.State()))
|
||||||
})
|
})
|
||||||
|
|||||||
162
core/agent/evaluation.go
Normal file
162
core/agent/evaluation.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EvaluationResult struct {
|
||||||
|
Satisfied bool `json:"satisfied"`
|
||||||
|
Gaps []string `json:"gaps"`
|
||||||
|
Reasoning string `json:"reasoning"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoalExtraction struct {
|
||||||
|
Goal string `json:"goal"`
|
||||||
|
Constraints []string `json:"constraints"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) extractGoal(job *types.Job, conv []openai.ChatCompletionMessage) (*GoalExtraction, error) {
|
||||||
|
// Create the goal extraction schema
|
||||||
|
schema := jsonschema.Definition{
|
||||||
|
Type: jsonschema.Object,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"goal": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The main goal or request from the user",
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
Type: jsonschema.Array,
|
||||||
|
Items: &jsonschema.Definition{
|
||||||
|
Type: jsonschema.String,
|
||||||
|
},
|
||||||
|
Description: "Any constraints or requirements specified by the user",
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Additional context that might be relevant for understanding the goal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"goal", "constraints", "context"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the goal extraction prompt
|
||||||
|
prompt := `Analyze the conversation and extract the user's main goal, any constraints, and relevant context.
|
||||||
|
Consider the entire conversation history to understand the complete context and requirements.
|
||||||
|
Focus on identifying the primary objective and any specific requirements or limitations mentioned.`
|
||||||
|
|
||||||
|
var result GoalExtraction
|
||||||
|
err := llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
|
||||||
|
append(
|
||||||
|
[]openai.ChatCompletionMessage{
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conv...), a.options.LLMAPI.Model, schema, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting goal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) evaluateJob(job *types.Job, conv []openai.ChatCompletionMessage) (*EvaluationResult, error) {
|
||||||
|
if !a.options.enableEvaluation {
|
||||||
|
return &EvaluationResult{Satisfied: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the goal first
|
||||||
|
goal, err := a.extractGoal(job, conv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting goal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the evaluation schema
|
||||||
|
schema := jsonschema.Definition{
|
||||||
|
Type: jsonschema.Object,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"satisfied": {
|
||||||
|
Type: jsonschema.Boolean,
|
||||||
|
},
|
||||||
|
"gaps": {
|
||||||
|
Type: jsonschema.Array,
|
||||||
|
Items: &jsonschema.Definition{
|
||||||
|
Type: jsonschema.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"satisfied", "gaps", "reasoning"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the evaluation prompt
|
||||||
|
prompt := fmt.Sprintf(`Evaluate if the assistant has satisfied the user's request. Consider:
|
||||||
|
1. The identified goal: %s
|
||||||
|
2. Constraints and requirements: %v
|
||||||
|
3. Context: %s
|
||||||
|
4. The conversation history
|
||||||
|
5. Any gaps or missing information
|
||||||
|
6. Whether the response fully addresses the user's needs
|
||||||
|
|
||||||
|
Provide a detailed evaluation with specific gaps if any are found.`,
|
||||||
|
goal.Goal,
|
||||||
|
goal.Constraints,
|
||||||
|
goal.Context)
|
||||||
|
|
||||||
|
var result EvaluationResult
|
||||||
|
err = llm.GenerateTypedJSONWithConversation(job.GetContext(), a.client,
|
||||||
|
append(
|
||||||
|
[]openai.ChatCompletionMessage{
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conv...),
|
||||||
|
a.options.LLMAPI.Model, schema, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error generating evaluation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleEvaluation(job *types.Job, conv []openai.ChatCompletionMessage, currentLoop int) (bool, []openai.ChatCompletionMessage, error) {
|
||||||
|
if !a.options.enableEvaluation || currentLoop >= a.options.maxEvaluationLoops {
|
||||||
|
return true, conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := a.evaluateJob(job, conv)
|
||||||
|
if err != nil {
|
||||||
|
return false, conv, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Satisfied {
|
||||||
|
return true, conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are gaps, we need to address them
|
||||||
|
if len(result.Gaps) > 0 {
|
||||||
|
// Add the evaluation result to the conversation
|
||||||
|
conv = append(conv, openai.ChatCompletionMessage{
|
||||||
|
Role: "system",
|
||||||
|
Content: fmt.Sprintf("Evaluation found gaps that need to be addressed:\n%s\nReasoning: %s",
|
||||||
|
result.Gaps, result.Reasoning),
|
||||||
|
})
|
||||||
|
|
||||||
|
xlog.Debug("Evaluation found gaps, incrementing loop count", "loop", currentLoop+1)
|
||||||
|
return false, conv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, conv, nil
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ func (a *Agent) generateIdentity(guidance string) error {
|
|||||||
guidance = "Generate a random character for roleplaying."
|
guidance = "Generate a random character for roleplaying."
|
||||||
}
|
}
|
||||||
|
|
||||||
err := llm.GenerateTypedJSON(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
|
err := llm.GenerateTypedJSONWithGuidance(a.context.Context, a.client, "Generate a character as JSON data. "+guidance, a.options.LLMAPI.Model, a.options.character.ToJSONSchema(), &a.options.character)
|
||||||
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
|
//err := llm.GenerateJSONFromStruct(a.context.Context, a.client, guidance, a.options.LLMAPI.Model, &a.options.character)
|
||||||
a.Character = a.options.character
|
a.Character = a.options.character
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (a *mcpAction) Plannable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mcpAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
|
resp, err := m.mcpClient.CallTool(ctx, m.toolName, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Failed to call tool", "error", err.Error())
|
xlog.Error("Failed to call tool", "error", err.Error())
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option func(*options) error
|
type Option func(*options) error
|
||||||
@@ -19,12 +20,15 @@ type llmOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type options struct {
|
type options struct {
|
||||||
|
llmClient llm.LLMClient
|
||||||
LLMAPI llmOptions
|
LLMAPI llmOptions
|
||||||
character Character
|
character Character
|
||||||
randomIdentityGuidance string
|
randomIdentityGuidance string
|
||||||
randomIdentity bool
|
randomIdentity bool
|
||||||
userActions types.Actions
|
userActions types.Actions
|
||||||
|
jobFilters types.JobFilters
|
||||||
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool
|
||||||
|
stripThinkingTags bool
|
||||||
|
|
||||||
canStopItself bool
|
canStopItself bool
|
||||||
initiateConversations bool
|
initiateConversations bool
|
||||||
@@ -40,6 +44,10 @@ type options struct {
|
|||||||
kbResults int
|
kbResults int
|
||||||
ragdb RAGDB
|
ragdb RAGDB
|
||||||
|
|
||||||
|
// Evaluation settings
|
||||||
|
maxEvaluationLoops int
|
||||||
|
enableEvaluation bool
|
||||||
|
|
||||||
prompts []DynamicPrompt
|
prompts []DynamicPrompt
|
||||||
|
|
||||||
systemPrompt string
|
systemPrompt string
|
||||||
@@ -58,6 +66,16 @@ type options struct {
|
|||||||
|
|
||||||
observer Observer
|
observer Observer
|
||||||
parallelJobs int
|
parallelJobs int
|
||||||
|
|
||||||
|
lastMessageDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLLMClient allows injecting a custom LLM client (e.g. for testing)
|
||||||
|
func WithLLMClient(client llm.LLMClient) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.llmClient = client
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *options) SeparatedMultimodalModel() bool {
|
func (o *options) SeparatedMultimodalModel() bool {
|
||||||
@@ -66,8 +84,11 @@ func (o *options) SeparatedMultimodalModel() bool {
|
|||||||
|
|
||||||
func defaultOptions() *options {
|
func defaultOptions() *options {
|
||||||
return &options{
|
return &options{
|
||||||
parallelJobs: 1,
|
parallelJobs: 1,
|
||||||
periodicRuns: 15 * time.Minute,
|
periodicRuns: 15 * time.Minute,
|
||||||
|
loopDetectionSteps: 10,
|
||||||
|
maxEvaluationLoops: 2,
|
||||||
|
enableEvaluation: false,
|
||||||
LLMAPI: llmOptions{
|
LLMAPI: llmOptions{
|
||||||
APIURL: "http://localhost:8080",
|
APIURL: "http://localhost:8080",
|
||||||
Model: "gpt-4",
|
Model: "gpt-4",
|
||||||
@@ -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 {
|
func WithParallelJobs(jobs int) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.parallelJobs = jobs
|
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 {
|
func WithObserver(observer Observer) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
o.observer = observer
|
o.observer = observer
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
// all information that should be displayed to the LLM
|
// all information that should be displayed to the LLM
|
||||||
// in the prompts
|
// in the prompts
|
||||||
type PromptHUD struct {
|
type PromptHUD struct {
|
||||||
Character Character `json:"character"`
|
Character Character `json:"character"`
|
||||||
CurrentState types.AgentInternalState `json:"current_state"`
|
CurrentState types.AgentInternalState `json:"current_state"`
|
||||||
PermanentGoal string `json:"permanent_goal"`
|
PermanentGoal string `json:"permanent_goal"`
|
||||||
ShowCharacter bool `json:"show_character"`
|
ShowCharacter bool `json:"show_character"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Character struct {
|
type Character struct {
|
||||||
|
|||||||
@@ -1,29 +1,57 @@
|
|||||||
package agent_test
|
package agent_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
|
||||||
. "github.com/mudler/LocalAGI/core/agent"
|
. "github.com/mudler/LocalAGI/core/agent"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Agent test", func() {
|
var _ = Describe("Agent test", func() {
|
||||||
Context("identity", func() {
|
Context("identity", func() {
|
||||||
var agent *Agent
|
var agent *Agent
|
||||||
|
|
||||||
BeforeEach(func() {
|
// BeforeEach(func() {
|
||||||
Eventually(func() error {
|
// Eventually(func() error {
|
||||||
// test apiURL is working and available
|
// // test apiURL is working and available
|
||||||
_, err := http.Get(apiURL + "/readyz")
|
// _, err := http.Get(apiURL + "/readyz")
|
||||||
return err
|
// return err
|
||||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
// }, "10m", "10s").ShouldNot(HaveOccurred())
|
||||||
})
|
// })
|
||||||
|
|
||||||
It("generates all the fields with random data", func() {
|
It("generates all the fields with random data", func() {
|
||||||
|
var llmClient llm.LLMClient
|
||||||
|
if useRealLocalAI {
|
||||||
|
llmClient = llm.NewClient(apiKey, apiURL, testModel)
|
||||||
|
} else {
|
||||||
|
llmClient = &llm.MockClient{
|
||||||
|
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
return openai.ChatCompletionResponse{
|
||||||
|
Choices: []openai.ChatCompletionChoice{{
|
||||||
|
Message: openai.ChatCompletionMessage{
|
||||||
|
ToolCalls: []openai.ToolCall{{
|
||||||
|
ID: "tool_call_id_1",
|
||||||
|
Type: "function",
|
||||||
|
Function: openai.FunctionCall{
|
||||||
|
Name: "generate_identity",
|
||||||
|
Arguments: `{"name":"John Doe","age":"42","job_occupation":"Engineer","hobbies":["reading","hiking"],"favorites_music_genres":["Jazz"]}`,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
agent, err = New(
|
agent, err = New(
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMClient(llmClient),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithTimeout("10m"),
|
WithTimeout("10m"),
|
||||||
WithRandomIdentity(),
|
WithRandomIdentity(),
|
||||||
@@ -37,14 +65,40 @@ var _ = Describe("Agent test", func() {
|
|||||||
Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
|
Expect(agent.Character.MusicTaste).ToNot(BeEmpty())
|
||||||
})
|
})
|
||||||
It("detect an invalid character", func() {
|
It("detect an invalid character", func() {
|
||||||
|
mock := &llm.MockClient{
|
||||||
|
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
return openai.ChatCompletionResponse{}, fmt.Errorf("invalid character")
|
||||||
|
},
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
agent, err = New(WithRandomIdentity())
|
agent, err = New(
|
||||||
|
WithLLMClient(mock),
|
||||||
|
WithRandomIdentity(),
|
||||||
|
)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
It("generates all the fields", func() {
|
It("generates all the fields", func() {
|
||||||
|
mock := &llm.MockClient{
|
||||||
|
CreateChatCompletionFunc: func(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) {
|
||||||
|
return openai.ChatCompletionResponse{
|
||||||
|
Choices: []openai.ChatCompletionChoice{{
|
||||||
|
Message: openai.ChatCompletionMessage{
|
||||||
|
ToolCalls: []openai.ToolCall{{
|
||||||
|
ID: "tool_call_id_2",
|
||||||
|
Type: "function",
|
||||||
|
Function: openai.FunctionCall{
|
||||||
|
Name: "generate_identity",
|
||||||
|
Arguments: `{"name":"Gandalf","age":"90","job_occupation":"Wizard","hobbies":["magic","reading"],"favorites_music_genres":["Classical"]}`,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
agent, err := New(
|
agent, err := New(
|
||||||
|
WithLLMClient(mock),
|
||||||
WithLLMAPIURL(apiURL),
|
WithLLMAPIURL(apiURL),
|
||||||
WithModel(testModel),
|
WithModel(testModel),
|
||||||
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),
|
WithRandomIdentity("An 90-year old man with a long beard, a wizard, who lives in a tower."),
|
||||||
|
|||||||
13
core/conversations/conversations_suite_test.go
Normal file
13
core/conversations/conversations_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package conversations_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConversations(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Conversations test suite")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package connectors
|
package conversations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package connectors_test
|
package conversations_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/services/connectors"
|
"github.com/mudler/LocalAGI/core/conversations"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
@@ -11,13 +11,13 @@ import (
|
|||||||
|
|
||||||
var _ = Describe("ConversationTracker", func() {
|
var _ = Describe("ConversationTracker", func() {
|
||||||
var (
|
var (
|
||||||
tracker *connectors.ConversationTracker[string]
|
tracker *conversations.ConversationTracker[string]
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
)
|
)
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
duration = 1 * time.Second
|
duration = 1 * time.Second
|
||||||
tracker = connectors.NewConversationTracker[string](duration)
|
tracker = conversations.NewConversationTracker[string](duration)
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should initialize with empty conversations", func() {
|
It("should initialize with empty conversations", func() {
|
||||||
@@ -81,8 +81,8 @@ var _ = Describe("ConversationTracker", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("should handle different key types", func() {
|
It("should handle different key types", func() {
|
||||||
trackerInt := connectors.NewConversationTracker[int](duration)
|
trackerInt := conversations.NewConversationTracker[int](duration)
|
||||||
trackerInt64 := connectors.NewConversationTracker[int64](duration)
|
trackerInt64 := conversations.NewConversationTracker[int64](duration)
|
||||||
|
|
||||||
message := openai.ChatCompletionMessage{
|
message := openai.ChatCompletionMessage{
|
||||||
Role: openai.ChatMessageRoleUser,
|
Role: openai.ChatMessageRoleUser,
|
||||||
@@ -31,6 +31,11 @@ func (d DynamicPromptsConfig) ToMap() map[string]string {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FiltersConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config string `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
Connector []ConnectorConfig `json:"connectors" form:"connectors" `
|
||||||
Actions []ActionsConfig `json:"actions" form:"actions"`
|
Actions []ActionsConfig `json:"actions" form:"actions"`
|
||||||
@@ -39,15 +44,17 @@ type AgentConfig struct {
|
|||||||
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
|
MCPSTDIOServers []agent.MCPSTDIOServer `json:"mcp_stdio_servers" form:"mcp_stdio_servers"`
|
||||||
MCPPrepareScript string `json:"mcp_prepare_script" form:"mcp_prepare_script"`
|
MCPPrepareScript string `json:"mcp_prepare_script" form:"mcp_prepare_script"`
|
||||||
MCPBoxURL string `json:"mcp_box_url" form:"mcp_box_url"`
|
MCPBoxURL string `json:"mcp_box_url" form:"mcp_box_url"`
|
||||||
|
Filters []FiltersConfig `json:"filters" form:"filters"`
|
||||||
|
|
||||||
Description string `json:"description" form:"description"`
|
Description string `json:"description" form:"description"`
|
||||||
|
|
||||||
Model string `json:"model" form:"model"`
|
Model string `json:"model" form:"model"`
|
||||||
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
MultimodalModel string `json:"multimodal_model" form:"multimodal_model"`
|
||||||
APIURL string `json:"api_url" form:"api_url"`
|
APIURL string `json:"api_url" form:"api_url"`
|
||||||
APIKey string `json:"api_key" form:"api_key"`
|
APIKey string `json:"api_key" form:"api_key"`
|
||||||
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
LocalRAGURL string `json:"local_rag_url" form:"local_rag_url"`
|
||||||
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
|
LocalRAGAPIKey string `json:"local_rag_api_key" form:"local_rag_api_key"`
|
||||||
|
LastMessageDuration string `json:"last_message_duration" form:"last_message_duration"`
|
||||||
|
|
||||||
Name string `json:"name" form:"name"`
|
Name string `json:"name" form:"name"`
|
||||||
HUD bool `json:"hud" form:"hud"`
|
HUD bool `json:"hud" form:"hud"`
|
||||||
@@ -67,9 +74,13 @@ type AgentConfig struct {
|
|||||||
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
|
||||||
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
|
SummaryLongTermMemory bool `json:"summary_long_term_memory" form:"summary_long_term_memory"`
|
||||||
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
|
ParallelJobs int `json:"parallel_jobs" form:"parallel_jobs"`
|
||||||
|
StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"`
|
||||||
|
EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"`
|
||||||
|
MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentConfigMeta struct {
|
type AgentConfigMeta struct {
|
||||||
|
Filters []config.FieldGroup
|
||||||
Fields []config.Field
|
Fields []config.Field
|
||||||
Connectors []config.FieldGroup
|
Connectors []config.FieldGroup
|
||||||
Actions []config.FieldGroup
|
Actions []config.FieldGroup
|
||||||
@@ -81,6 +92,7 @@ func NewAgentConfigMeta(
|
|||||||
actionsConfig []config.FieldGroup,
|
actionsConfig []config.FieldGroup,
|
||||||
connectorsConfig []config.FieldGroup,
|
connectorsConfig []config.FieldGroup,
|
||||||
dynamicPromptsConfig []config.FieldGroup,
|
dynamicPromptsConfig []config.FieldGroup,
|
||||||
|
filtersConfig []config.FieldGroup,
|
||||||
) AgentConfigMeta {
|
) AgentConfigMeta {
|
||||||
return AgentConfigMeta{
|
return AgentConfigMeta{
|
||||||
Fields: []config.Field{
|
Fields: []config.Field{
|
||||||
@@ -253,7 +265,7 @@ func NewAgentConfigMeta(
|
|||||||
Name: "enable_reasoning",
|
Name: "enable_reasoning",
|
||||||
Label: "Enable Reasoning",
|
Label: "Enable Reasoning",
|
||||||
Type: "checkbox",
|
Type: "checkbox",
|
||||||
DefaultValue: false,
|
DefaultValue: true,
|
||||||
HelpText: "Enable agent to explain its reasoning process",
|
HelpText: "Enable agent to explain its reasoning process",
|
||||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
},
|
},
|
||||||
@@ -292,6 +304,40 @@ func NewAgentConfigMeta(
|
|||||||
HelpText: "Script to prepare the MCP box",
|
HelpText: "Script to prepare the MCP box",
|
||||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "strip_thinking_tags",
|
||||||
|
Label: "Strip Thinking Tags",
|
||||||
|
Type: "checkbox",
|
||||||
|
DefaultValue: false,
|
||||||
|
HelpText: "Remove content between <thinking></thinking> and <think></think> tags from agent responses",
|
||||||
|
Tags: config.Tags{Section: "ModelSettings"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "enable_evaluation",
|
||||||
|
Label: "Enable Evaluation",
|
||||||
|
Type: "checkbox",
|
||||||
|
DefaultValue: false,
|
||||||
|
HelpText: "Enable automatic evaluation of agent responses to ensure they meet user requirements",
|
||||||
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "max_evaluation_loops",
|
||||||
|
Label: "Max Evaluation Loops",
|
||||||
|
Type: "number",
|
||||||
|
DefaultValue: 2,
|
||||||
|
Min: 1,
|
||||||
|
Step: 1,
|
||||||
|
HelpText: "Maximum number of evaluation loops to perform when addressing gaps in responses",
|
||||||
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "last_message_duration",
|
||||||
|
Label: "Last Message Duration",
|
||||||
|
Type: "text",
|
||||||
|
DefaultValue: "5m",
|
||||||
|
HelpText: "Duration for the last message to be considered in the conversation",
|
||||||
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
MCPServers: []config.Field{
|
MCPServers: []config.Field{
|
||||||
{
|
{
|
||||||
@@ -310,6 +356,7 @@ func NewAgentConfigMeta(
|
|||||||
DynamicPrompts: dynamicPromptsConfig,
|
DynamicPrompts: dynamicPromptsConfig,
|
||||||
Connectors: connectorsConfig,
|
Connectors: connectorsConfig,
|
||||||
Actions: actionsConfig,
|
Actions: actionsConfig,
|
||||||
|
Filters: filtersConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type AgentPool struct {
|
|||||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||||
connectors func(*AgentConfig) []Connector
|
connectors func(*AgentConfig) []Connector
|
||||||
dynamicPrompt func(*AgentConfig) []DynamicPrompt
|
dynamicPrompt func(*AgentConfig) []DynamicPrompt
|
||||||
|
filters func(*AgentConfig) types.JobFilters
|
||||||
timeout string
|
timeout string
|
||||||
conversationLogs string
|
conversationLogs string
|
||||||
}
|
}
|
||||||
@@ -78,6 +79,7 @@ func NewAgentPool(
|
|||||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
||||||
connectors func(*AgentConfig) []Connector,
|
connectors func(*AgentConfig) []Connector,
|
||||||
promptBlocks func(*AgentConfig) []DynamicPrompt,
|
promptBlocks func(*AgentConfig) []DynamicPrompt,
|
||||||
|
filters func(*AgentConfig) types.JobFilters,
|
||||||
timeout string,
|
timeout string,
|
||||||
withLogs bool,
|
withLogs bool,
|
||||||
) (*AgentPool, error) {
|
) (*AgentPool, error) {
|
||||||
@@ -110,6 +112,7 @@ func NewAgentPool(
|
|||||||
connectors: connectors,
|
connectors: connectors,
|
||||||
availableActions: availableActions,
|
availableActions: availableActions,
|
||||||
dynamicPrompt: promptBlocks,
|
dynamicPrompt: promptBlocks,
|
||||||
|
filters: filters,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
conversationLogs: conversationPath,
|
conversationLogs: conversationPath,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -135,6 +138,7 @@ func NewAgentPool(
|
|||||||
connectors: connectors,
|
connectors: connectors,
|
||||||
localRAGAPI: LocalRAGAPI,
|
localRAGAPI: LocalRAGAPI,
|
||||||
dynamicPrompt: promptBlocks,
|
dynamicPrompt: promptBlocks,
|
||||||
|
filters: filters,
|
||||||
availableActions: availableActions,
|
availableActions: availableActions,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
conversationLogs: conversationPath,
|
conversationLogs: conversationPath,
|
||||||
@@ -243,7 +247,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
|
|||||||
ImagePrompt string `json:"image_prompt"`
|
ImagePrompt string `json:"image_prompt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := llm.GenerateTypedJSON(
|
err := llm.GenerateTypedJSONWithGuidance(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
llm.NewClient(APIKey, APIURL, "10m"),
|
llm.NewClient(APIKey, APIURL, "10m"),
|
||||||
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
|
"Generate a prompt that I can use to create a random avatar for the bot '"+agent.Name+"', the description of the bot is: "+agent.Description,
|
||||||
@@ -337,6 +341,8 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
|
|
||||||
if config.Model != "" {
|
if config.Model != "" {
|
||||||
model = config.Model
|
model = config.Model
|
||||||
|
} else {
|
||||||
|
config.Model = model
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.MCPBoxURL != "" {
|
if config.MCPBoxURL != "" {
|
||||||
@@ -347,12 +353,17 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
config.PeriodicRuns = "10m"
|
config.PeriodicRuns = "10m"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: Why do we update the pool config from an Agent's config?
|
||||||
if config.APIURL != "" {
|
if config.APIURL != "" {
|
||||||
a.apiURL = config.APIURL
|
a.apiURL = config.APIURL
|
||||||
|
} else {
|
||||||
|
config.APIURL = a.apiURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.APIKey != "" {
|
if config.APIKey != "" {
|
||||||
a.apiKey = config.APIKey
|
a.apiKey = config.APIKey
|
||||||
|
} else {
|
||||||
|
config.APIKey = a.apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.LocalRAGURL != "" {
|
if config.LocalRAGURL != "" {
|
||||||
@@ -366,6 +377,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
connectors := a.connectors(config)
|
connectors := a.connectors(config)
|
||||||
promptBlocks := a.dynamicPrompt(config)
|
promptBlocks := a.dynamicPrompt(config)
|
||||||
actions := a.availableActions(config)(ctx, a)
|
actions := a.availableActions(config)(ctx, a)
|
||||||
|
filters := a.filters(config)
|
||||||
stateFile, characterFile := a.stateFiles(name)
|
stateFile, characterFile := a.stateFiles(name)
|
||||||
|
|
||||||
actionsLog := []string{}
|
actionsLog := []string{}
|
||||||
@@ -378,6 +390,11 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
|
connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filtersLog := []string{}
|
||||||
|
for _, filter := range filters {
|
||||||
|
filtersLog = append(filtersLog, filter.Name())
|
||||||
|
}
|
||||||
|
|
||||||
xlog.Info(
|
xlog.Info(
|
||||||
"Creating agent",
|
"Creating agent",
|
||||||
"name", name,
|
"name", name,
|
||||||
@@ -385,6 +402,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
"api_url", a.apiURL,
|
"api_url", a.apiURL,
|
||||||
"actions", actionsLog,
|
"actions", actionsLog,
|
||||||
"connectors", connectorLog,
|
"connectors", connectorLog,
|
||||||
|
"filters", filtersLog,
|
||||||
)
|
)
|
||||||
|
|
||||||
// dynamicPrompts := []map[string]string{}
|
// dynamicPrompts := []map[string]string{}
|
||||||
@@ -406,6 +424,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
WithMCPSTDIOServers(config.MCPSTDIOServers...),
|
||||||
WithMCPBoxURL(a.mcpBoxURL),
|
WithMCPBoxURL(a.mcpBoxURL),
|
||||||
WithPrompts(promptBlocks...),
|
WithPrompts(promptBlocks...),
|
||||||
|
WithJobFilters(filters...),
|
||||||
WithMCPPrepareScript(config.MCPPrepareScript),
|
WithMCPPrepareScript(config.MCPPrepareScript),
|
||||||
// WithDynamicPrompts(dynamicPrompts...),
|
// WithDynamicPrompts(dynamicPrompts...),
|
||||||
WithCharacter(Character{
|
WithCharacter(Character{
|
||||||
@@ -443,6 +462,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
}),
|
}),
|
||||||
WithSystemPrompt(config.SystemPrompt),
|
WithSystemPrompt(config.SystemPrompt),
|
||||||
WithMultimodalModel(multimodalModel),
|
WithMultimodalModel(multimodalModel),
|
||||||
|
WithLastMessageDuration(config.LastMessageDuration),
|
||||||
WithAgentResultCallback(func(state types.ActionState) {
|
WithAgentResultCallback(func(state types.ActionState) {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
if _, ok := a.agentStatus[name]; !ok {
|
if _, ok := a.agentStatus[name]; !ok {
|
||||||
@@ -526,6 +546,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
opts = append(opts, EnableForceReasoning)
|
opts = append(opts, EnableForceReasoning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.StripThinkingTags {
|
||||||
|
opts = append(opts, EnableStripThinkingTags)
|
||||||
|
}
|
||||||
|
|
||||||
if config.KnowledgeBaseResults > 0 {
|
if config.KnowledgeBaseResults > 0 {
|
||||||
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
|
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
|
||||||
}
|
}
|
||||||
@@ -538,6 +562,13 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
|||||||
opts = append(opts, WithParallelJobs(config.ParallelJobs))
|
opts = append(opts, WithParallelJobs(config.ParallelJobs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.EnableEvaluation {
|
||||||
|
opts = append(opts, EnableEvaluation())
|
||||||
|
if config.MaxEvaluationLoops > 0 {
|
||||||
|
opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
xlog.Info("Starting agent", "name", name, "config", config)
|
xlog.Info("Starting agent", "name", name, "config", config)
|
||||||
|
|
||||||
agent, err := New(opts...)
|
agent, err := New(opts...)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (a ActionDefinition) ToFunctionDefinition() *openai.FunctionDefinition {
|
|||||||
|
|
||||||
// Actions is something the agent can do
|
// Actions is something the agent can do
|
||||||
type Action interface {
|
type Action interface {
|
||||||
Run(ctx context.Context, action ActionParams) (ActionResult, error)
|
Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error)
|
||||||
Definition() ActionDefinition
|
Definition() ActionDefinition
|
||||||
Plannable() bool
|
Plannable() bool
|
||||||
}
|
}
|
||||||
|
|||||||
15
core/types/filters.go
Normal file
15
core/types/filters.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type JobFilter interface {
|
||||||
|
Name() string
|
||||||
|
Apply(job *Job) (bool, error)
|
||||||
|
IsTrigger() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobFilters []JobFilter
|
||||||
|
|
||||||
|
type FilterResult struct {
|
||||||
|
HasTriggers bool `json:"has_triggers"`
|
||||||
|
TriggeredBy string `json:"triggered_by,omitempty"`
|
||||||
|
FailedBy string `json:"failed_by,omitempty"`
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ type Job struct {
|
|||||||
ConversationHistory []openai.ChatCompletionMessage
|
ConversationHistory []openai.ChatCompletionMessage
|
||||||
UUID string
|
UUID string
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
|
DoneFilter bool
|
||||||
|
|
||||||
pastActions []*ActionRequest
|
pastActions []*ActionRequest
|
||||||
nextAction *Action
|
nextAction *Action
|
||||||
@@ -161,23 +162,23 @@ func newUUID() string {
|
|||||||
// To wait for a Job result, use JobResult.WaitResult()
|
// To wait for a Job result, use JobResult.WaitResult()
|
||||||
func NewJob(opts ...JobOption) *Job {
|
func NewJob(opts ...JobOption) *Job {
|
||||||
j := &Job{
|
j := &Job{
|
||||||
Result: NewJobResult(),
|
Result: NewJobResult(),
|
||||||
UUID: newUUID(),
|
UUID: uuid.New().String(),
|
||||||
}
|
Metadata: make(map[string]interface{}),
|
||||||
for _, o := range opts {
|
context: context.Background(),
|
||||||
o(j)
|
ConversationHistory: []openai.ChatCompletionMessage{},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx context.Context
|
for _, opt := range opts {
|
||||||
if j.context == nil {
|
opt(j)
|
||||||
ctx = context.Background()
|
|
||||||
} else {
|
|
||||||
ctx = j.context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context, cancel := context.WithCancel(ctx)
|
// Store the original request if it exists in the conversation history
|
||||||
j.context = context
|
|
||||||
|
ctx, cancel := context.WithCancel(j.context)
|
||||||
|
j.context = ctx
|
||||||
j.cancel = cancel
|
j.cancel = cancel
|
||||||
|
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,3 +207,23 @@ func WithObservable(obs *Observable) JobOption {
|
|||||||
j.Obs = obs
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Creation struct {
|
type Creation struct {
|
||||||
|
ChatCompletionMessage *openai.ChatCompletionMessage `json:"chat_completion_message,omitempty"`
|
||||||
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
|
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
|
||||||
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
|
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
|
||||||
FunctionParams ActionParams `json:"function_params,omitempty"`
|
FunctionParams ActionParams `json:"function_params,omitempty"`
|
||||||
@@ -23,7 +24,8 @@ type Completion struct {
|
|||||||
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
|
ChatCompletionResponse *openai.ChatCompletionResponse `json:"chat_completion_response,omitempty"`
|
||||||
Conversation []openai.ChatCompletionMessage `json:"conversation,omitempty"`
|
Conversation []openai.ChatCompletionMessage `json:"conversation,omitempty"`
|
||||||
ActionResult string `json:"action_result,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 {
|
type Observable struct {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/conversations"
|
||||||
|
)
|
||||||
|
|
||||||
// State is the structure
|
// State is the structure
|
||||||
// that is used to keep track of the current state
|
// that is used to keep track of the current state
|
||||||
@@ -20,6 +25,23 @@ type AgentInternalState struct {
|
|||||||
Goal string `json:"goal"`
|
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 = `=====================
|
const fmtT = `=====================
|
||||||
NowDoing: %s
|
NowDoing: %s
|
||||||
DoingNext: %s
|
DoingNext: %s
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ services:
|
|||||||
file: docker-compose.yaml
|
file: docker-compose.yaml
|
||||||
service: mcpbox
|
service: mcpbox
|
||||||
|
|
||||||
|
dind:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yaml
|
||||||
|
service: dind
|
||||||
|
|
||||||
localrecall:
|
localrecall:
|
||||||
extends:
|
extends:
|
||||||
file: docker-compose.yaml
|
file: docker-compose.yaml
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ services:
|
|||||||
extends:
|
extends:
|
||||||
file: docker-compose.yaml
|
file: docker-compose.yaml
|
||||||
service: mcpbox
|
service: mcpbox
|
||||||
|
|
||||||
|
dind:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yaml
|
||||||
|
service: dind
|
||||||
|
|
||||||
localrecall:
|
localrecall:
|
||||||
extends:
|
extends:
|
||||||
|
|||||||
@@ -54,14 +54,28 @@ services:
|
|||||||
- "8080"
|
- "8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/mcpbox:/app/data
|
- ./volumes/mcpbox:/app/data
|
||||||
# share docker socket if you want it to be able to run docker commands
|
environment:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- DOCKER_HOST=tcp://dind:2375
|
||||||
|
depends_on:
|
||||||
|
dind:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/processes"]
|
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/processes"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
|
dind:
|
||||||
|
image: docker:dind
|
||||||
|
privileged: true
|
||||||
|
environment:
|
||||||
|
- DOCKER_TLS_CERTDIR=""
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "docker", "info"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
localagi:
|
localagi:
|
||||||
depends_on:
|
depends_on:
|
||||||
localai:
|
localai:
|
||||||
|
|||||||
73
go.mod
73
go.mod
@@ -10,70 +10,77 @@ require (
|
|||||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||||
github.com/donseba/go-htmx v1.12.0
|
github.com/donseba/go-htmx v1.12.0
|
||||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
||||||
github.com/go-telegram/bot v1.14.2
|
github.com/go-telegram/bot v1.15.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.6
|
github.com/gofiber/fiber/v2 v2.52.6
|
||||||
github.com/gofiber/template/html/v2 v2.1.3
|
github.com/gofiber/template/html/v2 v2.1.3
|
||||||
github.com/google/go-github/v69 v69.2.0
|
github.com/google/go-github/v69 v69.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/metoro-io/mcp-golang v0.11.0
|
github.com/metoro-io/mcp-golang v0.11.0
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4
|
github.com/onsi/ginkgo/v2 v2.23.4
|
||||||
github.com/onsi/gomega v1.37.0
|
github.com/onsi/gomega v1.37.0
|
||||||
github.com/philippgille/chromem-go v0.7.0
|
github.com/philippgille/chromem-go v0.7.0
|
||||||
github.com/sashabaranov/go-openai v1.38.2
|
github.com/sashabaranov/go-openai v1.39.1
|
||||||
github.com/slack-go/slack v0.16.0
|
github.com/slack-go/slack v0.16.0
|
||||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
||||||
github.com/tmc/langchaingo v0.1.13
|
github.com/tmc/langchaingo v0.1.13
|
||||||
github.com/traefik/yaegi v0.16.1
|
github.com/traefik/yaegi v0.16.1
|
||||||
github.com/valyala/fasthttp v1.60.0
|
github.com/valyala/fasthttp v1.61.0
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.37.0
|
||||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||||
|
maunium.net/go/mautrix v0.17.0
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/antchfx/htmlquery v1.3.0 // indirect
|
github.com/antchfx/htmlquery v1.3.4 // indirect
|
||||||
github.com/antchfx/xmlquery v1.3.17 // indirect
|
github.com/antchfx/xmlquery v1.4.4 // indirect
|
||||||
github.com/antchfx/xpath v1.2.4 // indirect
|
github.com/antchfx/xpath v1.3.4 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/goccy/go-json v0.9.7 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gocolly/colly v1.2.0 // indirect
|
github.com/gocolly/colly v1.2.0 // indirect
|
||||||
github.com/gofiber/template v1.8.3 // indirect
|
github.com/gofiber/template v1.8.3 // indirect
|
||||||
github.com/gofiber/utils v1.1.0 // indirect
|
github.com/gofiber/utils v1.1.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||||
github.com/invopop/jsonschema v0.12.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/rs/zerolog v1.31.0 // indirect
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
@@ -81,17 +88,21 @@ require (
|
|||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
go.mau.fi/util v0.3.0 // indirect
|
||||||
|
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/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/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
280
go.sum
280
go.sum
@@ -1,72 +1,73 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/antchfx/htmlquery v1.3.4 h1:Isd0srPkni2iNTWCwVj/72t7uCphFeor5Q8nCzj1jdQ=
|
||||||
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
|
github.com/antchfx/htmlquery v1.3.4/go.mod h1:K9os0BwIEmLAvTqaNSua8tXLWRWZpocZIH73OzWQbwM=
|
||||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||||
github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk=
|
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||||
github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
|
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
||||||
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
|
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
|
||||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
github.com/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 h1:BlhV1ekv1RbFiM8XZUQeln1Ikb4D+bu2eDO4agREvok=
|
||||||
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
github.com/chasefleming/elem-go v0.30.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
|
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2 h1:flLYmnQFZNo04x2NPehMbf30m7Pli57xwZ0NFqR/hb0=
|
||||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
|
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2/go.mod h1:NtWqRzAp/1tw+twkW8uuBenEVVYndEAZACWU3F3xdoQ=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
|
github.com/donseba/go-htmx v1.12.0 h1:7tESER0uxaqsuGMv3yP3pK1drfBUXM6apG4H7/3+IgE=
|
||||||
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
|
github.com/donseba/go-htmx v1.12.0/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
||||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/go-telegram/bot v1.14.2 h1:j9hXerxTuvkw7yFi3sF5jjRVGozNVKkMQSKjMeBJ5FY=
|
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
|
||||||
github.com/go-telegram/bot v1.14.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
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 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||||
@@ -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/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
||||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
|
github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE=
|
||||||
@@ -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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
|
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||||
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
@@ -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/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||||
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
|
||||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
||||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||||
|
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||||
github.com/sashabaranov/go-openai v1.38.2 h1:akrssjj+6DY3lWuDwHv6cBvJ8Z+FZDM9XEaaYFt0Auo=
|
github.com/sashabaranov/go-openai v1.39.1 h1:TMD4w77Iy9WTFlgnjNaxbAASdsCJ9R/rMdzL+SN14oU=
|
||||||
github.com/sashabaranov/go-openai v1.38.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
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 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
||||||
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
@@ -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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||||
@@ -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/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
|
||||||
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
|
||||||
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
|
||||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
|
github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
|
||||||
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
|
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 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
|
||||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
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 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
|
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||||
|
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.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 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g=
|
||||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||||
|
maunium.net/go/mautrix v0.17.0 h1:scc1qlUbzPn+wc+3eAPquyD+3gZwwy/hBANBm+iGKK8=
|
||||||
|
maunium.net/go/mautrix v0.17.0/go.mod h1:j+puTEQCEydlVxhJ/dQP5chfa26TdvBO7X6F3Ataav8=
|
||||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -66,9 +66,11 @@ func main() {
|
|||||||
localRAG,
|
localRAG,
|
||||||
services.Actions(map[string]string{
|
services.Actions(map[string]string{
|
||||||
"browser-agent-runner-base-url": localOperatorBaseURL,
|
"browser-agent-runner-base-url": localOperatorBaseURL,
|
||||||
|
"deep-research-runner-base-url": localOperatorBaseURL,
|
||||||
}),
|
}),
|
||||||
services.Connectors,
|
services.Connectors,
|
||||||
services.DynamicPrompts,
|
services.DynamicPrompts,
|
||||||
|
services.Filters,
|
||||||
timeout,
|
timeout,
|
||||||
withLogs,
|
withLogs,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,33 @@
|
|||||||
package llm
|
package llm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
// Set up OpenAI client
|
||||||
if APIKey == "" {
|
if APIKey == "" {
|
||||||
//log.Fatal("OPENAI_API_KEY environment variable not set")
|
//log.Fatal("OPENAI_API_KEY environment variable not set")
|
||||||
@@ -18,11 +38,12 @@ func NewClient(APIKey, URL, timeout string) *openai.Client {
|
|||||||
|
|
||||||
dur, err := time.ParseDuration(timeout)
|
dur, err := time.ParseDuration(timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
xlog.Error("Failed to parse timeout", "error", err)
|
||||||
dur = 150 * time.Second
|
dur = 150 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
config.HTTPClient = &http.Client{
|
config.HTTPClient = &http.Client{
|
||||||
Timeout: dur,
|
Timeout: dur,
|
||||||
}
|
}
|
||||||
return openai.NewClientWithConfig(config)
|
return &realClient{openai.NewClientWithConfig(config)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ import (
|
|||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateTypedJSON(ctx context.Context, client *openai.Client, guidance, model string, i jsonschema.Definition, dst any) error {
|
func GenerateTypedJSONWithGuidance(ctx context.Context, client 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"
|
toolName := "json"
|
||||||
decision := openai.ChatCompletionRequest{
|
decision := openai.ChatCompletionRequest{
|
||||||
Model: model,
|
Model: model,
|
||||||
Messages: []openai.ChatCompletionMessage{
|
Messages: conv,
|
||||||
{
|
|
||||||
Role: "user",
|
|
||||||
Content: guidance,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tools: []openai.Tool{
|
Tools: []openai.Tool{
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
25
pkg/llm/mock_client.go
Normal file
25
pkg/llm/mock_client.go
Normal 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
|
||||||
|
}
|
||||||
@@ -4,69 +4,146 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents a client for interacting with the LocalOperator API
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new API client
|
func NewClient(baseURL string, timeout ...time.Duration) *Client {
|
||||||
func NewClient(baseURL string) *Client {
|
defaultTimeout := 30 * time.Second
|
||||||
|
if len(timeout) > 0 {
|
||||||
|
defaultTimeout = timeout[0]
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentRequest represents the request body for running an agent
|
|
||||||
type AgentRequest struct {
|
type AgentRequest struct {
|
||||||
Goal string `json:"goal"`
|
Goal string `json:"goal"`
|
||||||
MaxAttempts int `json:"max_attempts,omitempty"`
|
MaxAttempts int `json:"max_attempts,omitempty"`
|
||||||
MaxNoActionAttempts int `json:"max_no_action_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 {
|
type StateDescription struct {
|
||||||
CurrentURL string `json:"current_url"`
|
CurrentURL string `json:"current_url"`
|
||||||
PageTitle string `json:"page_title"`
|
PageTitle string `json:"page_title"`
|
||||||
PageContentDescription string `json:"page_content_description"`
|
PageContentDescription string `json:"page_content_description"`
|
||||||
Screenshot string `json:"screenshot"`
|
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 {
|
type StateHistory struct {
|
||||||
States []StateDescription `json:"states"`
|
States []StateDescription `json:"states"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunAgent sends a request to run an agent with the given goal
|
type DesktopStateDescription struct {
|
||||||
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
ScreenContent string `json:"screen_content"`
|
||||||
body, err := json.Marshal(req)
|
ScreenshotPath string `json:"screenshot_path"`
|
||||||
if err != nil {
|
}
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.httpClient.Post(
|
type DesktopStateHistory struct {
|
||||||
fmt.Sprintf("%s/api/browser/run", c.baseURL),
|
States []DesktopStateDescription `json:"states"`
|
||||||
"application/json",
|
}
|
||||||
bytes.NewBuffer(body),
|
|
||||||
)
|
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 {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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
|
return resp.Status, nil
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&state); err != nil {
|
}
|
||||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
||||||
}
|
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
|
||||||
|
var result T
|
||||||
return &state, nil
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ const (
|
|||||||
ActionSearch = "search"
|
ActionSearch = "search"
|
||||||
ActionCustom = "custom"
|
ActionCustom = "custom"
|
||||||
ActionBrowserAgentRunner = "browser-agent-runner"
|
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||||
|
ActionDeepResearchRunner = "deep-research-runner"
|
||||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||||
ActionGithubIssueOpener = "github-issue-opener"
|
ActionGithubIssueOpener = "github-issue-opener"
|
||||||
|
ActionGithubIssueEditor = "github-issue-editor"
|
||||||
ActionGithubIssueCloser = "github-issue-closer"
|
ActionGithubIssueCloser = "github-issue-closer"
|
||||||
ActionGithubIssueSearcher = "github-issue-searcher"
|
ActionGithubIssueSearcher = "github-issue-searcher"
|
||||||
ActionGithubRepositoryGet = "github-repository-get-content"
|
ActionGithubRepositoryGet = "github-repository-get-content"
|
||||||
@@ -33,6 +35,8 @@ const (
|
|||||||
ActionGithubPRCreator = "github-pr-creator"
|
ActionGithubPRCreator = "github-pr-creator"
|
||||||
ActionGithubGetAllContent = "github-get-all-repository-content"
|
ActionGithubGetAllContent = "github-get-all-repository-content"
|
||||||
ActionGithubREADME = "github-readme"
|
ActionGithubREADME = "github-readme"
|
||||||
|
ActionGithubRepositorySearchFiles = "github-repository-search-files"
|
||||||
|
ActionGithubRepositoryListFiles = "github-repository-list-files"
|
||||||
ActionScraper = "scraper"
|
ActionScraper = "scraper"
|
||||||
ActionWikipedia = "wikipedia"
|
ActionWikipedia = "wikipedia"
|
||||||
ActionBrowse = "browse"
|
ActionBrowse = "browse"
|
||||||
@@ -42,6 +46,7 @@ const (
|
|||||||
ActionCounter = "counter"
|
ActionCounter = "counter"
|
||||||
ActionCallAgents = "call_agents"
|
ActionCallAgents = "call_agents"
|
||||||
ActionShellcommand = "shell-command"
|
ActionShellcommand = "shell-command"
|
||||||
|
ActionSendTelegramMessage = "send-telegram-message"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableActions = []string{
|
var AvailableActions = []string{
|
||||||
@@ -49,11 +54,15 @@ var AvailableActions = []string{
|
|||||||
ActionCustom,
|
ActionCustom,
|
||||||
ActionGithubIssueLabeler,
|
ActionGithubIssueLabeler,
|
||||||
ActionGithubIssueOpener,
|
ActionGithubIssueOpener,
|
||||||
|
ActionGithubIssueEditor,
|
||||||
ActionGithubIssueCloser,
|
ActionGithubIssueCloser,
|
||||||
ActionGithubIssueSearcher,
|
ActionGithubIssueSearcher,
|
||||||
ActionGithubRepositoryGet,
|
ActionGithubRepositoryGet,
|
||||||
ActionGithubGetAllContent,
|
ActionGithubGetAllContent,
|
||||||
|
ActionGithubRepositorySearchFiles,
|
||||||
|
ActionGithubRepositoryListFiles,
|
||||||
ActionBrowserAgentRunner,
|
ActionBrowserAgentRunner,
|
||||||
|
ActionDeepResearchRunner,
|
||||||
ActionGithubRepositoryCreateOrUpdate,
|
ActionGithubRepositoryCreateOrUpdate,
|
||||||
ActionGithubIssueReader,
|
ActionGithubIssueReader,
|
||||||
ActionGithubIssueCommenter,
|
ActionGithubIssueCommenter,
|
||||||
@@ -71,6 +80,7 @@ var AvailableActions = []string{
|
|||||||
ActionCounter,
|
ActionCounter,
|
||||||
ActionCallAgents,
|
ActionCallAgents,
|
||||||
ActionShellcommand,
|
ActionShellcommand,
|
||||||
|
ActionSendTelegramMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
func Actions(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 a types.Action
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
config = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
case ActionCustom:
|
case ActionCustom:
|
||||||
a, err = action.NewCustom(config, "")
|
a, err = action.NewCustom(config, "")
|
||||||
@@ -115,12 +129,16 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewGithubIssueLabeler(config)
|
a = actions.NewGithubIssueLabeler(config)
|
||||||
case ActionGithubIssueOpener:
|
case ActionGithubIssueOpener:
|
||||||
a = actions.NewGithubIssueOpener(config)
|
a = actions.NewGithubIssueOpener(config)
|
||||||
|
case ActionGithubIssueEditor:
|
||||||
|
a = actions.NewGithubIssueEditor(config)
|
||||||
case ActionGithubIssueCloser:
|
case ActionGithubIssueCloser:
|
||||||
a = actions.NewGithubIssueCloser(config)
|
a = actions.NewGithubIssueCloser(config)
|
||||||
case ActionGithubIssueSearcher:
|
case ActionGithubIssueSearcher:
|
||||||
a = actions.NewGithubIssueSearch(config)
|
a = actions.NewGithubIssueSearch(config)
|
||||||
case ActionBrowserAgentRunner:
|
case ActionBrowserAgentRunner:
|
||||||
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
|
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
|
||||||
|
case ActionDeepResearchRunner:
|
||||||
|
a = actions.NewDeepResearchRunner(config, actionsConfigs["deep-research-runner-base-url"])
|
||||||
case ActionGithubIssueReader:
|
case ActionGithubIssueReader:
|
||||||
a = actions.NewGithubIssueReader(config)
|
a = actions.NewGithubIssueReader(config)
|
||||||
case ActionGithubPRReader:
|
case ActionGithubPRReader:
|
||||||
@@ -133,6 +151,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewGithubPRCreator(config)
|
a = actions.NewGithubPRCreator(config)
|
||||||
case ActionGithubGetAllContent:
|
case ActionGithubGetAllContent:
|
||||||
a = actions.NewGithubRepositoryGetAllContent(config)
|
a = actions.NewGithubRepositoryGetAllContent(config)
|
||||||
|
case ActionGithubRepositorySearchFiles:
|
||||||
|
a = actions.NewGithubRepositorySearchFiles(config)
|
||||||
|
case ActionGithubRepositoryListFiles:
|
||||||
|
a = actions.NewGithubRepositoryListFiles(config)
|
||||||
case ActionGithubIssueCommenter:
|
case ActionGithubIssueCommenter:
|
||||||
a = actions.NewGithubIssueCommenter(config)
|
a = actions.NewGithubIssueCommenter(config)
|
||||||
case ActionGithubRepositoryGet:
|
case ActionGithubRepositoryGet:
|
||||||
@@ -157,6 +179,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
|
a = actions.NewCallAgent(config, agentName, pool.InternalAPI())
|
||||||
case ActionShellcommand:
|
case ActionShellcommand:
|
||||||
a = actions.NewShell(config)
|
a = actions.NewShell(config)
|
||||||
|
case ActionSendTelegramMessage:
|
||||||
|
a = actions.NewSendTelegramMessageRunner(config)
|
||||||
default:
|
default:
|
||||||
xlog.Error("Action not found", "name", name)
|
xlog.Error("Action not found", "name", name)
|
||||||
return nil, fmt.Errorf("Action not found")
|
return nil, fmt.Errorf("Action not found")
|
||||||
@@ -181,6 +205,11 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Browser Agent Runner",
|
Label: "Browser Agent Runner",
|
||||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "deep-research-runner",
|
||||||
|
Label: "Deep Research Runner",
|
||||||
|
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "generate_image",
|
Name: "generate_image",
|
||||||
Label: "Generate Image",
|
Label: "Generate Image",
|
||||||
@@ -196,6 +225,11 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "GitHub Issue Opener",
|
Label: "GitHub Issue Opener",
|
||||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "github-issue-editor",
|
||||||
|
Label: "GitHub Issue Editor",
|
||||||
|
Fields: actions.GithubIssueEditorConfigMeta(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "github-issue-closer",
|
Name: "github-issue-closer",
|
||||||
Label: "GitHub Issue Closer",
|
Label: "GitHub Issue Closer",
|
||||||
@@ -226,6 +260,16 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "GitHub Get All Repository Content",
|
Label: "GitHub Get All Repository Content",
|
||||||
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "github-repository-search-files",
|
||||||
|
Label: "GitHub Repository Search Files",
|
||||||
|
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "github-repository-list-files",
|
||||||
|
Label: "GitHub Repository List Files",
|
||||||
|
Fields: actions.GithubRepositoryListFilesConfigMeta(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "github-repository-create-or-update-content",
|
Name: "github-repository-create-or-update-content",
|
||||||
Label: "GitHub Repository Create/Update Content",
|
Label: "GitHub Repository Create/Update Content",
|
||||||
@@ -299,7 +343,12 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
{
|
{
|
||||||
Name: "call_agents",
|
Name: "call_agents",
|
||||||
Label: "Call Agents",
|
Label: "Call Agents",
|
||||||
Fields: []config.Field{},
|
Fields: actions.CallAgentConfigMeta(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "send-telegram-message",
|
||||||
|
Label: "Send Telegram Message",
|
||||||
|
Fields: actions.SendTelegramMessageConfigMeta(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func NewBrowse(config map[string]string) *BrowseAction {
|
|||||||
|
|
||||||
type BrowseAction struct{}
|
type BrowseAction struct{}
|
||||||
|
|
||||||
func (a *BrowseAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *BrowseAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
@@ -24,7 +25,18 @@ func NewBrowserAgentRunner(config map[string]string, defaultURL string) *Browser
|
|||||||
config["baseURL"] = defaultURL
|
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{
|
return &BrowserAgentRunner{
|
||||||
client: client,
|
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{}
|
result := api.AgentRequest{}
|
||||||
err := params.Unmarshal(&result)
|
err := params.Unmarshal(&result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,5 +129,12 @@ func BrowserAgentRunnerConfigMeta() []config.Field {
|
|||||||
Type: config.FieldTypeText,
|
Type: config.FieldTypeText,
|
||||||
HelpText: "Custom name for this action",
|
HelpText: "Custom name for this action",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "timeout",
|
||||||
|
Label: "Client Timeout",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,56 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/state"
|
"github.com/mudler/LocalAGI/core/state"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func trimList(list []string) []string {
|
||||||
|
for i, v := range list {
|
||||||
|
list[i] = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction {
|
func NewCallAgent(config map[string]string, agentName string, pool *state.AgentPoolInternalAPI) *CallAgentAction {
|
||||||
|
whitelist := []string{}
|
||||||
|
blacklist := []string{}
|
||||||
|
if v, ok := config["whitelist"]; ok {
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
whitelist = trimList(strings.Split(v, ","))
|
||||||
|
} else {
|
||||||
|
whitelist = []string{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := config["blacklist"]; ok {
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
blacklist = trimList(strings.Split(v, ","))
|
||||||
|
} else {
|
||||||
|
blacklist = []string{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
return &CallAgentAction{
|
return &CallAgentAction{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
myName: agentName,
|
myName: agentName,
|
||||||
|
whitelist: whitelist,
|
||||||
|
blacklist: blacklist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CallAgentAction struct {
|
type CallAgentAction struct {
|
||||||
pool *state.AgentPoolInternalAPI
|
pool *state.AgentPoolInternalAPI
|
||||||
myName string
|
myName string
|
||||||
|
whitelist []string
|
||||||
|
blacklist []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *CallAgentAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
AgentName string `json:"agent_name"`
|
AgentName string `json:"agent_name"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -83,13 +113,32 @@ func (a *CallAgentAction) Run(ctx context.Context, params types.ActionParams) (t
|
|||||||
return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil
|
return types.ActionResult{Result: resp.Response, Metadata: metadata}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *CallAgentAction) isAllowedToBeCalled(agentName string) bool {
|
||||||
|
if agentName == a.myName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.whitelist) > 0 && len(a.blacklist) > 0 {
|
||||||
|
return slices.Contains(a.whitelist, agentName) && !slices.Contains(a.blacklist, agentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.whitelist) > 0 {
|
||||||
|
return slices.Contains(a.whitelist, agentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.blacklist) > 0 {
|
||||||
|
return !slices.Contains(a.blacklist, agentName)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (a *CallAgentAction) Definition() types.ActionDefinition {
|
func (a *CallAgentAction) Definition() types.ActionDefinition {
|
||||||
allAgents := a.pool.AllAgents()
|
allAgents := a.pool.AllAgents()
|
||||||
|
|
||||||
agents := []string{}
|
agents := []string{}
|
||||||
|
|
||||||
for _, ag := range allAgents {
|
for _, ag := range allAgents {
|
||||||
if ag != a.myName {
|
if a.isAllowedToBeCalled(ag) {
|
||||||
agents = append(agents, ag)
|
agents = append(agents, ag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,3 +174,21 @@ func (a *CallAgentAction) Definition() types.ActionDefinition {
|
|||||||
func (a *CallAgentAction) Plannable() bool {
|
func (a *CallAgentAction) Plannable() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CallAgentConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "whitelist",
|
||||||
|
Label: "Whitelist",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Comma-separated list of agent names to call. If not specified, all agents are allowed.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "blacklist",
|
||||||
|
Label: "Blacklist",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
HelpText: "Comma-separated list of agent names to exclude from the call. If not specified, all agents are allowed.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func NewCounter(config map[string]string) *CounterAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the counter action
|
// Run executes the counter action
|
||||||
func (a *CounterAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *CounterAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
// Parse parameters
|
// Parse parameters
|
||||||
request := struct {
|
request := struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
148
services/actions/deepresearchrunner.go
Normal file
148
services/actions/deepresearchrunner.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetadataDeepResearchResult = "deep_research_result"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeepResearchRunner struct {
|
||||||
|
baseURL, customActionName string
|
||||||
|
client *api.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
|
||||||
|
if config["baseURL"] == "" {
|
||||||
|
config["baseURL"] = defaultURL
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := "15m"
|
||||||
|
if config["timeout"] != "" {
|
||||||
|
timeout = config["timeout"]
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(timeout)
|
||||||
|
if err != nil {
|
||||||
|
// If parsing fails, use default 15 minutes
|
||||||
|
duration = 15 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
client := api.NewClient(config["baseURL"], duration)
|
||||||
|
|
||||||
|
return &DeepResearchRunner{
|
||||||
|
client: client,
|
||||||
|
baseURL: config["baseURL"],
|
||||||
|
customActionName: config["customActionName"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeepResearchRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
|
result := api.DeepResearchRequest{}
|
||||||
|
err := params.Unmarshal(&result)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := api.DeepResearchRequest{
|
||||||
|
Topic: result.Topic,
|
||||||
|
MaxCycles: result.MaxCycles,
|
||||||
|
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||||
|
MaxResults: result.MaxResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
researchResult, err := d.client.RunDeepResearch(req)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the research result into a readable string
|
||||||
|
var resultStr string
|
||||||
|
|
||||||
|
resultStr += "Deep research result\n"
|
||||||
|
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
|
||||||
|
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
|
||||||
|
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
|
||||||
|
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
|
||||||
|
|
||||||
|
if len(researchResult.Sources) > 0 {
|
||||||
|
resultStr += "Sources:\n"
|
||||||
|
for _, source := range researchResult.Sources {
|
||||||
|
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ActionResult{
|
||||||
|
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
|
||||||
|
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
|
||||||
|
actionName := "run_deep_research"
|
||||||
|
if d.customActionName != "" {
|
||||||
|
actionName = d.customActionName
|
||||||
|
}
|
||||||
|
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"topic": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The topic to research",
|
||||||
|
},
|
||||||
|
"max_cycles": {
|
||||||
|
Type: jsonschema.Number,
|
||||||
|
Description: "Maximum number of research cycles to perform (optional)",
|
||||||
|
},
|
||||||
|
"max_no_action_attempts": {
|
||||||
|
Type: jsonschema.Number,
|
||||||
|
Description: "Maximum number of attempts without taking an action (optional)",
|
||||||
|
},
|
||||||
|
"max_results": {
|
||||||
|
Type: jsonschema.Number,
|
||||||
|
Description: "Maximum number of results to collect (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"topic"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeepResearchRunner) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
|
||||||
|
func DeepResearchRunnerConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "baseURL",
|
||||||
|
Label: "Base URL",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Base URL of the LocalOperator API",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "customActionName",
|
||||||
|
Label: "Custom Action Name",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
HelpText: "Custom name for this action",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "timeout",
|
||||||
|
Label: "Client Timeout",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Client timeout duration (e.g. '15m', '1h'). Defaults to '15m' if not specified.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ type GenImageAction struct {
|
|||||||
imageModel string
|
imageModel string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *GenImageAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *GenImageAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
Size string `json:"size"`
|
Size string `json:"size"`
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ var _ = Describe("GenImageAction", func() {
|
|||||||
"size": "256x256",
|
"size": "256x256",
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := action.Run(ctx, params)
|
url, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(url).ToNot(BeEmpty())
|
Expect(url).ToNot(BeEmpty())
|
||||||
})
|
})
|
||||||
@@ -52,7 +52,7 @@ var _ = Describe("GenImageAction", func() {
|
|||||||
"size": "256x256",
|
"size": "256x256",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := action.Run(ctx, params)
|
_, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func NewGithubIssueCloser(config map[string]string) *GithubIssuesCloser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubIssuesCloser) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesCloser) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGithubIssueCommenter(config map[string]string) *GithubIssuesCommenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubIssuesCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesCommenter) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
151
services/actions/githubissueedit.go
Normal file
151
services/actions/githubissueedit.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v69/github"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GithubIssueEditor struct {
|
||||||
|
token, repository, owner, customActionName string
|
||||||
|
client *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGithubIssueEditor(config map[string]string) *GithubIssueEditor {
|
||||||
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
|
return &GithubIssueEditor{
|
||||||
|
client: client,
|
||||||
|
token: config["token"],
|
||||||
|
customActionName: config["customActionName"],
|
||||||
|
repository: config["repository"],
|
||||||
|
owner: config["owner"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubIssueEditor) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
|
result := struct {
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
IssueNumber int `json:"issue_number"`
|
||||||
|
}{}
|
||||||
|
err := params.Unmarshal(&result)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.repository != "" && g.owner != "" {
|
||||||
|
result.Repository = g.repository
|
||||||
|
result.Owner = g.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = g.client.Issues.Edit(ctx, result.Owner, result.Repository, result.IssueNumber,
|
||||||
|
&github.IssueRequest{
|
||||||
|
Body: &result.Description,
|
||||||
|
Title: &result.Title,
|
||||||
|
})
|
||||||
|
resultString := fmt.Sprintf("Updated issue %d in repository %s/%s", result.IssueNumber, result.Owner, result.Repository)
|
||||||
|
if err != nil {
|
||||||
|
resultString = fmt.Sprintf("Error updating issue %d in repository %s/%s: %v", result.IssueNumber, result.Owner, result.Repository, err)
|
||||||
|
}
|
||||||
|
return types.ActionResult{Result: resultString}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubIssueEditor) Definition() types.ActionDefinition {
|
||||||
|
actionName := "edit_github_issue"
|
||||||
|
if g.customActionName != "" {
|
||||||
|
actionName = g.customActionName
|
||||||
|
}
|
||||||
|
description := "Edit the title and description of a Github issue in a repository. Use this action after reading the issue"
|
||||||
|
if g.repository != "" && g.owner != "" {
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"issue_number": {
|
||||||
|
Type: jsonschema.Number,
|
||||||
|
Description: "The number of the issue to edit.",
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The new title for the issue.",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The new description for the issue.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"issue_number", "title", "description"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"issue_number": {
|
||||||
|
Type: jsonschema.Number,
|
||||||
|
Description: "The number of the issue to edit.",
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The repository containing the issue.",
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The owner of the repository.",
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The new title for the issue.",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The new description for the issue.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"issue_number", "repository", "owner", "title", "description"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GithubIssueEditor) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubIssueEditorConfigMeta returns the metadata for GitHub Issue Editor action configuration fields
|
||||||
|
func GithubIssueEditorConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "token",
|
||||||
|
Label: "GitHub Token",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "GitHub API token with repository access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "repository",
|
||||||
|
Label: "Repository",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "GitHub repository name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "owner",
|
||||||
|
Label: "Owner",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "GitHub repository owner",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "customActionName",
|
||||||
|
Label: "Custom Action Name",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
HelpText: "Custom name for this action",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ func NewGithubIssueLabeler(config map[string]string) *GithubIssuesLabeler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubIssuesLabeler) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesLabeler) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGithubIssueOpener(config map[string]string) *GithubIssuesOpener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubIssuesOpener) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesOpener) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Body string `json:"text"`
|
Body string `json:"text"`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGithubIssueReader(config map[string]string) *GithubIssuesReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubIssuesReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssuesReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func NewGithubIssueSearch(config map[string]string) *GithubIssueSearch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubIssueSearch) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubIssueSearch) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
@@ -17,96 +15,6 @@ type GithubPRCommenter struct {
|
|||||||
client *github.Client
|
client *github.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
patchRegex = regexp.MustCompile(`^@@.*\d [\+\-](\d+),?(\d+)?.+?@@`)
|
|
||||||
)
|
|
||||||
|
|
||||||
type commitFileInfo struct {
|
|
||||||
FileName string
|
|
||||||
hunkInfos []*hunkInfo
|
|
||||||
sha string
|
|
||||||
}
|
|
||||||
|
|
||||||
type hunkInfo struct {
|
|
||||||
hunkStart int
|
|
||||||
hunkEnd int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hi hunkInfo) isLineInHunk(line int) bool {
|
|
||||||
return line >= hi.hunkStart && line <= hi.hunkEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfi *commitFileInfo) getHunkInfo(line int) *hunkInfo {
|
|
||||||
for _, hunkInfo := range cfi.hunkInfos {
|
|
||||||
if hunkInfo.isLineInHunk(line) {
|
|
||||||
return hunkInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfi *commitFileInfo) isLineInChange(line int) bool {
|
|
||||||
return cfi.getHunkInfo(line) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfi commitFileInfo) calculatePosition(line int) *int {
|
|
||||||
hi := cfi.getHunkInfo(line)
|
|
||||||
if hi == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
position := line - hi.hunkStart
|
|
||||||
return &position
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHunkPositions(patch, filename string) ([]*hunkInfo, error) {
|
|
||||||
hunkInfos := make([]*hunkInfo, 0)
|
|
||||||
if patch != "" {
|
|
||||||
groups := patchRegex.FindAllStringSubmatch(patch, -1)
|
|
||||||
if len(groups) < 1 {
|
|
||||||
return hunkInfos, fmt.Errorf("the patch details for [%s] could not be resolved", filename)
|
|
||||||
}
|
|
||||||
for _, patchGroup := range groups {
|
|
||||||
endPos := 2
|
|
||||||
if len(patchGroup) > 2 && patchGroup[2] == "" {
|
|
||||||
endPos = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
hunkStart, err := strconv.Atoi(patchGroup[1])
|
|
||||||
if err != nil {
|
|
||||||
hunkStart = -1
|
|
||||||
}
|
|
||||||
hunkEnd, err := strconv.Atoi(patchGroup[endPos])
|
|
||||||
if err != nil {
|
|
||||||
hunkEnd = -1
|
|
||||||
}
|
|
||||||
hunkInfos = append(hunkInfos, &hunkInfo{
|
|
||||||
hunkStart: hunkStart,
|
|
||||||
hunkEnd: hunkEnd,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hunkInfos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCommitInfo(file *github.CommitFile) (*commitFileInfo, error) {
|
|
||||||
patch := file.GetPatch()
|
|
||||||
hunkInfos, err := parseHunkPositions(patch, *file.Filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sha := file.GetSHA()
|
|
||||||
if sha == "" {
|
|
||||||
return nil, fmt.Errorf("the sha details for [%s] could not be resolved", *file.Filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &commitFileInfo{
|
|
||||||
FileName: *file.Filename,
|
|
||||||
hunkInfos: hunkInfos,
|
|
||||||
sha: sha,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
|
||||||
client := github.NewClient(nil).WithAuthToken(config["token"])
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
@@ -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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
@@ -10,8 +11,16 @@ import (
|
|||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// forkCreationRetries is the number of times to retry checking if a fork is ready
|
||||||
|
forkCreationRetries = 30
|
||||||
|
// forkCreationRetryDelay is the duration to wait between fork creation checks
|
||||||
|
forkCreationRetryDelay = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
type GithubPRCreator struct {
|
type GithubPRCreator struct {
|
||||||
token, repository, owner, customActionName, defaultBranch string
|
token, repository, owner, customActionName, defaultBranch string
|
||||||
|
useFork bool
|
||||||
client *github.Client
|
client *github.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +32,8 @@ func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
|
|||||||
defaultBranch = "main" // Default to "main" if not specified
|
defaultBranch = "main" // Default to "main" if not specified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useFork := config["useFork"] == "true"
|
||||||
|
|
||||||
return &GithubPRCreator{
|
return &GithubPRCreator{
|
||||||
client: client,
|
client: client,
|
||||||
token: config["token"],
|
token: config["token"],
|
||||||
@@ -30,9 +41,45 @@ func NewGithubPRCreator(config map[string]string) *GithubPRCreator {
|
|||||||
owner: config["owner"],
|
owner: config["owner"],
|
||||||
customActionName: config["customActionName"],
|
customActionName: config["customActionName"],
|
||||||
defaultBranch: defaultBranch,
|
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 {
|
func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string, owner string, repository string) error {
|
||||||
// Get the latest commit SHA from the default branch
|
// Get the latest commit SHA from the default branch
|
||||||
ref, _, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+g.defaultBranch)
|
ref, _, err := g.client.Git.GetRef(ctx, owner, repository, "refs/heads/"+g.defaultBranch)
|
||||||
@@ -101,7 +148,7 @@ func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string,
|
|||||||
return nil
|
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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
@@ -128,15 +175,29 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
|
|||||||
result.BaseBranch = g.defaultBranch
|
result.BaseBranch = g.defaultBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var targetOwner, targetRepo string
|
||||||
|
if g.useFork {
|
||||||
|
// Ensure we have a fork and get the fork owner
|
||||||
|
forkOwner, err := g.ensureFork(ctx, result.Owner, result.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to ensure fork: %w", err)
|
||||||
|
}
|
||||||
|
targetOwner = forkOwner
|
||||||
|
targetRepo = result.Repository
|
||||||
|
} else {
|
||||||
|
targetOwner = result.Owner
|
||||||
|
targetRepo = result.Repository
|
||||||
|
}
|
||||||
|
|
||||||
// Create or update branch
|
// Create or update branch
|
||||||
err = g.createOrUpdateBranch(ctx, result.Branch, result.Owner, result.Repository)
|
err = g.createOrUpdateBranch(ctx, result.Branch, targetOwner, targetRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
|
return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update files
|
// Create or update files
|
||||||
for _, file := range result.Files {
|
for _, file := range result.Files {
|
||||||
err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path), 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 {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
|
return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err)
|
||||||
}
|
}
|
||||||
@@ -145,7 +206,7 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
|
|||||||
// Check if PR already exists for this branch
|
// Check if PR already exists for this branch
|
||||||
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
|
prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{
|
||||||
State: "open",
|
State: "open",
|
||||||
Head: fmt.Sprintf("%s:%s", result.Owner, result.Branch),
|
Head: fmt.Sprintf("%s:%s", targetOwner, result.Branch),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
|
return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err)
|
||||||
@@ -175,6 +236,12 @@ func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (t
|
|||||||
Base: &result.BaseBranch,
|
Base: &result.BaseBranch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If using a fork, we need to specify the full head reference
|
||||||
|
if g.useFork {
|
||||||
|
head := fmt.Sprintf("%s:%s", targetOwner, result.Branch)
|
||||||
|
newPR.Head = &head
|
||||||
|
}
|
||||||
|
|
||||||
createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR)
|
createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
|
return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err)
|
||||||
@@ -322,5 +389,12 @@ func GithubPRCreatorConfigMeta() []config.Field {
|
|||||||
Required: false,
|
Required: false,
|
||||||
HelpText: "Default branch to create PRs against (defaults to main)",
|
HelpText: "Default branch to create PRs against (defaults to main)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "useFork",
|
||||||
|
Label: "Use Fork",
|
||||||
|
Type: config.FieldTypeCheckbox,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Whether to create PRs from a fork (useful when you don't have write access to the repository). Set to 'true' to enable.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ var _ = Describe("GithubPRCreator", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := action.Run(ctx, params)
|
result, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(result.Result).To(ContainSubstring("pull request #"))
|
Expect(result.Result).To(ContainSubstring("pull request #"))
|
||||||
})
|
})
|
||||||
@@ -65,7 +65,7 @@ var _ = Describe("GithubPRCreator", func() {
|
|||||||
"body": "This is a test pull request",
|
"body": "This is a test pull request",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := action.Run(ctx, params)
|
_, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func NewGithubPRReader(config map[string]string) *GithubPRReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubPRReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubPRReader) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ var _ = Describe("GithubPRReviewer", func() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := reviewer.Run(ctx, params)
|
result, err := reviewer.Run(ctx, nil, params)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(result.Result).To(ContainSubstring("reviewed successfully"))
|
Expect(result.Result).To(ContainSubstring("reviewed successfully"))
|
||||||
})
|
})
|
||||||
@@ -70,7 +70,7 @@ var _ = Describe("GithubPRReviewer", func() {
|
|||||||
"review_action": "COMMENT",
|
"review_action": "COMMENT",
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := reviewer.Run(ctx, params)
|
result, err := reviewer.Run(ctx, nil, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
Expect(result.Result).To(ContainSubstring("not found"))
|
Expect(result.Result).To(ContainSubstring("not found"))
|
||||||
})
|
})
|
||||||
@@ -85,7 +85,7 @@ var _ = Describe("GithubPRReviewer", func() {
|
|||||||
"review_action": "INVALID_ACTION",
|
"review_action": "INVALID_ACTION",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := reviewer.Run(ctx, params)
|
_, err := reviewer.Run(ctx, nil, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewGithubRepositoryCreateOrUpdateContent(config map[string]string) *GithubR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryCreateOrUpdateContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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) {
|
func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string, owner string, repository string) (string, error) {
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
|
|
||||||
@@ -47,6 +73,13 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
|
|||||||
}
|
}
|
||||||
result.WriteString(subContent)
|
result.WriteString(subContent)
|
||||||
} else if item.GetType() == "file" {
|
} else if item.GetType() == "file" {
|
||||||
|
// Skip binary/image files
|
||||||
|
if !isTextFile(item.GetPath()) {
|
||||||
|
xlog.Warn("Skipping non-text file: ", "file", item.GetPath())
|
||||||
|
result.WriteString(fmt.Sprintf("Skipping non-text file: %s\n", item.GetPath()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Get file content
|
// Get file content
|
||||||
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
|
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,7 +101,7 @@ func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Contex
|
|||||||
return result.String(), nil
|
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 {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
|
|||||||
"path": ".",
|
"path": ".",
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := action.Run(ctx, params)
|
result, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(result.Result).NotTo(BeEmpty())
|
Expect(result.Result).NotTo(BeEmpty())
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ var _ = Describe("GithubRepositoryGetAllContent", func() {
|
|||||||
"path": "non-existent-path",
|
"path": "non-existent-path",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := action.Run(ctx, params)
|
_, err := action.Run(ctx, nil, params)
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGithubRepositoryGetContent(config map[string]string) *GithubRepositoryGe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubRepositoryGetContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryGetContent) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
|||||||
163
services/actions/githubrepositorylistfiles.go
Normal file
163
services/actions/githubrepositorylistfiles.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v69/github"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GithubRepositoryListFiles struct {
|
||||||
|
token, repository, owner, customActionName string
|
||||||
|
client *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGithubRepositoryListFiles(config map[string]string) *GithubRepositoryListFiles {
|
||||||
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
|
return &GithubRepositoryListFiles{
|
||||||
|
client: client,
|
||||||
|
token: config["token"],
|
||||||
|
repository: config["repository"],
|
||||||
|
owner: config["owner"],
|
||||||
|
customActionName: config["customActionName"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubRepositoryListFiles) listFilesRecursively(ctx context.Context, path string, owner string, repository string) ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
// Get content at the current path
|
||||||
|
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting content at path %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each item in the directory
|
||||||
|
for _, item := range directoryContent {
|
||||||
|
if item.GetType() == "dir" {
|
||||||
|
// Recursively list files in subdirectories
|
||||||
|
subFiles, err := g.listFilesRecursively(ctx, item.GetPath(), owner, repository)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, subFiles...)
|
||||||
|
} else if item.GetType() == "file" {
|
||||||
|
// Add file path to the list
|
||||||
|
files = append(files, item.GetPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubRepositoryListFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
|
result := struct {
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
}{}
|
||||||
|
err := params.Unmarshal(&result)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.repository != "" && g.owner != "" {
|
||||||
|
result.Repository = g.repository
|
||||||
|
result.Owner = g.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start from root if no path specified
|
||||||
|
if result.Path == "" {
|
||||||
|
result.Path = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := g.listFilesRecursively(ctx, result.Path, result.Owner, result.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all file paths with newlines for better readability
|
||||||
|
content := strings.Join(files, "\n")
|
||||||
|
return types.ActionResult{Result: content}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubRepositoryListFiles) Definition() types.ActionDefinition {
|
||||||
|
actionName := "list_github_repository_files"
|
||||||
|
if g.customActionName != "" {
|
||||||
|
actionName = g.customActionName
|
||||||
|
}
|
||||||
|
description := "List all files in a GitHub repository"
|
||||||
|
if g.repository != "" && g.owner != "" {
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"path": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Optional path to start listing from (defaults to repository root)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"path": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Optional path to start listing from (defaults to repository root)",
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The repository to list files from",
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The owner of the repository",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"repository", "owner"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GithubRepositoryListFiles) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubRepositoryListFilesConfigMeta returns the metadata for GitHub Repository List Files action configuration fields
|
||||||
|
func GithubRepositoryListFilesConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "token",
|
||||||
|
Label: "GitHub Token",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "GitHub API token with repository access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "repository",
|
||||||
|
Label: "Repository",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "GitHub repository name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "owner",
|
||||||
|
Label: "Owner",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "GitHub repository owner",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "customActionName",
|
||||||
|
Label: "Custom Action Name",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
HelpText: "Custom name for this action",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ func NewGithubRepositoryREADME(config map[string]string) *GithubRepositoryREADME
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GithubRepositoryREADME) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (g *GithubRepositoryREADME) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|||||||
187
services/actions/githubrepositorysearchfiles.go
Normal file
187
services/actions/githubrepositorysearchfiles.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v69/github"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GithubRepositorySearchFiles struct {
|
||||||
|
token, repository, owner, customActionName string
|
||||||
|
client *github.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGithubRepositorySearchFiles(config map[string]string) *GithubRepositorySearchFiles {
|
||||||
|
client := github.NewClient(nil).WithAuthToken(config["token"])
|
||||||
|
|
||||||
|
return &GithubRepositorySearchFiles{
|
||||||
|
client: client,
|
||||||
|
token: config["token"],
|
||||||
|
repository: config["repository"],
|
||||||
|
owner: config["owner"],
|
||||||
|
customActionName: config["customActionName"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubRepositorySearchFiles) searchFilesRecursively(ctx context.Context, path string, owner string, repository string, searchPattern string) (string, error) {
|
||||||
|
var result strings.Builder
|
||||||
|
|
||||||
|
// Get content at the current path
|
||||||
|
_, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting content at path %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each item in the directory
|
||||||
|
for _, item := range directoryContent {
|
||||||
|
if item.GetType() == "dir" {
|
||||||
|
// Recursively search in subdirectories
|
||||||
|
subContent, err := g.searchFilesRecursively(ctx, item.GetPath(), owner, repository, searchPattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result.WriteString(subContent)
|
||||||
|
} else if item.GetType() == "file" {
|
||||||
|
// Check if file name matches the search pattern
|
||||||
|
if strings.Contains(strings.ToLower(item.GetName()), strings.ToLower(searchPattern)) {
|
||||||
|
// Get file content
|
||||||
|
fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := fileContent.GetContent()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file content to result with clear markers
|
||||||
|
result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath()))
|
||||||
|
result.WriteString(content)
|
||||||
|
result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubRepositorySearchFiles) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
|
result := struct {
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
SearchPattern string `json:"searchPattern"`
|
||||||
|
}{}
|
||||||
|
err := params.Unmarshal(&result)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.repository != "" && g.owner != "" {
|
||||||
|
result.Repository = g.repository
|
||||||
|
result.Owner = g.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start from root if no path specified
|
||||||
|
if result.Path == "" {
|
||||||
|
result.Path = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := g.searchFilesRecursively(ctx, result.Path, result.Owner, result.Repository, result.SearchPattern)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ActionResult{Result: content}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GithubRepositorySearchFiles) Definition() types.ActionDefinition {
|
||||||
|
actionName := "search_github_repository_files"
|
||||||
|
if g.customActionName != "" {
|
||||||
|
actionName = g.customActionName
|
||||||
|
}
|
||||||
|
description := "Search for files in a GitHub repository and return their content"
|
||||||
|
if g.repository != "" && g.owner != "" {
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"path": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Optional path to start searching from (defaults to repository root)",
|
||||||
|
},
|
||||||
|
"searchPattern": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Pattern to search for in file names (case-insensitive)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"searchPattern"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(actionName),
|
||||||
|
Description: description,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"path": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Optional path to start searching from (defaults to repository root)",
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The repository to search in",
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The owner of the repository",
|
||||||
|
},
|
||||||
|
"searchPattern": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "Pattern to search for in file names (case-insensitive)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"repository", "owner", "searchPattern"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GithubRepositorySearchFiles) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubRepositorySearchFilesConfigMeta returns the metadata for GitHub Repository Search Files action configuration fields
|
||||||
|
func GithubRepositorySearchFilesConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "token",
|
||||||
|
Label: "GitHub Token",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "GitHub API token with repository access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "repository",
|
||||||
|
Label: "Repository",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "GitHub repository name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "owner",
|
||||||
|
Label: "Owner",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "GitHub repository owner",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "customActionName",
|
||||||
|
Label: "Custom Action Name",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
HelpText: "Custom name for this action",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ func NewScraper(config map[string]string) *ScraperAction {
|
|||||||
|
|
||||||
type ScraperAction struct{}
|
type ScraperAction struct{}
|
||||||
|
|
||||||
func (a *ScraperAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *ScraperAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func NewSearch(config map[string]string) *SearchAction {
|
|||||||
|
|
||||||
type SearchAction struct{ results int }
|
type SearchAction struct{ results int }
|
||||||
|
|
||||||
func (a *SearchAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *SearchAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type SendMailAction struct {
|
|||||||
smtpPort string
|
smtpPort string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SendMailAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *SendMailAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
To string `json:"to"`
|
To string `json:"to"`
|
||||||
|
|||||||
195
services/actions/sendtelegrammessage.go
Normal file
195
services/actions/sendtelegrammessage.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetadataTelegramMessageSent = "telegram_message_sent"
|
||||||
|
telegramMaxMessageLength = 3000
|
||||||
|
)
|
||||||
|
|
||||||
|
type SendTelegramMessageRunner struct {
|
||||||
|
token string
|
||||||
|
chatID int64
|
||||||
|
bot *bot.Bot
|
||||||
|
customName string
|
||||||
|
customDescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSendTelegramMessageRunner(config map[string]string) *SendTelegramMessageRunner {
|
||||||
|
token := config["token"]
|
||||||
|
if token == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse chat ID from config if present
|
||||||
|
var chatID int64
|
||||||
|
if configChatID := config["chat_id"]; configChatID != "" {
|
||||||
|
var err error
|
||||||
|
chatID, err = strconv.ParseInt(configChatID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := bot.New(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SendTelegramMessageRunner{
|
||||||
|
token: token,
|
||||||
|
chatID: chatID,
|
||||||
|
bot: b,
|
||||||
|
customName: config["custom_name"],
|
||||||
|
customDescription: config["custom_description"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramMessageParams struct {
|
||||||
|
ChatID int64 `json:"chat_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SendTelegramMessageRunner) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
|
var messageParams TelegramMessageParams
|
||||||
|
err := params.Unmarshal(&messageParams)
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.chatID != 0 {
|
||||||
|
messageParams.ChatID = s.chatID
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageParams.ChatID == 0 {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("chat_id is required either in config or parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageParams.Message == "" {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("message is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the message if it's too long
|
||||||
|
messages := xstrings.SplitParagraph(messageParams.Message, telegramMaxMessageLength)
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("empty message after splitting")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send each message part
|
||||||
|
for i, msg := range messages {
|
||||||
|
_, err = s.bot.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: messageParams.ChatID,
|
||||||
|
Text: msg,
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return types.ActionResult{}, fmt.Errorf("failed to send telegram message part %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedState.ConversationTracker.AddMessage(fmt.Sprintf("telegram:%d", messageParams.ChatID), openai.ChatCompletionMessage{
|
||||||
|
Content: messageParams.Message,
|
||||||
|
Role: "assistant",
|
||||||
|
})
|
||||||
|
|
||||||
|
return types.ActionResult{
|
||||||
|
Result: fmt.Sprintf("Message sent successfully to chat ID %d in %d parts", messageParams.ChatID, len(messages)),
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
MetadataTelegramMessageSent: true,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SendTelegramMessageRunner) Definition() types.ActionDefinition {
|
||||||
|
|
||||||
|
customName := "send_telegram_message"
|
||||||
|
if s.customName != "" {
|
||||||
|
customName = s.customName
|
||||||
|
}
|
||||||
|
|
||||||
|
customDescription := "Send a message to a Telegram user or group"
|
||||||
|
if s.customDescription != "" {
|
||||||
|
customDescription = s.customDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.chatID != 0 {
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(customName),
|
||||||
|
Description: customDescription,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"message": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The message to send",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"message"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ActionDefinition{
|
||||||
|
Name: types.ActionDefinitionName(customName),
|
||||||
|
Description: customDescription,
|
||||||
|
Properties: map[string]jsonschema.Definition{
|
||||||
|
"chat_id": {
|
||||||
|
Type: jsonschema.Number,
|
||||||
|
Description: "The Telegram chat ID to send the message to (optional if configured in config)",
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
Type: jsonschema.String,
|
||||||
|
Description: "The message to send",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"message", "chat_id"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SendTelegramMessageRunner) Plannable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendTelegramMessageConfigMeta returns the metadata for Send Telegram Message action configuration fields
|
||||||
|
func SendTelegramMessageConfigMeta() []config.Field {
|
||||||
|
return []config.Field{
|
||||||
|
{
|
||||||
|
Name: "token",
|
||||||
|
Label: "Telegram Token",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: true,
|
||||||
|
HelpText: "Telegram bot token for sending messages",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chat_id",
|
||||||
|
Label: "Default Chat ID",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Default Telegram chat ID to send messages to (can be overridden in parameters)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "custom_name",
|
||||||
|
Label: "Custom Name",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Custom name for the action (optional, defaults to 'send_telegram_message')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "custom_description",
|
||||||
|
Label: "Custom Description",
|
||||||
|
Type: config.FieldTypeText,
|
||||||
|
Required: false,
|
||||||
|
HelpText: "Custom description for the action (optional, defaults to 'Send a message to a Telegram user or group')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ type ShellAction struct {
|
|||||||
customDescription string
|
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 {
|
result := struct {
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type PostTweetAction struct {
|
|||||||
noCharacterLimit bool
|
noCharacterLimit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *PostTweetAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *PostTweetAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func NewWikipedia(config map[string]string) *WikipediaAction {
|
|||||||
|
|
||||||
type WikipediaAction struct{}
|
type WikipediaAction struct{}
|
||||||
|
|
||||||
func (a *WikipediaAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
func (a *WikipediaAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||||
result := struct {
|
result := struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
}{}
|
}{}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
ConnectorGithubIssues = "github-issues"
|
ConnectorGithubIssues = "github-issues"
|
||||||
ConnectorGithubPRs = "github-prs"
|
ConnectorGithubPRs = "github-prs"
|
||||||
ConnectorTwitter = "twitter"
|
ConnectorTwitter = "twitter"
|
||||||
|
ConnectorMatrix = "matrix"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableConnectors = []string{
|
var AvailableConnectors = []string{
|
||||||
@@ -29,6 +30,7 @@ var AvailableConnectors = []string{
|
|||||||
ConnectorGithubIssues,
|
ConnectorGithubIssues,
|
||||||
ConnectorGithubPRs,
|
ConnectorGithubPRs,
|
||||||
ConnectorTwitter,
|
ConnectorTwitter,
|
||||||
|
ConnectorMatrix,
|
||||||
}
|
}
|
||||||
|
|
||||||
func Connectors(a *state.AgentConfig) []state.Connector {
|
func Connectors(a *state.AgentConfig) []state.Connector {
|
||||||
@@ -66,6 +68,8 @@ func Connectors(a *state.AgentConfig) []state.Connector {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
conns = append(conns, cc)
|
conns = append(conns, cc)
|
||||||
|
case ConnectorMatrix:
|
||||||
|
conns = append(conns, connectors.NewMatrix(config))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conns
|
return conns
|
||||||
@@ -108,5 +112,10 @@ func ConnectorsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Twitter",
|
Label: "Twitter",
|
||||||
Fields: connectors.TwitterConfigMeta(),
|
Fields: connectors.TwitterConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "matrix",
|
||||||
|
Label: "Matrix",
|
||||||
|
Fields: connectors.MatrixConfigMeta(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package connectors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/mudler/LocalAGI/core/agent"
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
@@ -14,9 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Discord struct {
|
type Discord struct {
|
||||||
token string
|
token string
|
||||||
defaultChannel string
|
defaultChannel string
|
||||||
conversationTracker *ConversationTracker[string]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDiscord creates a new Discord connector
|
// NewDiscord creates a new Discord connector
|
||||||
@@ -25,15 +24,15 @@ type Discord struct {
|
|||||||
// - defaultChannel: Discord channel to always answer even if not mentioned
|
// - defaultChannel: Discord channel to always answer even if not mentioned
|
||||||
func NewDiscord(config map[string]string) *Discord {
|
func NewDiscord(config map[string]string) *Discord {
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
token := config["token"]
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
if !strings.HasPrefix(token, "Bot ") {
|
||||||
|
token = "Bot " + token
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Discord{
|
return &Discord{
|
||||||
conversationTracker: NewConversationTracker[string](duration),
|
token: token,
|
||||||
token: config["token"],
|
defaultChannel: config["defaultChannel"],
|
||||||
defaultChannel: config["defaultChannel"],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
|
||||||
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: m.Content,
|
Content: m.Content,
|
||||||
})
|
})
|
||||||
|
|
||||||
conv := d.conversationTracker.GetConversation(m.ChannelID)
|
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("discord:%s", m.ChannelID))
|
||||||
|
|
||||||
jobResult := a.Ask(
|
jobResult := a.Ask(
|
||||||
types.WithConversationHistory(conv),
|
types.WithConversationHistory(conv),
|
||||||
@@ -167,7 +166,7 @@ func (d *Discord) handleChannelMessage(a *agent.Agent, s *discordgo.Session, m *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
d.conversationTracker.AddMessage(m.ChannelID, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("discord:%s", m.ChannelID), openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: jobResult.Response,
|
Content: jobResult.Response,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,28 +15,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IRC struct {
|
type IRC struct {
|
||||||
server string
|
server string
|
||||||
port string
|
port string
|
||||||
nickname string
|
nickname string
|
||||||
channel string
|
channel string
|
||||||
conn *irc.Connection
|
conn *irc.Connection
|
||||||
alwaysReply bool
|
alwaysReply bool
|
||||||
conversationTracker *ConversationTracker[string]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIRC(config map[string]string) *IRC {
|
func NewIRC(config map[string]string) *IRC {
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
return &IRC{
|
return &IRC{
|
||||||
server: config["server"],
|
server: config["server"],
|
||||||
port: config["port"],
|
port: config["port"],
|
||||||
nickname: config["nickname"],
|
nickname: config["nickname"],
|
||||||
channel: config["channel"],
|
channel: config["channel"],
|
||||||
alwaysReply: config["alwaysReply"] == "true",
|
alwaysReply: config["alwaysReply"] == "true",
|
||||||
conversationTracker: NewConversationTracker[string](duration),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +109,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
cleanedMessage := cleanUpMessage(message, i.nickname)
|
cleanedMessage := cleanUpMessage(message, i.nickname)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conv := i.conversationTracker.GetConversation(channel)
|
conv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("irc:%s", channel))
|
||||||
|
|
||||||
conv = append(conv,
|
conv = append(conv,
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
@@ -125,7 +119,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Update the conversation history
|
// Update the conversation history
|
||||||
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
|
||||||
Content: cleanedMessage,
|
Content: cleanedMessage,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
})
|
})
|
||||||
@@ -140,7 +134,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the conversation history
|
// Update the conversation history
|
||||||
i.conversationTracker.AddMessage(channel, openai.ChatCompletionMessage{
|
a.SharedState().ConversationTracker.AddMessage(fmt.Sprintf("irc:%s", channel), openai.ChatCompletionMessage{
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
})
|
})
|
||||||
@@ -209,7 +203,7 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
// Start the IRC client in a goroutine
|
// Start the IRC client in a goroutine
|
||||||
go i.conn.Loop()
|
go i.conn.Loop()
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-a.Context().Done():
|
case <-a.Context().Done():
|
||||||
i.conn.Quit()
|
i.conn.Quit()
|
||||||
return
|
return
|
||||||
@@ -249,11 +243,5 @@ func IRCConfigMeta() []config.Field {
|
|||||||
Label: "Always Reply",
|
Label: "Always Reply",
|
||||||
Type: config.FieldTypeCheckbox,
|
Type: config.FieldTypeCheckbox,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "lastMessageDuration",
|
|
||||||
Label: "Last Message Duration",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
DefaultValue: "5m",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
304
services/connectors/matrix.go
Normal file
304
services/connectors/matrix.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
"github.com/mudler/LocalAGI/pkg/localoperator"
|
"github.com/mudler/LocalAGI/pkg/localoperator"
|
||||||
@@ -42,27 +41,19 @@ type Slack struct {
|
|||||||
// Track active jobs for cancellation
|
// Track active jobs for cancellation
|
||||||
activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing
|
activeJobs map[string][]*types.Job // map[channelID]bool to track if a channel has active processing
|
||||||
activeJobsMutex sync.RWMutex
|
activeJobsMutex sync.RWMutex
|
||||||
|
|
||||||
conversationTracker *ConversationTracker[string]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const thinkingMessage = ":hourglass: thinking..."
|
const thinkingMessage = ":hourglass: thinking..."
|
||||||
|
|
||||||
func NewSlack(config map[string]string) *Slack {
|
func NewSlack(config map[string]string) *Slack {
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Slack{
|
return &Slack{
|
||||||
appToken: config["appToken"],
|
appToken: config["appToken"],
|
||||||
botToken: config["botToken"],
|
botToken: config["botToken"],
|
||||||
channelID: config["channelID"],
|
channelID: config["channelID"],
|
||||||
channelMode: config["channelMode"] == "true",
|
channelMode: config["channelMode"] == "true",
|
||||||
conversationTracker: NewConversationTracker[string](duration),
|
placeholders: make(map[string]string),
|
||||||
placeholders: make(map[string]string),
|
activeJobs: make(map[string][]*types.Job),
|
||||||
activeJobs: make(map[string][]*types.Job),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,16 +131,6 @@ func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) strin
|
|||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractUserIDsFromMessage(message string) []string {
|
|
||||||
var userIDs []string
|
|
||||||
for _, part := range strings.Split(message, " ") {
|
|
||||||
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
|
|
||||||
userIDs = append(userIDs, strings.TrimPrefix(strings.TrimSuffix(part, ">"), "<@"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string {
|
func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string {
|
||||||
for _, part := range strings.Split(message, " ") {
|
for _, part := range strings.Split(message, " ") {
|
||||||
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
|
if strings.HasPrefix(part, "<@") && strings.HasSuffix(part, ">") {
|
||||||
@@ -279,7 +260,7 @@ func (t *Slack) handleChannelMessage(
|
|||||||
// Cancel any active job for this channel before starting a new one
|
// Cancel any active job for this channel before starting a new one
|
||||||
t.cancelActiveJobForChannel(ev.Channel)
|
t.cancelActiveJobForChannel(ev.Channel)
|
||||||
|
|
||||||
currentConv := t.conversationTracker.GetConversation(t.channelID)
|
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID))
|
||||||
|
|
||||||
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
|
message := replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(ev.Text, b))
|
||||||
|
|
||||||
@@ -323,8 +304,8 @@ func (t *Slack) handleChannelMessage(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
t.channelID, currentConv[len(currentConv)-1],
|
fmt.Sprintf("slack:%s", t.channelID), currentConv[len(currentConv)-1],
|
||||||
)
|
)
|
||||||
|
|
||||||
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
agentOptions = append(agentOptions, types.WithConversationHistory(currentConv))
|
||||||
@@ -370,14 +351,14 @@ func (t *Slack) handleChannelMessage(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
t.channelID, openai.ChatCompletionMessage{
|
fmt.Sprintf("slack:%s", t.channelID), openai.ChatCompletionMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Debug("After adding message to conversation tracker", "conversation", t.conversationTracker.GetConversation(t.channelID))
|
xlog.Debug("After adding message to conversation tracker", "conversation", a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("slack:%s", t.channelID)))
|
||||||
|
|
||||||
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
|
//res.Response = githubmarkdownconvertergo.Slack(res.Response)
|
||||||
|
|
||||||
@@ -752,6 +733,13 @@ func (t *Slack) Start(a *agent.Agent) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
|
||||||
}
|
}
|
||||||
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("slack:%s", t.channelID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,11 +823,5 @@ func SlackConfigMeta() []config.Field {
|
|||||||
Label: "Always Reply",
|
Label: "Always Reply",
|
||||||
Type: config.FieldTypeCheckbox,
|
Type: config.FieldTypeCheckbox,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "lastMessageDuration",
|
|
||||||
Label: "Last Message Duration",
|
|
||||||
Type: config.FieldTypeText,
|
|
||||||
DefaultValue: "5m",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,377 @@
|
|||||||
package connectors
|
package connectors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-telegram/bot"
|
"github.com/go-telegram/bot"
|
||||||
"github.com/go-telegram/bot/models"
|
"github.com/go-telegram/bot/models"
|
||||||
"github.com/mudler/LocalAGI/core/agent"
|
"github.com/mudler/LocalAGI/core/agent"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
|
"github.com/mudler/LocalAGI/pkg/localoperator"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/pkg/xstrings"
|
"github.com/mudler/LocalAGI/pkg/xstrings"
|
||||||
"github.com/mudler/LocalAGI/services/actions"
|
"github.com/mudler/LocalAGI/services/actions"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const telegramThinkingMessage = "🤔 thinking..."
|
||||||
|
const telegramMaxMessageLength = 3000
|
||||||
|
|
||||||
type Telegram struct {
|
type Telegram struct {
|
||||||
Token string
|
Token string
|
||||||
bot *bot.Bot
|
bot *bot.Bot
|
||||||
agent *agent.Agent
|
agent *agent.Agent
|
||||||
|
|
||||||
currentconversation map[int64][]openai.ChatCompletionMessage
|
|
||||||
lastMessageTime map[int64]time.Time
|
|
||||||
lastMessageDuration time.Duration
|
|
||||||
|
|
||||||
admins []string
|
admins []string
|
||||||
|
|
||||||
conversationTracker *ConversationTracker[int64]
|
// To track placeholder messages
|
||||||
|
placeholders map[string]int // map[jobUUID]messageID
|
||||||
|
placeholderMutex sync.RWMutex
|
||||||
|
|
||||||
|
// Track active jobs for cancellation
|
||||||
|
activeJobs map[int64][]*types.Job // map[chatID]bool to track if a chat has active processing
|
||||||
|
activeJobsMutex sync.RWMutex
|
||||||
|
|
||||||
|
channelID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send any text message to the bot after the bot has been started
|
// Send any text message to the bot after the bot has been started
|
||||||
|
|
||||||
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
|
func (t *Telegram) AgentResultCallback() func(state types.ActionState) {
|
||||||
return func(state types.ActionState) {
|
return func(state types.ActionState) {
|
||||||
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
|
// Mark the job as completed when we get the final result
|
||||||
Description: state.Reasoning,
|
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 {
|
func (t *Telegram) AgentReasoningCallback() func(state types.ActionCurrentState) bool {
|
||||||
return func(state types.ActionCurrentState) bool {
|
return func(state types.ActionCurrentState) bool {
|
||||||
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
|
// Check if we have a placeholder message for this job
|
||||||
Description: state.Reasoning,
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cancelActiveJobForChat cancels any active job for the given chat
|
||||||
|
func (t *Telegram) cancelActiveJobForChat(chatID int64) {
|
||||||
|
t.activeJobsMutex.RLock()
|
||||||
|
ctxs, exists := t.activeJobs[chatID]
|
||||||
|
t.activeJobsMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
xlog.Info("Cancelling active job for chat", "chatID", chatID)
|
||||||
|
|
||||||
|
// Mark the job as inactive
|
||||||
|
t.activeJobsMutex.Lock()
|
||||||
|
for _, c := range ctxs {
|
||||||
|
c.Cancel()
|
||||||
|
}
|
||||||
|
delete(t.activeJobs, chatID)
|
||||||
|
t.activeJobsMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendImageToTelegram downloads and sends an image to Telegram
|
||||||
|
func sendImageToTelegram(ctx context.Context, b *bot.Bot, chatID int64, url string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error downloading image: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read the entire body into memory
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading image body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send image with caption
|
||||||
|
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||||
|
ChatID: chatID,
|
||||||
|
Photo: &models.InputFileUpload{
|
||||||
|
Filename: "image.jpg",
|
||||||
|
Data: bytes.NewReader(bodyBytes),
|
||||||
|
},
|
||||||
|
Caption: "Generated image",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error sending photo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMultimediaContent processes and sends multimedia content from the agent's response
|
||||||
|
func (t *Telegram) handleMultimediaContent(ctx context.Context, chatID int64, res *types.JobResult) ([]string, error) {
|
||||||
|
var urls []string
|
||||||
|
|
||||||
|
for _, state := range res.State {
|
||||||
|
// Collect URLs from search action
|
||||||
|
if urlList, exists := state.Metadata[actions.MetadataUrls]; exists {
|
||||||
|
urls = append(urls, xstrings.UniqueSlice(urlList.([]string))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle images from gen image actions
|
||||||
|
if imagesUrls, exists := state.Metadata[actions.MetadataImages]; exists {
|
||||||
|
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
|
||||||
|
xlog.Debug("Sending photo", "url", url)
|
||||||
|
if err := sendImageToTelegram(ctx, t.bot, chatID, url); err != nil {
|
||||||
|
xlog.Error("Error handling image", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle browser agent screenshots
|
||||||
|
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
|
||||||
|
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
|
||||||
|
state := historyStruct.States[len(historyStruct.States)-1]
|
||||||
|
if state.Screenshot != "" {
|
||||||
|
// Decode base64 screenshot
|
||||||
|
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error decoding screenshot", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send screenshot with caption
|
||||||
|
_, err = t.bot.SendPhoto(ctx, &bot.SendPhotoParams{
|
||||||
|
ChatID: chatID,
|
||||||
|
Photo: &models.InputFileUpload{
|
||||||
|
Filename: "screenshot.png",
|
||||||
|
Data: bytes.NewReader(screenshotData),
|
||||||
|
},
|
||||||
|
Caption: "Browser Agent Screenshot",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending screenshot", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatResponseWithURLs formats the response text and creates message entities for URLs
|
||||||
|
func formatResponseWithURLs(response string, urls []string) string {
|
||||||
|
finalResponse := response
|
||||||
|
if len(urls) > 0 {
|
||||||
|
finalResponse += "\n\nReferences:\n"
|
||||||
|
for i, url := range urls {
|
||||||
|
finalResponse += fmt.Sprintf("🔗 %d. %s\n", i+1, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot.EscapeMarkdown(finalResponse)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
||||||
username := update.Message.From.Username
|
username := update.Message.From.Username
|
||||||
|
|
||||||
|
xlog.Debug("Received message from user", "username", username, "chatID", update.Message.Chat.ID, "message", update.Message.Text)
|
||||||
|
|
||||||
|
internalError := func(err error, msg *models.Message) {
|
||||||
|
xlog.Error("Error updating final message", "error", err)
|
||||||
|
b.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: "there was an internal error. try again!",
|
||||||
|
})
|
||||||
|
}
|
||||||
if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
|
if len(t.admins) > 0 && !slices.Contains(t.admins, username) {
|
||||||
xlog.Info("Unauthorized user", "username", username)
|
xlog.Info("Unauthorized user", "username", username)
|
||||||
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: "you are not authorized to use this bot!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending unauthorized message", "error", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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{
|
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||||
Content: update.Message.Text,
|
Content: update.Message.Text,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
})
|
})
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
a.SharedState().ConversationTracker.AddMessage(
|
||||||
update.Message.From.ID,
|
fmt.Sprintf("telegram:%d", update.Message.From.ID),
|
||||||
openai.ChatCompletionMessage{
|
openai.ChatCompletionMessage{
|
||||||
Content: update.Message.Text,
|
Content: update.Message.Text,
|
||||||
Role: "user",
|
Role: "user",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Info("New message", "username", username, "conversation", currentConv)
|
// Send initial placeholder message
|
||||||
res := a.Ask(
|
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
types.WithConversationHistory(currentConv),
|
ChatID: update.Message.Chat.ID,
|
||||||
)
|
Text: bot.EscapeMarkdown(telegramThinkingMessage),
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
xlog.Debug("Response", "response", res.Response)
|
})
|
||||||
|
if err != nil {
|
||||||
if res.Response == "" {
|
xlog.Error("Error sending initial message", "error", err)
|
||||||
xlog.Error("Empty response from agent")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.conversationTracker.AddMessage(
|
// Store the UUID->placeholder message mapping
|
||||||
update.Message.From.ID,
|
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{
|
openai.ChatCompletionMessage{
|
||||||
Content: res.Response,
|
Content: res.Response,
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
xlog.Debug("Sending message back to telegram", "response", res.Response)
|
// Handle any multimedia content in the response and collect URLs
|
||||||
|
urls, err := t.handleMultimediaContent(ctx, update.Message.Chat.ID, res)
|
||||||
for _, res := range res.State {
|
if err != nil {
|
||||||
// coming from the search action
|
xlog.Error("Error handling multimedia content", "error", err)
|
||||||
// if urls, exists := res.Metadata[actions.MetadataUrls]; exists {
|
|
||||||
// for _, url := range uniqueStringSlice(urls.([]string)) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// coming from the gen image actions
|
|
||||||
if imagesUrls, exists := res.Metadata[actions.MetadataImages]; exists {
|
|
||||||
for _, url := range xstrings.UniqueSlice(imagesUrls.([]string)) {
|
|
||||||
xlog.Debug("Sending photo", "url", url)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Error downloading image", "error", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
_, err = b.SendPhoto(ctx, &bot.SendPhotoParams{
|
|
||||||
ChatID: update.Message.Chat.ID,
|
|
||||||
Photo: &models.InputFileUpload{
|
|
||||||
Filename: "image.jpg",
|
|
||||||
Data: resp.Body,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("Error sending photo", "error", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
|
||||||
// ParseMode: models.ParseModeMarkdown,
|
// Update the message with the final response
|
||||||
ChatID: update.Message.Chat.ID,
|
formattedResponse := formatResponseWithURLs(res.Response, urls)
|
||||||
Text: res.Response,
|
|
||||||
|
// 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,
|
||||||
|
MessageID: msg.ID,
|
||||||
|
Text: messages[0],
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error sending message", "error", err)
|
internalError(fmt.Errorf("internal error: %w", err), msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send additional chunks as new messages
|
||||||
|
for i := 1; i < len(messages); i++ {
|
||||||
|
_, err = b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: messages[i],
|
||||||
|
ParseMode: models.ParseModeMarkdown,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
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...)
|
b, err := bot.New(t.Token, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
xlog.Error("Error creating bot", "error", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.bot = b
|
t.bot = b
|
||||||
t.agent = a
|
t.agent = a
|
||||||
|
|
||||||
// go func() {
|
// go func() {
|
||||||
// for m := range a.ConversationChannel() {
|
// forc m := range a.ConversationChannel() {
|
||||||
// t.handleNewMessage(ctx, b, m)
|
// t.handleNewMessage(ctx, b, m)
|
||||||
// }
|
// }
|
||||||
// }()
|
// }()
|
||||||
|
|
||||||
|
if t.channelID != "" {
|
||||||
|
// handle new conversations
|
||||||
|
a.AddSubscriber(func(ccm openai.ChatCompletionMessage) {
|
||||||
|
xlog.Debug("Subscriber(telegram)", "message", ccm.Content)
|
||||||
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: t.channelID,
|
||||||
|
Text: ccm.Content,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Error sending message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.agent.SharedState().ConversationTracker.AddMessage(
|
||||||
|
fmt.Sprintf("telegram:%s", t.channelID),
|
||||||
|
openai.ChatCompletionMessage{
|
||||||
|
Content: ccm.Content,
|
||||||
|
Role: "assistant",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
b.Start(ctx)
|
b.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,11 +442,6 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
|
|||||||
return nil, errors.New("token is required")
|
return nil, errors.New("token is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := time.ParseDuration(config["lastMessageDuration"])
|
|
||||||
if err != nil {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
admins := []string{}
|
admins := []string{}
|
||||||
|
|
||||||
if _, ok := config["admins"]; ok {
|
if _, ok := config["admins"]; ok {
|
||||||
@@ -196,12 +449,11 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Telegram{
|
return &Telegram{
|
||||||
Token: token,
|
Token: token,
|
||||||
lastMessageDuration: duration,
|
admins: admins,
|
||||||
admins: admins,
|
placeholders: make(map[string]int),
|
||||||
currentconversation: map[int64][]openai.ChatCompletionMessage{},
|
activeJobs: make(map[int64][]*types.Job),
|
||||||
lastMessageTime: map[int64]time.Time{},
|
channelID: config["channel_id"],
|
||||||
conversationTracker: NewConversationTracker[int64](duration),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +473,10 @@ func TelegramConfigMeta() []config.Field {
|
|||||||
HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot",
|
HelpText: "Comma-separated list of Telegram usernames that are allowed to interact with the bot",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "lastMessageDuration",
|
Name: "channel_id",
|
||||||
Label: "Last Message Duration",
|
Label: "Channel ID",
|
||||||
Type: config.FieldTypeText,
|
Type: config.FieldTypeText,
|
||||||
DefaultValue: "5m",
|
HelpText: "Telegram channel ID to send messages to if the agent needs to initiate a conversation",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
services/filters.go
Normal file
44
services/filters.go
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
120
services/filters/classifier.go
Normal file
120
services/filters/classifier.go
Normal 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
86
services/filters/regex.go
Normal 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 ""
|
||||||
|
}
|
||||||
45
webui/app.go
45
webui/app.go
@@ -11,12 +11,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/mudler/LocalAGI/core/conversations"
|
||||||
coreTypes "github.com/mudler/LocalAGI/core/types"
|
coreTypes "github.com/mudler/LocalAGI/core/types"
|
||||||
|
internalTypes "github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||||
"github.com/mudler/LocalAGI/services"
|
"github.com/mudler/LocalAGI/services"
|
||||||
"github.com/mudler/LocalAGI/services/connectors"
|
|
||||||
"github.com/mudler/LocalAGI/webui/types"
|
"github.com/mudler/LocalAGI/webui/types"
|
||||||
|
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"github.com/sashabaranov/go-openai/jsonschema"
|
"github.com/sashabaranov/go-openai/jsonschema"
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ type (
|
|||||||
htmx *htmx.HTMX
|
htmx *htmx.HTMX
|
||||||
config *Config
|
config *Config
|
||||||
*fiber.App
|
*fiber.App
|
||||||
|
sharedState *internalTypes.AgentSharedState
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,9 +50,10 @@ func NewApp(opts ...Option) *App {
|
|||||||
})
|
})
|
||||||
|
|
||||||
a := &App{
|
a := &App{
|
||||||
htmx: htmx.New(),
|
htmx: htmx.New(),
|
||||||
config: config,
|
config: config,
|
||||||
App: webapp,
|
App: webapp,
|
||||||
|
sharedState: internalTypes.NewAgentSharedState(5 * time.Minute),
|
||||||
}
|
}
|
||||||
|
|
||||||
a.registerRoutes(config.Pool, webapp)
|
a.registerRoutes(config.Pool, webapp)
|
||||||
@@ -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 {
|
return func(c *fiber.Ctx) error {
|
||||||
payload := struct {
|
payload := struct {
|
||||||
Config map[string]string `json:"config"`
|
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)
|
ctx, cancel := context.WithTimeout(c.Context(), 200*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
res, err := a.Run(ctx, payload.Params)
|
res, err := a.Run(ctx, app.sharedState, payload.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error running action", "error", err)
|
xlog.Error("Error running action", "error", err)
|
||||||
return errorJSONMessage(c, err.Error())
|
return errorJSONMessage(c, err.Error())
|
||||||
@@ -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 {
|
return func(c *fiber.Ctx) error {
|
||||||
var request types.RequestBody
|
var request types.RequestBody
|
||||||
if err := c.BodyParser(&request); err != nil {
|
if err := c.BodyParser(&request); err != nil {
|
||||||
@@ -552,7 +580,7 @@ func (a *App) GenerateGroupProfiles(pool *state.AgentPool) func(c *fiber.Ctx) er
|
|||||||
|
|
||||||
xlog.Debug("Generating group", "description", request.Descript)
|
xlog.Debug("Generating group", "description", request.Descript)
|
||||||
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
|
client := llm.NewClient(a.config.LLMAPIKey, a.config.LLMAPIURL, "10m")
|
||||||
err := llm.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,
|
Type: jsonschema.Object,
|
||||||
Properties: map[string]jsonschema.Definition{
|
Properties: map[string]jsonschema.Definition{
|
||||||
"agents": {
|
"agents": {
|
||||||
@@ -620,6 +648,7 @@ func (a *App) GetAgentConfigMeta() func(c *fiber.Ctx) error {
|
|||||||
services.ActionsConfigMeta(),
|
services.ActionsConfigMeta(),
|
||||||
services.ConnectorsConfigMeta(),
|
services.ConnectorsConfigMeta(),
|
||||||
services.DynamicPromptsConfigMeta(),
|
services.DynamicPromptsConfigMeta(),
|
||||||
|
services.FiltersConfigMeta(),
|
||||||
)
|
)
|
||||||
return c.JSON(configMeta)
|
return c.JSON(configMeta)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.25.1",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.3",
|
||||||
"@vitejs/plugin-react": "^4.4.0",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.25.1",
|
||||||
"eslint-plugin-react-hooks": "^6.0.0",
|
"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",
|
"globals": "^16.0.0",
|
||||||
"react-router-dom": "^7.5.1",
|
"react-router-dom": "^7.5.3",
|
||||||
"vite": "^6.3.2",
|
"vite": "^6.3.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -133,11 +133,11 @@
|
|||||||
|
|
||||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
|
||||||
|
|
||||||
"@eslint/core": ["@eslint/core@0.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/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=="],
|
"@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": ["@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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
|
|
||||||
|
|
||||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,15 @@
|
|||||||
"highlight.js": "^11.11.1"
|
"highlight.js": "^11.11.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.25.1",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.3",
|
||||||
"@vitejs/plugin-react": "^4.4.0",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.25.1",
|
||||||
"eslint-plugin-react-hooks": "^6.0.0",
|
"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",
|
"globals": "^16.0.0",
|
||||||
"react-router-dom": "^7.5.1",
|
"react-router-dom": "^7.5.3",
|
||||||
"vite": "^6.3.2"
|
"vite": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ModelSettingsSection from './agent-form-sections/ModelSettingsSection';
|
|||||||
import PromptsGoalsSection from './agent-form-sections/PromptsGoalsSection';
|
import PromptsGoalsSection from './agent-form-sections/PromptsGoalsSection';
|
||||||
import AdvancedSettingsSection from './agent-form-sections/AdvancedSettingsSection';
|
import AdvancedSettingsSection from './agent-form-sections/AdvancedSettingsSection';
|
||||||
import ExportSection from './agent-form-sections/ExportSection';
|
import ExportSection from './agent-form-sections/ExportSection';
|
||||||
|
import FiltersSection from './agent-form-sections/FiltersSection';
|
||||||
|
|
||||||
const AgentForm = ({
|
const AgentForm = ({
|
||||||
isEdit = false,
|
isEdit = false,
|
||||||
@@ -189,6 +190,13 @@ const AgentForm = ({
|
|||||||
<i className="fas fa-plug"></i>
|
<i className="fas fa-plug"></i>
|
||||||
Connectors
|
Connectors
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
className={`wizard-nav-item ${activeSection === 'filters-section' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleSectionChange('filters-section')}
|
||||||
|
>
|
||||||
|
<i className="fas fa-shield"></i>
|
||||||
|
Filters & Triggers
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
className={`wizard-nav-item ${activeSection === 'actions-section' ? 'active' : ''}`}
|
className={`wizard-nav-item ${activeSection === 'actions-section' ? 'active' : ''}`}
|
||||||
onClick={() => handleSectionChange('actions-section')}
|
onClick={() => handleSectionChange('actions-section')}
|
||||||
@@ -255,6 +263,10 @@ const AgentForm = ({
|
|||||||
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorChange={handleConnectorChange} metadata={metadata} />
|
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorChange={handleConnectorChange} metadata={metadata} />
|
||||||
</div>
|
</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' }}>
|
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
|
||||||
<ActionsSection formData={formData} setFormData={setFormData} metadata={metadata} />
|
<ActionsSection formData={formData} setFormData={setFormData} metadata={metadata} />
|
||||||
</div>
|
</div>
|
||||||
@@ -306,6 +318,10 @@ const AgentForm = ({
|
|||||||
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorChange={handleConnectorChange} metadata={metadata} />
|
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorChange={handleConnectorChange} metadata={metadata} />
|
||||||
</div>
|
</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' }}>
|
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
|
||||||
<ActionsSection formData={formData} setFormData={setFormData} metadata={metadata} />
|
<ActionsSection formData={formData} setFormData={setFormData} metadata={metadata} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
103
webui/react-ui/src/components/CollapsibleRawSections.jsx
Normal file
103
webui/react-ui/src/components/CollapsibleRawSections.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import FormFieldDefinition from './common/FormFieldDefinition';
|
|||||||
* @param {String} props.itemType - Type of items being configured ('action', 'connector', etc.)
|
* @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.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.addButtonText - Text for the add button
|
||||||
|
* @param {String} props.saveAllFieldsAsString - Whether to save all fields as string or the appropriate JSON type
|
||||||
*/
|
*/
|
||||||
const ConfigForm = ({
|
const ConfigForm = ({
|
||||||
items = [],
|
items = [],
|
||||||
@@ -22,7 +23,8 @@ const ConfigForm = ({
|
|||||||
onAdd,
|
onAdd,
|
||||||
itemType = 'item',
|
itemType = 'item',
|
||||||
typeField = 'type',
|
typeField = 'type',
|
||||||
addButtonText = 'Add Item'
|
addButtonText = 'Add Item',
|
||||||
|
saveAllFieldsAsString = true,
|
||||||
}) => {
|
}) => {
|
||||||
// Generate options from fieldGroups
|
// Generate options from fieldGroups
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
@@ -62,8 +64,10 @@ const ConfigForm = ({
|
|||||||
const item = items[index];
|
const item = items[index];
|
||||||
const config = parseConfig(item);
|
const config = parseConfig(item);
|
||||||
|
|
||||||
if (type === 'checkbox')
|
if (type === 'number' && !saveAllFieldsAsString)
|
||||||
config[key] = checked ? 'true' : 'false';
|
config[key] = Number(value);
|
||||||
|
else if (type === 'checkbox')
|
||||||
|
config[key] = saveAllFieldsAsString ? (checked ? 'true' : 'false') : checked;
|
||||||
else
|
else
|
||||||
config[key] = value;
|
config[key] = value;
|
||||||
|
|
||||||
|
|||||||
27
webui/react-ui/src/components/FilterForm.jsx
Normal file
27
webui/react-ui/src/components/FilterForm.jsx
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useOutletContext, useNavigate } from 'react-router-dom';
|
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() {
|
function ActionsPlayground() {
|
||||||
const { showToast } = useOutletContext();
|
const { showToast } = useOutletContext();
|
||||||
@@ -12,6 +17,10 @@ function ActionsPlayground() {
|
|||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingActions, setLoadingActions] = useState(true);
|
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
|
// Update document title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,21 +45,106 @@ function ActionsPlayground() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchActions();
|
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
|
// Handle action selection
|
||||||
const handleActionChange = (e) => {
|
const handleActionChange = (e) => {
|
||||||
setSelectedAction(e.target.value);
|
setSelectedAction(e.target.value);
|
||||||
|
setConfigJson('{}');
|
||||||
|
setParamsJson('{}');
|
||||||
setResult(null);
|
setResult(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle JSON input changes
|
// Helper to generate onChange handlers for form fields
|
||||||
const handleConfigChange = (e) => {
|
const makeFieldChangeHandler = (fields, updateFn) => (e) => {
|
||||||
setConfigJson(e.target.value);
|
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) => {
|
// Handle form field changes
|
||||||
setParamsJson(e.target.value);
|
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
|
// Execute the selected action
|
||||||
@@ -135,34 +229,31 @@ function ActionsPlayground() {
|
|||||||
|
|
||||||
{selectedAction && (
|
{selectedAction && (
|
||||||
<div className="section-box">
|
<div className="section-box">
|
||||||
<h2>Action Configuration</h2>
|
|
||||||
|
|
||||||
<form onSubmit={handleExecuteAction}>
|
<form onSubmit={handleExecuteAction}>
|
||||||
<div className="form-group mb-6">
|
{configFields.length > 0 && (
|
||||||
<label htmlFor="config-json">Configuration (JSON):</label>
|
<>
|
||||||
<textarea
|
<h2>Configuration</h2>
|
||||||
id="config-json"
|
<FormFieldDefinition
|
||||||
value={configJson}
|
fields={configFields}
|
||||||
onChange={handleConfigChange}
|
values={JSON.parse(configJson)}
|
||||||
className="form-control"
|
onChange={makeFieldChangeHandler(configFields, handleConfigChange)}
|
||||||
rows="5"
|
idPrefix="config_"
|
||||||
placeholder='{"key": "value"}'
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="form-group mb-6">
|
{paramFields.length > 0 && (
|
||||||
<label htmlFor="params-json">Parameters (JSON):</label>
|
<>
|
||||||
<textarea
|
<h2>Parameters</h2>
|
||||||
id="params-json"
|
<FormFieldDefinition
|
||||||
value={paramsJson}
|
fields={paramFields}
|
||||||
onChange={handleParamsChange}
|
values={JSON.parse(paramsJson)}
|
||||||
className="form-control"
|
onChange={makeFieldChangeHandler(paramFields, handleParamsChange)}
|
||||||
rows="5"
|
idPrefix="param_"
|
||||||
placeholder='{"key": "value"}'
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
@@ -194,9 +285,9 @@ function ActionsPlayground() {
|
|||||||
backgroundColor: 'rgba(30, 30, 30, 0.7)'
|
backgroundColor: 'rgba(30, 30, 30, 0.7)'
|
||||||
}}>
|
}}>
|
||||||
{typeof result === 'object' ? (
|
{typeof result === 'object' ? (
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<pre className="hljs"><code>
|
||||||
{JSON.stringify(result, null, 2)}
|
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(result, null, 2), { language: 'json' }).value }}></div>
|
||||||
</pre>
|
</code></pre>
|
||||||
) : (
|
) : (
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{result}
|
{result}
|
||||||
|
|||||||
@@ -1,4 +1,200 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 { useParams, Link } from 'react-router-dom';
|
||||||
import hljs from 'highlight.js/lib/core';
|
import hljs from 'highlight.js/lib/core';
|
||||||
import json from 'highlight.js/lib/languages/json';
|
import json from 'highlight.js/lib/languages/json';
|
||||||
@@ -7,7 +203,7 @@ import 'highlight.js/styles/monokai.css';
|
|||||||
hljs.registerLanguage('json', json);
|
hljs.registerLanguage('json', json);
|
||||||
|
|
||||||
function AgentStatus() {
|
function AgentStatus() {
|
||||||
const [showStatus, setShowStatus] = useState(true);
|
const [showStatus, setShowStatus] = useState(false);
|
||||||
const { name } = useParams();
|
const { name } = useParams();
|
||||||
const [statusData, setStatusData] = useState(null);
|
const [statusData, setStatusData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -94,17 +290,16 @@ function AgentStatus() {
|
|||||||
setObservableMap(prevMap => {
|
setObservableMap(prevMap => {
|
||||||
const prev = prevMap[data.id] || {};
|
const prev = prevMap[data.id] || {};
|
||||||
const updated = {
|
const updated = {
|
||||||
...prev,
|
|
||||||
...data,
|
...data,
|
||||||
creation: data.creation,
|
...prev,
|
||||||
progress: data.progress,
|
|
||||||
completion: data.completion,
|
|
||||||
};
|
};
|
||||||
// Events can be received out of order
|
// Events can be received out of order
|
||||||
if (data.creation)
|
if (data.creation)
|
||||||
updated.creation = data.creation;
|
updated.creation = data.creation;
|
||||||
if (data.completion)
|
if (data.completion)
|
||||||
updated.completion = 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])
|
if (data.parent_id && !prevMap[data.parent_id])
|
||||||
prevMap[data.parent_id] = {
|
prevMap[data.parent_id] = {
|
||||||
id: data.parent_id,
|
id: data.parent_id,
|
||||||
@@ -252,12 +447,17 @@ function AgentStatus() {
|
|||||||
setExpandedCards(new Map(expandedCards).set(container.id, newExpanded));
|
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>
|
<i className={`fas fa-${container.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
|
||||||
<span>
|
<span style={{ width: '100%' }}>
|
||||||
<span className='stat-label'>{container.name}</span>#<span className='stat-label'>{container.id}</span>
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||||
</span>
|
<span>
|
||||||
</div>
|
<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' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<i
|
<i
|
||||||
className={`fas fa-chevron-${expandedCards.get(container.id) ? 'up' : 'down'}`}
|
className={`fas fa-chevron-${expandedCards.get(container.id) ? 'up' : 'down'}`}
|
||||||
@@ -279,18 +479,23 @@ function AgentStatus() {
|
|||||||
const isExpanded = expandedCards.get(childKey);
|
const isExpanded = expandedCards.get(childKey);
|
||||||
return (
|
return (
|
||||||
<div key={`${container.id}-child-${child.id}`} className='card' style={{ background: '#222', marginBottom: '0.5em' }}>
|
<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={() => {
|
onClick={() => {
|
||||||
const newExpanded = !expandedCards.get(childKey);
|
const newExpanded = !expandedCards.get(childKey);
|
||||||
setExpandedCards(new Map(expandedCards).set(childKey, newExpanded));
|
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>
|
<i className={`fas fa-${child.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
|
||||||
<span>
|
<span style={{ width: '100%' }}>
|
||||||
<span className='stat-label'>{child.name}</span>#<span className='stat-label'>{child.id}</span>
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||||
</span>
|
<span>
|
||||||
</div>
|
<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' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<i
|
<i
|
||||||
className={`fas fa-chevron-${isExpanded ? 'up' : 'down'}`}
|
className={`fas fa-chevron-${isExpanded ? 'up' : 'down'}`}
|
||||||
@@ -303,60 +508,14 @@ function AgentStatus() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: isExpanded ? 'block' : 'none' }}>
|
<div style={{ display: isExpanded ? 'block' : 'none' }}>
|
||||||
{child.creation && (
|
<CollapsibleRawSections container={child} />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{container.creation && (
|
<CollapsibleRawSections container={container} />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
webui/react-ui/src/utils/api.js
vendored
58
webui/react-ui/src/utils/api.js
vendored
@@ -24,6 +24,50 @@ const buildUrl = (endpoint) => {
|
|||||||
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : 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
|
// Agent-related API calls
|
||||||
export const agentApi = {
|
export const agentApi = {
|
||||||
// Get list of all agents
|
// Get list of all agents
|
||||||
@@ -77,6 +121,7 @@ export const agentApi = {
|
|||||||
groupedMetadata.actions = metadata.Actions;
|
groupedMetadata.actions = metadata.Actions;
|
||||||
}
|
}
|
||||||
groupedMetadata.dynamicPrompts = metadata.DynamicPrompts;
|
groupedMetadata.dynamicPrompts = metadata.DynamicPrompts;
|
||||||
|
groupedMetadata.filters = metadata.Filters;
|
||||||
|
|
||||||
return groupedMetadata;
|
return groupedMetadata;
|
||||||
}
|
}
|
||||||
@@ -215,7 +260,18 @@ export const actionApi = {
|
|||||||
});
|
});
|
||||||
return handleResponse(response);
|
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
|
// Execute an action for an agent
|
||||||
executeAction: async (name, actionData) => {
|
executeAction: async (name, actionData) => {
|
||||||
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {
|
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user