Compare commits
11 Commits
chore/mcpb
...
chore/qwen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45969d3187 | ||
|
|
a1efa07b24 | ||
|
|
29f7644577 | ||
|
|
f3884c0244 | ||
|
|
6516af6c34 | ||
|
|
77680c6fee | ||
|
|
5faa599321 | ||
|
|
6209ededff | ||
|
|
f6b6d5246c | ||
|
|
b81624bfc2 | ||
|
|
c1844f7230 |
@@ -20,10 +20,12 @@ COPY . .
|
|||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
|
RUN CGO_ENABLED=0 GOOS=linux go build -o mcpbox ./cmd/mcpbox
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM alpine:3.19
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apk add --no-cache ca-certificates tzdata docker
|
RUN apt-get update && apt-get install -y ca-certificates tzdata docker.io bash wget curl
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
#RUN adduser -D -g '' appuser
|
#RUN adduser -D -g '' appuser
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -11,7 +11,7 @@ cleanup-tests:
|
|||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
tests: prepare-tests
|
tests: prepare-tests
|
||||||
LOCALAGI_MCPBOX_URL="http://localhost:9090" LOCALAGI_MODEL="gemma-3-12b-it-qat" LOCALAI_API_URL="http://localhost:8081" LOCALAGI_API_URL="http://localhost:8080" $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --fail-fast -v -r ./...
|
LOCALAGI_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 ./...
|
||||||
|
|
||||||
run-nokb:
|
run-nokb:
|
||||||
$(MAKE) run KBDISABLEINDEX=true
|
$(MAKE) run KBDISABLEINDEX=true
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -69,6 +69,11 @@ Now you can access and manage your agents at [http://localhost:8080](http://loca
|
|||||||
|
|
||||||
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
|
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
|
||||||
|
|
||||||
|
## Videos
|
||||||
|
|
||||||
|
[](https://youtu.be/HtVwIxW3ePg)
|
||||||
|
[](https://youtu.be/v82rswGJt_M)
|
||||||
|
|
||||||
## 📚🆕 Local Stack Family
|
## 📚🆕 Local Stack Family
|
||||||
|
|
||||||
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
|
||||||
@@ -115,7 +120,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
|
|||||||
- Supports text, multimodal, and image generation models
|
- Supports text, multimodal, and image generation models
|
||||||
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
|
- Run with: `docker compose -f docker-compose.nvidia.yaml up`
|
||||||
- Default models:
|
- Default models:
|
||||||
- Text: `gemma-3-12b-it-qat`
|
- Text: `qwen3-8b`
|
||||||
- Multimodal: `minicpm-v-2_6`
|
- Multimodal: `minicpm-v-2_6`
|
||||||
- Image: `sd-1.5-ggml`
|
- Image: `sd-1.5-ggml`
|
||||||
- Environment variables:
|
- Environment variables:
|
||||||
@@ -131,7 +136,7 @@ LocalAGI supports multiple hardware configurations through Docker Compose profil
|
|||||||
- Supports text, multimodal, and image generation models
|
- Supports text, multimodal, and image generation models
|
||||||
- Run with: `docker compose -f docker-compose.intel.yaml up`
|
- Run with: `docker compose -f docker-compose.intel.yaml up`
|
||||||
- Default models:
|
- Default models:
|
||||||
- Text: `gemma-3-12b-it-qat`
|
- Text: `qwen3-8b`
|
||||||
- Multimodal: `minicpm-v-2_6`
|
- Multimodal: `minicpm-v-2_6`
|
||||||
- Image: `sd-1.5-ggml`
|
- Image: `sd-1.5-ggml`
|
||||||
- Environment variables:
|
- Environment variables:
|
||||||
@@ -162,7 +167,7 @@ docker compose -f docker-compose.intel.yaml up
|
|||||||
```
|
```
|
||||||
|
|
||||||
If no models are specified, it will use the defaults:
|
If no models are specified, it will use the defaults:
|
||||||
- Text model: `gemma-3-12b-it-qat`
|
- Text model: `qwen3-8b`
|
||||||
- Multimodal model: `minicpm-v-2_6`
|
- Multimodal model: `minicpm-v-2_6`
|
||||||
- Image model: `sd-1.5-ggml`
|
- Image model: `sd-1.5-ggml`
|
||||||
|
|
||||||
@@ -180,14 +185,6 @@ Good (relatively small) models that have been tested are:
|
|||||||
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
|
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
|
||||||
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
|
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
|
||||||
|
|
||||||
## 🌐 The Local Ecosystem
|
|
||||||
|
|
||||||
LocalAGI is part of the powerful Local family of privacy-focused AI tools:
|
|
||||||
|
|
||||||
- [**LocalAI**](https://github.com/mudler/LocalAI): Run Large Language Models locally.
|
|
||||||
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
|
|
||||||
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
|
|
||||||
|
|
||||||
## 🌟 Screenshots
|
## 🌟 Screenshots
|
||||||
|
|
||||||
### Powerful Web UI
|
### Powerful Web UI
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ func (a *CustomAction) Run(ctx context.Context, params types.ActionParams) (type
|
|||||||
|
|
||||||
func (a *CustomAction) Definition() types.ActionDefinition {
|
func (a *CustomAction) Definition() types.ActionDefinition {
|
||||||
|
|
||||||
|
if a.i == nil {
|
||||||
|
xlog.Error("Interpreter is not initialized for custom action", "action", a.config["name"])
|
||||||
|
return types.ActionDefinition{}
|
||||||
|
}
|
||||||
|
|
||||||
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
v, err := a.i.Eval(fmt.Sprintf("%s.Definition", a.config["name"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("Error getting custom action definition", "error", err)
|
xlog.Error("Error getting custom action definition", "error", err)
|
||||||
|
|||||||
@@ -180,6 +180,12 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if j.Obs != nil {
|
if j.Obs != nil {
|
||||||
|
if len(j.ConversationHistory) > 0 {
|
||||||
|
m := j.ConversationHistory[len(j.ConversationHistory)-1]
|
||||||
|
j.Obs.Creation = &types.Creation{ ChatCompletionMessage: &m }
|
||||||
|
a.observer.Update(*j.Obs)
|
||||||
|
}
|
||||||
|
|
||||||
j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
|
j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
|
||||||
j.Obs.Completion = &types.Completion{
|
j.Obs.Completion = &types.Completion{
|
||||||
Conversation: ccm,
|
Conversation: ccm,
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func NewAgentConfigMeta(
|
|||||||
Name: "enable_reasoning",
|
Name: "enable_reasoning",
|
||||||
Label: "Enable Reasoning",
|
Label: "Enable Reasoning",
|
||||||
Type: "checkbox",
|
Type: "checkbox",
|
||||||
DefaultValue: false,
|
DefaultValue: true,
|
||||||
HelpText: "Enable agent to explain its reasoning process",
|
HelpText: "Enable agent to explain its reasoning process",
|
||||||
Tags: config.Tags{Section: "AdvancedSettings"},
|
Tags: config.Tags{Section: "AdvancedSettings"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Creation struct {
|
type Creation struct {
|
||||||
|
ChatCompletionMessage *openai.ChatCompletionMessage `json:"chat_completion_message,omitempty"`
|
||||||
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
|
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
|
||||||
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
|
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
|
||||||
FunctionParams ActionParams `json:"function_params,omitempty"`
|
FunctionParams ActionParams `json:"function_params,omitempty"`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
# Image list (dockerhub): https://hub.docker.com/r/localai/localai
|
||||||
image: localai/localai:master-ffmpeg-core
|
image: localai/localai:master-ffmpeg-core
|
||||||
command:
|
command:
|
||||||
- ${MODEL_NAME:-gemma-3-12b-it-qat}
|
- ${MODEL_NAME:-qwen3-8b}
|
||||||
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
- ${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||||
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
- ${IMAGE_MODEL:-sd-1.5-ggml}
|
||||||
- granite-embedding-107m-multilingual
|
- granite-embedding-107m-multilingual
|
||||||
@@ -91,7 +91,7 @@ services:
|
|||||||
- 8080:3000
|
- 8080:3000
|
||||||
#image: quay.io/mudler/localagi:master
|
#image: quay.io/mudler/localagi:master
|
||||||
environment:
|
environment:
|
||||||
- LOCALAGI_MODEL=${MODEL_NAME:-gemma-3-12b-it-qat}
|
- LOCALAGI_MODEL=${MODEL_NAME:-qwen3-8b}
|
||||||
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
- LOCALAGI_MULTIMODAL_MODEL=${MULTIMODAL_MODEL:-minicpm-v-2_6}
|
||||||
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
- LOCALAGI_IMAGE_MODEL=${IMAGE_MODEL:-sd-1.5-ggml}
|
||||||
- LOCALAGI_LLM_API_URL=http://localai:8080
|
- LOCALAGI_LLM_API_URL=http://localai:8080
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -66,6 +66,7 @@ func main() {
|
|||||||
localRAG,
|
localRAG,
|
||||||
services.Actions(map[string]string{
|
services.Actions(map[string]string{
|
||||||
"browser-agent-runner-base-url": localOperatorBaseURL,
|
"browser-agent-runner-base-url": localOperatorBaseURL,
|
||||||
|
"deep-research-runner-base-url": localOperatorBaseURL,
|
||||||
}),
|
}),
|
||||||
services.Connectors,
|
services.Connectors,
|
||||||
services.DynamicPrompts,
|
services.DynamicPrompts,
|
||||||
|
|||||||
@@ -4,69 +4,146 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents a client for interacting with the LocalOperator API
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new API client
|
func NewClient(baseURL string, timeout ...time.Duration) *Client {
|
||||||
func NewClient(baseURL string) *Client {
|
defaultTimeout := 30 * time.Second
|
||||||
|
if len(timeout) > 0 {
|
||||||
|
defaultTimeout = timeout[0]
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentRequest represents the request body for running an agent
|
|
||||||
type AgentRequest struct {
|
type AgentRequest struct {
|
||||||
Goal string `json:"goal"`
|
Goal string `json:"goal"`
|
||||||
MaxAttempts int `json:"max_attempts,omitempty"`
|
MaxAttempts int `json:"max_attempts,omitempty"`
|
||||||
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateDescription represents a single state in the agent's history
|
type DesktopAgentRequest struct {
|
||||||
|
AgentRequest
|
||||||
|
DesktopURL string `json:"desktop_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeepResearchRequest struct {
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
MaxCycles int `json:"max_cycles,omitempty"`
|
||||||
|
MaxNoActionAttempts int `json:"max_no_action_attempts,omitempty"`
|
||||||
|
MaxResults int `json:"max_results,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response types
|
||||||
type StateDescription struct {
|
type StateDescription struct {
|
||||||
CurrentURL string `json:"current_url"`
|
CurrentURL string `json:"current_url"`
|
||||||
PageTitle string `json:"page_title"`
|
PageTitle string `json:"page_title"`
|
||||||
PageContentDescription string `json:"page_content_description"`
|
PageContentDescription string `json:"page_content_description"`
|
||||||
Screenshot string `json:"screenshot"`
|
Screenshot string `json:"screenshot"`
|
||||||
ScreenshotMimeType string `json:"screenshot_mime_type"` // MIME type of the screenshot (e.g., "image/png")
|
ScreenshotMimeType string `json:"screenshot_mime_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateHistory represents the complete history of states during agent execution
|
|
||||||
type StateHistory struct {
|
type StateHistory struct {
|
||||||
States []StateDescription `json:"states"`
|
States []StateDescription `json:"states"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunAgent sends a request to run an agent with the given goal
|
type DesktopStateDescription struct {
|
||||||
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
ScreenContent string `json:"screen_content"`
|
||||||
body, err := json.Marshal(req)
|
ScreenshotPath string `json:"screenshot_path"`
|
||||||
if err != nil {
|
}
|
||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.httpClient.Post(
|
type DesktopStateHistory struct {
|
||||||
fmt.Sprintf("%s/api/browser/run", c.baseURL),
|
States []DesktopStateDescription `json:"states"`
|
||||||
"application/json",
|
}
|
||||||
bytes.NewBuffer(body),
|
|
||||||
)
|
type SearchResult struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResearchResult struct {
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Sources []SearchResult `json:"sources"`
|
||||||
|
KnowledgeGaps []string `json:"knowledge_gaps"`
|
||||||
|
SearchQueries []string `json:"search_queries"`
|
||||||
|
ResearchCycles int `json:"research_cycles"`
|
||||||
|
CompletionTime time.Duration `json:"completion_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RunBrowserAgent(req AgentRequest) (*StateHistory, error) {
|
||||||
|
return post[*StateHistory](c.httpClient, c.baseURL+"/api/browser/run", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RunDesktopAgent(req DesktopAgentRequest) (*DesktopStateHistory, error) {
|
||||||
|
return post[*DesktopStateHistory](c.httpClient, c.baseURL+"/api/desktop/run", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RunDeepResearch(req DeepResearchRequest) (*ResearchResult, error) {
|
||||||
|
return post[*ResearchResult](c.httpClient, c.baseURL+"/api/deep-research/run", req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Readyz() (string, error) {
|
||||||
|
return c.get("/readyz")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Healthz() (string, error) {
|
||||||
|
return c.get("/healthz")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) get(path string) (string, error) {
|
||||||
|
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
return "", fmt.Errorf("failed to make request: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
var state StateHistory
|
return resp.Status, nil
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&state); err != nil {
|
}
|
||||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
||||||
}
|
func post[T any](client *http.Client, url string, body interface{}) (T, error) {
|
||||||
|
var result T
|
||||||
return &state, nil
|
jsonBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("failed to marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Sending request", "url", url, "body", string(jsonBody))
|
||||||
|
|
||||||
|
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonBody))
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("failed to make request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Response", "status", resp.StatusCode)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return result, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
ActionSearch = "search"
|
ActionSearch = "search"
|
||||||
ActionCustom = "custom"
|
ActionCustom = "custom"
|
||||||
ActionBrowserAgentRunner = "browser-agent-runner"
|
ActionBrowserAgentRunner = "browser-agent-runner"
|
||||||
|
ActionDeepResearchRunner = "deep-research-runner"
|
||||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||||
ActionGithubIssueOpener = "github-issue-opener"
|
ActionGithubIssueOpener = "github-issue-opener"
|
||||||
ActionGithubIssueCloser = "github-issue-closer"
|
ActionGithubIssueCloser = "github-issue-closer"
|
||||||
@@ -54,6 +55,7 @@ var AvailableActions = []string{
|
|||||||
ActionGithubRepositoryGet,
|
ActionGithubRepositoryGet,
|
||||||
ActionGithubGetAllContent,
|
ActionGithubGetAllContent,
|
||||||
ActionBrowserAgentRunner,
|
ActionBrowserAgentRunner,
|
||||||
|
ActionDeepResearchRunner,
|
||||||
ActionGithubRepositoryCreateOrUpdate,
|
ActionGithubRepositoryCreateOrUpdate,
|
||||||
ActionGithubIssueReader,
|
ActionGithubIssueReader,
|
||||||
ActionGithubIssueCommenter,
|
ActionGithubIssueCommenter,
|
||||||
@@ -121,6 +123,8 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewGithubIssueSearch(config)
|
a = actions.NewGithubIssueSearch(config)
|
||||||
case ActionBrowserAgentRunner:
|
case ActionBrowserAgentRunner:
|
||||||
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
|
a = actions.NewBrowserAgentRunner(config, actionsConfigs["browser-agent-runner-base-url"])
|
||||||
|
case ActionDeepResearchRunner:
|
||||||
|
a = actions.NewDeepResearchRunner(config, actionsConfigs["deep-research-runner-base-url"])
|
||||||
case ActionGithubIssueReader:
|
case ActionGithubIssueReader:
|
||||||
a = actions.NewGithubIssueReader(config)
|
a = actions.NewGithubIssueReader(config)
|
||||||
case ActionGithubPRReader:
|
case ActionGithubPRReader:
|
||||||
@@ -181,6 +185,11 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Browser Agent Runner",
|
Label: "Browser Agent Runner",
|
||||||
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
Fields: actions.BrowserAgentRunnerConfigMeta(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "deep-research-runner",
|
||||||
|
Label: "Deep Research Runner",
|
||||||
|
Fields: actions.DeepResearchRunnerConfigMeta(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "generate_image",
|
Name: "generate_image",
|
||||||
Label: "Generate Image",
|
Label: "Generate Image",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package actions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/config"
|
"github.com/mudler/LocalAGI/pkg/config"
|
||||||
@@ -24,7 +25,7 @@ func NewBrowserAgentRunner(config map[string]string, defaultURL string) *Browser
|
|||||||
config["baseURL"] = defaultURL
|
config["baseURL"] = defaultURL
|
||||||
}
|
}
|
||||||
|
|
||||||
client := api.NewClient(config["baseURL"])
|
client := api.NewClient(config["baseURL"], 15*time.Minute)
|
||||||
|
|
||||||
return &BrowserAgentRunner{
|
return &BrowserAgentRunner{
|
||||||
client: client,
|
client: client,
|
||||||
|
|||||||
130
services/actions/deepresearchrunner.go
Normal file
130
services/actions/deepresearchrunner.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
24
webui/app.go
24
webui/app.go
@@ -419,6 +419,30 @@ 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 {
|
func (a *App) ExecuteAction(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
payload := struct {
|
payload := struct {
|
||||||
|
|||||||
103
webui/react-ui/src/components/CollapsibleRawSections.jsx
Normal file
103
webui/react-ui/src/components/CollapsibleRawSections.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import hljs from 'highlight.js/lib/core';
|
||||||
|
import json from 'highlight.js/lib/languages/json';
|
||||||
|
import 'highlight.js/styles/monokai.css';
|
||||||
|
|
||||||
|
hljs.registerLanguage('json', json);
|
||||||
|
|
||||||
|
export default function CollapsibleRawSections({ container }) {
|
||||||
|
const [showCreation, setShowCreation] = useState(false);
|
||||||
|
const [showProgress, setShowProgress] = useState(false);
|
||||||
|
const [showCompletion, setShowCompletion] = useState(false);
|
||||||
|
const [copied, setCopied] = useState({ creation: false, progress: false, completion: false });
|
||||||
|
|
||||||
|
const handleCopy = (section, data) => {
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
||||||
|
setCopied(prev => ({ ...prev, [section]: true }));
|
||||||
|
setTimeout(() => setCopied(prev => ({ ...prev, [section]: false })), 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Creation Section */}
|
||||||
|
{container.creation && (
|
||||||
|
<div>
|
||||||
|
<h4 style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', flex: 1 }}
|
||||||
|
onClick={() => setShowCreation(v => !v)}
|
||||||
|
>
|
||||||
|
<i className={`fas fa-chevron-${showCreation ? 'down' : 'right'}`} style={{ marginRight: 6 }} />
|
||||||
|
Creation
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
title="Copy Creation JSON"
|
||||||
|
onClick={e => { e.stopPropagation(); handleCopy('creation', container.creation); }}
|
||||||
|
style={{ marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: '#ccc' }}
|
||||||
|
>
|
||||||
|
{copied.creation ? <span style={{ color: '#6f6' }}>Copied!</span> : <i className="fas fa-copy" />}
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
{showCreation && (
|
||||||
|
<pre className="hljs"><code>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.creation || {}, null, 2), { language: 'json' }).value }} />
|
||||||
|
</code></pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Progress Section */}
|
||||||
|
{container.progress && container.progress.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', flex: 1 }}
|
||||||
|
onClick={() => setShowProgress(v => !v)}
|
||||||
|
>
|
||||||
|
<i className={`fas fa-chevron-${showProgress ? 'down' : 'right'}`} style={{ marginRight: 6 }} />
|
||||||
|
Progress
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
title="Copy Progress JSON"
|
||||||
|
onClick={e => { e.stopPropagation(); handleCopy('progress', container.progress); }}
|
||||||
|
style={{ marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: '#ccc' }}
|
||||||
|
>
|
||||||
|
{copied.progress ? <span style={{ color: '#6f6' }}>Copied!</span> : <i className="fas fa-copy" />}
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
{showProgress && (
|
||||||
|
<pre className="hljs"><code>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.progress || {}, null, 2), { language: 'json' }).value }} />
|
||||||
|
</code></pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Completion Section */}
|
||||||
|
{container.completion && (
|
||||||
|
<div>
|
||||||
|
<h4 style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', flex: 1 }}
|
||||||
|
onClick={() => setShowCompletion(v => !v)}
|
||||||
|
>
|
||||||
|
<i className={`fas fa-chevron-${showCompletion ? 'down' : 'right'}`} style={{ marginRight: 6 }} />
|
||||||
|
Completion
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
title="Copy Completion JSON"
|
||||||
|
onClick={e => { e.stopPropagation(); handleCopy('completion', container.completion); }}
|
||||||
|
style={{ marginLeft: 8, border: 'none', background: 'none', cursor: 'pointer', color: '#ccc' }}
|
||||||
|
>
|
||||||
|
{copied.completion ? <span style={{ color: '#6f6' }}>Copied!</span> : <i className="fas fa-copy" />}
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
{showCompletion && (
|
||||||
|
<pre className="hljs"><code>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.completion || {}, null, 2), { language: 'json' }).value }} />
|
||||||
|
</code></pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useOutletContext, useNavigate } from 'react-router-dom';
|
import { useOutletContext, useNavigate } from 'react-router-dom';
|
||||||
import { actionApi } from '../utils/api';
|
import { actionApi, agentApi } from '../utils/api';
|
||||||
|
import FormFieldDefinition from '../components/common/FormFieldDefinition';
|
||||||
|
import hljs from 'highlight.js/lib/core';
|
||||||
|
import json from 'highlight.js/lib/languages/json';
|
||||||
|
import 'highlight.js/styles/monokai.css';
|
||||||
|
hljs.registerLanguage('json', json);
|
||||||
|
|
||||||
function ActionsPlayground() {
|
function ActionsPlayground() {
|
||||||
const { showToast } = useOutletContext();
|
const { showToast } = useOutletContext();
|
||||||
@@ -12,6 +17,10 @@ function ActionsPlayground() {
|
|||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingActions, setLoadingActions] = useState(true);
|
const [loadingActions, setLoadingActions] = useState(true);
|
||||||
|
const [actionMetadata, setActionMetadata] = useState(null);
|
||||||
|
const [agentMetadata, setAgentMetadata] = useState(null);
|
||||||
|
const [configFields, setConfigFields] = useState([]);
|
||||||
|
const [paramFields, setParamFields] = useState([]);
|
||||||
|
|
||||||
// Update document title
|
// Update document title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,21 +45,106 @@ function ActionsPlayground() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchActions();
|
fetchActions();
|
||||||
}, [showToast]);
|
}, []);
|
||||||
|
|
||||||
|
// Fetch agent metadata on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAgentMetadata = async () => {
|
||||||
|
try {
|
||||||
|
const metadata = await agentApi.getAgentConfigMetadata();
|
||||||
|
setAgentMetadata(metadata);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching agent metadata:', err);
|
||||||
|
showToast('Failed to load agent metadata', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAgentMetadata();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch action definition when action is selected or config changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedAction) return;
|
||||||
|
|
||||||
|
const fetchActionDefinition = async () => {
|
||||||
|
try {
|
||||||
|
// Get config fields from agent metadata
|
||||||
|
const actionMeta = agentMetadata?.actions?.find(action => action.name === selectedAction);
|
||||||
|
const configFields = actionMeta?.fields || [];
|
||||||
|
console.debug('Config fields:', configFields);
|
||||||
|
setConfigFields(configFields);
|
||||||
|
|
||||||
|
// Parse current config to pass to action definition
|
||||||
|
let currentConfig = {};
|
||||||
|
try {
|
||||||
|
currentConfig = JSON.parse(configJson);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing current config:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parameter fields from action definition
|
||||||
|
const paramFields = await actionApi.getActionDefinition(selectedAction, currentConfig);
|
||||||
|
console.debug('Parameter fields:', paramFields);
|
||||||
|
setParamFields(paramFields);
|
||||||
|
|
||||||
|
// Reset JSON to match the new fields
|
||||||
|
setConfigJson(JSON.stringify(currentConfig, null, 2));
|
||||||
|
setParamsJson(JSON.stringify({}, null, 2));
|
||||||
|
setResult(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching action definition:', err);
|
||||||
|
showToast('Failed to load action definition', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchActionDefinition();
|
||||||
|
}, [selectedAction, agentMetadata]);
|
||||||
|
|
||||||
// Handle action selection
|
// Handle action selection
|
||||||
const handleActionChange = (e) => {
|
const handleActionChange = (e) => {
|
||||||
setSelectedAction(e.target.value);
|
setSelectedAction(e.target.value);
|
||||||
|
setConfigJson('{}');
|
||||||
|
setParamsJson('{}');
|
||||||
setResult(null);
|
setResult(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle JSON input changes
|
// Helper to generate onChange handlers for form fields
|
||||||
const handleConfigChange = (e) => {
|
const makeFieldChangeHandler = (fields, updateFn) => (e) => {
|
||||||
setConfigJson(e.target.value);
|
let value;
|
||||||
|
if (e && e.target) {
|
||||||
|
const fieldName = e.target.name;
|
||||||
|
const fieldDef = fields.find(f => f.name === fieldName);
|
||||||
|
const fieldType = fieldDef ? fieldDef.type : undefined;
|
||||||
|
if (fieldType === 'checkbox') {
|
||||||
|
value = e.target.checked;
|
||||||
|
} else if (fieldType === 'number') {
|
||||||
|
value = e.target.value === '' ? '' : Number(e.target.value);
|
||||||
|
} else {
|
||||||
|
value = e.target.value;
|
||||||
|
}
|
||||||
|
updateFn(fieldName, value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleParamsChange = (e) => {
|
// Handle form field changes
|
||||||
setParamsJson(e.target.value);
|
const handleConfigChange = (field, value) => {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(configJson);
|
||||||
|
config[field] = value;
|
||||||
|
setConfigJson(JSON.stringify(config, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating config:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParamsChange = (field, value) => {
|
||||||
|
try {
|
||||||
|
const params = JSON.parse(paramsJson);
|
||||||
|
params[field] = value;
|
||||||
|
setParamsJson(JSON.stringify(params, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating params:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute the selected action
|
// Execute the selected action
|
||||||
@@ -135,34 +229,31 @@ function ActionsPlayground() {
|
|||||||
|
|
||||||
{selectedAction && (
|
{selectedAction && (
|
||||||
<div className="section-box">
|
<div className="section-box">
|
||||||
<h2>Action Configuration</h2>
|
|
||||||
|
|
||||||
<form onSubmit={handleExecuteAction}>
|
<form onSubmit={handleExecuteAction}>
|
||||||
<div className="form-group mb-6">
|
{configFields.length > 0 && (
|
||||||
<label htmlFor="config-json">Configuration (JSON):</label>
|
<>
|
||||||
<textarea
|
<h2>Configuration</h2>
|
||||||
id="config-json"
|
<FormFieldDefinition
|
||||||
value={configJson}
|
fields={configFields}
|
||||||
onChange={handleConfigChange}
|
values={JSON.parse(configJson)}
|
||||||
className="form-control"
|
onChange={makeFieldChangeHandler(configFields, handleConfigChange)}
|
||||||
rows="5"
|
idPrefix="config_"
|
||||||
placeholder='{"key": "value"}'
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="form-group mb-6">
|
{paramFields.length > 0 && (
|
||||||
<label htmlFor="params-json">Parameters (JSON):</label>
|
<>
|
||||||
<textarea
|
<h2>Parameters</h2>
|
||||||
id="params-json"
|
<FormFieldDefinition
|
||||||
value={paramsJson}
|
fields={paramFields}
|
||||||
onChange={handleParamsChange}
|
values={JSON.parse(paramsJson)}
|
||||||
className="form-control"
|
onChange={makeFieldChangeHandler(paramFields, handleParamsChange)}
|
||||||
rows="5"
|
idPrefix="param_"
|
||||||
placeholder='{"key": "value"}'
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
@@ -194,9 +285,9 @@ function ActionsPlayground() {
|
|||||||
backgroundColor: 'rgba(30, 30, 30, 0.7)'
|
backgroundColor: 'rgba(30, 30, 30, 0.7)'
|
||||||
}}>
|
}}>
|
||||||
{typeof result === 'object' ? (
|
{typeof result === 'object' ? (
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<pre className="hljs"><code>
|
||||||
{JSON.stringify(result, null, 2)}
|
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(result, null, 2), { language: 'json' }).value }}></div>
|
||||||
</pre>
|
</code></pre>
|
||||||
) : (
|
) : (
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{result}
|
{result}
|
||||||
|
|||||||
@@ -1,4 +1,181 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import CollapsibleRawSections from '../components/CollapsibleRawSections';
|
||||||
|
|
||||||
|
function ObservableSummary({ observable }) {
|
||||||
|
// --- CREATION SUMMARIES ---
|
||||||
|
const creation = observable?.creation || {};
|
||||||
|
// ChatCompletionRequest summary
|
||||||
|
let creationChatMsg = '';
|
||||||
|
// Prefer chat_completion_message if present (for jobs/top-level containers)
|
||||||
|
if (creation?.chat_completion_message && creation.chat_completion_message.content) {
|
||||||
|
creationChatMsg = creation.chat_completion_message.content;
|
||||||
|
} else {
|
||||||
|
const messages = creation?.chat_completion_request?.messages;
|
||||||
|
if (Array.isArray(messages) && messages.length > 0) {
|
||||||
|
const lastMsg = messages[messages.length - 1];
|
||||||
|
creationChatMsg = lastMsg?.content || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FunctionDefinition summary
|
||||||
|
let creationFunctionDef = '';
|
||||||
|
if (creation?.function_definition?.name) {
|
||||||
|
creationFunctionDef = `Function: ${creation.function_definition.name}`;
|
||||||
|
}
|
||||||
|
// FunctionParams summary
|
||||||
|
let creationFunctionParams = '';
|
||||||
|
if (creation?.function_params && Object.keys(creation.function_params).length > 0) {
|
||||||
|
creationFunctionParams = `Params: ${JSON.stringify(creation.function_params)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- COMPLETION SUMMARIES ---
|
||||||
|
const completion = observable?.completion || {};
|
||||||
|
// ChatCompletionResponse summary
|
||||||
|
let completionChatMsg = '';
|
||||||
|
const chatCompletion = completion?.chat_completion_response;
|
||||||
|
if (
|
||||||
|
chatCompletion &&
|
||||||
|
Array.isArray(chatCompletion.choices) &&
|
||||||
|
chatCompletion.choices.length > 0
|
||||||
|
) {
|
||||||
|
const lastChoice = chatCompletion.choices[chatCompletion.choices.length - 1];
|
||||||
|
// Prefer tool_call summary if present
|
||||||
|
let toolCallSummary = '';
|
||||||
|
const toolCalls = lastChoice?.message?.tool_calls;
|
||||||
|
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||||
|
toolCallSummary = toolCalls.map(tc => {
|
||||||
|
let args = '';
|
||||||
|
// For OpenAI-style, arguments are in tc.function.arguments, function name in tc.function.name
|
||||||
|
if (tc.function && tc.function.arguments) {
|
||||||
|
try {
|
||||||
|
args = typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments);
|
||||||
|
} catch (e) {
|
||||||
|
args = '[Unserializable arguments]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toolName = tc.function?.name || tc.name || 'unknown';
|
||||||
|
return `Tool call: ${toolName}(${args})`;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
completionChatMsg = lastChoice?.message?.content || '';
|
||||||
|
// Attach toolCallSummary to completionChatMsg for rendering
|
||||||
|
if (toolCallSummary) {
|
||||||
|
completionChatMsg = { toolCallSummary, message: completionChatMsg };
|
||||||
|
}
|
||||||
|
// Else, it's just a string
|
||||||
|
|
||||||
|
}
|
||||||
|
// Conversation summary
|
||||||
|
let completionConversation = '';
|
||||||
|
if (Array.isArray(completion?.conversation) && completion.conversation.length > 0) {
|
||||||
|
const lastConv = completion.conversation[completion.conversation.length - 1];
|
||||||
|
completionConversation = lastConv?.content ? `${lastConv.content}` : '';
|
||||||
|
}
|
||||||
|
// ActionResult summary
|
||||||
|
let completionActionResult = '';
|
||||||
|
if (completion?.action_result) {
|
||||||
|
completionActionResult = `Action Result: ${String(completion.action_result).slice(0, 100)}`;
|
||||||
|
}
|
||||||
|
// AgentState summary
|
||||||
|
let completionAgentState = '';
|
||||||
|
if (completion?.agent_state) {
|
||||||
|
completionAgentState = `Agent State: ${JSON.stringify(completion.agent_state)}`;
|
||||||
|
}
|
||||||
|
// Error summary
|
||||||
|
let completionError = '';
|
||||||
|
if (completion?.error) {
|
||||||
|
completionError = `Error: ${completion.error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show if any summary is present
|
||||||
|
if (!creationChatMsg && !creationFunctionDef && !creationFunctionParams &&
|
||||||
|
!completionChatMsg && !completionConversation && !completionActionResult && !completionAgentState && !completionError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, margin: '2px 0 0 0' }}>
|
||||||
|
{/* CREATION */}
|
||||||
|
{creationChatMsg && (
|
||||||
|
<div title={creationChatMsg} style={{ display: 'flex', alignItems: 'center', color: '#cfc', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-comment-dots" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{creationChatMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{creationFunctionDef && (
|
||||||
|
<div title={creationFunctionDef} style={{ display: 'flex', alignItems: 'center', color: '#cfc', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-code" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{creationFunctionDef}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{creationFunctionParams && (
|
||||||
|
<div title={creationFunctionParams} style={{ display: 'flex', alignItems: 'center', color: '#fc9', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-sliders-h" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{creationFunctionParams}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* COMPLETION */}
|
||||||
|
{/* COMPLETION: Tool call summary if present */}
|
||||||
|
{completionChatMsg && typeof completionChatMsg === 'object' && completionChatMsg.toolCallSummary && (
|
||||||
|
<div
|
||||||
|
title={completionChatMsg.toolCallSummary}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: '#ffd966', // Distinct color for tool calls
|
||||||
|
fontSize: 14,
|
||||||
|
marginTop: 2,
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fas fa-tools" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ whiteSpace: 'pre-line', display: 'block' }}>{completionChatMsg.toolCallSummary}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* COMPLETION: Message content if present */}
|
||||||
|
{completionChatMsg && ((typeof completionChatMsg === 'object' && completionChatMsg.message) || typeof completionChatMsg === 'string') && (
|
||||||
|
<div
|
||||||
|
title={typeof completionChatMsg === 'object' ? completionChatMsg.message : completionChatMsg}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: '#8fc7ff',
|
||||||
|
fontSize: 14,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fas fa-robot" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{typeof completionChatMsg === 'object' ? completionChatMsg.message : completionChatMsg}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completionConversation && (
|
||||||
|
<div title={completionConversation} style={{ display: 'flex', alignItems: 'center', color: '#b8e2ff', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-comments" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionConversation}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completionActionResult && (
|
||||||
|
<div title={completionActionResult} style={{ display: 'flex', alignItems: 'center', color: '#ffd700', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-bolt" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionActionResult}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completionAgentState && (
|
||||||
|
<div title={completionAgentState} style={{ display: 'flex', alignItems: 'center', color: '#ffb8b8', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-brain" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionAgentState}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completionError && (
|
||||||
|
<div title={completionError} style={{ display: 'flex', alignItems: 'center', color: '#f66', fontSize: 14 }}>
|
||||||
|
<i className="fas fa-exclamation-triangle" style={{ marginRight: 6, flex: '0 0 auto' }}></i>
|
||||||
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{completionError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import hljs from 'highlight.js/lib/core';
|
import hljs from 'highlight.js/lib/core';
|
||||||
import json from 'highlight.js/lib/languages/json';
|
import json from 'highlight.js/lib/languages/json';
|
||||||
@@ -7,7 +184,7 @@ import 'highlight.js/styles/monokai.css';
|
|||||||
hljs.registerLanguage('json', json);
|
hljs.registerLanguage('json', json);
|
||||||
|
|
||||||
function AgentStatus() {
|
function AgentStatus() {
|
||||||
const [showStatus, setShowStatus] = useState(true);
|
const [showStatus, setShowStatus] = useState(false);
|
||||||
const { name } = useParams();
|
const { name } = useParams();
|
||||||
const [statusData, setStatusData] = useState(null);
|
const [statusData, setStatusData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -94,17 +271,16 @@ function AgentStatus() {
|
|||||||
setObservableMap(prevMap => {
|
setObservableMap(prevMap => {
|
||||||
const prev = prevMap[data.id] || {};
|
const prev = prevMap[data.id] || {};
|
||||||
const updated = {
|
const updated = {
|
||||||
...prev,
|
|
||||||
...data,
|
...data,
|
||||||
creation: data.creation,
|
...prev,
|
||||||
progress: data.progress,
|
|
||||||
completion: data.completion,
|
|
||||||
};
|
};
|
||||||
// Events can be received out of order
|
// Events can be received out of order
|
||||||
if (data.creation)
|
if (data.creation)
|
||||||
updated.creation = data.creation;
|
updated.creation = data.creation;
|
||||||
if (data.completion)
|
if (data.completion)
|
||||||
updated.completion = data.completion;
|
updated.completion = data.completion;
|
||||||
|
if ((data.progress?.length ?? 0) > (prev.progress?.length ?? 0))
|
||||||
|
updated.progress = data.progress;
|
||||||
if (data.parent_id && !prevMap[data.parent_id])
|
if (data.parent_id && !prevMap[data.parent_id])
|
||||||
prevMap[data.parent_id] = {
|
prevMap[data.parent_id] = {
|
||||||
id: data.parent_id,
|
id: data.parent_id,
|
||||||
@@ -252,12 +428,17 @@ function AgentStatus() {
|
|||||||
setExpandedCards(new Map(expandedCards).set(container.id, newExpanded));
|
setExpandedCards(new Map(expandedCards).set(container.id, newExpanded));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', maxWidth: '90%' }}>
|
||||||
<i className={`fas fa-${container.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
|
<i className={`fas fa-${container.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
|
||||||
<span>
|
<span style={{ width: '100%' }}>
|
||||||
<span className='stat-label'>{container.name}</span>#<span className='stat-label'>{container.id}</span>
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||||
</span>
|
<span>
|
||||||
</div>
|
<span className='stat-label'>{container.name}</span>#<span className='stat-label'>{container.id}</span>
|
||||||
|
</span>
|
||||||
|
<ObservableSummary observable={container} />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<i
|
<i
|
||||||
className={`fas fa-chevron-${expandedCards.get(container.id) ? 'up' : 'down'}`}
|
className={`fas fa-chevron-${expandedCards.get(container.id) ? 'up' : 'down'}`}
|
||||||
@@ -279,18 +460,23 @@ function AgentStatus() {
|
|||||||
const isExpanded = expandedCards.get(childKey);
|
const isExpanded = expandedCards.get(childKey);
|
||||||
return (
|
return (
|
||||||
<div key={`${container.id}-child-${child.id}`} className='card' style={{ background: '#222', marginBottom: '0.5em' }}>
|
<div key={`${container.id}-child-${child.id}`} className='card' style={{ background: '#222', marginBottom: '0.5em' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'hand', maxWidth: '100%' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newExpanded = !expandedCards.get(childKey);
|
const newExpanded = !expandedCards.get(childKey);
|
||||||
setExpandedCards(new Map(expandedCards).set(childKey, newExpanded));
|
setExpandedCards(new Map(expandedCards).set(childKey, newExpanded));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', maxWidth: '90%', gap: '10px', alignItems: 'center' }}>
|
||||||
<i className={`fas fa-${child.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
|
<i className={`fas fa-${child.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
|
||||||
<span>
|
<span style={{ width: '100%' }}>
|
||||||
<span className='stat-label'>{child.name}</span>#<span className='stat-label'>{child.id}</span>
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||||
</span>
|
<span>
|
||||||
</div>
|
<span className='stat-label'>{child.name}</span>#<span className='stat-label'>{child.id}</span>
|
||||||
|
</span>
|
||||||
|
<ObservableSummary observable={child} />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<i
|
<i
|
||||||
className={`fas fa-chevron-${isExpanded ? 'up' : 'down'}`}
|
className={`fas fa-chevron-${isExpanded ? 'up' : 'down'}`}
|
||||||
@@ -303,60 +489,14 @@ function AgentStatus() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: isExpanded ? 'block' : 'none' }}>
|
<div style={{ display: isExpanded ? 'block' : 'none' }}>
|
||||||
{child.creation && (
|
<CollapsibleRawSections container={child} />
|
||||||
<div>
|
|
||||||
<h5>Creation:</h5>
|
|
||||||
<pre className="hljs"><code>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.creation || {}, null, 2), { language: 'json' }).value }}></div>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{child.progress && child.progress.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h5>Progress:</h5>
|
|
||||||
<pre className="hljs"><code>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.progress || {}, null, 2), { language: 'json' }).value }}></div>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{child.completion && (
|
|
||||||
<div>
|
|
||||||
<h5>Completion:</h5>
|
|
||||||
<pre className="hljs"><code>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.completion || {}, null, 2), { language: 'json' }).value }}></div>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{container.creation && (
|
<CollapsibleRawSections container={container} />
|
||||||
<div>
|
|
||||||
<h4>Creation:</h4>
|
|
||||||
<pre className="hljs"><code>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.creation || {}, null, 2), { language: 'json' }).value }}></div>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{container.progress && container.progress.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h4>Progress:</h4>
|
|
||||||
<pre className="hljs"><code>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.progress || {}, null, 2), { language: 'json' }).value }}></div>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{container.completion && (
|
|
||||||
<div>
|
|
||||||
<h4>Completion:</h4>
|
|
||||||
<pre className="hljs"><code>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.completion || {}, null, 2), { language: 'json' }).value }}></div>
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
webui/react-ui/src/utils/api.js
vendored
55
webui/react-ui/src/utils/api.js
vendored
@@ -24,6 +24,50 @@ const buildUrl = (endpoint) => {
|
|||||||
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
|
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to convert ActionDefinition to FormFieldDefinition format
|
||||||
|
const convertActionDefinitionToFields = (definition) => {
|
||||||
|
if (!definition || !definition.Properties) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
const required = definition.Required || [];
|
||||||
|
|
||||||
|
console.debug('Action definition:', definition);
|
||||||
|
|
||||||
|
Object.entries(definition.Properties).forEach(([name, property]) => {
|
||||||
|
const field = {
|
||||||
|
name,
|
||||||
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
||||||
|
type: 'text', // Default to text, we'll enhance this later
|
||||||
|
required: required.includes(name),
|
||||||
|
helpText: property.Description || '',
|
||||||
|
defaultValue: property.Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (property.enum && property.enum.length > 0) {
|
||||||
|
field.type = 'select';
|
||||||
|
field.options = property.enum;
|
||||||
|
} else {
|
||||||
|
switch (property.type) {
|
||||||
|
case 'integer':
|
||||||
|
field.type = 'number';
|
||||||
|
field.min = property.Minimum;
|
||||||
|
field.max = property.Maximum;
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
field.type = 'checkbox';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO: Handle Object and Array types which require nested fields
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.push(field);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
|
|
||||||
// Agent-related API calls
|
// Agent-related API calls
|
||||||
export const agentApi = {
|
export const agentApi = {
|
||||||
// Get list of all agents
|
// Get list of all agents
|
||||||
@@ -216,6 +260,17 @@ export const actionApi = {
|
|||||||
return handleResponse(response);
|
return handleResponse(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get action definition
|
||||||
|
getActionDefinition: async (name, config = {}) => {
|
||||||
|
const response = await fetch(buildUrl(API_CONFIG.endpoints.actionDefinition(name)), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: API_CONFIG.headers,
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
});
|
||||||
|
const definition = await handleResponse(response);
|
||||||
|
return convertActionDefinitionToFields(definition);
|
||||||
|
},
|
||||||
|
|
||||||
// Execute an action for an agent
|
// Execute an action for an agent
|
||||||
executeAction: async (name, actionData) => {
|
executeAction: async (name, actionData) => {
|
||||||
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {
|
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {
|
||||||
|
|||||||
1
webui/react-ui/src/utils/config.js
vendored
1
webui/react-ui/src/utils/config.js
vendored
@@ -43,6 +43,7 @@ export const API_CONFIG = {
|
|||||||
|
|
||||||
// Action endpoints
|
// Action endpoints
|
||||||
listActions: '/api/actions',
|
listActions: '/api/actions',
|
||||||
|
actionDefinition: (name) => `/api/action/${name}/definition`,
|
||||||
executeAction: (name) => `/api/action/${name}/run`,
|
executeAction: (name) => `/api/action/${name}/run`,
|
||||||
|
|
||||||
// Status endpoint
|
// Status endpoint
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
|||||||
// Add endpoint for getting agent config metadata
|
// Add endpoint for getting agent config metadata
|
||||||
webapp.Get("/api/meta/agent/config", app.GetAgentConfigMeta())
|
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.Post("/api/action/:name/run", app.ExecuteAction(pool))
|
||||||
webapp.Get("/api/actions", app.ListActions())
|
webapp.Get("/api/actions", app.ListActions())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user