Compare commits
2 Commits
chore/qwen
...
obs-detail
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1da915d58 | ||
|
|
f5d8e0c9cf |
@@ -20,12 +20,10 @@ COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
|
||||
|
||||
# Final stage
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
FROM alpine:3.19
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y ca-certificates tzdata docker.io bash wget curl
|
||||
RUN apk add --no-cache ca-certificates tzdata docker
|
||||
|
||||
# Create non-root user
|
||||
#RUN adduser -D -g '' appuser
|
||||
|
||||
2
Makefile
2
Makefile
@@ -11,7 +11,7 @@ cleanup-tests:
|
||||
docker compose down
|
||||
|
||||
tests: prepare-tests
|
||||
LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="qwen3-8b" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
|
||||
LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="gemma-3-12b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
|
||||
|
||||
run-nokb:
|
||||
$(MAKE) run KBDISABLEINDEX=true
|
||||
|
||||
@@ -120,7 +120,7 @@ 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: `qwen3-8b`
|
||||
- Text: `gemma-3-12b-it-qat`
|
||||
- Multimodal: `minicpm-v-2_6`
|
||||
- Image: `sd-1.5-ggml`
|
||||
- Environment variables:
|
||||
@@ -136,7 +136,7 @@ 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: `qwen3-8b`
|
||||
- Text: `gemma-3-12b-it-qat`
|
||||
- Multimodal: `minicpm-v-2_6`
|
||||
- Image: `sd-1.5-ggml`
|
||||
- Environment variables:
|
||||
@@ -167,7 +167,7 @@ docker compose -f docker-compose.intel.yaml up
|
||||
```
|
||||
|
||||
If no models are specified, it will use the defaults:
|
||||
- Text model: `qwen3-8b`
|
||||
- Text model: `gemma-3-12b-it-qat`
|
||||
- Multimodal model: `minicpm-v-2_6`
|
||||
- Image model: `sd-1.5-ggml`
|
||||
|
||||
|
||||
@@ -95,11 +95,6 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
|
||||
|
||||
func (a *CustomAction) Definition() types.ActionDefinition {
|
||||
|
||||
if a.i == nil {
|
||||
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
|
||||
return types.ActionDefinition{}
|
||||
}
|
||||
|
||||
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
||||
if err != nil {
|
||||
xlog.Error("Error getting custom action definition", "error", err)
|
||||
|
||||
@@ -253,7 +253,7 @@ func NewAgentConfigMeta(
|
||||
Name: "enable_reasoning",
|
||||
Label: "Enable Reasoning",
|
||||
Type: "checkbox",
|
||||
DefaultValue: true,
|
||||
DefaultValue: false,
|
||||
HelpText: "Enable agent to explain its reasoning process",
|
||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||
},
|
||||
|
||||
@@ -12,16 +12,6 @@ services:
|
||||
- /dev/dri/card1
|
||||
- /dev/dri/renderD129
|
||||
|
||||
mcpbox:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: mcpbox
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
|
||||
@@ -17,16 +17,6 @@ services:
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
|
||||
mcpbox:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: mcpbox
|
||||
|
||||
dind:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
service: dind
|
||||
|
||||
localrecall:
|
||||
extends:
|
||||
file: docker-compose.yaml
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||
image: localai/localai:master-ffmpeg-core
|
||||
command:
|
||||
- ${MODEL_NAME:-qwen3-8b}
|
||||
- ${MODEL_NAME:-gemma-3-12b-it-qat}
|
||||
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- granite-embedding-107m-multilingual
|
||||
@@ -54,28 +54,14 @@ services:
|
||||
- "8080"
|
||||
volumes:
|
||||
- ./volumes/mcpbox:/app/data
|
||||
environment:
|
||||
- DOCKER_HOST=tcp://dind:2375
|
||||
depends_on:
|
||||
dind:
|
||||
condition: service_healthy
|
||||
# share docker socket if you want it to be able to run docker commands
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/processes"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
dind:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
environment:
|
||||
- DOCKER_TLS_CERTDIR=""
|
||||
healthcheck:
|
||||
test: ["CMD", "docker", "info"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
localagi:
|
||||
depends_on:
|
||||
localai:
|
||||
@@ -91,7 +77,7 @@ services:
|
||||
- 8080:3000
|
||||
#image: quay.io/mudler/localagi:master
|
||||
environment:
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-qwen3-8b}
|
||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-12b-it-qat}
|
||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||
|
||||
1
main.go
1
main.go
@@ -66,7 +66,6 @@ func main() {
|
||||
localRAG,
|
||||
services.Actions(map[string]string{
|
||||
"browser-agent-runner-base-url": localOperatorBaseURL,
|
||||
"deep-research-runner-base-url": localOperatorBaseURL,
|
||||
}),
|
||||
services.Connectors,
|
||||
services.DynamicPrompts,
|
||||
|
||||
@@ -4,146 +4,69 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client represents a client for interacting with the LocalOperator API
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string, timeout ...time.Duration) *Client {
|
||||
defaultTimeout := 30 * time.Second
|
||||
if len(timeout) > 0 {
|
||||
defaultTimeout = timeout[0]
|
||||
}
|
||||
|
||||
// NewClient creates a new API client
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// AgentRequest represents the request body for running an agent
|
||||
type AgentRequest struct {
|
||||
Goal string `json:"goal"`
|
||||
MaxAttempts int `json:"max_attempts,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
}
|
||||
|
||||
type DesktopAgentRequest struct {
|
||||
AgentRequest
|
||||
DesktopURL string `json:"desktop_url"`
|
||||
}
|
||||
|
||||
type DeepResearchRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
MaxCycles int `json:"max_cycles,omitempty"`
|
||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||
MaxResults int `json:"max_results,omitempty"`
|
||||
}
|
||||
|
||||
// Response types
|
||||
// StateDescription represents a single state in the agent's history
|
||||
type StateDescription struct {
|
||||
CurrentURL string `json:"current_url"`
|
||||
PageTitle string `json:"page_title"`
|
||||
PageContentDescription string `json:"page_content_description"`
|
||||
Screenshot string `json:"screenshot"`
|
||||
ScreenshotMimeType string `json:"screenshot_mime_type"`
|
||||
ScreenshotMimeType string `json:"screenshot_mime_type"` // MIME type of the screenshot (e.g., "image/png")
|
||||
}
|
||||
|
||||
// StateHistory represents the complete history of states during agent execution
|
||||
type StateHistory struct {
|
||||
States []StateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type DesktopStateDescription struct {
|
||||
ScreenContent string `json:"screen_content"`
|
||||
ScreenshotPath string `json:"screenshot_path"`
|
||||
}
|
||||
|
||||
type DesktopStateHistory struct {
|
||||
States []DesktopStateDescription `json:"states"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ResearchResult struct {
|
||||
Topic string `json:"topic"`
|
||||
Summary string `json:"summary"`
|
||||
Sources []SearchResult `json:"sources"`
|
||||
KnowledgeGaps []string `json:"knowledge_gaps"`
|
||||
SearchQueries []string `json:"search_queries"`
|
||||
ResearchCycles int `json:"research_cycles"`
|
||||
CompletionTime time.Duration `json:"completion_time"`
|
||||
}
|
||||
|
||||
// RunAgent sends a request to run an agent with the given goal
|
||||
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
||||
return post[*StateHistory](c.httpClient, c.baseURL+"/api/browser/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDesktopAgent(req DesktopAgentRequest) (*DesktopStateHistory, error) {
|
||||
return post[*DesktopStateHistory](c.httpClient, c.baseURL+"/api/desktop/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) RunDeepResearch(req DeepResearchRequest) (*ResearchResult, error) {
|
||||
return post[*ResearchResult](c.httpClient, c.baseURL+"/api/deep-research/run", req)
|
||||
}
|
||||
|
||||
func (c *Client) Readyz() (string, error) {
|
||||
return c.get("/readyz")
|
||||
}
|
||||
|
||||
func (c *Client) Healthz() (string, error) {
|
||||
return c.get("/healthz")
|
||||
}
|
||||
|
||||
func (c *Client) get(path string) (string, error) {
|
||||
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to make request: %w", err)
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Post(
|
||||
fmt.Sprintf("%s/api/browser/run", c.baseURL),
|
||||
"application/json",
|
||||
bytes.NewBuffer(body),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp.Status, nil
|
||||
var state StateHistory
|
||||
if err := json.NewDecoder(resp.Body).Decode(&state); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
|
||||
var result T
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Sending request", "url", url, "body", string(jsonBody))
|
||||
|
||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Println("Response", "status", resp.StatusCode)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return result, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return result, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ const (
|
||||
ActionSearch = "search"
|
||||
ActionCustom = "custom"
|
||||
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||
ActionDeepResearchRunner = "deep-research-runner"
|
||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||
ActionGithubIssueOpener = "github-issue-opener"
|
||||
ActionGithubIssueCloser = "github-issue-closer"
|
||||
@@ -55,7 +54,6 @@ var AvailableActions = []string{
|
||||
ActionGithubRepositoryGet,
|
||||
ActionGithubGetAllContent,
|
||||
ActionBrowserAgentRunner,
|
||||
ActionDeepResearchRunner,
|
||||
ActionGithubRepositoryCreateOrUpdate,
|
||||
ActionGithubIssueReader,
|
||||
ActionGithubIssueCommenter,
|
||||
@@ -123,8 +121,6 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
||||
a = actions.NewGithubIssueSearch(config)
|
||||
case ActionBrowserAgentRunner:
|
||||
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
|
||||
case ActionDeepResearchRunner:
|
||||
a = actions.NewDeepResearchRunner(config, actionsConfigs["deep-research-runner-base-url"])
|
||||
case ActionGithubIssueReader:
|
||||
a = actions.NewGithubIssueReader(config)
|
||||
case ActionGithubPRReader:
|
||||
@@ -185,11 +181,6 @@ func ActionsConfigMeta() []config.FieldGroup {
|
||||
Label: "Browser Agent Runner",
|
||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "deep-research-runner",
|
||||
Label: "Deep Research Runner",
|
||||
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||
},
|
||||
{
|
||||
Name: "generate_image",
|
||||
Label: "Generate Image",
|
||||
|
||||
@@ -3,7 +3,6 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
@@ -25,7 +24,7 @@ func NewBrowserAgentRunner(config map[string]string, defaultURL string) *Browser
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], 15*time.Minute)
|
||||
client := api.NewClient(config["baseURL"])
|
||||
|
||||
return &BrowserAgentRunner{
|
||||
client: client,
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mudler/LocalAGI/core/types"
|
||||
"github.com/mudler/LocalAGI/pkg/config"
|
||||
api "github.com/mudler/LocalAGI/pkg/localoperator"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataDeepResearchResult = "deep_research_result"
|
||||
)
|
||||
|
||||
type DeepResearchRunner struct {
|
||||
baseURL, customActionName string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewDeepResearchRunner(config map[string]string, defaultURL string) *DeepResearchRunner {
|
||||
if config["baseURL"] == "" {
|
||||
config["baseURL"] = defaultURL
|
||||
}
|
||||
|
||||
client := api.NewClient(config["baseURL"], 15*time.Minute)
|
||||
|
||||
return &DeepResearchRunner{
|
||||
client: client,
|
||||
baseURL: config["baseURL"],
|
||||
customActionName: config["customActionName"],
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
|
||||
result := api.DeepResearchRequest{}
|
||||
err := params.Unmarshal(&result)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
req := api.DeepResearchRequest{
|
||||
Topic: result.Topic,
|
||||
MaxCycles: result.MaxCycles,
|
||||
MaxNoActionAttempts: result.MaxNoActionAttempts,
|
||||
MaxResults: result.MaxResults,
|
||||
}
|
||||
|
||||
researchResult, err := d.client.RunDeepResearch(req)
|
||||
if err != nil {
|
||||
return types.ActionResult{}, fmt.Errorf("failed to run deep research: %w", err)
|
||||
}
|
||||
|
||||
// Format the research result into a readable string
|
||||
var resultStr string
|
||||
|
||||
resultStr += "Deep research result\n"
|
||||
resultStr += fmt.Sprintf("Topic: %s\n", researchResult.Topic)
|
||||
resultStr += fmt.Sprintf("Summary: %s\n", researchResult.Summary)
|
||||
resultStr += fmt.Sprintf("Research Cycles: %d\n", researchResult.ResearchCycles)
|
||||
resultStr += fmt.Sprintf("Completion Time: %s\n\n", researchResult.CompletionTime)
|
||||
|
||||
if len(researchResult.Sources) > 0 {
|
||||
resultStr += "Sources:\n"
|
||||
for _, source := range researchResult.Sources {
|
||||
resultStr += fmt.Sprintf("- %s (%s)\n %s\n", source.Title, source.URL, source.Description)
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionResult{
|
||||
Result: fmt.Sprintf("Deep research completed successfully.\n%s", resultStr),
|
||||
Metadata: map[string]interface{}{MetadataDeepResearchResult: researchResult},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Definition() types.ActionDefinition {
|
||||
actionName := "run_deep_research"
|
||||
if d.customActionName != "" {
|
||||
actionName = d.customActionName
|
||||
}
|
||||
description := "Run a deep research on a specific topic, gathering information from multiple sources and providing a comprehensive summary"
|
||||
return types.ActionDefinition{
|
||||
Name: types.ActionDefinitionName(actionName),
|
||||
Description: description,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"topic": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The topic to research",
|
||||
},
|
||||
"max_cycles": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of research cycles to perform (optional)",
|
||||
},
|
||||
"max_no_action_attempts": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of attempts without taking an action (optional)",
|
||||
},
|
||||
"max_results": {
|
||||
Type: jsonschema.Number,
|
||||
Description: "Maximum number of results to collect (optional)",
|
||||
},
|
||||
},
|
||||
Required: []string{"topic"},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DeepResearchRunner) Plannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// DeepResearchRunnerConfigMeta returns the metadata for Deep Research Runner action configuration fields
|
||||
func DeepResearchRunnerConfigMeta() []config.Field {
|
||||
return []config.Field{
|
||||
{
|
||||
Name: "baseURL",
|
||||
Label: "Base URL",
|
||||
Type: config.FieldTypeText,
|
||||
Required: false,
|
||||
HelpText: "Base URL of the LocalOperator API",
|
||||
},
|
||||
{
|
||||
Name: "customActionName",
|
||||
Label: "Custom Action Name",
|
||||
Type: config.FieldTypeText,
|
||||
HelpText: "Custom name for this action",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -30,15 +30,9 @@ func NewDiscord(config map[string]string) *Discord {
|
||||
duration = 5 * time.Minute
|
||||
}
|
||||
|
||||
token := config["token"]
|
||||
|
||||
if !strings.HasPrefix(token, "Bot ") {
|
||||
token = "Bot " + token
|
||||
}
|
||||
|
||||
return &Discord{
|
||||
conversationTracker: NewConversationTracker[string](duration),
|
||||
token: token,
|
||||
token: config["token"],
|
||||
defaultChannel: config["defaultChannel"],
|
||||
}
|
||||
}
|
||||
|
||||
24
webui/app.go
24
webui/app.go
@@ -419,30 +419,6 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetActionDefinition(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
payload := struct {
|
||||
Config map[string]string `json:"config"`
|
||||
}{}
|
||||
|
||||
if err := c.BodyParser(&payload); err != nil {
|
||||
xlog.Error("Error parsing action payload", "error", err)
|
||||
return errorJSONMessage(c, err.Error())
|
||||
}
|
||||
|
||||
actionName := c.Params("name")
|
||||
|
||||
xlog.Debug("Executing action", "action", actionName, "config", payload.Config)
|
||||
a, err := services.Action(actionName, "", payload.Config, pool, map[string]string{})
|
||||
if err != nil {
|
||||
xlog.Error("Error creating action", "error", err)
|
||||
return errorJSONMessage(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(a.Definition())
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
payload := struct {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useOutletContext, useNavigate } from 'react-router-dom';
|
||||
import { actionApi, agentApi } from '../utils/api';
|
||||
import FormFieldDefinition from '../components/common/FormFieldDefinition';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import 'highlight.js/styles/monokai.css';
|
||||
hljs.registerLanguage('json', json);
|
||||
import { actionApi } from '../utils/api';
|
||||
|
||||
function ActionsPlayground() {
|
||||
const { showToast } = useOutletContext();
|
||||
@@ -17,10 +12,6 @@ function ActionsPlayground() {
|
||||
const [result, setResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingActions, setLoadingActions] = useState(true);
|
||||
const [actionMetadata, setActionMetadata] = useState(null);
|
||||
const [agentMetadata, setAgentMetadata] = useState(null);
|
||||
const [configFields, setConfigFields] = useState([]);
|
||||
const [paramFields, setParamFields] = useState([]);
|
||||
|
||||
// Update document title
|
||||
useEffect(() => {
|
||||
@@ -45,106 +36,21 @@ function ActionsPlayground() {
|
||||
};
|
||||
|
||||
fetchActions();
|
||||
}, []);
|
||||
|
||||
// Fetch agent metadata on mount
|
||||
useEffect(() => {
|
||||
const fetchAgentMetadata = async () => {
|
||||
try {
|
||||
const metadata = await agentApi.getAgentConfigMetadata();
|
||||
setAgentMetadata(metadata);
|
||||
} catch (err) {
|
||||
console.error('Error fetching agent metadata:', err);
|
||||
showToast('Failed to load agent metadata', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgentMetadata();
|
||||
}, []);
|
||||
|
||||
// Fetch action definition when action is selected or config changes
|
||||
useEffect(() => {
|
||||
if (!selectedAction) return;
|
||||
|
||||
const fetchActionDefinition = async () => {
|
||||
try {
|
||||
// Get config fields from agent metadata
|
||||
const actionMeta = agentMetadata?.actions?.find(action => action.name === selectedAction);
|
||||
const configFields = actionMeta?.fields || [];
|
||||
console.debug('Config fields:', configFields);
|
||||
setConfigFields(configFields);
|
||||
|
||||
// Parse current config to pass to action definition
|
||||
let currentConfig = {};
|
||||
try {
|
||||
currentConfig = JSON.parse(configJson);
|
||||
} catch (err) {
|
||||
console.error('Error parsing current config:', err);
|
||||
}
|
||||
|
||||
// Get parameter fields from action definition
|
||||
const paramFields = await actionApi.getActionDefinition(selectedAction, currentConfig);
|
||||
console.debug('Parameter fields:', paramFields);
|
||||
setParamFields(paramFields);
|
||||
|
||||
// Reset JSON to match the new fields
|
||||
setConfigJson(JSON.stringify(currentConfig, null, 2));
|
||||
setParamsJson(JSON.stringify({}, null, 2));
|
||||
setResult(null);
|
||||
} catch (err) {
|
||||
console.error('Error fetching action definition:', err);
|
||||
showToast('Failed to load action definition', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
fetchActionDefinition();
|
||||
}, [selectedAction, agentMetadata]);
|
||||
}, [showToast]);
|
||||
|
||||
// Handle action selection
|
||||
const handleActionChange = (e) => {
|
||||
setSelectedAction(e.target.value);
|
||||
setConfigJson('{}');
|
||||
setParamsJson('{}');
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
// Helper to generate onChange handlers for form fields
|
||||
const makeFieldChangeHandler = (fields, updateFn) => (e) => {
|
||||
let value;
|
||||
if (e && e.target) {
|
||||
const fieldName = e.target.name;
|
||||
const fieldDef = fields.find(f => f.name === fieldName);
|
||||
const fieldType = fieldDef ? fieldDef.type : undefined;
|
||||
if (fieldType === 'checkbox') {
|
||||
value = e.target.checked;
|
||||
} else if (fieldType === 'number') {
|
||||
value = e.target.value === '' ? '' : Number(e.target.value);
|
||||
} else {
|
||||
value = e.target.value;
|
||||
}
|
||||
updateFn(fieldName, value);
|
||||
}
|
||||
// Handle JSON input changes
|
||||
const handleConfigChange = (e) => {
|
||||
setConfigJson(e.target.value);
|
||||
};
|
||||
|
||||
// Handle form field changes
|
||||
const handleConfigChange = (field, value) => {
|
||||
try {
|
||||
const config = JSON.parse(configJson);
|
||||
config[field] = value;
|
||||
setConfigJson(JSON.stringify(config, null, 2));
|
||||
} catch (err) {
|
||||
console.error('Error updating config:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleParamsChange = (field, value) => {
|
||||
try {
|
||||
const params = JSON.parse(paramsJson);
|
||||
params[field] = value;
|
||||
setParamsJson(JSON.stringify(params, null, 2));
|
||||
} catch (err) {
|
||||
console.error('Error updating params:', err);
|
||||
}
|
||||
const handleParamsChange = (e) => {
|
||||
setParamsJson(e.target.value);
|
||||
};
|
||||
|
||||
// Execute the selected action
|
||||
@@ -229,31 +135,34 @@ function ActionsPlayground() {
|
||||
|
||||
{selectedAction && (
|
||||
<div className="section-box">
|
||||
<h2>Action Configuration</h2>
|
||||
|
||||
<form onSubmit={handleExecuteAction}>
|
||||
{configFields.length > 0 && (
|
||||
<>
|
||||
<h2>Configuration</h2>
|
||||
<FormFieldDefinition
|
||||
fields={configFields}
|
||||
values={JSON.parse(configJson)}
|
||||
onChange={makeFieldChangeHandler(configFields, handleConfigChange)}
|
||||
idPrefix="config_"
|
||||
<div className="form-group mb-6">
|
||||
<label htmlFor="config-json">Configuration (JSON):</label>
|
||||
<textarea
|
||||
id="config-json"
|
||||
value={configJson}
|
||||
onChange={handleConfigChange}
|
||||
className="form-control"
|
||||
rows="5"
|
||||
placeholder='{"key": "value"}'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
|
||||
</div>
|
||||
|
||||
{paramFields.length > 0 && (
|
||||
<>
|
||||
<h2>Parameters</h2>
|
||||
<FormFieldDefinition
|
||||
fields={paramFields}
|
||||
values={JSON.parse(paramsJson)}
|
||||
onChange={makeFieldChangeHandler(paramFields, handleParamsChange)}
|
||||
idPrefix="param_"
|
||||
<div className="form-group mb-6">
|
||||
<label htmlFor="params-json">Parameters (JSON):</label>
|
||||
<textarea
|
||||
id="params-json"
|
||||
value={paramsJson}
|
||||
onChange={handleParamsChange}
|
||||
className="form-control"
|
||||
rows="5"
|
||||
placeholder='{"key": "value"}'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
@@ -285,9 +194,9 @@ function ActionsPlayground() {
|
||||
backgroundColor: 'rgba(30, 30, 30, 0.7)'
|
||||
}}>
|
||||
{typeof result === 'object' ? (
|
||||
<pre className="hljs"><code>
|
||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(result, null, 2), { language: 'json' }).value }}></div>
|
||||
</code></pre>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{result}
|
||||
|
||||
55
webui/react-ui/src/utils/api.js
vendored
55
webui/react-ui/src/utils/api.js
vendored
@@ -24,50 +24,6 @@ const buildUrl = (endpoint) => {
|
||||
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
|
||||
};
|
||||
|
||||
// Helper function to convert ActionDefinition to FormFieldDefinition format
|
||||
const convertActionDefinitionToFields = (definition) => {
|
||||
if (!definition || !definition.Properties) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields = [];
|
||||
const required = definition.Required || [];
|
||||
|
||||
console.debug('Action definition:', definition);
|
||||
|
||||
Object.entries(definition.Properties).forEach(([name, property]) => {
|
||||
const field = {
|
||||
name,
|
||||
label: name.charAt(0).toUpperCase() + name.slice(1),
|
||||
type: 'text', // Default to text, we'll enhance this later
|
||||
required: required.includes(name),
|
||||
helpText: property.Description || '',
|
||||
defaultValue: property.Default,
|
||||
};
|
||||
|
||||
if (property.enum && property.enum.length > 0) {
|
||||
field.type = 'select';
|
||||
field.options = property.enum;
|
||||
} else {
|
||||
switch (property.type) {
|
||||
case 'integer':
|
||||
field.type = 'number';
|
||||
field.min = property.Minimum;
|
||||
field.max = property.Maximum;
|
||||
break;
|
||||
case 'boolean':
|
||||
field.type = 'checkbox';
|
||||
break;
|
||||
}
|
||||
// TODO: Handle Object and Array types which require nested fields
|
||||
}
|
||||
|
||||
fields.push(field);
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
// Agent-related API calls
|
||||
export const agentApi = {
|
||||
// Get list of all agents
|
||||
@@ -260,17 +216,6 @@ export const actionApi = {
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
// Get action definition
|
||||
getActionDefinition: async (name, config = {}) => {
|
||||
const response = await fetch(buildUrl(API_CONFIG.endpoints.actionDefinition(name)), {
|
||||
method: 'POST',
|
||||
headers: API_CONFIG.headers,
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
const definition = await handleResponse(response);
|
||||
return convertActionDefinitionToFields(definition);
|
||||
},
|
||||
|
||||
// Execute an action for an agent
|
||||
executeAction: async (name, actionData) => {
|
||||
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {
|
||||
|
||||
1
webui/react-ui/src/utils/config.js
vendored
1
webui/react-ui/src/utils/config.js
vendored
@@ -43,7 +43,6 @@ export const API_CONFIG = {
|
||||
|
||||
// Action endpoints
|
||||
listActions: '/api/actions',
|
||||
actionDefinition: (name) => `/api/action/${name}/definition`,
|
||||
executeAction: (name) => `/api/action/${name}/run`,
|
||||
|
||||
// Status endpoint
|
||||
|
||||
@@ -188,7 +188,6 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
// Add endpoint for getting agent config metadata
|
||||
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta())
|
||||
|
||||
webapp.Post("/api/action/:name/definition", app.GetActionDefinition(pool))
|
||||
webapp.Post("/api/action/:name/run", app.ExecuteAction(pool))
|
||||
webapp.Get("/api/actions", app.ListActions())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user