mirror of
https://github.com/mudler/LocalAGI.git
synced 2026-08-04 07:56:16 -04:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d456315f19 | |||
| 983f5599d5 | |||
| 471c937d57 | |||
| ad61551db5 | |||
| 2bad9c399f | |||
| 580ad7d46f | |||
| 47f013f3e1 | |||
| d5219a9379 | |||
| 00460b1538 | |||
| 98e62edf12 | |||
| 592969f976 | |||
| 6be849e409 | |||
| d42c2a5749 | |||
| 8d2eafbc35 | |||
| 848f688ae9 | |||
| b813842f85 | |||
| 684ce43eb8 | |||
| 39d13d155f | |||
| 2d5523a129 | |||
| 7fe3ed2169 | |||
| 6db3b5f31f | |||
| f6ac820a3f | |||
| d955284278 | |||
| 3fd6134bcd | |||
| 4d875e9e2a | |||
| 79b72ec311 | |||
| 8487459d90 | |||
| be336adf02 | |||
| be1acb06fd | |||
| 26ec479596 | |||
| efee3c462d | |||
| 4cf52ec698 | |||
| ba8ea0117e | |||
| 36770b70ae | |||
| 5784be2853 | |||
| 5b93e5c21c | |||
| b0a43b0cb7 | |||
| 51bb368073 | |||
| 153fcf6cf0 | |||
| b52571dbea | |||
| c0a4f1026f | |||
| aab73ead5c | |||
| f05af02864 | |||
| 46adfb5258 | |||
| d6784f11a7 | |||
| 4bd86bd842 | |||
| e630ead9c0 | |||
| 5bd97908b5 | |||
| f98c306b48 | |||
| cf60dd88c4 | |||
| dc7badd034 | |||
| 20511283b5 | |||
| 5c233c8aa5 | |||
| 9500ec7af0 | |||
| 1fb7f8bc75 | |||
| 62723267a9 | |||
| e4da4b307c | |||
| 578de58e44 | |||
| aff460c7c2 | |||
| ff6890c9c1 | |||
| 9160ca598e | |||
| 7a8b3d93cf | |||
| 86659e704a | |||
| 6e999927ec | |||
| 863c6f3dcb | |||
| 2a6378764b | |||
| da7cb91f83 | |||
| 13e0f12c1b | |||
| a0a9299cda | |||
| 15237d3137 | |||
| e97cd43cdb | |||
| 1313f805ae | |||
| aafbd9159a | |||
| cff90bc709 | |||
| e2de768e09 | |||
| 80871db0de | |||
| 50cad776aa | |||
| 56b6f7240c |
@@ -11,7 +11,7 @@ cleanup-tests:
|
||||
docker compose down
|
||||
|
||||
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-4b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
|
||||
|
||||
run-nokb:
|
||||
$(MAKE) run KBDISABLEINDEX=true
|
||||
|
||||
@@ -31,7 +31,7 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
|
||||
- 🎛 **No-Code Agents**: Easy-to-configure multiple agents via Web UI.
|
||||
- 🖥 **Web-Based Interface**: Simple and intuitive agent management.
|
||||
- 🤖 **Advanced Agent Teaming**: Instantly create cooperative agent teams from a single prompt.
|
||||
- 📡 **Connectors Galore**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
|
||||
- 📡 **Connectors**: Built-in integrations with Discord, Slack, Telegram, GitHub Issues, and IRC.
|
||||
- 🛠 **Comprehensive REST API**: Seamless integration into your workflows. Every agent created will support OpenAI Responses API out of the box.
|
||||
- 📚 **Short & Long-Term Memory**: Powered by [LocalRecall](https://github.com/mudler/LocalRecall).
|
||||
- 🧠 **Planning & Reasoning**: Agents intelligently plan, reason, and adapt.
|
||||
@@ -58,12 +58,15 @@ docker compose -f docker-compose.nvidia.yaml up
|
||||
# Intel GPU setup (for Intel Arc and integrated GPUs)
|
||||
docker compose -f docker-compose.intel.yaml up
|
||||
|
||||
# AMD GPU setup
|
||||
docker compose -f docker-compose.amd.yaml up
|
||||
|
||||
# Start with a specific model (see available models in models.localai.io, or localai.io to use any model in huggingface)
|
||||
MODEL_NAME=gemma-3-12b-it docker compose up
|
||||
|
||||
# NVIDIA GPU setup with custom multimodal and image models
|
||||
MODEL_NAME=gemma-3-12b-it \
|
||||
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
||||
MULTIMODAL_MODEL=moondream2-20250414 \
|
||||
IMAGE_MODEL=flux.1-dev-ggml \
|
||||
docker compose -f docker-compose.nvidia.yaml up
|
||||
```
|
||||
@@ -126,8 +129,8 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
|
||||
- Supports text, multimodal, and image generation models
|
||||
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
|
||||
- Default models:
|
||||
- Text: `gemma-3-12b-it-qat`
|
||||
- Multimodal: `minicpm-v-2_6`
|
||||
- Text: `gemma-3-4b-it-qat`
|
||||
- Multimodal: `moondream2-20250414`
|
||||
- Image: `sd-1.5-ggml`
|
||||
- Environment variables:
|
||||
- `MODEL_NAME`: Text model to use
|
||||
@@ -142,8 +145,8 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
|
||||
- Supports text, multimodal, and image generation models
|
||||
- Run with: `docker compose -f docker-compose.intel.yaml up`
|
||||
- Default models:
|
||||
- Text: `gemma-3-12b-it-qat`
|
||||
- Multimodal: `minicpm-v-2_6`
|
||||
- Text: `gemma-3-4b-it-qat`
|
||||
- Multimodal: `moondream2-20250414`
|
||||
- Image: `sd-1.5-ggml`
|
||||
- Environment variables:
|
||||
- `MODEL_NAME`: Text model to use
|
||||
@@ -161,20 +164,23 @@ MODEL_NAME=gemma-3-12b-it docker compose up
|
||||
|
||||
# NVIDIA GPU with custom models
|
||||
MODEL_NAME=gemma-3-12b-it \
|
||||
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
||||
MULTIMODAL_MODEL=moondream2-20250414 \
|
||||
IMAGE_MODEL=flux.1-dev-ggml \
|
||||
docker compose -f docker-compose.nvidia.yaml up
|
||||
|
||||
# Intel GPU with custom models
|
||||
MODEL_NAME=gemma-3-12b-it \
|
||||
MULTIMODAL_MODEL=minicpm-v-2_6 \
|
||||
MULTIMODAL_MODEL=moondream2-20250414 \
|
||||
IMAGE_MODEL=sd-1.5-ggml \
|
||||
docker compose -f docker-compose.intel.yaml up
|
||||
|
||||
# With custom actions directory
|
||||
LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions docker compose up
|
||||
```
|
||||
|
||||
If no models are specified, it will use the defaults:
|
||||
- Text model: `gemma-3-12b-it-qat`
|
||||
- Multimodal model: `minicpm-v-2_6`
|
||||
- Text model: `gemma-3-4b-it-qat`
|
||||
- Multimodal model: `moondream2-20250414`
|
||||
- Image model: `sd-1.5-ggml`
|
||||
|
||||
Good (relatively small) models that have been tested are:
|
||||
@@ -234,6 +240,7 @@ LocalAGI supports environment configurations. Note that these environment variab
|
||||
| `LOCALAGI_LOCALRAG_URL` | LocalRecall connection |
|
||||
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
|
||||
|
||||
## Installation Options
|
||||
|
||||
@@ -414,6 +421,279 @@ For more details about available configuration options and features, refer to th
|
||||
|
||||
</details>
|
||||
|
||||
## 🔧 Extending LocalAGI
|
||||
|
||||
LocalAGI provides two powerful ways to extend its functionality with custom actions:
|
||||
|
||||
### 1. Custom Actions (Go Code)
|
||||
|
||||
LocalAGI supports custom actions written in Go that can be defined inline when creating an agent. These actions are interpreted at runtime, so no compilation is required.
|
||||
|
||||
#### Automatic Custom Actions Loading
|
||||
|
||||
You can also place custom Go action files in a directory and have LocalAGI automatically load them. Set the `LOCALAGI_CUSTOM_ACTIONS_DIR` environment variable to point to a directory containing your custom action files. Each `.go` file in this directory will be automatically loaded and made available to all agents.
|
||||
|
||||
**Example setup:**
|
||||
```bash
|
||||
# Set the environment variable
|
||||
export LOCALAGI_CUSTOM_ACTIONS_DIR="/path/to/custom/actions"
|
||||
|
||||
# Or in docker-compose.yaml
|
||||
environment:
|
||||
- LOCALAGI_CUSTOM_ACTIONS_DIR=/app/custom-actions
|
||||
```
|
||||
|
||||
**Directory structure:**
|
||||
```
|
||||
custom-actions/
|
||||
├── weather_action.go
|
||||
├── file_processor.go
|
||||
└── database_query.go
|
||||
```
|
||||
|
||||
Each file should contain the three required functions (`Run`, `Definition`, `RequiredFields`) as described below.
|
||||
|
||||
#### How Custom Actions Work
|
||||
|
||||
When creating a new Agent, in the action sections select the "custom" action, you can add the Golang code directly there.
|
||||
|
||||
Custom actions in LocalAGI require three main functions:
|
||||
|
||||
1. **`Run(config map[string]interface{}) (string, map[string]interface{}, error)`** - The main execution function
|
||||
2. **`Definition() map[string][]string`** - Defines the action's parameters and their types
|
||||
3. **`RequiredFields() []string`** - Specifies which parameters are required
|
||||
|
||||
Note: You can't use additional modules, but just use libraries that are included in Go.
|
||||
|
||||
#### Example: Weather Information Action
|
||||
|
||||
Here's a practical example of a custom action that fetches weather information:
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"io"
|
||||
)
|
||||
|
||||
type WeatherParams struct {
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
type WeatherResponse struct {
|
||||
Main struct {
|
||||
Temp float64 `json:"temp"`
|
||||
Humidity int `json:"humidity"`
|
||||
} `json:"main"`
|
||||
Weather []struct {
|
||||
Description string `json:"description"`
|
||||
} `json:"weather"`
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
// Parse parameters
|
||||
p := WeatherParams{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
// Make API call to weather service
|
||||
url := fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s,%s&appid=YOUR_API_KEY&units=metric", p.City, p.Country)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
var weather WeatherResponse
|
||||
if err := json.Unmarshal(body, &weather); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
// Format response
|
||||
result := fmt.Sprintf("Weather in %s, %s: %.1f°C, %s, Humidity: %d%%",
|
||||
p.City, p.Country, weather.Main.Temp, weather.Weather[0].Description, weather.Main.Humidity)
|
||||
|
||||
return result, map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"city": []string{
|
||||
"string",
|
||||
"The city name to get weather for",
|
||||
},
|
||||
"country": []string{
|
||||
"string",
|
||||
"The country code (e.g., US, UK, DE)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"city", "country"}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example: File System Action
|
||||
|
||||
Here's another example that demonstrates file system operations:
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FileParams struct {
|
||||
Path string `json:"path"`
|
||||
Action string `json:"action"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
p := FileParams{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
switch p.Action {
|
||||
case "read":
|
||||
content, err := os.ReadFile(p.Path)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
return string(content), map[string]interface{}{}, nil
|
||||
|
||||
case "write":
|
||||
err := os.WriteFile(p.Path, []byte(p.Content), 0644)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
return fmt.Sprintf("Successfully wrote to %s", p.Path), map[string]interface{}{}, nil
|
||||
|
||||
case "list":
|
||||
files, err := os.ReadDir(p.Path)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
var fileList []string
|
||||
for _, file := range files {
|
||||
fileList = append(fileList, file.Name())
|
||||
}
|
||||
|
||||
result, _ := json.Marshal(fileList)
|
||||
return string(result), map[string]interface{}{}, nil
|
||||
|
||||
default:
|
||||
return "", map[string]interface{}{}, fmt.Errorf("unknown action: %s", p.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"path": []string{
|
||||
"string",
|
||||
"The file or directory path",
|
||||
},
|
||||
"action": []string{
|
||||
"string",
|
||||
"The action to perform: read, write, or list",
|
||||
},
|
||||
"content": []string{
|
||||
"string",
|
||||
"Content to write (required for write action)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"path", "action"}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Custom Actions in Agents
|
||||
|
||||
To use custom actions, add them to your agent configuration:
|
||||
|
||||
1. **Via Web UI**: In the agent creation form, add a "Custom" action and paste your Go code
|
||||
2. **Via API**: Include the custom action in your agent configuration JSON
|
||||
3. **Via Library**: Add the custom action to your agent's actions list
|
||||
|
||||
### 2. MCP (Model Context Protocol) Servers
|
||||
|
||||
LocalAGI supports both local and remote MCP servers, allowing you to extend functionality with external tools and services.
|
||||
|
||||
#### What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) is a standard for connecting AI applications to external data sources and tools. LocalAGI can connect to any MCP-compliant server to access additional capabilities.
|
||||
|
||||
#### Local MCP Servers
|
||||
|
||||
Local MCP servers run as processes that LocalAGI can spawn and communicate with via STDIO.
|
||||
|
||||
##### Example: GitHub MCP Server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Remote MCP Servers
|
||||
|
||||
Remote MCP servers are HTTP-based and can be accessed over the network.
|
||||
|
||||
#### Creating Your Own MCP Server
|
||||
|
||||
You can create MCP servers in any language that supports the MCP protocol and add the URLs of the servers to LocalAGI.
|
||||
|
||||
#### Configuring MCP Servers in LocalAGI
|
||||
|
||||
1. **Via Web UI**: In the MCP Settings section of agent creation, add MCP servers
|
||||
2. **Via API**: Include MCP server configuration in your agent config
|
||||
|
||||
#### Best Practices
|
||||
|
||||
- **Security**: Always validate inputs and use proper authentication for remote MCP servers
|
||||
- **Error Handling**: Implement robust error handling in your MCP servers
|
||||
- **Documentation**: Provide clear descriptions for all tools exposed by your MCP server
|
||||
- **Testing**: Test your MCP servers independently before integrating with LocalAGI
|
||||
- **Resource Management**: Ensure your MCP servers properly clean up resources
|
||||
|
||||
### Development
|
||||
|
||||
The development workflow is similar to the source build, but with additional steps for hot reloading of the frontend:
|
||||
@@ -733,6 +1013,7 @@ LocalAGI supports environment configurations. Note that these environment variab
|
||||
| `LOCALAGI_MCPBOX_URL` | LocalAGI MCPBox URL, e.g. http://mcpbox:8080 |
|
||||
| `LOCALAGI_ENABLE_CONVERSATIONS_LOGGING` | Toggle conversation logs |
|
||||
| `LOCALAGI_API_KEYS` | A comma separated list of api keys used for authentication |
|
||||
| `LOCALAGI_CUSTOM_ACTIONS_DIR` | Directory containing custom Go action files to be automatically loaded |
|
||||
</details>
|
||||
|
||||
## LICENSE
|
||||
|
||||
+42
-5
@@ -3,6 +3,8 @@ package action
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
@@ -24,7 +26,7 @@ func NewCustom(config map[string]string, goPkgPath string) (*CustomAction, error
|
||||
}
|
||||
|
||||
if err := a.callInit(); err != nil {
|
||||
xlog.Error("Error calling custom action init", "error", err)
|
||||
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
|
||||
}
|
||||
|
||||
return a, nil
|
||||
@@ -43,18 +45,23 @@ func (a *CustomAction) callInit() error {
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
|
||||
if err != nil {
|
||||
return err
|
||||
xlog.Warn("No init function found for custom action", "error", err, "action", a.config["name"])
|
||||
return nil
|
||||
}
|
||||
|
||||
run := v.Interface().(func() error)
|
||||
run, ok := v.Interface().(func(string) error)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return run()
|
||||
return run(a.config["configuration"])
|
||||
}
|
||||
|
||||
func (a *CustomAction) initializeInterpreter() error {
|
||||
if _, exists := a.config["code"]; exists && a.i == nil {
|
||||
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
|
||||
i := interp.New(interp.Options{
|
||||
Env: os.Environ(),
|
||||
GoPath: a.goPkgPath,
|
||||
Unrestricted: unsafe,
|
||||
})
|
||||
@@ -66,6 +73,15 @@ func (a *CustomAction) initializeInterpreter() error {
|
||||
a.config["name"] = "custom"
|
||||
}
|
||||
|
||||
// let's find first if there is already a package declarated in the code
|
||||
// the user might want to specify it to not break syntax with IDEs
|
||||
re := regexp.MustCompile("package (\\w+)")
|
||||
packageName := re.FindStringSubmatch(a.config["code"])
|
||||
if len(packageName) > 1 {
|
||||
// remove it from the code, normalize to `name`
|
||||
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
|
||||
}
|
||||
|
||||
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -106,6 +122,21 @@ func (a *CustomAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
description := ""
|
||||
desc, err := a.i.Eval(fmt.Sprintf("%s.Description", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Warn("No description found for custom action", "error", err, "action", a.config["name"])
|
||||
} else {
|
||||
d, ok := desc.Interface().(func() string)
|
||||
if ok {
|
||||
description = d()
|
||||
}
|
||||
}
|
||||
|
||||
if a.config["description"] != "" {
|
||||
description = a.config["description"]
|
||||
}
|
||||
|
||||
properties := v.Interface().(func() map[string][]string)
|
||||
|
||||
v, err = a.i.Eval(fmt.Sprintf("%s.RequiredFields", a.config["name"]))
|
||||
@@ -130,7 +161,7 @@ func (a *CustomAction) Definition() types.ActionDefinition {
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(a.config["name"]),
|
||||
Description: a.config["description"],
|
||||
Description: description,
|
||||
Properties: prop,
|
||||
Required: requiredFields(),
|
||||
}
|
||||
@@ -164,5 +195,11 @@ func CustomConfigMeta() []config.Field {
|
||||
Type: config.FieldTypeCheckbox,
|
||||
HelpText: "Allow unsafe code execution",
|
||||
},
|
||||
{
|
||||
Name: "configuration",
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration for the custom action",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+110
-35
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
@@ -12,12 +13,24 @@ import (
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const parameterReasoningPrompt = `You are tasked with generating the optimal parameters for the action "%s". The action requires the following parameters:
|
||||
%s
|
||||
|
||||
Your task is to:
|
||||
1. Generate the best possible values for each required parameter
|
||||
2. If the parameter requires code, provide complete, working code
|
||||
3. If the parameter requires text or documentation, provide comprehensive, well-structured content
|
||||
4. Ensure all parameters are complete and ready to be used
|
||||
|
||||
Focus on quality and completeness. Do not explain your reasoning or analyze the action's purpose - just provide the best possible parameter values.`
|
||||
|
||||
type decisionResult struct {
|
||||
actionParams types.ActionParams
|
||||
message string
|
||||
actioName string
|
||||
actionName string
|
||||
}
|
||||
|
||||
// decision forces the agent to take one of the available actions
|
||||
@@ -131,7 +144,7 @@ func (a *Agent) decision(
|
||||
a.observer.Update(*obs)
|
||||
}
|
||||
|
||||
return &decisionResult{actionParams: params, actioName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
||||
return &decisionResult{actionParams: params, actionName: msg.ToolCalls[0].Function.Name, message: msg.Content}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to make a decision after %d attempts: %w", maxRetries, lastErr)
|
||||
@@ -204,6 +217,7 @@ func (m Messages) Save(path string) error {
|
||||
}
|
||||
|
||||
func (m Messages) GetLatestUserMessage() *openai.ChatCompletionMessage {
|
||||
xlog.Debug("Getting latest user message", "messages", m)
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
msg := m[i]
|
||||
if msg.Role == UserRole {
|
||||
@@ -223,7 +237,9 @@ func (m Messages) IsLastMessageFromRole(role string) bool {
|
||||
}
|
||||
|
||||
func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) {
|
||||
|
||||
if act == nil {
|
||||
return nil, fmt.Errorf("action is nil")
|
||||
}
|
||||
if len(act.Definition().Properties) > 0 {
|
||||
xlog.Debug("Action has properties", "action", act.Definition().Name, "properties", act.Definition().Properties)
|
||||
} else {
|
||||
@@ -248,9 +264,32 @@ func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act type
|
||||
|
||||
cc := conversation
|
||||
if a.options.forceReasoning {
|
||||
// First, get the LLM to reason about optimal parameter usage
|
||||
parameterReasoningPrompt := fmt.Sprintf(parameterReasoningPrompt,
|
||||
act.Definition().Name,
|
||||
formatProperties(act.Definition().Properties))
|
||||
|
||||
// Get initial reasoning about parameters using askLLM
|
||||
paramReasoningMsg, err := a.askLLM(job.GetContext(),
|
||||
append(conversation, openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: parameterReasoningPrompt,
|
||||
}),
|
||||
maxAttempts,
|
||||
)
|
||||
if err != nil {
|
||||
xlog.Warn("Failed to get parameter reasoning", "error", err)
|
||||
}
|
||||
|
||||
// Combine original reasoning with parameter-specific reasoning
|
||||
enhancedReasoning := reasoning
|
||||
if paramReasoningMsg.Content != "" {
|
||||
enhancedReasoning = fmt.Sprintf("%s\n\nParameter Analysis:\n%s", reasoning, paramReasoningMsg.Content)
|
||||
}
|
||||
|
||||
cc = append(conversation, openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, reasoning),
|
||||
Content: fmt.Sprintf("The agent decided to use the tool %s with the following reasoning: %s", act.Definition().Name, enhancedReasoning),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -273,6 +312,15 @@ func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act type
|
||||
return nil, fmt.Errorf("failed to generate parameters after %d attempts: %w", maxAttempts, attemptErr)
|
||||
}
|
||||
|
||||
// Helper function to format properties for the prompt
|
||||
func formatProperties(props map[string]jsonschema.Definition) string {
|
||||
var result strings.Builder
|
||||
for name, prop := range props {
|
||||
result.WriteString(fmt.Sprintf("- %s: %s\n", name, prop.Description))
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction types.Action, actionParams types.ActionParams, reasoning string, pickTemplate string, conv Messages) (Messages, error) {
|
||||
// Planning: run all the actions in sequence
|
||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||
@@ -318,6 +366,11 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
|
||||
)
|
||||
|
||||
subTaskAction := a.availableActions().Find(subtask.Action)
|
||||
if subTaskAction == nil {
|
||||
xlog.Error("Action not found: %s", subtask.Action)
|
||||
return conv, fmt.Errorf("action %s not found", subtask.Action)
|
||||
}
|
||||
|
||||
subTaskReasoning := fmt.Sprintf("%s Overall goal is: %s", subtask.Reasoning, planResult.Goal)
|
||||
|
||||
params, err := a.generateParameters(job, pickTemplate, subTaskAction, conv, subTaskReasoning, maxRetries)
|
||||
@@ -368,12 +421,28 @@ func (a *Agent) handlePlanning(ctx context.Context, job *types.Job, chosenAction
|
||||
job.Result.SetResult(stateResult)
|
||||
job.CallbackWithResult(stateResult)
|
||||
xlog.Debug("[subtask] Action executed", "agent", a.Character.Name, "action", subTaskAction.Definition().Name, "result", result)
|
||||
conv = a.addFunctionResultToConversation(subTaskAction, actionParams, result, conv)
|
||||
conv = a.addFunctionResultToConversation(job.GetContext(), subTaskAction, actionParams, result, conv)
|
||||
}
|
||||
|
||||
return conv, nil
|
||||
}
|
||||
|
||||
// getAvailableActionsForJob returns available actions including user-defined ones for a specific job
|
||||
func (a *Agent) getAvailableActionsForJob(job *types.Job) types.Actions {
|
||||
// Start with regular available actions
|
||||
baseActions := a.availableActions()
|
||||
|
||||
// Add user-defined actions from the job
|
||||
userTools := job.GetUserTools()
|
||||
if len(userTools) > 0 {
|
||||
userDefinedActions := types.CreateUserDefinedActions(userTools)
|
||||
baseActions = append(baseActions, userDefinedActions...)
|
||||
xlog.Debug("Added user-defined actions", "definitions", userTools)
|
||||
}
|
||||
|
||||
return baseActions
|
||||
}
|
||||
|
||||
func (a *Agent) availableActions() types.Actions {
|
||||
// defaultActions := append(a.options.userActions, action.NewReply())
|
||||
|
||||
@@ -440,27 +509,30 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
||||
|
||||
xlog.Debug("[pickAction] picking action starts", "messages", messages)
|
||||
|
||||
// Get available actions including user-defined ones
|
||||
availableActions := a.getAvailableActionsForJob(job)
|
||||
|
||||
// Identify the goal of this conversation
|
||||
|
||||
if !a.options.forceReasoning {
|
||||
xlog.Debug("not forcing reasoning")
|
||||
if !a.options.forceReasoning || job.ToolChoice != "" {
|
||||
xlog.Debug("not forcing reasoning", "forceReasoning", a.options.forceReasoning, "ToolChoice", job.ToolChoice)
|
||||
// We also could avoid to use functions here and get just a reply from the LLM
|
||||
// and then use the reply to get the action
|
||||
thought, err := a.decision(job,
|
||||
messages,
|
||||
a.availableActions().ToTools(),
|
||||
"",
|
||||
availableActions.ToTools(),
|
||||
job.ToolChoice,
|
||||
maxRetries)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
|
||||
xlog.Debug(fmt.Sprintf("thought action Name: %v", thought.actioName))
|
||||
xlog.Debug(fmt.Sprintf("thought message: %v", thought.message))
|
||||
xlog.Debug("thought action Name", "actionName", thought.actionName)
|
||||
xlog.Debug("thought message", "message", thought.message)
|
||||
|
||||
// Find the action
|
||||
chosenAction := a.availableActions().Find(thought.actioName)
|
||||
if chosenAction == nil || thought.actioName == "" {
|
||||
chosenAction := availableActions.Find(thought.actionName)
|
||||
if chosenAction == nil || thought.actionName == "" {
|
||||
xlog.Debug("no answer")
|
||||
|
||||
// LLM replied with an answer?
|
||||
@@ -471,6 +543,7 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
||||
return chosenAction, thought.actionParams, thought.message, nil
|
||||
}
|
||||
|
||||
// Force the LLM to think and we extract a "reasoning" to pick a specific action and with which parameters
|
||||
xlog.Debug("[pickAction] forcing reasoning")
|
||||
|
||||
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
|
||||
@@ -488,33 +561,35 @@ func (a *Agent) pickAction(job *types.Job, templ string, messages []openai.ChatC
|
||||
}, c...)
|
||||
}
|
||||
|
||||
reasoningAction := action.NewReasoning()
|
||||
thought, err := a.decision(job,
|
||||
c,
|
||||
types.Actions{reasoningAction}.ToTools(),
|
||||
reasoningAction.Definition().Name.String(), maxRetries)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
// Create a detailed prompt for reasoning that includes available actions and their properties
|
||||
reasoningPrompt := "Analyze the current situation and determine the best course of action. Consider the following:\n\n"
|
||||
reasoningPrompt += "Available Actions:\n"
|
||||
for _, act := range a.availableActions() {
|
||||
reasoningPrompt += fmt.Sprintf("- %s: %s\n", act.Definition().Name, act.Definition().Description)
|
||||
if len(act.Definition().Properties) > 0 {
|
||||
reasoningPrompt += " Properties:\n"
|
||||
for name, prop := range act.Definition().Properties {
|
||||
reasoningPrompt += fmt.Sprintf(" - %s: %s\n", name, prop.Description)
|
||||
}
|
||||
}
|
||||
reasoningPrompt += "\n"
|
||||
}
|
||||
if thought.actioName != "" && thought.actioName != reasoningAction.Definition().Name.String() {
|
||||
return nil, nil, "", fmt.Errorf("Expected reasoning action not: %s", thought.actioName)
|
||||
reasoningPrompt += "\nProvide a detailed reasoning about what action would be most appropriate in this situation and why. You can also just reply with a simple message by choosing the 'reply' or 'answer' action."
|
||||
|
||||
// Get reasoning using askLLM
|
||||
reasoningMsg, err := a.askLLM(job.GetContext(),
|
||||
append(c, openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: reasoningPrompt,
|
||||
}),
|
||||
maxRetries)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("failed to get reasoning: %w", err)
|
||||
}
|
||||
|
||||
originalReasoning := ""
|
||||
response := &action.ReasoningResponse{}
|
||||
if thought.actionParams != nil {
|
||||
if err := thought.actionParams.Unmarshal(response); err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
originalReasoning = response.Reasoning
|
||||
}
|
||||
if thought.message != "" {
|
||||
originalReasoning = thought.message
|
||||
}
|
||||
originalReasoning := reasoningMsg.Content
|
||||
|
||||
xlog.Debug("[pickAction] picking action", "messages", c)
|
||||
// thought, err := a.askLLM(ctx,
|
||||
// c,
|
||||
|
||||
actionsID := []string{"reply"}
|
||||
for _, m := range a.availableActions() {
|
||||
|
||||
+246
-47
@@ -371,10 +371,7 @@ func (a *Agent) runAction(job *types.Job, chosenAction types.Action, params type
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *Agent) processPrompts(conversation Messages) Messages {
|
||||
//if job.Image != "" {
|
||||
// TODO: Use llava to explain the image content
|
||||
//}
|
||||
func (a *Agent) processPrompts(ctx context.Context, conversation Messages) Messages {
|
||||
// Add custom prompts
|
||||
for _, prompt := range a.options.prompts {
|
||||
message, err := prompt.Render(a)
|
||||
@@ -382,15 +379,49 @@ func (a *Agent) processPrompts(conversation Messages) Messages {
|
||||
xlog.Error("Error rendering prompt", "error", err)
|
||||
continue
|
||||
}
|
||||
if message == "" {
|
||||
if message.Content == "" && message.ImageBase64 == "" {
|
||||
xlog.Debug("Prompt is empty, skipping", "agent", a.Character.Name)
|
||||
continue
|
||||
}
|
||||
if !conversation.Exist(a.options.systemPrompt) {
|
||||
|
||||
if message.ImageBase64 != "" {
|
||||
// iF model support both images and text, process it as a single multicontent message and return
|
||||
if !a.options.SeparatedMultimodalModel() {
|
||||
conversation = append([]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: prompt.Role(),
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
Text: message.Content,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: message.ImageBase64,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}, conversation...)
|
||||
|
||||
} else {
|
||||
// We need to describe the image first, and we will process the text separately (we do not return here)
|
||||
imageDescription, err := a.describeImage(ctx, a.options.LLMAPI.MultimodalModel, message.ImageBase64)
|
||||
if err != nil {
|
||||
xlog.Error("Error describing image", "error", err)
|
||||
} else {
|
||||
conversation = append([]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: prompt.Role(),
|
||||
Content: fmt.Sprintf("%s\n\nImage description: %s", message.Content, imageDescription),
|
||||
}}, conversation...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conversation = append([]openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: prompt.Role(),
|
||||
Content: message,
|
||||
Content: message.Content,
|
||||
}}, conversation...)
|
||||
}
|
||||
}
|
||||
@@ -410,7 +441,7 @@ func (a *Agent) processPrompts(conversation Messages) Messages {
|
||||
}
|
||||
|
||||
func (a *Agent) describeImage(ctx context.Context, model, imageURL string) (string, error) {
|
||||
xlog.Debug("Describing image", "model", model, "image", imageURL)
|
||||
xlog.Debug("Describing image", "model", model)
|
||||
resp, err := a.client.CreateChatCompletion(ctx,
|
||||
openai.ChatCompletionRequest{
|
||||
Model: model,
|
||||
@@ -444,12 +475,13 @@ func (a *Agent) describeImage(ctx context.Context, model, imageURL string) (stri
|
||||
return resp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func extractImageContent(message openai.ChatCompletionMessage) (imageURL, text string, e error) {
|
||||
// extractAllImageContent extracts all images from a message
|
||||
func extractAllImageContent(message openai.ChatCompletionMessage) (images []string, text string, e error) {
|
||||
e = fmt.Errorf("no image found")
|
||||
if message.MultiContent != nil {
|
||||
for _, content := range message.MultiContent {
|
||||
if content.Type == openai.ChatMessagePartTypeImageURL {
|
||||
imageURL = content.ImageURL.URL
|
||||
images = append(images, content.ImageURL.URL)
|
||||
e = nil
|
||||
}
|
||||
if content.Type == openai.ChatMessagePartTypeText {
|
||||
@@ -461,42 +493,77 @@ func extractImageContent(message openai.ChatCompletionMessage) (imageURL, text s
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Agent) processUserInputs(job *types.Job, role string, conv Messages) Messages {
|
||||
func (a *Agent) processUserInputs(conv Messages) Messages {
|
||||
|
||||
// walk conversation history, and check if last message from user contains image.
|
||||
// If it does, we need to describe the image first with a model that supports image understanding (if the current model doesn't support it)
|
||||
// and add it to the conversation context
|
||||
// walk conversation history, and check if any message contains images.
|
||||
// If they do, we need to describe the images first with a model that supports image understanding (if the current model doesn't support it)
|
||||
// and add them to the conversation context
|
||||
if !a.options.SeparatedMultimodalModel() {
|
||||
return conv
|
||||
}
|
||||
lastUserMessage := conv.GetLatestUserMessage()
|
||||
if lastUserMessage != nil && conv.IsLastMessageFromRole(UserRole) {
|
||||
imageURL, text, err := extractImageContent(*lastUserMessage)
|
||||
if err == nil {
|
||||
// We have an image, we need to describe it first
|
||||
// and add it to the conversation context
|
||||
imageDescription, err := a.describeImage(a.context.Context, a.options.LLMAPI.MultimodalModel, imageURL)
|
||||
if err != nil {
|
||||
xlog.Error("Error describing image", "error", err)
|
||||
} else {
|
||||
// We replace the user message with the image description
|
||||
// and add the user text to the conversation
|
||||
explainerMessage := openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("The user shared an image which can be described as: %s", imageDescription),
|
||||
}
|
||||
|
||||
// remove lastUserMessage from the conversation
|
||||
conv = conv.RemoveLastUserMessage()
|
||||
conv = append(conv, explainerMessage)
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: role,
|
||||
Content: text,
|
||||
})
|
||||
xlog.Debug("Processing user inputs", "agent", a.Character.Name, "conversation", conv)
|
||||
|
||||
// Process all messages in the conversation to extract and describe images
|
||||
var processedMessages Messages
|
||||
var messagesToRemove []int
|
||||
|
||||
for i, message := range conv {
|
||||
images, text, err := extractAllImageContent(message)
|
||||
if err == nil && len(images) > 0 {
|
||||
xlog.Debug("Found images in message", "messageIndex", i, "imageCount", len(images), "role", message.Role)
|
||||
|
||||
// Mark this message for removal
|
||||
messagesToRemove = append(messagesToRemove, i)
|
||||
|
||||
// Process each image in the message
|
||||
var imageDescriptions []string
|
||||
for j, image := range images {
|
||||
imageDescription, err := a.describeImage(a.context.Context, a.options.LLMAPI.MultimodalModel, image)
|
||||
if err != nil {
|
||||
xlog.Error("Error describing image", "error", err, "messageIndex", i, "imageIndex", j)
|
||||
imageDescriptions = append(imageDescriptions, fmt.Sprintf("Image %d: [Error describing image: %v]", j+1, err))
|
||||
} else {
|
||||
imageDescriptions = append(imageDescriptions, fmt.Sprintf("Image %d: %s", j+1, imageDescription))
|
||||
}
|
||||
}
|
||||
|
||||
// Add the text content as a new message with the same role first
|
||||
if text != "" {
|
||||
textMessage := openai.ChatCompletionMessage{
|
||||
Role: message.Role,
|
||||
Content: text,
|
||||
}
|
||||
processedMessages = append(processedMessages, textMessage)
|
||||
|
||||
// Add the image descriptions as a system message after the text
|
||||
explainerMessage := openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("The above message also contains %d image(s) which can be described as: %s",
|
||||
len(images), strings.Join(imageDescriptions, "; ")),
|
||||
}
|
||||
processedMessages = append(processedMessages, explainerMessage)
|
||||
} else {
|
||||
// If there's no text, just add the image descriptions as a system message
|
||||
explainerMessage := openai.ChatCompletionMessage{
|
||||
Role: "system",
|
||||
Content: fmt.Sprintf("Message contains %d image(s) which can be described as: %s",
|
||||
len(images), strings.Join(imageDescriptions, "; ")),
|
||||
}
|
||||
processedMessages = append(processedMessages, explainerMessage)
|
||||
}
|
||||
} else {
|
||||
// No image found, keep the original message
|
||||
processedMessages = append(processedMessages, message)
|
||||
}
|
||||
}
|
||||
|
||||
// If we found and processed any images, replace the conversation
|
||||
if len(messagesToRemove) > 0 {
|
||||
xlog.Info("Processed images in conversation", "messagesWithImages", len(messagesToRemove), "agent", a.Character.Name)
|
||||
return processedMessages
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
@@ -567,6 +634,80 @@ func (a *Agent) filterJob(job *types.Job) (ok bool, err error) {
|
||||
return failedBy == "" && (!hasTriggers || triggeredBy != ""), nil
|
||||
}
|
||||
|
||||
// validateBuiltinTools checks that builtin tools specified by the user can be matched to available actions
|
||||
func (a *Agent) validateBuiltinTools(job *types.Job) {
|
||||
builtinTools := job.GetBuiltinTools()
|
||||
if len(builtinTools) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get available actions
|
||||
availableActions := a.mcpActions
|
||||
|
||||
for _, tool := range builtinTools {
|
||||
functionName := tool.Name
|
||||
|
||||
// Check if this is a web search builtin tool
|
||||
if strings.HasPrefix(string(functionName), "web_search_") {
|
||||
// Look for a search action
|
||||
searchAction := availableActions.Find("search")
|
||||
if searchAction == nil {
|
||||
xlog.Warn("Web search builtin tool specified but no 'search' action available",
|
||||
"function_name", functionName,
|
||||
"agent", a.Character.Name)
|
||||
} else {
|
||||
xlog.Debug("Web search builtin tool matched to search action",
|
||||
"function_name", functionName,
|
||||
"agent", a.Character.Name)
|
||||
}
|
||||
} else {
|
||||
// For future builtin tools, add more matching logic here
|
||||
xlog.Warn("Unknown builtin tool specified",
|
||||
"function_name", functionName,
|
||||
"agent", a.Character.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replyWithToolCall handles user-defined actions by recording the action state without setting Response
|
||||
func (a *Agent) replyWithToolCall(job *types.Job, conv []openai.ChatCompletionMessage, params types.ActionParams, chosenAction types.Action, reasoning string) {
|
||||
// Record the action state so the webui can detect this is a user-defined action
|
||||
stateResult := types.ActionState{
|
||||
ActionCurrentState: types.ActionCurrentState{
|
||||
Job: job,
|
||||
Action: chosenAction,
|
||||
Params: params,
|
||||
Reasoning: reasoning,
|
||||
},
|
||||
ActionResult: types.ActionResult{
|
||||
Result: reasoning, // The reasoning/message to show to user
|
||||
},
|
||||
}
|
||||
|
||||
// Add the action state to the job result
|
||||
job.Result.SetResult(stateResult)
|
||||
|
||||
// Used by the observer
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
ToolCalls: []openai.ToolCall{
|
||||
{
|
||||
Type: openai.ToolTypeFunction,
|
||||
Function: openai.FunctionCall{
|
||||
Name: chosenAction.Definition().ToFunctionDefinition().Name,
|
||||
Arguments: params.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Set conversation but leave Response empty
|
||||
// The webui will detect the user-defined action and generate the proper tool call response
|
||||
job.Result.Conversation = conv
|
||||
// job.Result.Response remains empty - this signals to webui that it should check State
|
||||
job.Result.Finish(nil)
|
||||
}
|
||||
|
||||
func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||
if err := job.GetContext().Err(); err != nil {
|
||||
job.Result.Finish(fmt.Errorf("expired"))
|
||||
@@ -606,7 +747,7 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||
}()
|
||||
}
|
||||
|
||||
conv = a.processPrompts(conv)
|
||||
conv = a.processPrompts(job.GetContext(), conv)
|
||||
if ok, err := a.filterJob(job); !ok || err != nil {
|
||||
if err != nil {
|
||||
job.Result.Finish(fmt.Errorf("Error in job filter: %w", err))
|
||||
@@ -615,11 +756,14 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||
}
|
||||
return
|
||||
}
|
||||
conv = a.processUserInputs(job, role, conv)
|
||||
conv = a.processUserInputs(conv)
|
||||
|
||||
// RAG
|
||||
conv = a.knowledgeBaseLookup(job, conv)
|
||||
|
||||
// Validate builtin tools against available actions
|
||||
a.validateBuiltinTools(job)
|
||||
|
||||
var pickTemplate string
|
||||
var reEvaluationTemplate string
|
||||
|
||||
@@ -838,6 +982,13 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||
}
|
||||
|
||||
if !chosenAction.Definition().Name.Is(action.PlanActionName) {
|
||||
// Check if this is a user-defined action
|
||||
if types.IsActionUserDefined(chosenAction) {
|
||||
xlog.Debug("User-defined action chosen, returning tool call", "action", chosenAction.Definition().Name)
|
||||
a.replyWithToolCall(job, conv, actionParams, chosenAction, reasoning)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := a.runAction(job, chosenAction, actionParams)
|
||||
if err != nil {
|
||||
result.Result = fmt.Sprintf("Error running tool: %v", err)
|
||||
@@ -856,7 +1007,7 @@ func (a *Agent) consumeJob(job *types.Job, role string, retries int) {
|
||||
job.CallbackWithResult(stateResult)
|
||||
xlog.Debug("Action executed", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "result", result)
|
||||
|
||||
conv = a.addFunctionResultToConversation(chosenAction, actionParams, result, conv)
|
||||
conv = a.addFunctionResultToConversation(job.GetContext(), chosenAction, actionParams, result, conv)
|
||||
}
|
||||
|
||||
// given the result, we can now re-evaluate the conversation
|
||||
@@ -994,7 +1145,7 @@ func (a *Agent) reply(job *types.Job, role string, conv Messages, actionParams t
|
||||
job.Result.Finish(nil)
|
||||
}
|
||||
|
||||
func (a *Agent) addFunctionResultToConversation(chosenAction types.Action, actionParams types.ActionParams, result types.ActionResult, conv Messages) Messages {
|
||||
func (a *Agent) addFunctionResultToConversation(ctx context.Context, chosenAction types.Action, actionParams types.ActionParams, result types.ActionResult, conv Messages) Messages {
|
||||
// calling the function
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
@@ -1010,12 +1161,60 @@ func (a *Agent) addFunctionResultToConversation(chosenAction types.Action, actio
|
||||
})
|
||||
|
||||
// result of calling the function
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: result.Result,
|
||||
Name: chosenAction.Definition().Name.String(),
|
||||
ToolCallID: chosenAction.Definition().Name.String(),
|
||||
})
|
||||
|
||||
// If it contains an image, we need to put it in the conversation (if supported by the model)
|
||||
if result.ImageBase64Result != "" {
|
||||
// iF model support both images and text, process it as a single multicontent message and return
|
||||
if !a.options.SeparatedMultimodalModel() {
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
Text: result.Result,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: result.ImageBase64Result,
|
||||
},
|
||||
},
|
||||
},
|
||||
Name: chosenAction.Definition().Name.String(),
|
||||
ToolCallID: chosenAction.Definition().Name.String(),
|
||||
})
|
||||
|
||||
return conv
|
||||
} else {
|
||||
// We need to describe the image first, and we will process the text separately (we do not return here)
|
||||
imageDescription, err := a.describeImage(ctx, a.options.LLMAPI.MultimodalModel, result.ImageBase64Result)
|
||||
if err != nil {
|
||||
xlog.Error("Error describing image", "error", err)
|
||||
} else {
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: fmt.Sprintf("Tool generated an image, the description of the image is: %s", imageDescription),
|
||||
Name: chosenAction.Definition().Name.String(),
|
||||
ToolCallID: chosenAction.Definition().Name.String(),
|
||||
})
|
||||
if result.Result != "" {
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: result.Result,
|
||||
Name: chosenAction.Definition().Name.String(),
|
||||
ToolCallID: chosenAction.Definition().Name.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleTool,
|
||||
Content: result.Result,
|
||||
Name: chosenAction.Definition().Name.String(),
|
||||
ToolCallID: chosenAction.Definition().Name.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package agent
|
||||
|
||||
import "github.com/mudler/LocalAGI/core/types"
|
||||
|
||||
type DynamicPrompt interface {
|
||||
Render(a *Agent) (string, error)
|
||||
Render(a *Agent) (types.PromptResult, error)
|
||||
Role() string
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
|
||||
Actions: definitions,
|
||||
HUD: hud,
|
||||
Reasoning: reasoning,
|
||||
Time: time.Now().Format(time.RFC3339),
|
||||
Time: time.Now().UTC().Format(time.RFC1123),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -80,7 +80,8 @@ Current State:
|
||||
- Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
|
||||
- Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
|
||||
- Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
|
||||
Current Time: {{.Time}}`
|
||||
|
||||
Current Time and Date: {{.Time}}`
|
||||
|
||||
const pickSelfTemplate = `
|
||||
You are an autonomous AI agent with a defined character and state (as shown above).
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ type AgentPool struct {
|
||||
imageModel, localRAGAPI, localRAGKey, apiKey string
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action
|
||||
connectors func(*AgentConfig) []Connector
|
||||
dynamicPrompt func(*AgentConfig) []DynamicPrompt
|
||||
dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt
|
||||
filters func(*AgentConfig) types.JobFilters
|
||||
timeout string
|
||||
conversationLogs string
|
||||
@@ -78,7 +78,7 @@ func NewAgentPool(
|
||||
LocalRAGAPI string,
|
||||
availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action,
|
||||
connectors func(*AgentConfig) []Connector,
|
||||
promptBlocks func(*AgentConfig) []DynamicPrompt,
|
||||
promptBlocks func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt,
|
||||
filters func(*AgentConfig) types.JobFilters,
|
||||
timeout string,
|
||||
withLogs bool,
|
||||
@@ -375,7 +375,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig, obs O
|
||||
}
|
||||
|
||||
connectors := a.connectors(config)
|
||||
promptBlocks := a.dynamicPrompt(config)
|
||||
promptBlocks := a.dynamicPrompt(config)(ctx, a)
|
||||
actions := a.availableActions(config)(ctx, a)
|
||||
filters := a.filters(config)
|
||||
stateFile, characterFile := a.stateFiles(name)
|
||||
|
||||
+59
-3
@@ -3,6 +3,7 @@ package types
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
@@ -29,9 +30,10 @@ func NewActionContext(ctx context.Context, cancel context.CancelFunc) *ActionCon
|
||||
type ActionParams map[string]interface{}
|
||||
|
||||
type ActionResult struct {
|
||||
Job *Job
|
||||
Result string
|
||||
Metadata map[string]interface{}
|
||||
Job *Job
|
||||
Result string
|
||||
ImageBase64Result string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (ap ActionParams) Read(s string) error {
|
||||
@@ -93,6 +95,60 @@ type Action interface {
|
||||
Plannable() bool
|
||||
}
|
||||
|
||||
// UserDefinedChecker interface to identify user-defined actions
|
||||
type UserDefinedChecker interface {
|
||||
IsUserDefined() bool
|
||||
}
|
||||
|
||||
// BaseAction provides default implementation for Action interface
|
||||
// Embed this in action implementations to get the default IsUserDefined behavior
|
||||
type BaseAction struct{}
|
||||
|
||||
func (b *BaseAction) IsUserDefined() bool {
|
||||
return false // Regular actions are not user-defined
|
||||
}
|
||||
|
||||
// IsActionUserDefined checks if an action is user-defined
|
||||
func IsActionUserDefined(action Action) bool {
|
||||
if checker, ok := action.(UserDefinedChecker); ok {
|
||||
return checker.IsUserDefined()
|
||||
}
|
||||
return false // Actions without UserDefinedChecker are not user-defined
|
||||
}
|
||||
|
||||
// UserDefinedAction represents a user-defined function tool
|
||||
type UserDefinedAction struct {
|
||||
ActionDef *ActionDefinition
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Run(ctx context.Context, sharedState *AgentSharedState, action ActionParams) (ActionResult, error) {
|
||||
// User-defined actions should not be executed directly
|
||||
return ActionResult{}, fmt.Errorf("user-defined action '%s' cannot be executed by agent", u.ActionDef.Name)
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Definition() ActionDefinition {
|
||||
return *u.ActionDef
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) Plannable() bool {
|
||||
return true // User-defined actions are plannable
|
||||
}
|
||||
|
||||
func (u *UserDefinedAction) IsUserDefined() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateUserDefinedActions converts user tools to UserDefinedAction instances
|
||||
func CreateUserDefinedActions(userTools []ActionDefinition) []Action {
|
||||
var actions []Action
|
||||
for _, tool := range userTools {
|
||||
actions = append(actions, &UserDefinedAction{
|
||||
ActionDef: &tool,
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
type Actions []Action
|
||||
|
||||
func (a Actions) ToTools() []openai.Tool {
|
||||
|
||||
@@ -20,6 +20,11 @@ type Job struct {
|
||||
UUID string
|
||||
Metadata map[string]interface{}
|
||||
DoneFilter bool
|
||||
|
||||
// Tools available for this job
|
||||
BuiltinTools []ActionDefinition // Built-in tools like web search
|
||||
UserTools []ActionDefinition // User-defined function tools
|
||||
ToolChoice string
|
||||
|
||||
pastActions []*ActionRequest
|
||||
nextAction *Action
|
||||
@@ -45,6 +50,24 @@ func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithBuiltinTools(tools []ActionDefinition) JobOption {
|
||||
return func(j *Job) {
|
||||
j.BuiltinTools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserTools(tools []ActionDefinition) JobOption {
|
||||
return func(j *Job) {
|
||||
j.UserTools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func WithToolChoice(choice string) JobOption {
|
||||
return func(j *Job) {
|
||||
j.ToolChoice = choice
|
||||
}
|
||||
}
|
||||
|
||||
func WithReasoningCallback(f func(ActionCurrentState) bool) JobOption {
|
||||
return func(r *Job) {
|
||||
r.ReasoningCallback = f
|
||||
@@ -227,3 +250,21 @@ func (j *Job) IncrementEvaluationLoop() {
|
||||
currentLoop := j.GetEvaluationLoop()
|
||||
j.Metadata["evaluation_loop"] = currentLoop + 1
|
||||
}
|
||||
|
||||
// GetBuiltinTools returns the builtin tools for this job
|
||||
func (j *Job) GetBuiltinTools() []ActionDefinition {
|
||||
return j.BuiltinTools
|
||||
}
|
||||
|
||||
// GetUserTools returns the user tools for this job
|
||||
func (j *Job) GetUserTools() []ActionDefinition {
|
||||
return j.UserTools
|
||||
}
|
||||
|
||||
// GetAllTools returns all tools (builtin + user) for this job
|
||||
func (j *Job) GetAllTools() []ActionDefinition {
|
||||
allTools := make([]ActionDefinition, 0, len(j.BuiltinTools)+len(j.UserTools))
|
||||
allTools = append(allTools, j.BuiltinTools...)
|
||||
allTools = append(allTools, j.UserTools...)
|
||||
return allTools
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package types
|
||||
|
||||
type PromptResult struct {
|
||||
Content string
|
||||
ImageBase64 string
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
services:
|
||||
localai:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localai
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-gpu-hipblas
|
||||
devices:
|
||||
- /dev/dri
|
||||
- /dev/kfd
|
||||
|
||||
mcpbox:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: mcpbox
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall
|
||||
|
||||
localrecall-healthcheck:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localrecall-healthcheck
|
||||
|
||||
localagi:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: localagi
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-sycl-f32
|
||||
image: localai/localai:master-gpu-intel
|
||||
devices:
|
||||
# On a system with integrated GPU and an Arc 770, this is the Arc 770
|
||||
- /dev/dri/card1
|
||||
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
environment:
|
||||
- LOCALAI_SINGLE_ACTIVE_BACKEND=true
|
||||
- DEBUG=true
|
||||
image: localai/localai:master-cublas-cuda12
|
||||
image: localai/localai:master-gpu-nvidia-cuda-12
|
||||
# For images with python backends, use:
|
||||
# image: localai/localai:master-cublas-cuda12-ffmpeg
|
||||
deploy:
|
||||
|
||||
+6
-5
@@ -7,8 +7,8 @@ services:
|
||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||
image: localai/localai:master
|
||||
command:
|
||||
- ${MODEL_NAME:-gemma-3-12b-it-qat}
|
||||
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||
- ${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- ${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- granite-embedding-107m-multilingual
|
||||
healthcheck:
|
||||
@@ -22,7 +22,8 @@ services:
|
||||
- DEBUG=true
|
||||
#- LOCALAI_API_KEY=sk-1234567890
|
||||
volumes:
|
||||
- ./volumes/models:/build/models:cached
|
||||
- ./volumes/models:/models
|
||||
- ./volumes/backends:/backends
|
||||
- ./volumes/images:/tmp/generated/images
|
||||
|
||||
localrecall:
|
||||
@@ -105,8 +106,8 @@ services:
|
||||
- 8080:3000
|
||||
#image: quay.io/mudler/localagi:master
|
||||
environment:
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-12b-it-qat}
|
||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-4b-it-qat}
|
||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-moondream2-20250414}
|
||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
#- LOCALAGI_LLM_API_KEY=sk-1234567890
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package custom_actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
Message string `json:"message"` // field name
|
||||
}
|
||||
|
||||
func Run(config map[string]interface{}) (string, map[string]interface{}, error) {
|
||||
p := Params{}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
return "", map[string]interface{}{}, err
|
||||
}
|
||||
|
||||
return "Hello, " + p.Message + "!", map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
func Description() string {
|
||||
return "Send a message to the user"
|
||||
}
|
||||
|
||||
func Definition() map[string][]string {
|
||||
return map[string][]string{
|
||||
"message": { // field name
|
||||
"string", // type
|
||||
"The message to send", // description
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RequiredFields() []string {
|
||||
return []string{"message"} // field name
|
||||
}
|
||||
|
||||
var config string
|
||||
|
||||
func Init(configuration string) error {
|
||||
// Do something with the configuration that was passed-by
|
||||
config = configuration
|
||||
return nil
|
||||
}
|
||||
|
||||
// DynamicPrompt
|
||||
func Render() (string, string, error) {
|
||||
return "Hello, " + config + "!", "", nil
|
||||
}
|
||||
|
||||
func Role() string {
|
||||
return "system" // Role for the dynamic prompt
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from RealtimeSTT import AudioToTextRecorder
|
||||
|
||||
def process_text(text):
|
||||
print(text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
recorder = AudioToTextRecorder(wake_words="jarvis")
|
||||
|
||||
while True:
|
||||
recorder.text(process_text)
|
||||
@@ -5,38 +5,42 @@ go 1.24
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.28.1
|
||||
github.com/bwmarrin/discordgo v0.29.0
|
||||
github.com/chasefleming/elem-go v0.30.0
|
||||
github.com/dave-gray101/v2keyauth v0.0.0-20240624150259-c45d584d25e2
|
||||
github.com/donseba/go-htmx v1.12.0
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10
|
||||
github.com/go-telegram/bot v1.15.0
|
||||
github.com/go-telegram/bot v1.17.0
|
||||
github.com/gofiber/fiber/v2 v2.52.8
|
||||
github.com/gofiber/template/html/v2 v2.1.3
|
||||
github.com/google/go-github/v69 v69.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/metoro-io/mcp-golang v0.13.0
|
||||
github.com/onsi/ginkgo/v2 v2.23.4
|
||||
github.com/onsi/gomega v1.37.0
|
||||
github.com/metoro-io/mcp-golang v0.16.0
|
||||
github.com/onsi/ginkgo/v2 v2.25.3
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/philippgille/chromem-go v0.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sashabaranov/go-openai v1.40.0
|
||||
github.com/slack-go/slack v0.16.0
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
github.com/slack-go/slack v0.17.3
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
github.com/valyala/fasthttp v1.62.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
maunium.net/go/mautrix v0.17.0
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
)
|
||||
|
||||
require github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
require (
|
||||
github.com/JohannesKaufmann/dom v0.2.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
@@ -52,11 +56,11 @@ require (
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||
github.com/emersion/go-message v0.18.2
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||
github.com/emersion/go-smtp v0.22.0
|
||||
github.com/emersion/go-smtp v0.24.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
@@ -106,12 +110,12 @@ require (
|
||||
go.uber.org/automaxprocs v1.6.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.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
|
||||
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2 h1:eeMLttqTjTgILD6no79Ge96V7Wv8pWDfMVn4jy+koIY=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2/go.mod h1:HtsP+1Fchp4dVvaiIsLHAl/yqL3H1YLwqLC9kNwqQEg=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0 h1:C0/TerKdQX9Y9pbYi1EsLr5LDNANsqunyI/btpyfCg8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0/go.mod h1:OLaKh+giepO8j7teevrNwiy/fwf8LXgoc9g7rwaE1jk=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
@@ -19,8 +21,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
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=
|
||||
@@ -47,8 +49,8 @@ github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7
|
||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.22.0 h1:/d3HWxkZZ4riB+0kzfoODh9X+xyCrLEezMnAAa1LEMU=
|
||||
github.com/emersion/go-smtp v0.22.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10 h1:mL93ADvYMOeT15DcGtK9AaFFc+RcWcy6kQBC6yS/5f4=
|
||||
github.com/eritikass/githubmarkdownconvertergo v0.1.10/go.mod h1:BdpHs6imOtzE5KorbUtKa6bZ0ZBh1yFcrTTAL8FwDKY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
@@ -57,8 +59,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -69,10 +71,10 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-telegram/bot v1.15.0 h1:/ba5pp084MUhjR5sQDymQ7JNZ001CQa7QjtxLWcuGpg=
|
||||
github.com/go-telegram/bot v1.15.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
|
||||
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
@@ -99,7 +101,6 @@ github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -145,8 +146,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/metoro-io/mcp-golang v0.13.0 h1:54TFBJIW76VRB55CJovQQje9x4GnXg0BQQwGRtXrbCE=
|
||||
github.com/metoro-io/mcp-golang v0.13.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
|
||||
github.com/metoro-io/mcp-golang v0.16.0 h1:7NrP8Hca4IDLipPitZaTClzmN8uQcQWX8IsziXU813Y=
|
||||
github.com/metoro-io/mcp-golang v0.16.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -154,10 +155,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
|
||||
@@ -182,20 +183,19 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sashabaranov/go-openai v1.40.0 h1:Peg9Iag5mUJtPW00aYatlsn97YML0iNULiLNe74iPrU=
|
||||
github.com/sashabaranov/go-openai v1.40.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
|
||||
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=
|
||||
github.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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=
|
||||
@@ -234,14 +234,16 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
|
||||
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/util v0.3.0 h1:Lt3lbRXP6ZBqTINK0EieRWor3zEwwwrDT14Z5N8RUCs=
|
||||
go.mau.fi/util v0.3.0/go.mod h1:9dGsBCCbZJstx16YgnVMVi3O2bOizELoKpugLD4FoGs=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE=
|
||||
go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/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=
|
||||
@@ -251,8 +253,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -270,8 +272,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -293,8 +295,8 @@ 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -304,8 +306,8 @@ 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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -317,24 +319,24 @@ 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
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=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -24,6 +24,7 @@ var imageModel = os.Getenv("LOCALAGI_IMAGE_MODEL")
|
||||
var conversationDuration = os.Getenv("LOCALAGI_CONVERSATION_DURATION")
|
||||
var localOperatorBaseURL = os.Getenv("LOCALOPERATOR_BASE_URL")
|
||||
var mcpboxURL = os.Getenv("LOCALAGI_MCPBOX_URL")
|
||||
var customActionsDir = os.Getenv("LOCALAGI_CUSTOM_ACTIONS_DIR")
|
||||
var sshBoxURL = os.Getenv("LOCALAGI_SSHBOX_URL")
|
||||
|
||||
func init() {
|
||||
@@ -31,7 +32,7 @@ func init() {
|
||||
panic("LOCALAGI_MODEL not set")
|
||||
}
|
||||
if apiURL == "" {
|
||||
panic("LOCALAGI_API_URL not set")
|
||||
panic("LOCALAGI_LLM_API_URL not set")
|
||||
}
|
||||
if timeout == "" {
|
||||
timeout = "5m"
|
||||
@@ -69,9 +70,14 @@ func main() {
|
||||
services.ActionConfigBrowserAgentRunner: localOperatorBaseURL,
|
||||
services.ActionConfigDeepResearchRunner: localOperatorBaseURL,
|
||||
services.ActionConfigSSHBoxURL: sshBoxURL,
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts,
|
||||
services.DynamicPrompts(map[string]string{
|
||||
services.ConfigStateDir: stateDir,
|
||||
services.CustomActionsDir: customActionsDir,
|
||||
}),
|
||||
services.Filters,
|
||||
timeout,
|
||||
withLogs,
|
||||
@@ -88,6 +94,7 @@ func main() {
|
||||
webui.WithLLMAPIUrl(apiURL),
|
||||
webui.WithLLMAPIKey(apiKey),
|
||||
webui.WithLLMModel(baseModel),
|
||||
webui.WithCustomActionsDir(customActionsDir),
|
||||
webui.WithStateDir(stateDir),
|
||||
)
|
||||
|
||||
|
||||
+94
-6
@@ -4,18 +4,58 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// UserLocation represents the user's location for web search
|
||||
type UserLocation struct {
|
||||
Type string `json:"type"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// Function tool fields (used when type == "function")
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Parameters *jsonschema.Definition `json:"parameters,omitempty"`
|
||||
|
||||
// Web search tool fields (used when type == "web_search_preview" etc.)
|
||||
SearchContextSize *string `json:"search_context_size,omitempty"`
|
||||
UserLocation *UserLocation `json:"user_location,omitempty"`
|
||||
}
|
||||
|
||||
type ToolChoice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// RequestBody represents the message request to the AI model
|
||||
type RequestBody struct {
|
||||
Model string `json:"model"`
|
||||
Input any `json:"input"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
ToolChoice *ToolChoice `json:"tool_choice"`
|
||||
MaxTokens *int `json:"max_output_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type InputFunctionToolCallOutput struct {
|
||||
CallID string `json:"call_id"`
|
||||
Output string `json:"output"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// InputMessage represents a user input message
|
||||
type InputMessage struct {
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
@@ -29,10 +69,49 @@ type ContentItem struct {
|
||||
|
||||
// ResponseBody represents the response from the AI model
|
||||
type ResponseBody struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
Error any `json:"error,omitempty"`
|
||||
Output []ResponseMessage `json:"output"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Status string `json:"status"`
|
||||
Error any `json:"error,omitempty"`
|
||||
Output []ResponseBase `json:"output"`
|
||||
Tools []Tool `json:"tools"`
|
||||
}
|
||||
|
||||
type ResponseType string
|
||||
|
||||
const (
|
||||
ResponseTypeFunctionToolCall ResponseType = "function_call"
|
||||
ResponseTypeMessage ResponseType = "message"
|
||||
)
|
||||
|
||||
type ResponseBase json.RawMessage
|
||||
|
||||
func (r *ResponseBase) UnmarshalJSON(data []byte) error {
|
||||
return (*json.RawMessage)(r).UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
func (r *ResponseBase) ToMessage() (msg ResponseMessage, err error) {
|
||||
err = json.Unmarshal(*r, &msg)
|
||||
if msg.Type != string(ResponseTypeMessage) {
|
||||
return ResponseMessage{}, fmt.Errorf("Expected %s, not %s", ResponseTypeMessage, msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *ResponseBase) ToFunctionToolCall() (msg ResponseFunctionToolCall, err error) {
|
||||
err = json.Unmarshal(*r, &msg)
|
||||
if msg.Type != string(ResponseTypeFunctionToolCall) {
|
||||
return ResponseFunctionToolCall{}, fmt.Errorf("Expected %s, not %s", ResponseTypeFunctionToolCall, msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ResponseFunctionToolCall struct {
|
||||
Arguments string `json:"arguments"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ResponseMessage represents a message in the response
|
||||
@@ -85,7 +164,11 @@ func (c *Client) SimpleAIResponse(agentName, input string) (string, error) {
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, msg := range response.Output {
|
||||
for _, out := range response.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("out.ToMessage: %w", err)
|
||||
}
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
@@ -113,7 +196,12 @@ func (c *Client) ChatAIResponse(agentName string, messages []InputMessage) (stri
|
||||
}
|
||||
|
||||
// Extract the text response from the output
|
||||
for _, msg := range response.Output {
|
||||
for _, out := range response.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("out.ToMessage: %w", err)
|
||||
}
|
||||
|
||||
if msg.Role == "assistant" {
|
||||
for _, content := range msg.Content {
|
||||
if content.Type == "output_text" {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package ptr
|
||||
|
||||
func To[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
+315
-172
@@ -4,6 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/action"
|
||||
"github.com/mudler/LocalAGI/core/state"
|
||||
@@ -50,6 +54,17 @@ const (
|
||||
ActionSetReminder = "set_reminder"
|
||||
ActionListReminders = "list_reminders"
|
||||
ActionRemoveReminder = "remove_reminder"
|
||||
ActionAddToMemory = "add_to_memory"
|
||||
ActionListMemory = "list_memory"
|
||||
ActionRemoveFromMemory = "remove_from_memory"
|
||||
ActionPiKVMPowerControl = "pikvm_power_control"
|
||||
ActionWebhook = "webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
nameField = "name"
|
||||
descriptionField = "description"
|
||||
configurationField = "configuration"
|
||||
)
|
||||
|
||||
var AvailableActions = []string{
|
||||
@@ -87,14 +102,261 @@ var AvailableActions = []string{
|
||||
ActionSetReminder,
|
||||
ActionListReminders,
|
||||
ActionRemoveReminder,
|
||||
ActionAddToMemory,
|
||||
ActionListMemory,
|
||||
ActionRemoveFromMemory,
|
||||
ActionPiKVMPowerControl,
|
||||
ActionWebhook,
|
||||
}
|
||||
|
||||
var DefaultActions = []config.FieldGroup{
|
||||
{
|
||||
Name: "search",
|
||||
Label: "Search",
|
||||
Fields: actions.SearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "browser-agent-runner",
|
||||
Label: "Browser Agent Runner",
|
||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "deep-research-runner",
|
||||
Label: "Deep Research Runner",
|
||||
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "add_to_memory",
|
||||
Label: "Add to Memory",
|
||||
Fields: actions.AddToMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "list_memory",
|
||||
Label: "List Memory",
|
||||
Fields: actions.ListMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "remove_from_memory",
|
||||
Label: "Remove from Memory",
|
||||
Fields: actions.RemoveFromMemoryConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
Fields: actions.GithubIssueLabelerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-opener",
|
||||
Label: "GitHub Issue Opener",
|
||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-editor",
|
||||
Label: "GitHub Issue Editor",
|
||||
Fields: actions.GithubIssueEditorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-closer",
|
||||
Label: "GitHub Issue Closer",
|
||||
Fields: actions.GithubIssueCloserConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-commenter",
|
||||
Label: "GitHub Issue Commenter",
|
||||
Fields: actions.GithubIssueCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-reader",
|
||||
Label: "GitHub Issue Reader",
|
||||
Fields: actions.GithubIssueReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-searcher",
|
||||
Label: "GitHub Issue Search",
|
||||
Fields: actions.GithubIssueSearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-get-content",
|
||||
Label: "GitHub Repository Get Content",
|
||||
Fields: actions.GithubRepositoryGetContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-get-all-repository-content",
|
||||
Label: "GitHub Get All Repository Content",
|
||||
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-search-files",
|
||||
Label: "GitHub Repository Search Files",
|
||||
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-list-files",
|
||||
Label: "GitHub Repository List Files",
|
||||
Fields: actions.GithubRepositoryListFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-create-or-update-content",
|
||||
Label: "GitHub Repository Create/Update Content",
|
||||
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-readme",
|
||||
Label: "GitHub Repository README",
|
||||
Fields: actions.GithubRepositoryREADMEConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reader",
|
||||
Label: "GitHub PR Reader",
|
||||
Fields: actions.GithubPRReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-commenter",
|
||||
Label: "GitHub PR Commenter",
|
||||
Fields: actions.GithubPRCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reviewer",
|
||||
Label: "GitHub PR Reviewer",
|
||||
Fields: actions.GithubPRReviewerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-creator",
|
||||
Label: "GitHub PR Creator",
|
||||
Fields: actions.GithubPRCreatorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "twitter-post",
|
||||
Label: "Twitter Post",
|
||||
Fields: actions.TwitterPostConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-mail",
|
||||
Label: "Send Mail",
|
||||
Fields: actions.SendMailConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "shell-command",
|
||||
Label: "Shell Command",
|
||||
Fields: actions.ShellConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "custom",
|
||||
Label: "Custom",
|
||||
Fields: action.CustomConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "scraper",
|
||||
Label: "Scraper",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "wikipedia",
|
||||
Label: "Wikipedia",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "browse",
|
||||
Label: "Browse",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "counter",
|
||||
Label: "Counter",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "call_agents",
|
||||
Label: "Call Agents",
|
||||
Fields: actions.CallAgentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-telegram-message",
|
||||
Label: "Send Telegram Message",
|
||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "set_reminder",
|
||||
Label: "Set Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "list_reminders",
|
||||
Label: "List Reminders",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "remove_reminder",
|
||||
Label: "Remove Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "pikvm_power_control",
|
||||
Label: "PiKVM Power Control",
|
||||
Fields: actions.PiKVMConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "webhook",
|
||||
Label: "Webhook",
|
||||
Fields: actions.WebhookConfigMeta(),
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
ActionConfigBrowserAgentRunner = "browser-agent-runner-base-url"
|
||||
ActionConfigDeepResearchRunner = "deep-research-runner-base-url"
|
||||
ActionConfigSSHBoxURL = "sshbox-url"
|
||||
ConfigStateDir = "state-dir"
|
||||
CustomActionsDir = "custom-actions-dir"
|
||||
)
|
||||
|
||||
func customActions(customActionsDir string, existingActionConfigs map[string]map[string]string) (allActions []types.Action) {
|
||||
files, err := os.ReadDir(customActionsDir)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom actions directory", "error", err)
|
||||
return allActions
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".go" {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(customActionsDir, file.Name()))
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
actionName := strings.TrimSuffix(file.Name(), ".go")
|
||||
|
||||
actionConfig := map[string]string{
|
||||
"name": actionName,
|
||||
"description": "",
|
||||
"code": string(content),
|
||||
"unsafe": "false",
|
||||
}
|
||||
|
||||
if c, exists := existingActionConfigs[actionName]; exists {
|
||||
// We allow the user to customize name and description
|
||||
actionConfig[descriptionField] = c[descriptionField]
|
||||
actionConfig[nameField] = c[nameField]
|
||||
actionConfig[configurationField] = c[configurationField]
|
||||
}
|
||||
a, err := Action(ActionCustom, "", actionConfig, nil, map[string]string{})
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom action", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
return func(ctx context.Context, pool *state.AgentPool) []types.Action {
|
||||
@@ -102,6 +364,7 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
|
||||
|
||||
agentName := a.Name
|
||||
|
||||
existingActionConfigs := map[string]map[string]string{}
|
||||
for _, a := range a.Actions {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
||||
@@ -109,6 +372,8 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
|
||||
continue
|
||||
}
|
||||
|
||||
existingActionConfigs[a.Name] = config
|
||||
|
||||
a, err := Action(a.Name, agentName, config, pool, actionsConfigs)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -116,6 +381,11 @@ func Actions(actionsConfigs map[string]string) func(a *state.AgentConfig) func(c
|
||||
allActions = append(allActions, a)
|
||||
}
|
||||
|
||||
// Now we will scan a directory for custom actions
|
||||
if actionsConfigs[CustomActionsDir] != "" {
|
||||
allActions = append(allActions, customActions(actionsConfigs[CustomActionsDir], existingActionConfigs)...)
|
||||
}
|
||||
|
||||
return allActions
|
||||
}
|
||||
}
|
||||
@@ -130,6 +400,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
memoryFilePath := memoryPath(agentName, actionsConfigs)
|
||||
|
||||
switch name {
|
||||
case ActionCustom:
|
||||
a, err = action.NewCustom(config, "")
|
||||
@@ -183,6 +455,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = actions.NewBrowse(config)
|
||||
case ActionSendMail:
|
||||
a = actions.NewSendMail(config)
|
||||
case ActionWebhook:
|
||||
a = actions.NewWebhook(config)
|
||||
case ActionTwitterPost:
|
||||
a = actions.NewPostTweet(config)
|
||||
case ActionCounter:
|
||||
@@ -199,6 +473,14 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = action.NewListReminders()
|
||||
case ActionRemoveReminder:
|
||||
a = action.NewRemoveReminder()
|
||||
case ActionAddToMemory:
|
||||
a, _, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionListMemory:
|
||||
_, a, _ = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionRemoveFromMemory:
|
||||
_, _, a = actions.NewMemoryActions(memoryFilePath, config)
|
||||
case ActionPiKVMPowerControl:
|
||||
a = actions.NewPiKVMAction(config)
|
||||
default:
|
||||
xlog.Error("Action not found", "name", name)
|
||||
return nil, fmt.Errorf("Action not found")
|
||||
@@ -211,177 +493,38 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func ActionsConfigMeta() []config.FieldGroup {
|
||||
return []config.FieldGroup{
|
||||
{
|
||||
Name: "search",
|
||||
Label: "Search",
|
||||
Fields: actions.SearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "browser-agent-runner",
|
||||
Label: "Browser Agent Runner",
|
||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "deep-research-runner",
|
||||
Label: "Deep Research Runner",
|
||||
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
Fields: actions.GenImageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-labeler",
|
||||
Label: "GitHub Issue Labeler",
|
||||
Fields: actions.GithubIssueLabelerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-opener",
|
||||
Label: "GitHub Issue Opener",
|
||||
Fields: actions.GithubIssueOpenerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-editor",
|
||||
Label: "GitHub Issue Editor",
|
||||
Fields: actions.GithubIssueEditorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-closer",
|
||||
Label: "GitHub Issue Closer",
|
||||
Fields: actions.GithubIssueCloserConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-commenter",
|
||||
Label: "GitHub Issue Commenter",
|
||||
Fields: actions.GithubIssueCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-reader",
|
||||
Label: "GitHub Issue Reader",
|
||||
Fields: actions.GithubIssueReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-issue-searcher",
|
||||
Label: "GitHub Issue Search",
|
||||
Fields: actions.GithubIssueSearchConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-get-content",
|
||||
Label: "GitHub Repository Get Content",
|
||||
Fields: actions.GithubRepositoryGetContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-get-all-repository-content",
|
||||
Label: "GitHub Get All Repository Content",
|
||||
Fields: actions.GithubRepositoryGetAllContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-search-files",
|
||||
Label: "GitHub Repository Search Files",
|
||||
Fields: actions.GithubRepositorySearchFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-list-files",
|
||||
Label: "GitHub Repository List Files",
|
||||
Fields: actions.GithubRepositoryListFilesConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-repository-create-or-update-content",
|
||||
Label: "GitHub Repository Create/Update Content",
|
||||
Fields: actions.GithubRepositoryCreateOrUpdateContentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-readme",
|
||||
Label: "GitHub Repository README",
|
||||
Fields: actions.GithubRepositoryREADMEConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reader",
|
||||
Label: "GitHub PR Reader",
|
||||
Fields: actions.GithubPRReaderConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-commenter",
|
||||
Label: "GitHub PR Commenter",
|
||||
Fields: actions.GithubPRCommenterConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-reviewer",
|
||||
Label: "GitHub PR Reviewer",
|
||||
Fields: actions.GithubPRReviewerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "github-pr-creator",
|
||||
Label: "GitHub PR Creator",
|
||||
Fields: actions.GithubPRCreatorConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "twitter-post",
|
||||
Label: "Twitter Post",
|
||||
Fields: actions.TwitterPostConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-mail",
|
||||
Label: "Send Mail",
|
||||
Fields: actions.SendMailConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "shell-command",
|
||||
Label: "Shell Command",
|
||||
Fields: actions.ShellConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "custom",
|
||||
Label: "Custom",
|
||||
Fields: action.CustomConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "scraper",
|
||||
Label: "Scraper",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "wikipedia",
|
||||
Label: "Wikipedia",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "browse",
|
||||
Label: "Browse",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "counter",
|
||||
Label: "Counter",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "call_agents",
|
||||
Label: "Call Agents",
|
||||
Fields: actions.CallAgentConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "send-telegram-message",
|
||||
Label: "Send Telegram Message",
|
||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "set_reminder",
|
||||
Label: "Set Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "list_reminders",
|
||||
Label: "List Reminders",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
{
|
||||
Name: "remove_reminder",
|
||||
Label: "Remove Reminder",
|
||||
Fields: []config.Field{},
|
||||
},
|
||||
func ActionsConfigMeta(customActionDir string) []config.FieldGroup {
|
||||
all := slices.Clone(DefaultActions)
|
||||
|
||||
if customActionDir != "" {
|
||||
actions := customActions(customActionDir, map[string]map[string]string{})
|
||||
|
||||
for _, a := range actions {
|
||||
all = append(all, config.FieldGroup{
|
||||
Name: a.Definition().Name.String(),
|
||||
Label: a.Definition().Name.String(),
|
||||
Fields: []config.Field{
|
||||
{
|
||||
Name: nameField,
|
||||
Label: "Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Name of the custom action",
|
||||
},
|
||||
{
|
||||
Name: descriptionField,
|
||||
Label: "Description",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Description of the custom action",
|
||||
},
|
||||
{
|
||||
Name: configurationField,
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration of the custom action",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// Remove global const and mutex, and add them as fields to a struct
|
||||
|
||||
type MemoryActions struct {
|
||||
filePath string
|
||||
customName string
|
||||
customDescription string
|
||||
}
|
||||
|
||||
type AddToMemoryAction struct{ *MemoryActions }
|
||||
type ListMemoryAction struct{ *MemoryActions }
|
||||
type RemoveFromMemoryAction struct{ *MemoryActions }
|
||||
|
||||
// NewMemoryActions returns the three actions, using the provided filePath and config
|
||||
func NewMemoryActions(filePath string, config map[string]string) (*AddToMemoryAction, *ListMemoryAction, *RemoveFromMemoryAction) {
|
||||
ma := &MemoryActions{filePath: filePath}
|
||||
if config != nil {
|
||||
ma.customName = config["custom_name"]
|
||||
ma.customDescription = config["custom_description"]
|
||||
}
|
||||
return &AddToMemoryAction{ma}, &ListMemoryAction{ma}, &RemoveFromMemoryAction{ma}
|
||||
}
|
||||
|
||||
type addToMemoryParams struct {
|
||||
Item string `json:"item"`
|
||||
}
|
||||
|
||||
type removeFromMemoryParams struct {
|
||||
Index *int `json:"index,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MemoryActions) readMemory() ([]string, error) {
|
||||
f, err := os.Open(m.filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var items []string
|
||||
if err := json.NewDecoder(f).Decode(&items); err != nil {
|
||||
if err == io.EOF {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (m *MemoryActions) writeMemory(items []string) error {
|
||||
f, err := os.Create(m.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewEncoder(f).Encode(items)
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req addToMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
if req.Item == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("item cannot be empty")
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
items = append(items, req.Item)
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Added item to memory: %s", req.Item),
|
||||
Metadata: map[string]any{"item": req.Item, "count": len(items)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
outputResult := "Number of items in memory: " + strconv.Itoa(len(items)) + "\n"
|
||||
for i, item := range items {
|
||||
outputResult += fmt.Sprintf("%d) %s\n", i, item)
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: outputResult,
|
||||
Metadata: map[string]any{"items": items},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req removeFromMemoryParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
items, err := a.readMemory()
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
var removed string
|
||||
if req.Index != nil {
|
||||
idx := *req.Index
|
||||
if idx < 0 || idx >= len(items) {
|
||||
return types.ActionResult{}, fmt.Errorf("index out of range")
|
||||
}
|
||||
removed = items[idx]
|
||||
items = append(items[:idx], items[idx+1:]...)
|
||||
} else if req.Value != "" {
|
||||
found := false
|
||||
for i, v := range items {
|
||||
if v == req.Value {
|
||||
removed = v
|
||||
items = append(items[:i], items[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return types.ActionResult{}, fmt.Errorf("value not found in memory")
|
||||
}
|
||||
} else {
|
||||
return types.ActionResult{}, fmt.Errorf("must provide index or value to remove")
|
||||
}
|
||||
if err := a.writeMemory(items); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Removed item from memory: %s", removed),
|
||||
Metadata: map[string]any{"removed": removed, "count": len(items)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "add_to_memory"
|
||||
description := "Add a string item to memory (stored in a JSON file)."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"item": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The string item to add to memory.",
|
||||
},
|
||||
},
|
||||
Required: []string{"item"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ListMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "list_memory"
|
||||
description := "List all items currently stored in memory."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *RemoveFromMemoryAction) Definition() types.ActionDefinition {
|
||||
name := "remove_from_memory"
|
||||
description := "Remove an item from memory by index or value."
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"index": {
|
||||
Type: jsonschema.Integer,
|
||||
Description: "The index of the item to remove (optional, 0-based)",
|
||||
},
|
||||
"value": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The value of the item to remove (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AddToMemoryAction) Plannable() bool { return true }
|
||||
func (a *ListMemoryAction) Plannable() bool { return true }
|
||||
func (a *RemoveFromMemoryAction) Plannable() bool { return true }
|
||||
|
||||
// AddToMemoryConfigMeta returns the metadata for AddToMemory action configuration fields
|
||||
func AddToMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'add_to_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Add a string item to memory (stored in a JSON file).')",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListMemoryConfigMeta returns the metadata for ListMemory action configuration fields
|
||||
func ListMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'list_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'List all items currently stored in memory.')",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveFromMemoryConfigMeta returns the metadata for RemoveFromMemory action configuration fields
|
||||
func RemoveFromMemoryConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for the action (optional, defaults to 'remove_from_memory')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for the action (optional, defaults to 'Remove an item from memory by index or value.')",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("MemoryActions", func() {
|
||||
var (
|
||||
tmpFile string
|
||||
aAdd *actions.AddToMemoryAction
|
||||
aList *actions.ListMemoryAction
|
||||
aRemove *actions.RemoveFromMemoryAction
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
f, err := os.CreateTemp("", "memory_test_*.json")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
tmpFile = f.Name()
|
||||
f.Close()
|
||||
aAdd, aList, aRemove = actions.NewMemoryActions(tmpFile, map[string]string{})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.Remove(tmpFile)
|
||||
})
|
||||
|
||||
It("adds and lists items", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, err := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Metadata["items"]).To(ContainElements("foo", "bar"))
|
||||
})
|
||||
|
||||
It("removes by index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 0})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("bar"))
|
||||
})
|
||||
|
||||
It("removes by value", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "bar"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
res, _ := aList.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(res.Metadata["items"]).To(ConsistOf("foo"))
|
||||
})
|
||||
|
||||
It("returns error for out of range index", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"index": 2})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for value not found", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{"value": "bar"})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error for empty item", func() {
|
||||
_, err := aAdd.Run(context.TODO(), nil, types.ActionParams{"item": ""})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error if neither index nor value provided", func() {
|
||||
_, _ = aAdd.Run(context.TODO(), nil, types.ActionParams{"item": "foo"})
|
||||
_, err := aRemove.Run(context.TODO(), nil, types.ActionParams{})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func NewPiKVMAction(config map[string]string) *PiKVMAction {
|
||||
return &PiKVMAction{
|
||||
hostname: config["hostname"],
|
||||
username: config["username"],
|
||||
password: config["password"],
|
||||
customName: config["custom_name"],
|
||||
customDescription: config["custom_description"],
|
||||
insecure: config["insecure"] == "true",
|
||||
}
|
||||
}
|
||||
|
||||
type PiKVMAction struct {
|
||||
hostname string
|
||||
username string
|
||||
password string
|
||||
customName string
|
||||
customDescription string
|
||||
insecure bool
|
||||
}
|
||||
|
||||
type pikvmPowerParams struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
func (a *PiKVMAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
var req pikvmPowerParams
|
||||
if err := params.Unmarshal(&req); err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Validate action parameter
|
||||
validActions := map[string]bool{
|
||||
"on": true,
|
||||
"off": true,
|
||||
"off_hard": true,
|
||||
"reset_hard": true,
|
||||
}
|
||||
if !validActions[req.Action] {
|
||||
return types.ActionResult{}, fmt.Errorf("invalid action: %s. Valid actions are: on, off, off_hard, reset_hard", req.Action)
|
||||
}
|
||||
|
||||
// Check if required config is provided
|
||||
if a.hostname == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("hostname is required in action configuration")
|
||||
}
|
||||
if a.username == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("username is required in action configuration")
|
||||
}
|
||||
if a.password == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("password is required in action configuration")
|
||||
}
|
||||
|
||||
// Build the API URL
|
||||
apiURL := fmt.Sprintf("https://%s/api/atx/power", a.hostname)
|
||||
|
||||
insecure := false
|
||||
if a.insecure {
|
||||
insecure = true
|
||||
}
|
||||
// Create HTTP client with basic auth
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
reqHTTP, err := http.NewRequestWithContext(ctx, "POST", apiURL, nil)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
// Set basic authentication
|
||||
reqHTTP.SetBasicAuth(a.username, a.password)
|
||||
|
||||
// Add query parameters
|
||||
q := reqHTTP.URL.Query()
|
||||
q.Add("action", req.Action)
|
||||
reqHTTP.URL.RawQuery = q.Encode()
|
||||
|
||||
// Make the request
|
||||
resp, err := client.Do(reqHTTP)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to make HTTP request to PiKVM: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return types.ActionResult{}, fmt.Errorf("PiKVM API returned status %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// Determine action description for user-friendly response
|
||||
actionDesc := map[string]string{
|
||||
"on": "power on",
|
||||
"off": "power off",
|
||||
"off_hard": "hard power off",
|
||||
"reset_hard": "hard reset",
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Successfully sent %s command to PiKVM at %s", actionDesc[req.Action], a.hostname)
|
||||
|
||||
return types.ActionResult{
|
||||
Result: result,
|
||||
Metadata: map[string]any{
|
||||
"action": req.Action,
|
||||
"hostname": a.hostname,
|
||||
"status": "success",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *PiKVMAction) Definition() types.ActionDefinition {
|
||||
name := "pikvm_power_control"
|
||||
description := "Control power state of a PiKVM device using ATX power management."
|
||||
|
||||
if a.customName != "" {
|
||||
name = a.customName
|
||||
}
|
||||
if a.customDescription != "" {
|
||||
description = a.customDescription
|
||||
}
|
||||
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(name),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"action": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The power action to perform on the PiKVM device.",
|
||||
Enum: []string{"on", "off", "off_hard", "reset_hard"},
|
||||
},
|
||||
},
|
||||
Required: []string{"action"},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *PiKVMAction) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PiKVMConfigMeta returns the metadata for PiKVM action configuration fields
|
||||
func PiKVMConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "hostname",
|
||||
Label: "PiKVM Hostname",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "The hostname or IP address of the PiKVM device (e.g., pikvm.local or 192.168.1.100)",
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Label: "Username",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Username for PiKVM authentication (usually 'admin')",
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Label: "Password",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Password for PiKVM authentication",
|
||||
},
|
||||
{
|
||||
Name: "custom_name",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom name for this action (optional, defaults to 'pikvm_power_control')",
|
||||
},
|
||||
{
|
||||
Name: "custom_description",
|
||||
Label: "Custom Description",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Custom description for this action (optional)",
|
||||
},
|
||||
{
|
||||
Name: "insecure",
|
||||
Label: "Insecure",
|
||||
Type: config.FieldTypeCheckbox,
|
||||
Required: false,
|
||||
HelpText: "Skip certificate verification (optional)",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Package actions contains action implementations used by LocalAGI.
|
||||
// This file implements the "webhook" action which can send an HTTP request
|
||||
// to an external service with a configurable method, content type, and payload.
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// NewWebhook constructs a WebhookAction using provided configuration values:
|
||||
// - url: Destination endpoint for the HTTP request (required).
|
||||
// - method: HTTP method to use (GET, POST, PUT, DELETE, ...). Defaults to POST.
|
||||
// - contentType: Value for the Content-Type header (e.g., application/json).
|
||||
// - payloadTemplate: Optional template for the request body; the runtime parameter
|
||||
// "payload" (if provided) will replace the "{{payload}}" placeholder inside this template.
|
||||
func NewWebhook(cfg map[string]string) *WebhookAction {
|
||||
wa := &WebhookAction{
|
||||
url: strings.TrimSpace(cfg["url"]),
|
||||
method: strings.ToUpper(strings.TrimSpace(cfg["method"])),
|
||||
contentType: strings.TrimSpace(cfg["contentType"]),
|
||||
payloadTemplate: cfg["payloadTemplate"],
|
||||
}
|
||||
if wa.method == "" {
|
||||
wa.method = http.MethodPost
|
||||
}
|
||||
return wa
|
||||
}
|
||||
|
||||
// WebhookAction holds the static configuration for the webhook.
|
||||
// These values come from the action configuration (UI/agent config),
|
||||
// while the runtime parameter only carries the dynamic payload.
|
||||
// - url: Target endpoint for the request.
|
||||
// - method: HTTP method to use. Defaults to POST if not provided.
|
||||
// - contentType: Sets the Content-Type header when a body is sent.
|
||||
// - payloadTemplate: Optional template used to build the request body; occurrences
|
||||
// of "{{payload}}" get replaced with the runtime payload string.
|
||||
// If no placeholder is present, the template is used as-is.
|
||||
// For GET requests the body is omitted regardless of payload.
|
||||
//
|
||||
// Note: This action does not follow redirects!
|
||||
type WebhookAction struct {
|
||||
url string
|
||||
method string
|
||||
contentType string
|
||||
payloadTemplate string
|
||||
}
|
||||
|
||||
// Run executes the webhook call.
|
||||
// It reads the runtime parameter "payload" (optional), merges it into the
|
||||
// configured payloadTemplate (if any), constructs an HTTP request using the
|
||||
// configured URL, method and content type, and then returns a summary with the
|
||||
// response status and body (truncated to 4KiB for safety).
|
||||
func (a *WebhookAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
|
||||
// Runtime parameters: only payload
|
||||
type input struct {
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
var in input
|
||||
if err := params.Unmarshal(&in); err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Validate essential configuration. The URL must be provided via the
|
||||
// action configuration (not via runtime parameters).
|
||||
if a.url == "" {
|
||||
return types.ActionResult{}, fmt.Errorf("configuration.url is required")
|
||||
}
|
||||
|
||||
method := a.method
|
||||
|
||||
// Build the request body based on template and payload:
|
||||
// - If a payloadTemplate is provided, replace occurrences of "{{payload}}"
|
||||
// with the runtime payload value.
|
||||
// - If the template does not contain the placeholder but is provided, we use
|
||||
// the template as-is (common for static JSON bodies prepared at config time).
|
||||
// - If no template is configured, we send the runtime payload as-is.
|
||||
// - For GET requests the body is omitted regardless of payload.
|
||||
var payload string
|
||||
if a.payloadTemplate != "" {
|
||||
payload = strings.ReplaceAll(a.payloadTemplate, "{{payload}}", in.Payload)
|
||||
if payload == a.payloadTemplate && in.Payload != "" {
|
||||
// If no placeholder found, fallback to template or payload alone
|
||||
payload = a.payloadTemplate
|
||||
}
|
||||
} else {
|
||||
payload = in.Payload
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if method != http.MethodGet && payload != "" {
|
||||
body = bytes.NewBufferString(payload)
|
||||
}
|
||||
|
||||
// Create the HTTP request bound to the provided context so that cancellation
|
||||
// or timeouts from the caller propagate to the outbound call.
|
||||
req, err := http.NewRequestWithContext(ctx, method, a.url, body)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
|
||||
// Set Content-Type header if configured. For GET requests this header is
|
||||
// typically ignored by servers as there is no body.
|
||||
if a.contentType != "" {
|
||||
req.Header.Set("Content-Type", a.contentType)
|
||||
}
|
||||
|
||||
// Use a new http.Client with default settings. Consider configuring timeouts
|
||||
// at the caller level via the context, or wiring a custom client if needed.
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read and safely truncate the response body to avoid flooding the agent's
|
||||
// context with very large payloads. Errors on ReadAll are ignored here as
|
||||
// we already have the status code.
|
||||
respBytes, _ := io.ReadAll(resp.Body)
|
||||
respBody := string(respBytes)
|
||||
if len(respBody) > 4096 {
|
||||
respBody = respBody[:4096] + "... (truncated)"
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
// Return the response body as the result.
|
||||
// If the response body is empty, use the status text as the result (e.g. "OK" for status code 200).
|
||||
Result: func() string {
|
||||
if respBody == "" {
|
||||
return http.StatusText(resp.StatusCode)
|
||||
}
|
||||
return respBody
|
||||
}(),
|
||||
// Include the response status code in the metadata.
|
||||
Metadata: map[string]interface{}{
|
||||
"statusCode": resp.StatusCode,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Definition returns the action schema exposed to the planner/runtime.
|
||||
// Only the runtime parameter "payload" is accepted; all connection details
|
||||
// are configured statically via the action configuration UI.
|
||||
func (a *WebhookAction) Definition() types.ActionDefinition {
|
||||
return types.ActionDefinition{
|
||||
Name: "webhook",
|
||||
Description: "Send an HTTP request to a configured URL/method/content-type. Accepts a runtime payload parameter optionally inserted into the configured payload template.",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"payload": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Payload/body to send with the request at runtime. If a payloadTemplate is configured, '{{payload}}' will be replaced by this value.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Plannable indicates the action can be suggested/used by planners without
|
||||
// requiring hidden context; inputs are straightforward and safe.
|
||||
func (a *WebhookAction) Plannable() bool { return true }
|
||||
|
||||
// WebhookConfigMeta returns the metadata for Webhook action configuration fields:
|
||||
// - url: The endpoint to send requests to (required).
|
||||
// - method: One of GET/POST/PUT/DELETE. Defaults to POST.
|
||||
// - contentType: Common content types selectable from a dropdown.
|
||||
// - payloadTemplate: Optional body template. At runtime, "{{payload}}" is
|
||||
// replaced by the provided payload parameter. If missing, the template is used
|
||||
// as-is; for GET, no body is sent regardless.
|
||||
func WebhookConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "url",
|
||||
Label: "URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: true,
|
||||
HelpText: "Destination URL for the webhook",
|
||||
},
|
||||
{
|
||||
Name: "method",
|
||||
Label: "HTTP Method",
|
||||
Type: config.FieldTypeSelect,
|
||||
Options: []config.FieldOption{{Value: http.MethodGet, Label: "GET"}, {Value: http.MethodPost, Label: "POST"}, {Value: http.MethodPut, Label: "PUT"}, {Value: http.MethodDelete, Label: "DELETE"}},
|
||||
DefaultValue: http.MethodPost,
|
||||
Required: true,
|
||||
HelpText: "HTTP method to use",
|
||||
},
|
||||
{
|
||||
Name: "contentType",
|
||||
Label: "Content Type",
|
||||
Type: config.FieldTypeSelect,
|
||||
Options: []config.FieldOption{
|
||||
{Value: "application/json", Label: "application/json"},
|
||||
{Value: "text/plain", Label: "text/plain"},
|
||||
{Value: "application/x-www-form-urlencoded", Label: "application/x-www-form-urlencoded"},
|
||||
},
|
||||
Required: true,
|
||||
HelpText: "Content-Type header to send",
|
||||
},
|
||||
{
|
||||
Name: "payloadTemplate",
|
||||
Label: "Payload Template",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Optional template used to craft the request body. Use '{{payload}}' as placeholder for the runtime payload.",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
)
|
||||
|
||||
func memoryPath(agentName string, actionsConfigs map[string]string) string {
|
||||
// Compose memory file path based on stateDir and agentName, using a subdirectory
|
||||
memoryFilePath := "memory.json"
|
||||
if actionsConfigs != nil {
|
||||
if stateDir, ok := actionsConfigs[ConfigStateDir]; ok && stateDir != "" {
|
||||
memoryDir := fmt.Sprintf("%s/memory", stateDir)
|
||||
err := os.MkdirAll(memoryDir, 0755) // ensure the directory exists
|
||||
if err != nil {
|
||||
xlog.Error("Error creating memory directory", "error", err)
|
||||
return memoryFilePath
|
||||
}
|
||||
memoryFilePath = fmt.Sprintf("%s/%s.json", memoryDir, agentName)
|
||||
} else {
|
||||
memoryFilePath = fmt.Sprintf("%s.memory.json", agentName)
|
||||
}
|
||||
}
|
||||
|
||||
return memoryFilePath
|
||||
}
|
||||
+153
-140
@@ -208,9 +208,15 @@ func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, c
|
||||
return
|
||||
}
|
||||
|
||||
func scanImagesInMessages(api *slack.Client, ev *slackevents.MessageEvent) (*bytes.Buffer, string) {
|
||||
imageBytes := new(bytes.Buffer)
|
||||
mimeType := "image/jpeg"
|
||||
// ImageData represents a single image with its metadata
|
||||
type ImageData struct {
|
||||
Data []byte
|
||||
MimeType string
|
||||
}
|
||||
|
||||
// scanImagesInMessages scans for all images in a message and returns them as a slice
|
||||
func scanImagesInMessages(api *slack.Client, ev *slackevents.MessageEvent) []ImageData {
|
||||
var images []ImageData
|
||||
|
||||
// Fetch the message using the API
|
||||
messages, _, _, err := api.GetConversationReplies(&slack.GetConversationRepliesParameters{
|
||||
@@ -220,25 +226,142 @@ func scanImagesInMessages(api *slack.Client, ev *slackevents.MessageEvent) (*byt
|
||||
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error fetching messages: %v", err))
|
||||
} else {
|
||||
for _, msg := range messages {
|
||||
if len(msg.Files) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, attachment := range msg.Files {
|
||||
if attachment.URLPrivate != "" {
|
||||
xlog.Debug(fmt.Sprintf("Getting Attachment: %+v", attachment))
|
||||
// download image with slack api
|
||||
mimeType = attachment.Mimetype
|
||||
if err := api.GetFile(attachment.URLPrivate, imageBytes); err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error downloading image: %v", err))
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
xlog.Debug("Scanning images in messages", "messages", messages)
|
||||
for _, msg := range messages {
|
||||
if len(msg.Files) == 0 {
|
||||
xlog.Debug("No files in message", "message", msg.Text)
|
||||
continue
|
||||
}
|
||||
xlog.Debug("Files in message", "files", msg.Files)
|
||||
for _, attachment := range msg.Files {
|
||||
if attachment.URLPrivate != "" {
|
||||
xlog.Debug(fmt.Sprintf("Getting Attachment: %+v", attachment))
|
||||
// download image with slack api
|
||||
imageBytes := new(bytes.Buffer)
|
||||
if err := api.GetFile(attachment.URLPrivate, imageBytes); err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error downloading image: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
images = append(images, ImageData{
|
||||
Data: imageBytes.Bytes(),
|
||||
MimeType: attachment.Mimetype,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageBytes, mimeType
|
||||
return images
|
||||
}
|
||||
|
||||
// scanImagesInAppMentionEvent scans for all images in an app mention event
|
||||
func scanImagesInAppMentionEvent(api *slack.Client, ev *slackevents.AppMentionEvent) []ImageData {
|
||||
var images []ImageData
|
||||
|
||||
// Fetch the message using the API
|
||||
messages, _, _, err := api.GetConversationReplies(&slack.GetConversationRepliesParameters{
|
||||
ChannelID: ev.Channel,
|
||||
Timestamp: ev.TimeStamp,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error fetching messages: %v", err))
|
||||
return images
|
||||
}
|
||||
|
||||
xlog.Debug("Scanning images in app mention event", "messages", messages)
|
||||
for _, msg := range messages {
|
||||
if len(msg.Files) == 0 {
|
||||
xlog.Debug("No files in message", "message", msg.Text)
|
||||
continue
|
||||
}
|
||||
xlog.Debug("Files in message", "files", msg.Files)
|
||||
for _, attachment := range msg.Files {
|
||||
if attachment.URLPrivate != "" {
|
||||
xlog.Debug(fmt.Sprintf("Getting Attachment: %+v", attachment))
|
||||
// download image with slack api
|
||||
imageBytes := new(bytes.Buffer)
|
||||
if err := api.GetFile(attachment.URLPrivate, imageBytes); err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error downloading image: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
images = append(images, ImageData{
|
||||
Data: imageBytes.Bytes(),
|
||||
MimeType: attachment.Mimetype,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
// scanImagesInThreadMessage scans for all images in a single thread message
|
||||
func scanImagesInThreadMessage(api *slack.Client, msg slack.Message) []ImageData {
|
||||
var images []ImageData
|
||||
|
||||
if len(msg.Files) == 0 {
|
||||
return images
|
||||
}
|
||||
|
||||
xlog.Debug("found files in the message", "files", len(msg.Files))
|
||||
for _, attachment := range msg.Files {
|
||||
if attachment.URLPrivate != "" {
|
||||
xlog.Debug(fmt.Sprintf("Getting Attachment: %+v", attachment))
|
||||
// download image with slack api
|
||||
imageBytes := new(bytes.Buffer)
|
||||
if err := api.GetFile(attachment.URLPrivate, imageBytes); err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error downloading image: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
images = append(images, ImageData{
|
||||
Data: imageBytes.Bytes(),
|
||||
MimeType: attachment.Mimetype,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
// createMultiContentMessage creates a ChatCompletionMessage with text and multiple images
|
||||
func createMultiContentMessage(role, text string, images []ImageData) openai.ChatCompletionMessage {
|
||||
multiContent := []openai.ChatMessagePart{
|
||||
{
|
||||
Text: text,
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
},
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
imgBase64, err := encodeImageFromBytes(img.Data)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error encoding image to base64: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
multiContent = append(multiContent, openai.ChatMessagePart{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: fmt.Sprintf("data:%s;base64,%s", img.MimeType, imgBase64),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return openai.ChatCompletionMessage{
|
||||
Role: role,
|
||||
MultiContent: multiContent,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeImageFromBytes encodes image bytes to base64
|
||||
func encodeImageFromBytes(imageData []byte) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(imageData), nil
|
||||
}
|
||||
|
||||
func (t *Slack) handleChannelMessage(
|
||||
@@ -266,37 +389,15 @@ func (t *Slack) handleChannelMessage(
|
||||
|
||||
go func() {
|
||||
|
||||
imageBytes, mimeType := scanImagesInMessages(api, ev)
|
||||
images := scanImagesInMessages(api, ev)
|
||||
|
||||
agentOptions := []types.JobOption{
|
||||
types.WithUUID(ev.ThreadTimeStamp),
|
||||
}
|
||||
|
||||
// If the last message has an image, we send it as a multi content message
|
||||
if len(imageBytes.Bytes()) > 0 {
|
||||
// // Encode the image to base64
|
||||
imgBase64, err := encodeImageFromURL(*imageBytes)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error encoding image to base64: %v", err))
|
||||
} else {
|
||||
currentConv = append(currentConv,
|
||||
openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Text: message,
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: fmt.Sprintf("data:%s;base64,%s", mimeType, imgBase64),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
if len(images) > 0 {
|
||||
currentConv = append(currentConv, createMultiContentMessage("user", message, images))
|
||||
} else {
|
||||
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
@@ -367,14 +468,6 @@ func (t *Slack) handleChannelMessage(
|
||||
}()
|
||||
}
|
||||
|
||||
// Function to download the image from a URL and encode it to base64
|
||||
func encodeImageFromURL(imageBytes bytes.Buffer) (string, error) {
|
||||
|
||||
// Encode the image data to base64
|
||||
base64Image := base64.StdEncoding.EncodeToString(imageBytes.Bytes())
|
||||
return base64Image, nil
|
||||
}
|
||||
|
||||
func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackevents.MessageEvent, postMessageParams slack.PostMessageParameters, res *types.JobResult) {
|
||||
if len(finalResponse) > 4000 {
|
||||
// split response in multiple messages, and update the first
|
||||
@@ -528,7 +621,7 @@ func (t *Slack) handleMention(
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error fetching thread messages: %v", err))
|
||||
} else {
|
||||
for i, msg := range messages {
|
||||
for _, msg := range messages {
|
||||
// Skip our placeholder message
|
||||
if msg.Timestamp == msgTs {
|
||||
continue
|
||||
@@ -539,52 +632,19 @@ func (t *Slack) handleMention(
|
||||
role = "user"
|
||||
}
|
||||
|
||||
imageBytes := new(bytes.Buffer)
|
||||
mimeType := "image/jpeg"
|
||||
images := scanImagesInThreadMessage(api, msg)
|
||||
|
||||
xlog.Debug(fmt.Sprintf("Message: %+v", msg))
|
||||
if len(msg.Files) > 0 {
|
||||
for _, attachment := range msg.Files {
|
||||
|
||||
if attachment.URLPrivate != "" {
|
||||
xlog.Debug(fmt.Sprintf("Getting Attachment: %+v", attachment))
|
||||
mimeType = attachment.Mimetype
|
||||
// download image with slack api
|
||||
if err := api.GetFile(attachment.URLPrivate, imageBytes); err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error downloading image: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the last message has an image, we send it as a multi content message
|
||||
if len(imageBytes.Bytes()) > 0 && i == len(messages)-1 {
|
||||
if len(images) > 0 {
|
||||
|
||||
// // Encode the image to base64
|
||||
imgBase64, err := encodeImageFromURL(*imageBytes)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error encoding image to base64: %v", err))
|
||||
}
|
||||
xlog.Debug("found image in an existing thread", "image", len(images))
|
||||
|
||||
threadMessages = append(
|
||||
threadMessages,
|
||||
openai.ChatCompletionMessage{
|
||||
Role: role,
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Text: replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(msg.Text, b)),
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: fmt.Sprintf("data:%s;base64,%s", mimeType, imgBase64),
|
||||
// URL: imgUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createMultiContentMessage(role, replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(msg.Text, b)), images),
|
||||
)
|
||||
} else {
|
||||
xlog.Debug("no image in the last message of the thread", "message", msg.Text)
|
||||
threadMessages = append(
|
||||
threadMessages,
|
||||
openai.ChatCompletionMessage{
|
||||
@@ -597,62 +657,15 @@ func (t *Slack) handleMention(
|
||||
}
|
||||
} else {
|
||||
|
||||
imageBytes := new(bytes.Buffer)
|
||||
mimeType := "image/jpeg"
|
||||
|
||||
// Fetch the message using the API
|
||||
messages, _, _, err := api.GetConversationReplies(&slack.GetConversationRepliesParameters{
|
||||
ChannelID: ev.Channel,
|
||||
Timestamp: ev.TimeStamp,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error fetching messages: %v", err))
|
||||
} else {
|
||||
for _, msg := range messages {
|
||||
if len(msg.Files) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, attachment := range msg.Files {
|
||||
if attachment.URLPrivate != "" {
|
||||
xlog.Debug(fmt.Sprintf("Getting Attachment: %+v", attachment))
|
||||
// download image with slack api
|
||||
mimeType = attachment.Mimetype
|
||||
if err := api.GetFile(attachment.URLPrivate, imageBytes); err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error downloading image: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
images := scanImagesInAppMentionEvent(api, ev)
|
||||
|
||||
// If the last message has an image, we send it as a multi content message
|
||||
if len(imageBytes.Bytes()) > 0 {
|
||||
|
||||
// // Encode the image to base64
|
||||
imgBase64, err := encodeImageFromURL(*imageBytes)
|
||||
if err != nil {
|
||||
xlog.Error(fmt.Sprintf("Error encoding image to base64: %v", err))
|
||||
}
|
||||
if len(images) > 0 {
|
||||
|
||||
xlog.Debug("found image in the last message of the thread", "image", len(images))
|
||||
threadMessages = append(
|
||||
threadMessages,
|
||||
openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Text: replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(message, b)),
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
// URL: imgURL,
|
||||
URL: fmt.Sprintf("data:%s;base64,%s", mimeType, imgBase64),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createMultiContentMessage("user", replaceUserIDsWithNamesInMessage(api, cleanUpUsernameFromMessage(message, b)), images),
|
||||
)
|
||||
} else {
|
||||
threadMessages = append(threadMessages, openai.ChatCompletionMessage{
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
@@ -54,6 +55,68 @@ func (t *Telegram) isBotMentioned(message string, botUsername string) bool {
|
||||
return strings.Contains(message, "@"+botUsername)
|
||||
}
|
||||
|
||||
func (t *Telegram) chatFromMessage(update *models.Update) (openai.ChatCompletionMessage, error) {
|
||||
|
||||
if len(update.Message.Photo) == 0 {
|
||||
return openai.ChatCompletionMessage{
|
||||
Content: update.Message.Text,
|
||||
Role: "user",
|
||||
}, nil
|
||||
}
|
||||
|
||||
xlog.Debug("Image", "found image")
|
||||
// Get the largest photo
|
||||
photo := update.Message.Photo[len(update.Message.Photo)-1]
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
// Download the photo
|
||||
file, err := t.bot.GetFile(ctx, &bot.GetFileParams{
|
||||
FileID: photo.FileID,
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error("Error getting file", "error", err)
|
||||
} else {
|
||||
// Construct the full URL for downloading the file
|
||||
fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", t.Token, file.FilePath)
|
||||
|
||||
// Download the file content
|
||||
resp, err := http.Get(fileURL)
|
||||
if err != nil {
|
||||
xlog.Error("Error downloading file", "error", err)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
imageBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading image", "error", err)
|
||||
} else {
|
||||
// Encode to base64
|
||||
imgBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
xlog.Debug("Image", "sending encoded image")
|
||||
// Add to conversation as multi-content message
|
||||
return openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Text: update.Message.Caption,
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: fmt.Sprintf("data:image/jpeg;base64,%s", imgBase64),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return openai.ChatCompletionMessage{}, errors.New("no image found")
|
||||
}
|
||||
|
||||
// handleGroupMessage handles messages in group chats
|
||||
func (t *Telegram) handleGroupMessage(ctx context.Context, b *bot.Bot, a *agent.Agent, update *models.Update) {
|
||||
xlog.Debug("Handling group message", "update", update)
|
||||
@@ -83,11 +146,9 @@ func (t *Telegram) handleGroupMessage(ctx context.Context, b *bot.Bot, a *agent.
|
||||
// Cancel any active job for this chat before starting a new one
|
||||
t.cancelActiveJobForChat(update.Message.Chat.ID)
|
||||
|
||||
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.Chat.ID))
|
||||
|
||||
// Clean up the message by removing bot mentions
|
||||
message := strings.ReplaceAll(update.Message.Text, "@"+botInfo.Username, "")
|
||||
message = strings.TrimSpace(message)
|
||||
update.Message.Text = strings.TrimSpace(message)
|
||||
|
||||
// Send initial placeholder message
|
||||
msg, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||
@@ -115,62 +176,18 @@ func (t *Telegram) handleGroupMessage(ctx context.Context, b *bot.Bot, a *agent.
|
||||
"chatID": update.Message.Chat.ID,
|
||||
}
|
||||
|
||||
// Handle images if present
|
||||
if len(update.Message.Photo) > 0 {
|
||||
// Get the largest photo
|
||||
photo := update.Message.Photo[len(update.Message.Photo)-1]
|
||||
|
||||
// Download the photo
|
||||
file, err := b.GetFile(ctx, &bot.GetFileParams{
|
||||
FileID: photo.FileID,
|
||||
})
|
||||
if err != nil {
|
||||
xlog.Error("Error getting file", "error", err)
|
||||
} else {
|
||||
// Download the file content
|
||||
resp, err := http.Get(file.FilePath)
|
||||
if err != nil {
|
||||
xlog.Error("Error downloading file", "error", err)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
imageBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading image", "error", err)
|
||||
} else {
|
||||
// Encode to base64
|
||||
imgBase64 := base64.StdEncoding.EncodeToString(imageBytes)
|
||||
|
||||
// Add to conversation as multi-content message
|
||||
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
MultiContent: []openai.ChatMessagePart{
|
||||
{
|
||||
Text: message,
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
},
|
||||
{
|
||||
Type: openai.ChatMessagePartTypeImageURL,
|
||||
ImageURL: &openai.ChatMessageImageURL{
|
||||
URL: fmt.Sprintf("data:image/jpeg;base64,%s", imgBase64),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||
Content: message,
|
||||
Role: "user",
|
||||
})
|
||||
chatMessage, err := t.chatFromMessage(update)
|
||||
if err != nil {
|
||||
xlog.Error("Error extracting chat message", "error", err)
|
||||
}
|
||||
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("telegram:%d", update.Message.Chat.ID),
|
||||
currentConv[len(currentConv)-1],
|
||||
chatMessage,
|
||||
)
|
||||
|
||||
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.Chat.ID))
|
||||
|
||||
// Create a new job with the conversation history and metadata
|
||||
job := types.NewJob(
|
||||
types.WithConversationHistory(currentConv),
|
||||
@@ -486,17 +503,18 @@ func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent,
|
||||
t.cancelActiveJobForChat(update.Message.Chat.ID)
|
||||
|
||||
currentConv := a.SharedState().ConversationTracker.GetConversation(fmt.Sprintf("telegram:%d", update.Message.From.ID))
|
||||
currentConv = append(currentConv, openai.ChatCompletionMessage{
|
||||
Content: update.Message.Text,
|
||||
Role: "user",
|
||||
})
|
||||
|
||||
message, err := t.chatFromMessage(update)
|
||||
if err != nil {
|
||||
xlog.Error("Error extracting chat message", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentConv = append(currentConv, message)
|
||||
|
||||
a.SharedState().ConversationTracker.AddMessage(
|
||||
fmt.Sprintf("telegram:%d", update.Message.From.ID),
|
||||
openai.ChatCompletionMessage{
|
||||
Content: update.Message.Text,
|
||||
Role: "user",
|
||||
},
|
||||
message,
|
||||
)
|
||||
|
||||
// Send initial placeholder message
|
||||
|
||||
+129
-20
@@ -1,10 +1,15 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/mudler/LocalAGI/services/actions"
|
||||
"github.com/mudler/LocalAGI/services/prompts"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
@@ -13,36 +18,140 @@ import (
|
||||
|
||||
const (
|
||||
DynamicPromptCustom = "custom"
|
||||
DynamicPromptMemory = "memory"
|
||||
)
|
||||
|
||||
var AvailableBlockPrompts = []string{
|
||||
DynamicPromptCustom,
|
||||
DynamicPromptMemory,
|
||||
}
|
||||
|
||||
func DynamicPromptsConfigMeta() []config.FieldGroup {
|
||||
return []config.FieldGroup{
|
||||
prompts.NewDynamicPromptConfigMeta(),
|
||||
type dynamicPrompt struct {
|
||||
agent.DynamicPrompt
|
||||
Name string
|
||||
}
|
||||
|
||||
func dynamicPrompts(customDirectory string, existingConfigs map[string]map[string]string) (allPrompts []dynamicPrompt) {
|
||||
files, err := os.ReadDir(customDirectory)
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom actions directory", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func DynamicPrompts(a *state.AgentConfig) []agent.DynamicPrompt {
|
||||
promptblocks := []agent.DynamicPrompt{}
|
||||
|
||||
for _, c := range a.DynamicPrompts {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(c.Config), &config); err != nil {
|
||||
xlog.Info("Error unmarshalling connector config", err)
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".go" {
|
||||
continue
|
||||
}
|
||||
switch c.Type {
|
||||
case DynamicPromptCustom:
|
||||
prompt, err := prompts.NewDynamicPrompt(config, "")
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom prompt", "error", err)
|
||||
continue
|
||||
}
|
||||
promptblocks = append(promptblocks, prompt)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(customDirectory, file.Name()))
|
||||
if err != nil {
|
||||
xlog.Error("Error reading custom action file", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
dynamicPromptName := strings.TrimSuffix(file.Name(), ".go")
|
||||
|
||||
dynamicPromptConfig := map[string]string{
|
||||
"name": dynamicPromptName,
|
||||
"code": string(content),
|
||||
}
|
||||
|
||||
if c, exists := existingConfigs[dynamicPromptName]; exists {
|
||||
dynamicPromptConfig["configuration"] = c["configuration"]
|
||||
}
|
||||
|
||||
a, err := prompts.NewDynamicCustomPrompt(dynamicPromptConfig, "")
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom dynamic prompt", "error", err, "file", file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if !a.CanRender() {
|
||||
continue
|
||||
}
|
||||
|
||||
allPrompts = append(allPrompts, dynamicPrompt{
|
||||
DynamicPrompt: a,
|
||||
Name: dynamicPromptName,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DynamicPromptsConfigMeta(customDirectory string) []config.FieldGroup {
|
||||
defaultDynamicPrompts := []config.FieldGroup{
|
||||
prompts.NewDynamicPromptConfigMeta(),
|
||||
prompts.NewMemoryPromptConfigMeta(),
|
||||
}
|
||||
|
||||
if customDirectory != "" {
|
||||
prompts := dynamicPrompts(customDirectory, map[string]map[string]string{})
|
||||
for _, p := range prompts {
|
||||
defaultDynamicPrompts = append(defaultDynamicPrompts, config.FieldGroup{
|
||||
Name: p.Name,
|
||||
Label: p.Name,
|
||||
Fields: []config.Field{
|
||||
{
|
||||
Name: "configuration",
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration for the custom prompt",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return defaultDynamicPrompts
|
||||
}
|
||||
|
||||
func DynamicPrompts(dynamicConfig map[string]string) func(*state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []agent.DynamicPrompt {
|
||||
return func(a *state.AgentConfig) func(ctx context.Context, pool *state.AgentPool) []agent.DynamicPrompt {
|
||||
return func(ctx context.Context, pool *state.AgentPool) []agent.DynamicPrompt {
|
||||
customDirectory := dynamicConfig[CustomActionsDir]
|
||||
|
||||
existingDynamicPromptsConfigs := map[string]map[string]string{}
|
||||
for _, c := range a.DynamicPrompts {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(c.Config), &config); err != nil {
|
||||
xlog.Info("Error unmarshalling connector config", err)
|
||||
continue
|
||||
}
|
||||
|
||||
existingDynamicPromptsConfigs[c.Type] = config
|
||||
}
|
||||
|
||||
dynamicPromptsFound := dynamicPrompts(customDirectory, existingDynamicPromptsConfigs)
|
||||
|
||||
memoryFilePath := memoryPath(a.Name, dynamicConfig)
|
||||
promptblocks := []agent.DynamicPrompt{}
|
||||
|
||||
for _, c := range a.DynamicPrompts {
|
||||
config := existingDynamicPromptsConfigs[c.Type]
|
||||
|
||||
switch c.Type {
|
||||
case DynamicPromptCustom:
|
||||
prompt, err := prompts.NewDynamicCustomPrompt(config, "")
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom prompt", "error", err)
|
||||
continue
|
||||
}
|
||||
promptblocks = append(promptblocks, prompt)
|
||||
case DynamicPromptMemory:
|
||||
_, memory, _ := actions.NewMemoryActions(memoryFilePath, dynamicConfig)
|
||||
|
||||
promptblocks = append(promptblocks,
|
||||
prompts.NewMemoryPrompt(config, memory),
|
||||
)
|
||||
default:
|
||||
// Check if we have configured a custom dynamic prompt coming from a directory
|
||||
for _, p := range dynamicPromptsFound {
|
||||
if p.Name == c.Type {
|
||||
promptblocks = append(promptblocks, p.DynamicPrompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return promptblocks
|
||||
}
|
||||
}
|
||||
return promptblocks
|
||||
}
|
||||
|
||||
+51
-15
@@ -2,23 +2,26 @@ package prompts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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/traefik/yaegi/interp"
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
)
|
||||
|
||||
type DynamicPrompt struct {
|
||||
type DynamicCustomPrompt struct {
|
||||
config map[string]string
|
||||
goPkgPath string
|
||||
i *interp.Interpreter
|
||||
}
|
||||
|
||||
func NewDynamicPrompt(config map[string]string, goPkgPath string) (*DynamicPrompt, error) {
|
||||
a := &DynamicPrompt{
|
||||
func NewDynamicCustomPrompt(config map[string]string, goPkgPath string) (*DynamicCustomPrompt, error) {
|
||||
a := &DynamicCustomPrompt{
|
||||
config: config,
|
||||
goPkgPath: goPkgPath,
|
||||
}
|
||||
@@ -34,19 +37,20 @@ func NewDynamicPrompt(config map[string]string, goPkgPath string) (*DynamicPromp
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *DynamicPrompt) callInit() error {
|
||||
func (a *DynamicCustomPrompt) callInit() error {
|
||||
if a.i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Init", a.config["name"]))
|
||||
if err != nil {
|
||||
return err
|
||||
xlog.Warn("No init function found for custom prompt", "error", err, "action", a.config["name"])
|
||||
return nil
|
||||
}
|
||||
|
||||
run := v.Interface().(func() error)
|
||||
run := v.Interface().(func(string) error)
|
||||
|
||||
return run()
|
||||
return run(a.config["configuration"])
|
||||
}
|
||||
|
||||
func NewDynamicPromptConfigMeta() config.FieldGroup {
|
||||
@@ -77,16 +81,23 @@ func NewDynamicPromptConfigMeta() config.FieldGroup {
|
||||
Required: false,
|
||||
HelpText: "Enable if the code needs to use unsafe Go features",
|
||||
},
|
||||
{
|
||||
Name: "configuration",
|
||||
Label: "Configuration",
|
||||
Type: config.FieldTypeTextarea,
|
||||
HelpText: "Configuration for the custom prompt",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *DynamicPrompt) initializeInterpreter() error {
|
||||
func (a *DynamicCustomPrompt) initializeInterpreter() error {
|
||||
if _, exists := a.config["code"]; exists && a.i == nil {
|
||||
unsafe := strings.ToLower(a.config["unsafe"]) == "true"
|
||||
i := interp.New(interp.Options{
|
||||
GoPath: a.goPkgPath,
|
||||
Unrestricted: unsafe,
|
||||
Env: os.Environ(),
|
||||
})
|
||||
if err := i.Use(stdlib.Symbols); err != nil {
|
||||
return err
|
||||
@@ -96,6 +107,15 @@ func (a *DynamicPrompt) initializeInterpreter() error {
|
||||
a.config["name"] = "custom"
|
||||
}
|
||||
|
||||
// let's find first if there is already a package declarated in the code
|
||||
// the user might want to specify it to not break syntax with IDEs
|
||||
re := regexp.MustCompile("package (\\w+)")
|
||||
packageName := re.FindStringSubmatch(a.config["code"])
|
||||
if len(packageName) > 1 {
|
||||
// remove it from the code, normalize to `name`
|
||||
a.config["code"] = re.ReplaceAllString(a.config["code"], "")
|
||||
}
|
||||
|
||||
_, err := i.Eval(fmt.Sprintf("package %s\n%s", a.config["name"], a.config["code"]))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -107,18 +127,34 @@ func (a *DynamicPrompt) initializeInterpreter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *DynamicPrompt) Render(c *agent.Agent) (string, error) {
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"]))
|
||||
func (a *DynamicCustomPrompt) CanRender() bool {
|
||||
_, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"]))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return false
|
||||
}
|
||||
|
||||
run := v.Interface().(func() (string, error))
|
||||
|
||||
return run()
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *DynamicPrompt) Role() string {
|
||||
func (a *DynamicCustomPrompt) Render(c *agent.Agent) (types.PromptResult, error) {
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Render", a.config["name"]))
|
||||
if err != nil {
|
||||
return types.PromptResult{}, err
|
||||
}
|
||||
|
||||
run := v.Interface().(func() (string, string, error))
|
||||
content, image, err := run()
|
||||
if err != nil {
|
||||
return types.PromptResult{}, err
|
||||
}
|
||||
|
||||
return types.PromptResult{
|
||||
Content: content,
|
||||
ImageBase64: image,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *DynamicCustomPrompt) Role() string {
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Role", a.config["name"]))
|
||||
if err != nil {
|
||||
return "system"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/agent"
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
)
|
||||
|
||||
type MemoryLayer interface {
|
||||
Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error)
|
||||
}
|
||||
type MemoryPrompt struct {
|
||||
config map[string]string
|
||||
memory MemoryLayer
|
||||
}
|
||||
|
||||
func NewMemoryPrompt(config map[string]string, memory MemoryLayer) *MemoryPrompt {
|
||||
return &MemoryPrompt{
|
||||
config: config,
|
||||
memory: memory,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMemoryPromptConfigMeta() config.FieldGroup {
|
||||
return config.FieldGroup{
|
||||
Name: "memory",
|
||||
Label: "Memory",
|
||||
}
|
||||
}
|
||||
|
||||
func (a *MemoryPrompt) Render(c *agent.Agent) (types.PromptResult, error) {
|
||||
result, err := a.memory.Run(c.Context(), c.SharedState(), types.ActionParams{})
|
||||
if err != nil {
|
||||
return types.PromptResult{}, err
|
||||
}
|
||||
|
||||
return types.PromptResult{
|
||||
Content: result.Result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *MemoryPrompt) Role() string {
|
||||
return "system"
|
||||
}
|
||||
+317
-3
@@ -1,10 +1,15 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
localagi "github.com/mudler/LocalAGI/pkg/client"
|
||||
"github.com/mudler/LocalAGI/pkg/utils/ptr"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -15,23 +20,332 @@ var _ = Describe("Agent test", func() {
|
||||
BeforeEach(func() {
|
||||
Eventually(func() error {
|
||||
// test apiURL is working and available
|
||||
_, err := http.Get(apiURL + "/readyz")
|
||||
_, err := http.Get(localagiURL + "/readyz")
|
||||
return err
|
||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||
|
||||
client := localagi.NewClient(localagiURL, "", time.Minute)
|
||||
err := client.DeleteAgent("testagent1")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("create agent", func() {
|
||||
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
|
||||
|
||||
err := client.CreateAgent(&localagi.AgentConfig{
|
||||
Name: "testagent",
|
||||
Name: "testagent1",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
result, err := client.SimpleAIResponse("testagent", "hello")
|
||||
result, err := client.SimpleAIResponse("testagent1", "hello")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(result).ToNot(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("Can do user defined tool calls", Ordered, func() {
|
||||
BeforeAll(func() {
|
||||
Eventually(func() error {
|
||||
// test apiURL is working and available
|
||||
_, err := http.Get(localagiURL + "/readyz")
|
||||
return err
|
||||
}, "10m", "10s").ShouldNot(HaveOccurred())
|
||||
|
||||
client := localagi.NewClient(localagiURL, "", time.Minute)
|
||||
err := client.DeleteAgent("testagent2")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = client.CreateAgent(&localagi.AgentConfig{
|
||||
Name: "testagent2",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("can create a task", func() {
|
||||
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
|
||||
|
||||
req := localagi.RequestBody{
|
||||
Model: "testagent2",
|
||||
Input: "Create an appointment next week on wednesday at 10:00 am for the whole day. The topic is about AI and you include ABC and DEF to the appointment.",
|
||||
Tools: []localagi.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Name: ptr.To("CreateTask"),
|
||||
Description: ptr.To("Write the needed details whenever you're asked to create something like an info, appointment, e-mail or when you're asked to remind of anything or create a remainder. Also use this if you're supposed to answer an e-mail."),
|
||||
Parameters: ptr.To(jsonschema.Definition{
|
||||
Type: "object",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"task": {
|
||||
Type: "string",
|
||||
Description: "Look for the name of the task you're supposed to do or create ",
|
||||
Enum: []string{
|
||||
"appointment",
|
||||
"E-mail",
|
||||
},
|
||||
},
|
||||
"subject": {
|
||||
Type: "string",
|
||||
Description: "A subject the task is about. Infer this from the given context data and user prompt.",
|
||||
},
|
||||
"reply": {
|
||||
Type: "string",
|
||||
Description: "A sharp and short reply to the contextual data given. Use a friendly and neutral general greeting.",
|
||||
},
|
||||
"recipient": {
|
||||
Type: "array",
|
||||
Description: "A list of names and abbreviations to send our task to. Abbreviations always have to match exactly. If the user gives you first names you can deduce the last name.",
|
||||
Items: &jsonschema.Definition{
|
||||
Type: "string",
|
||||
Enum: []string{
|
||||
"ABC",
|
||||
"DEF",
|
||||
},
|
||||
},
|
||||
},
|
||||
"datestart": {
|
||||
Type: "string",
|
||||
Description: "The date and time when the task should start. Discard any older dates than today. Use tomorrow as default. Use the format DD/MM/YYYY HH:MM",
|
||||
},
|
||||
"dateend": {
|
||||
Type: "string",
|
||||
Description: "The date and time when a meeting should end. Default to start date. If the duration of an appointment is given, calculate the end with the start date. Use the format DD/MM/YYYY HH:MM",
|
||||
},
|
||||
"datedone": {
|
||||
Type: "string",
|
||||
Description: "The date and time when the task should be done. Use the format DD/MM/YYYY HH:MM",
|
||||
},
|
||||
"private": {
|
||||
Type: "boolean",
|
||||
Description: "Whether the task should be private or not. Default to false.",
|
||||
},
|
||||
"includeall": {
|
||||
Type: "boolean",
|
||||
Description: "Whether the task should include every mentioned person or not. Default to true. If you find explicitly mentioned people in the prompt whilst ignoring the contextual xml schema you choose false unless it is mentioned that you should include everyone.",
|
||||
},
|
||||
"wholedayappointment": {
|
||||
Type: "boolean",
|
||||
Description: "Whether the appointment should be done for the whole days. Default to false unless mentioned by the user prompt. Ignore the xml schema for this.",
|
||||
},
|
||||
"remainder": {
|
||||
Type: "boolean",
|
||||
Description: "Whether you are explicitly supposed to remind of something or not. Default to false. Ignore the xml schema for this.",
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"task",
|
||||
"recipient",
|
||||
"datestart",
|
||||
"dateend",
|
||||
"datedone",
|
||||
"private",
|
||||
"wholedayappointment",
|
||||
"remainder",
|
||||
"subject",
|
||||
"reply",
|
||||
"includeall",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}}
|
||||
result, err := client.GetAIResponse(&req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(result).ToNot(BeNil())
|
||||
|
||||
var call localagi.ResponseFunctionToolCall
|
||||
var args struct {
|
||||
Task string `json:"task"`
|
||||
Subject string `json:"subject"`
|
||||
Reply string `json:"reply"`
|
||||
Recipient []string `json:"recipient"`
|
||||
DateStart string `json:"datestart"`
|
||||
DateEnd string `json:"dateend"`
|
||||
DateDone string `json:"datedone"`
|
||||
Private bool `json:"private"`
|
||||
IncludeAll bool `json:"includeall"`
|
||||
WholeDayAppointment bool `json:"wholedayappointment"`
|
||||
Remainder bool `json:"remainder"`
|
||||
}
|
||||
|
||||
for _, out := range result.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err == nil && msg.Role == "assistant" {
|
||||
xlog.Info("Agent returned message", "message", msg)
|
||||
Expect(len(result.Output)).To(BeNumerically(">", 1))
|
||||
continue
|
||||
}
|
||||
fnc, err := out.ToFunctionToolCall()
|
||||
call = fnc
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(fnc.Type)).To(Equal("function_call"))
|
||||
Expect(fnc.Name).To(Equal("CreateTask"))
|
||||
|
||||
err = json.Unmarshal([]byte(fnc.Arguments), &args)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(args.Task).To(Equal("appointment"))
|
||||
Expect(args.Subject).ToNot(BeEmpty())
|
||||
Expect(args.Reply).ToNot(BeEmpty())
|
||||
}
|
||||
|
||||
req = localagi.RequestBody{
|
||||
Model: "testagent2",
|
||||
Input: []any{
|
||||
localagi.InputMessage{
|
||||
Type: "message",
|
||||
Role: "user",
|
||||
Content: "Create an appointment next week on wednesday at 10:00 am for the whole day. The topic is about AI and you include ABC and DEF to the appointment.",
|
||||
},
|
||||
call,
|
||||
localagi.InputFunctionToolCallOutput{
|
||||
Type: "function_call_output",
|
||||
CallID: call.CallID,
|
||||
Output: fmt.Sprintf("Successfully created %s: %s", args.Task, args.Subject),
|
||||
},
|
||||
localagi.InputMessage{
|
||||
Type: "message",
|
||||
Role: "user",
|
||||
Content: "Was the appointment created?",
|
||||
},
|
||||
},
|
||||
Tools: []localagi.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Name: ptr.To("ChooseAnswer"),
|
||||
Description: ptr.To("Select Yes or No"),
|
||||
Parameters: ptr.To(jsonschema.Definition{
|
||||
Type: "object",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"answer": {
|
||||
Type: "boolean",
|
||||
Description: "Set true for Yes and false for no",
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"answer",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
ToolChoice: &localagi.ToolChoice{
|
||||
Type: "function",
|
||||
Name: "ChooseAnswer",
|
||||
},
|
||||
}
|
||||
result, err = client.GetAIResponse(&req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(result.Output)).To(BeNumerically(">", 0))
|
||||
fnc, err := result.Output[len(result.Output)-1].ToFunctionToolCall()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(fnc.Arguments).To(ContainSubstring("true"))
|
||||
})
|
||||
|
||||
It("can tool call; web search", func() {
|
||||
client := localagi.NewClient(localagiURL, "", 5*time.Minute)
|
||||
|
||||
req := localagi.RequestBody{
|
||||
Model: "testagent2",
|
||||
Input: "What's the weather like in Berlin?",
|
||||
Tools: []localagi.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Name: ptr.To("WebSearch"),
|
||||
Description: ptr.To("Search the internet for up-to-date information"),
|
||||
Parameters: ptr.To(jsonschema.Definition{
|
||||
Type: "object",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"SearchPrompt": {
|
||||
Type: "string",
|
||||
Description: "The query that is sent to a search engine",
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"SearchPrompt",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}}
|
||||
result, err := client.GetAIResponse(&req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(result).ToNot(BeNil())
|
||||
|
||||
var call localagi.ResponseFunctionToolCall
|
||||
var args struct {
|
||||
SearchPrompt string `json:"SearchPrompt"`
|
||||
}
|
||||
|
||||
for _, out := range result.Output {
|
||||
msg, err := out.ToMessage()
|
||||
if err == nil && msg.Role == "assistant" {
|
||||
xlog.Info("Agent returned message", "message", msg)
|
||||
continue
|
||||
}
|
||||
fnc, err := out.ToFunctionToolCall()
|
||||
call = fnc
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(fnc.Type)).To(Equal("function_call"))
|
||||
Expect(fnc.Name).To(Equal("WebSearch"))
|
||||
|
||||
err = json.Unmarshal([]byte(fnc.Arguments), &args)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(args.SearchPrompt).To(ContainSubstring("Berlin"))
|
||||
}
|
||||
|
||||
req = localagi.RequestBody{
|
||||
Model: "testagent2",
|
||||
Input: []any{
|
||||
localagi.InputMessage{
|
||||
Type: "message",
|
||||
Role: "user",
|
||||
Content: "What's the weather like in Berlin compared to normal for Spring/Autumn?",
|
||||
},
|
||||
call,
|
||||
localagi.InputFunctionToolCallOutput{
|
||||
Type: "function_call_output",
|
||||
CallID: call.CallID,
|
||||
Output: "The current weather in Berlin: temp 30C!, heavy rain with risk of flooding, light wind, Humidity 90%, Pressure 900mb, visibility poor",
|
||||
},
|
||||
},
|
||||
Tools: []localagi.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Name: ptr.To("ChooseAnswer"),
|
||||
Description: ptr.To("Select a multiple choice answer from a predefined list"),
|
||||
Parameters: ptr.To(jsonschema.Definition{
|
||||
Type: "object",
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"answer": {
|
||||
Type: "string",
|
||||
Description: "Choice of answer",
|
||||
Enum: []string{
|
||||
"Very hot and wet",
|
||||
"Very hot and dry",
|
||||
"About Average temp and very wet",
|
||||
"About Average",
|
||||
"About Average temp and very dry",
|
||||
"Very cold and wet",
|
||||
"Very cold and dry",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"answer",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
ToolChoice: &localagi.ToolChoice{
|
||||
Type: "function",
|
||||
Name: "ChooseAnswer",
|
||||
},
|
||||
}
|
||||
result, err = client.GetAIResponse(&req)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(result.Output)).To(BeNumerically(">", 0))
|
||||
fnc, err := result.Output[len(result.Output)-1].ToFunctionToolCall()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(fnc.Arguments).To(ContainSubstring("Very hot and wet"))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
+111
-15
@@ -488,6 +488,63 @@ func (a *App) ListActions() func(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// createToolCallResponse generates a proper tool call response for user-defined actions
|
||||
func (a *App) createToolCallResponse(id, agentName string, actionState coreTypes.ActionState, conv []openai.ChatCompletionMessage) types.ResponseBody {
|
||||
// Create tool call ID
|
||||
toolCallID := fmt.Sprintf("call_%d", time.Now().UnixNano())
|
||||
|
||||
// Get function name and arguments
|
||||
functionName := actionState.Action.Definition().Name.String()
|
||||
argumentsJSON, err := json.Marshal(actionState.Params)
|
||||
if err != nil {
|
||||
xlog.Error("Error marshaling action params for tool call", "error", err)
|
||||
// Fallback to empty arguments
|
||||
argumentsJSON = []byte("{}")
|
||||
}
|
||||
|
||||
// Create message object with reasoning
|
||||
messageObj := types.ResponseMessage{
|
||||
Type: "message",
|
||||
ID: fmt.Sprintf("msg_%d", time.Now().UnixNano()),
|
||||
Status: "completed",
|
||||
Role: "assistant",
|
||||
Content: []types.MessageContentItem{
|
||||
{
|
||||
Type: "output_text",
|
||||
Text: actionState.Reasoning,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create function tool call object
|
||||
functionToolCall := types.FunctionToolCall{
|
||||
Arguments: string(argumentsJSON),
|
||||
CallID: toolCallID,
|
||||
Name: functionName,
|
||||
Type: "function_call",
|
||||
ID: fmt.Sprintf("tool_%d", time.Now().UnixNano()),
|
||||
Status: "completed",
|
||||
}
|
||||
|
||||
// Create response with both message and tool call in output array
|
||||
return types.ResponseBody{
|
||||
ID: id,
|
||||
Object: "response",
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Status: "completed",
|
||||
Model: agentName,
|
||||
Output: []interface{}{
|
||||
messageObj,
|
||||
functionToolCall,
|
||||
},
|
||||
Usage: types.UsageInfo{
|
||||
InputTokens: 0, // TODO: calculate actual usage
|
||||
OutputTokens: 0,
|
||||
TotalTokens: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Responses(pool *state.AgentPool, tracker *conversations.ConversationTracker[string]) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
var request types.RequestBody
|
||||
@@ -507,15 +564,38 @@ func (a *App) Responses(pool *state.AgentPool, tracker *conversations.Conversati
|
||||
agentName := request.Model
|
||||
messages := append(conv, request.ToChatCompletionMessages()...)
|
||||
|
||||
a := pool.GetAgent(agentName)
|
||||
if a == nil {
|
||||
agent := pool.GetAgent(agentName)
|
||||
if agent == nil {
|
||||
xlog.Info("Agent not found in pool", c.Params("name"))
|
||||
return c.Status(http.StatusInternalServerError).JSON(types.ResponseBody{Error: "Agent not found"})
|
||||
}
|
||||
|
||||
res := a.Ask(
|
||||
// Prepare job options
|
||||
jobOptions := []coreTypes.JobOption{
|
||||
coreTypes.WithConversationHistory(messages),
|
||||
)
|
||||
}
|
||||
|
||||
// Add tools if present in the request
|
||||
if len(request.Tools) > 0 {
|
||||
builtinTools, userTools := types.SeparateTools(request.Tools)
|
||||
if len(builtinTools) > 0 {
|
||||
jobOptions = append(jobOptions, coreTypes.WithBuiltinTools(builtinTools))
|
||||
xlog.Debug("Adding builtin tools to job", "count", len(builtinTools), "agent", agentName)
|
||||
}
|
||||
if len(userTools) > 0 {
|
||||
jobOptions = append(jobOptions, coreTypes.WithUserTools(userTools))
|
||||
xlog.Debug("Adding user tools to job", "count", len(userTools), "agent", agentName)
|
||||
}
|
||||
}
|
||||
|
||||
var choice types.ToolChoice
|
||||
if err := json.Unmarshal(request.ToolChoice, &choice); err == nil {
|
||||
if choice.Type == "function" {
|
||||
jobOptions = append(jobOptions, coreTypes.WithToolChoice(choice.Name))
|
||||
}
|
||||
}
|
||||
|
||||
res := agent.Ask(jobOptions...)
|
||||
if res.Error != nil {
|
||||
xlog.Error("Error asking agent", "agent", agentName, "error", res.Error)
|
||||
|
||||
@@ -524,28 +604,44 @@ func (a *App) Responses(pool *state.AgentPool, tracker *conversations.Conversati
|
||||
xlog.Info("we got a response from the agent", "agent", agentName, "response", res.Response)
|
||||
}
|
||||
|
||||
id := uuid.New().String()
|
||||
|
||||
// Check if this is a user-defined tool call
|
||||
if res.Response == "" && len(res.State) > 0 {
|
||||
// Get the last action from state
|
||||
lastAction := res.State[len(res.State)-1]
|
||||
if coreTypes.IsActionUserDefined(lastAction.Action) {
|
||||
xlog.Debug("Detected user-defined action, creating tool call response", "action", lastAction.Action.Definition().Name)
|
||||
|
||||
// Generate tool call response
|
||||
response := a.createToolCallResponse(id, agentName, lastAction, conv)
|
||||
tracker.SetConversation(id, conv) // Save conversation without adding assistant message
|
||||
return c.JSON(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Regular text response
|
||||
conv = append(conv, openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
Content: res.Response,
|
||||
})
|
||||
|
||||
id := uuid.New().String()
|
||||
|
||||
tracker.SetConversation(id, conv)
|
||||
|
||||
response := types.ResponseBody{
|
||||
ID: id,
|
||||
Object: "response",
|
||||
// "created_at": 1741476542,
|
||||
ID: id,
|
||||
Object: "response",
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Status: "completed",
|
||||
Output: []types.ResponseMessage{
|
||||
{
|
||||
Model: agentName,
|
||||
Output: []interface{}{
|
||||
types.ResponseMessage{
|
||||
Type: "message",
|
||||
ID: fmt.Sprintf("msg_%d", time.Now().UnixNano()),
|
||||
Status: "completed",
|
||||
Role: "assistant",
|
||||
Content: []types.MessageContentItem{
|
||||
types.MessageContentItem{
|
||||
{
|
||||
Type: "output_text",
|
||||
Text: res.Response,
|
||||
},
|
||||
@@ -641,13 +737,13 @@ func (a *App) CreateGroup(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// GetAgentConfigMeta returns the metadata for agent configuration fields
|
||||
func (a *App) GetAgentConfigMeta() func(c *fiber.Ctx) error {
|
||||
func (a *App) GetAgentConfigMeta(customDirectory string) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Create a new instance of AgentConfigMeta
|
||||
configMeta := state.NewAgentConfigMeta(
|
||||
services.ActionsConfigMeta(),
|
||||
services.ActionsConfigMeta(customDirectory),
|
||||
services.ConnectorsConfigMeta(),
|
||||
services.DynamicPromptsConfigMeta(),
|
||||
services.DynamicPromptsConfigMeta(customDirectory),
|
||||
services.FiltersConfigMeta(),
|
||||
)
|
||||
return c.JSON(configMeta)
|
||||
|
||||
@@ -14,6 +14,7 @@ type Config struct {
|
||||
LLMAPIKey string
|
||||
LLMModel string
|
||||
StateDir string
|
||||
CustomActionsDir string
|
||||
ConversationStoreDuration time.Duration
|
||||
}
|
||||
|
||||
@@ -59,6 +60,12 @@ func WithLLMAPIKey(key string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithCustomActionsDir(dir string) Option {
|
||||
return func(c *Config) {
|
||||
c.CustomActionsDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func WithPool(pool *state.AgentPool) Option {
|
||||
return func(c *Config) {
|
||||
c.Pool = pool
|
||||
|
||||
+285
-73
@@ -9,71 +9,73 @@
|
||||
"react-dom": "^19.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"eslint": "^9.27.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react-hooks": "^6.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.2.0",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"vite": "^6.3.5",
|
||||
"globals": "^16.3.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"vite": "^7.1.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.27.5", "", {}, "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
|
||||
"@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="],
|
||||
|
||||
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="],
|
||||
|
||||
@@ -125,23 +127,23 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.6.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw=="],
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
|
||||
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
|
||||
|
||||
"@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.27.0", "", {}, "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA=="],
|
||||
"@eslint/js": ["@eslint/js@9.35.0", "", {}, "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
@@ -151,7 +153,9 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
@@ -159,49 +163,51 @@
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.34", "", {}, "sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
@@ -211,17 +217,17 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="],
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="],
|
||||
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.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", "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg=="],
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.2", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.34", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
@@ -269,17 +275,17 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.27.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.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.27.0", "@eslint/plugin-kit": "^0.3.1", "@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-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q=="],
|
||||
"eslint": ["eslint@9.35.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@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.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.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-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg=="],
|
||||
|
||||
"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.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.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
@@ -295,7 +301,7 @@
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
@@ -311,7 +317,7 @@
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="],
|
||||
"globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
@@ -381,9 +387,9 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
@@ -395,13 +401,13 @@
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-router": ["react-router@7.6.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ=="],
|
||||
"react-router": ["react-router@7.7.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw=="],
|
||||
|
||||
"react-router-dom": ["react-router-dom@7.6.1", "", { "dependencies": { "react-router": "7.6.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA=="],
|
||||
"react-router-dom": ["react-router-dom@7.7.0", "", { "dependencies": { "react-router": "7.7.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
|
||||
"rollup": ["rollup@4.50.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.0", "@rollup/rollup-android-arm64": "4.50.0", "@rollup/rollup-darwin-arm64": "4.50.0", "@rollup/rollup-darwin-x64": "4.50.0", "@rollup/rollup-freebsd-arm64": "4.50.0", "@rollup/rollup-freebsd-x64": "4.50.0", "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", "@rollup/rollup-linux-arm-musleabihf": "4.50.0", "@rollup/rollup-linux-arm64-gnu": "4.50.0", "@rollup/rollup-linux-arm64-musl": "4.50.0", "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", "@rollup/rollup-linux-ppc64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-gnu": "4.50.0", "@rollup/rollup-linux-riscv64-musl": "4.50.0", "@rollup/rollup-linux-s390x-gnu": "4.50.0", "@rollup/rollup-linux-x64-gnu": "4.50.0", "@rollup/rollup-linux-x64-musl": "4.50.0", "@rollup/rollup-openharmony-arm64": "4.50.0", "@rollup/rollup-win32-arm64-msvc": "4.50.0", "@rollup/rollup-win32-ia32-msvc": "4.50.0", "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
@@ -419,7 +425,7 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
@@ -427,7 +433,7 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"vite": ["vite@6.3.5", "", { "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-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "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-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -441,12 +447,218 @@
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
|
||||
|
||||
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
"@ampproject/remapping/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@babel/core/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@babel/generator/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/parser/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/plugin-transform-private-methods/@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
|
||||
|
||||
"@babel/template/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
|
||||
"@babel/template/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||
|
||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@types/babel__core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@types/babel__generator/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@types/babel__template/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="],
|
||||
|
||||
"tinyglobby/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
|
||||
|
||||
"eslint-plugin-react-hooks/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@
|
||||
"highlight.js": "^11.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"eslint": "^9.27.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react-hooks": "^6.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.2.0",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"vite": "^6.3.5"
|
||||
"globals": "^16.3.0",
|
||||
"react-router-dom": "^7.7.0",
|
||||
"vite": "^7.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,28 +99,28 @@ const AgentForm = ({
|
||||
const handleAddDynamicPrompt = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
dynamicPrompts: [
|
||||
...(formData.dynamicPrompts || []),
|
||||
dynamic_prompts: [
|
||||
...(formData.dynamic_prompts || []),
|
||||
{ type: '', config: '{}' }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveDynamicPrompt = (index) => {
|
||||
const updatedDynamicPrompts = [...formData.dynamicPrompts];
|
||||
const updatedDynamicPrompts = [...formData.dynamic_prompts];
|
||||
updatedDynamicPrompts.splice(index, 1);
|
||||
setFormData({
|
||||
...formData,
|
||||
dynamicPrompts: updatedDynamicPrompts,
|
||||
dynamic_prompts: updatedDynamicPrompts,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDynamicPromptChange = (index, updatedPrompt) => {
|
||||
const updatedPrompts = [...formData.dynamicPrompts];
|
||||
const updatedPrompts = [...formData.dynamic_prompts];
|
||||
updatedPrompts[index] = updatedPrompt;
|
||||
setFormData({
|
||||
...formData,
|
||||
dynamicPrompts: updatedPrompts
|
||||
dynamic_prompts: updatedPrompts
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const PromptsGoalsSection = ({
|
||||
/>
|
||||
|
||||
<DynamicPromptForm
|
||||
prompts={formData.dynamicPrompts || []}
|
||||
prompts={formData.dynamic_prompts || []}
|
||||
onAddPrompt={onAddPrompt}
|
||||
onRemovePrompt={onRemovePrompt}
|
||||
onChange={handleDynamicPromptChange}
|
||||
|
||||
@@ -16,6 +16,14 @@ function ObservableSummary({ observable }) {
|
||||
creationChatMsg = lastMsg?.content || '';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:: Probably we have an array of two objects: [{type:"text", ...}, {type:"image_url", image_url: {url:"..."}}]
|
||||
// We could display the image and text along with multimedia icon
|
||||
if (typeof creationChatMsg === 'object') {
|
||||
console.log("Multimedia message?", creationChatMsg);
|
||||
creationChatMsg = 'Multimedia message';
|
||||
}
|
||||
|
||||
// FunctionDefinition summary
|
||||
let creationFunctionDef = '';
|
||||
if (creation?.function_definition?.name) {
|
||||
@@ -31,7 +39,14 @@ function ObservableSummary({ observable }) {
|
||||
const completion = observable?.completion || {};
|
||||
// ChatCompletionResponse summary
|
||||
let completionChatMsg = '';
|
||||
const chatCompletion = completion?.chat_completion_response;
|
||||
let chatCompletion = completion?.chat_completion_response;
|
||||
|
||||
if (!chatCompletion && Array.isArray(completion?.conversation) && completion.conversation.length > 0) {
|
||||
chatCompletion = { choices: completion.conversation.map(m => {
|
||||
return { message: m }
|
||||
}) }
|
||||
}
|
||||
|
||||
if (
|
||||
chatCompletion &&
|
||||
Array.isArray(chatCompletion.choices) &&
|
||||
@@ -62,14 +77,8 @@ function ObservableSummary({ observable }) {
|
||||
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) {
|
||||
@@ -100,11 +109,11 @@ function ObservableSummary({ observable }) {
|
||||
|
||||
// Only show if any summary is present
|
||||
if (!creationChatMsg && !creationFunctionDef && !creationFunctionParams &&
|
||||
!completionChatMsg && !completionConversation && !completionActionResult &&
|
||||
!completionChatMsg && !completionActionResult &&
|
||||
!completionAgentState && !completionError && !completionFilter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, margin: '2px 0 0 0' }}>
|
||||
{/* CREATION */}
|
||||
@@ -161,12 +170,6 @@ function ObservableSummary({ observable }) {
|
||||
<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>
|
||||
@@ -286,7 +289,6 @@ function AgentStatus() {
|
||||
|
||||
sse.addEventListener('observable_update', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(data);
|
||||
setObservableMap(prevMap => {
|
||||
const prev = prevMap[data.id] || {};
|
||||
const updated = {
|
||||
|
||||
+2
-2
@@ -183,10 +183,10 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
webapp.Put("/api/agent/:name/config", app.UpdateAgentConfig(pool))
|
||||
|
||||
// Metadata endpoint for agent configuration fields
|
||||
webapp.Get("/api/agent/config/metadata", app.GetAgentConfigMeta())
|
||||
webapp.Get("/api/agent/config/metadata", app.GetAgentConfigMeta(app.config.CustomActionsDir))
|
||||
|
||||
// Add endpoint for getting agent config metadata
|
||||
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta())
|
||||
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta(app.config.CustomActionsDir))
|
||||
|
||||
webapp.Post("/api/action/:name/definition", app.GetActionDefinition(pool))
|
||||
webapp.Post("/api/action/:name/run", app.ExecuteAction(pool))
|
||||
|
||||
+428
-48
@@ -1,13 +1,155 @@
|
||||
package types
|
||||
|
||||
import "github.com/sashabaranov/go-openai"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
coreTypes "github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/xlog"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
// Input represents either a string or a slice of Message
|
||||
type Input struct {
|
||||
Text *string `json:"-"`
|
||||
Messages *[]Message `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshaling for Input
|
||||
func (i *Input) UnmarshalJSON(data []byte) error {
|
||||
// Try to unmarshal as string first
|
||||
var text string
|
||||
if err := json.Unmarshal(data, &text); err == nil {
|
||||
i.Text = &text
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to unmarshal as []Message
|
||||
var messages []Message
|
||||
if err := json.Unmarshal(data, &messages); err == nil {
|
||||
i.Messages = &messages
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, &struct{}{}) // fallback to empty struct
|
||||
}
|
||||
|
||||
// MarshalJSON implements custom JSON marshaling for Input
|
||||
func (i *Input) MarshalJSON() ([]byte, error) {
|
||||
if i.Text != nil {
|
||||
return json.Marshal(*i.Text)
|
||||
}
|
||||
if i.Messages != nil {
|
||||
return json.Marshal(*i.Messages)
|
||||
}
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
// IsText returns true if the input contains text
|
||||
func (i *Input) IsText() bool {
|
||||
return i.Text != nil
|
||||
}
|
||||
|
||||
// IsMessages returns true if the input contains messages
|
||||
func (i *Input) IsMessages() bool {
|
||||
return i.Messages != nil
|
||||
}
|
||||
|
||||
// GetText returns the text value or empty string
|
||||
func (i *Input) GetText() string {
|
||||
if i.Text != nil {
|
||||
return *i.Text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetMessages returns the messages value or empty slice
|
||||
func (i *Input) GetMessages() []Message {
|
||||
if i.Messages != nil {
|
||||
return *i.Messages
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Message represents different types of messages in the input
|
||||
type Message struct {
|
||||
// Common fields
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// InputMessage fields (when this is a regular chat message)
|
||||
Role *string `json:"role,omitempty"`
|
||||
Content *Content `json:"content,omitempty"`
|
||||
|
||||
// WebSearchToolCall fields (when type == "web_search_call")
|
||||
ID *string `json:"id,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
|
||||
// Function call and function call output
|
||||
Arguments *string `json:"arguments,omitempty"`
|
||||
CallId *string `json:"call_id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Output *string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
// IsInputMessage returns true if this is a regular chat message
|
||||
func (m *Message) IsInputMessage() bool {
|
||||
return m.Role != nil
|
||||
}
|
||||
|
||||
// IsWebSearchCall returns true if this is a web search tool call
|
||||
func (m *Message) IsWebSearchCall() bool {
|
||||
return m.Type == "web_search_call"
|
||||
}
|
||||
|
||||
func (m *Message) IsFunctionCall() bool {
|
||||
return m.Type == "function_call"
|
||||
}
|
||||
|
||||
func (m *Message) IsFunctionCallOutput() bool {
|
||||
return m.Type == "function_call_output"
|
||||
}
|
||||
|
||||
func (m *Message) IsValidFunctionCallOutput() (bool, error) {
|
||||
var errors []string
|
||||
|
||||
if m.Type != "function_call_output" {
|
||||
errors = append(errors, "Type should be 'function_call_output'")
|
||||
}
|
||||
|
||||
if m.CallId == nil {
|
||||
errors = append(errors, "CallId not found")
|
||||
}
|
||||
|
||||
if m.Output == nil {
|
||||
errors = append(errors, "Output not found")
|
||||
}
|
||||
|
||||
return len(errors) < 1, fmt.Errorf("Invalid function_call_output message: %s", strings.Join(errors, ", "))
|
||||
}
|
||||
|
||||
// ToInputMessage converts to InputMessage if this is a regular message
|
||||
func (m *Message) ToInputMessage() *InputMessage {
|
||||
if m.IsInputMessage() && m.Role != nil && m.Content != nil {
|
||||
content := *m.Content
|
||||
return &InputMessage{
|
||||
Role: *m.Role,
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ToolChoice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// RequestBody represents the request body structure for the OpenAI API
|
||||
type RequestBody struct {
|
||||
Model string `json:"model"`
|
||||
Input interface{} `json:"input"`
|
||||
InputText string `json:"input_text"`
|
||||
InputMessages []InputMessage `json:"input_messages"`
|
||||
Input Input `json:"input"`
|
||||
Include []string `json:"include,omitempty"`
|
||||
Instructions *string `json:"instructions,omitempty"`
|
||||
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
|
||||
@@ -19,67 +161,107 @@ type RequestBody struct {
|
||||
Stream *bool `json:"stream,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Text *TextConfig `json:"text,omitempty"`
|
||||
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||||
Tools []interface{} `json:"tools,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
Truncation *string `json:"truncation,omitempty"`
|
||||
}
|
||||
|
||||
func (r *RequestBody) SetInputByType() {
|
||||
switch input := r.Input.(type) {
|
||||
case string:
|
||||
r.InputText = input
|
||||
case []any:
|
||||
for _, i := range input {
|
||||
switch i := i.(type) {
|
||||
case InputMessage:
|
||||
r.InputMessages = append(r.InputMessages, i)
|
||||
}
|
||||
}
|
||||
// This method is no longer needed as Input handles unmarshaling automatically
|
||||
if r.Input.IsText() {
|
||||
xlog.Debug("[Parse Request] Set input type as text", "input", r.Input.GetText())
|
||||
} else if r.Input.IsMessages() {
|
||||
xlog.Debug("[Parse Request] Input messages parsed", "messages", r.Input.GetMessages())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RequestBody) ToChatCompletionMessages() []openai.ChatCompletionMessage {
|
||||
result := []openai.ChatCompletionMessage{}
|
||||
|
||||
for _, m := range r.InputMessages {
|
||||
content := []openai.ChatMessagePart{}
|
||||
oneImageWasFound := false
|
||||
for _, c := range m.Content {
|
||||
switch c.Type {
|
||||
case "text":
|
||||
content = append(content, openai.ChatMessagePart{
|
||||
Type: "text",
|
||||
Text: c.Text,
|
||||
})
|
||||
case "image":
|
||||
oneImageWasFound = true
|
||||
content = append(content, openai.ChatMessagePart{
|
||||
Type: "image",
|
||||
ImageURL: &openai.ChatMessageImageURL{URL: c.ImageURL},
|
||||
if r.Input.IsMessages() {
|
||||
for _, m := range r.Input.GetMessages() {
|
||||
|
||||
if m.IsFunctionCall() {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: "assistant",
|
||||
ToolCalls: []openai.ToolCall{
|
||||
{
|
||||
Type: "function",
|
||||
ID: *m.CallId,
|
||||
Function: openai.FunctionCall{
|
||||
Arguments: *m.Arguments,
|
||||
Name: *m.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if oneImageWasFound {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: m.Role,
|
||||
MultiContent: content,
|
||||
})
|
||||
} else {
|
||||
for _, c := range content {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: m.Role,
|
||||
Content: c.Text,
|
||||
if m.IsFunctionCallOutput() {
|
||||
if ok, err := m.IsValidFunctionCallOutput(); !ok {
|
||||
xlog.Debug("Error processing input message", "error", err)
|
||||
} else {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: "tool",
|
||||
Content: *m.Output,
|
||||
ToolCallID: *m.CallId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !m.IsInputMessage() {
|
||||
continue
|
||||
}
|
||||
|
||||
content := []openai.ChatMessagePart{}
|
||||
oneImageWasFound := false
|
||||
|
||||
if m.Content != nil && m.Content.IsText() && m.Content.GetText() != "" {
|
||||
content = append(content, openai.ChatMessagePart{
|
||||
Type: "text",
|
||||
Text: m.Content.GetText(),
|
||||
})
|
||||
}
|
||||
|
||||
if m.Content != nil && m.Content.IsItems() {
|
||||
for _, c := range m.Content.GetItems() {
|
||||
switch c.Type {
|
||||
case "text":
|
||||
content = append(content, openai.ChatMessagePart{
|
||||
Type: "text",
|
||||
Text: c.Text,
|
||||
})
|
||||
case "image":
|
||||
oneImageWasFound = true
|
||||
content = append(content, openai.ChatMessagePart{
|
||||
Type: "image",
|
||||
ImageURL: &openai.ChatMessageImageURL{URL: c.ImageURL},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if oneImageWasFound {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: *m.Role,
|
||||
MultiContent: content,
|
||||
})
|
||||
} else {
|
||||
for _, c := range content {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: *m.Role,
|
||||
Content: c.Text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.InputText != "" {
|
||||
if r.Input.IsText() && r.Input.GetText() != "" {
|
||||
result = append(result, openai.ChatCompletionMessage{
|
||||
Role: "user",
|
||||
Content: r.InputText,
|
||||
Content: r.Input.GetText(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,6 +300,16 @@ type MessageContentItem struct {
|
||||
Annotations []interface{} `json:"annotations"`
|
||||
}
|
||||
|
||||
// FunctionToolCall represents a function tool call as a top-level object in the output array
|
||||
type FunctionToolCall struct {
|
||||
Arguments string `json:"arguments"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// UsageInfo represents token usage information
|
||||
type UsageInfo struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
@@ -144,7 +336,7 @@ type ResponseBody struct {
|
||||
Instructions interface{} `json:"instructions"`
|
||||
MaxOutputTokens interface{} `json:"max_output_tokens"`
|
||||
Model string `json:"model"`
|
||||
Output []ResponseMessage `json:"output"`
|
||||
Output []interface{} `json:"output"`
|
||||
ParallelToolCalls bool `json:"parallel_tool_calls"`
|
||||
PreviousResponseID interface{} `json:"previous_response_id"`
|
||||
Reasoning ReasoningConfig `json:"reasoning"`
|
||||
@@ -152,7 +344,7 @@ type ResponseBody struct {
|
||||
Temperature float64 `json:"temperature"`
|
||||
Text TextConfig `json:"text"`
|
||||
ToolChoice string `json:"tool_choice"`
|
||||
Tools []interface{} `json:"tools"`
|
||||
Tools []Tool `json:"tools"`
|
||||
TopP float64 `json:"top_p"`
|
||||
Truncation string `json:"truncation"`
|
||||
Usage UsageInfo `json:"usage"`
|
||||
@@ -160,10 +352,72 @@ type ResponseBody struct {
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// Content represents either a string or a slice of ContentItem
|
||||
type Content struct {
|
||||
Text *string `json:"-"`
|
||||
Items *[]ContentItem `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshaling for Content
|
||||
func (c *Content) UnmarshalJSON(data []byte) error {
|
||||
// Try to unmarshal as string first
|
||||
var text string
|
||||
if err := json.Unmarshal(data, &text); err == nil {
|
||||
c.Text = &text
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to unmarshal as []ContentItem
|
||||
var items []ContentItem
|
||||
if err := json.Unmarshal(data, &items); err == nil {
|
||||
c.Items = &items
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, &struct{}{}) // fallback to empty struct
|
||||
}
|
||||
|
||||
// MarshalJSON implements custom JSON marshaling for Content
|
||||
func (c *Content) MarshalJSON() ([]byte, error) {
|
||||
if c.Text != nil {
|
||||
return json.Marshal(*c.Text)
|
||||
}
|
||||
if c.Items != nil {
|
||||
return json.Marshal(*c.Items)
|
||||
}
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
|
||||
// IsText returns true if the content contains text
|
||||
func (c *Content) IsText() bool {
|
||||
return c.Text != nil
|
||||
}
|
||||
|
||||
// IsItems returns true if the content contains items
|
||||
func (c *Content) IsItems() bool {
|
||||
return c.Items != nil
|
||||
}
|
||||
|
||||
// GetText returns the text value or empty string
|
||||
func (c *Content) GetText() string {
|
||||
if c.Text != nil {
|
||||
return *c.Text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetItems returns the items value or empty slice
|
||||
func (c *Content) GetItems() []ContentItem {
|
||||
if c.Items != nil {
|
||||
return *c.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InputMessage represents a user input message
|
||||
type InputMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content []ContentItem `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Content Content `json:"content"`
|
||||
}
|
||||
|
||||
// ContentItem represents an item in a content array
|
||||
@@ -172,3 +426,129 @@ type ContentItem struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL string `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// Tool represents a tool that can be called by the assistant
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// Function tool fields (used when type == "function")
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Parameters *jsonschema.Definition `json:"parameters,omitempty"`
|
||||
Strict *bool `json:"strict,omitempty"`
|
||||
|
||||
// Web search tool fields (used when type == "web_search_preview" etc.)
|
||||
SearchContextSize *string `json:"search_context_size,omitempty"`
|
||||
UserLocation *UserLocation `json:"user_location,omitempty"`
|
||||
}
|
||||
|
||||
// IsFunction returns true if this is a function tool
|
||||
func (t *Tool) IsFunction() bool {
|
||||
return t.Type == "function"
|
||||
}
|
||||
|
||||
// IsWebSearch returns true if this is a web search tool
|
||||
func (t *Tool) IsWebSearch() bool {
|
||||
return t.Type == "web_search_preview" || t.Type == "web_search_preview_2025_03_11"
|
||||
}
|
||||
|
||||
// ToActionDefinition converts this tool to an ActionDefinition
|
||||
func (t *Tool) ToActionDefinition() *coreTypes.ActionDefinition {
|
||||
if t.IsFunction() && t.Name != nil {
|
||||
// Regular function tool
|
||||
properties := make(map[string]jsonschema.Definition)
|
||||
required := []string{}
|
||||
|
||||
if t.Parameters != nil {
|
||||
properties = t.Parameters.Properties
|
||||
required = t.Parameters.Required
|
||||
}
|
||||
|
||||
desc := ""
|
||||
if t.Description != nil {
|
||||
desc = *t.Description
|
||||
}
|
||||
|
||||
return &coreTypes.ActionDefinition{
|
||||
Name: coreTypes.ActionDefinitionName(*t.Name),
|
||||
Description: desc,
|
||||
Properties: properties,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
if t.IsWebSearch() {
|
||||
// Convert web search builtin to ActionDefinition
|
||||
name := "web_search_" + t.Type
|
||||
desc := "Web search tool for finding relevant information online"
|
||||
|
||||
// Create parameters schema for web search options
|
||||
properties := map[string]jsonschema.Definition{
|
||||
"search_context_size": {
|
||||
Type: jsonschema.String,
|
||||
Enum: []string{"low", "medium", "high"},
|
||||
Description: "Amount of context window space to use for search",
|
||||
},
|
||||
"user_location": {
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"type": {
|
||||
Type: jsonschema.String,
|
||||
Enum: []string{"approximate"},
|
||||
Description: "Type of location approximation",
|
||||
},
|
||||
"city": {
|
||||
Type: jsonschema.String,
|
||||
Description: "City of the user",
|
||||
},
|
||||
"country": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Two-letter ISO country code",
|
||||
},
|
||||
"region": {
|
||||
Type: jsonschema.String,
|
||||
Description: "Region of the user",
|
||||
},
|
||||
"timezone": {
|
||||
Type: jsonschema.String,
|
||||
Description: "IANA timezone of the user",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &coreTypes.ActionDefinition{
|
||||
Name: coreTypes.ActionDefinitionName(name),
|
||||
Description: desc,
|
||||
Properties: properties,
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeparateTools separates a slice of Tools into builtin tools and user tools as ActionDefinitions
|
||||
func SeparateTools(tools []Tool) (builtinTools []coreTypes.ActionDefinition, userTools []coreTypes.ActionDefinition) {
|
||||
for _, tool := range tools {
|
||||
if actionDef := tool.ToActionDefinition(); actionDef != nil {
|
||||
if tool.IsFunction() {
|
||||
// User-defined function tool
|
||||
userTools = append(userTools, *actionDef)
|
||||
} else if tool.IsWebSearch() {
|
||||
// Builtin tool (web search)
|
||||
builtinTools = append(builtinTools, *actionDef)
|
||||
}
|
||||
}
|
||||
}
|
||||
return builtinTools, userTools
|
||||
}
|
||||
|
||||
// UserLocation represents the user's location for web search
|
||||
type UserLocation struct {
|
||||
Type string `json:"type"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user