Compare commits

..

18 Commits

Author SHA1 Message Date
mudler
9bea6c7322 chore(tests): fix env var
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 15:46:39 +02:00
mudler
0ab7a615f0 fix: do not override actions
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 13:10:34 +02:00
mudler
b4b77c564e Not needed anymore, using tests
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 13:09:24 +02:00
mudler
24c3e41d27 Improve setup' 2025-04-24 13:07:06 +02:00
mudler
ebf26ebb64 Wire mcpbox to tests 2025-04-24 12:59:22 +02:00
mudler
3e80bd9608 Add container image to CI builds 2025-04-24 12:58:54 +02:00
mudler
34d0954171 Make it working, expose MCP prepare script to UI
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:46:47 +02:00
Ettore Di Giacinto
9b7344fbdf fixes
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:46:47 +02:00
mudler
608d7aba85 quality of life improvements
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:46:47 +02:00
mudler
bd903a3f33 add one-time process, attach to UI the mcp server json configuration
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:46:46 +02:00
mudler
0e1106eaf5 Revert "Attach to ui"
This reverts commit 088d0c47e87ee8f84297e47d178fb7384bbe6d45.

Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
e9cd6a1073 Attach to ui
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
4b727116cd Attach to config
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
3ebeb82f0d Add to dockerfile
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
6d9c58e6c0 Attach mcp stdio box to agent
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
02490ea8a2 Add mcpbox dockerfile and entrypoint
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
eec88d74fe Add groups to mcpbox
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
Ettore Di Giacinto
33b8aaddfe wip
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: mudler <mudler@localai.io>
2025-04-24 12:45:52 +02:00
22 changed files with 127 additions and 632 deletions

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v2
- run: |
# Add Docker's official GPG key:
sudo apt-get update

View File

@@ -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

View File

@@ -2,7 +2,7 @@
<img src="./webui/react-ui/public/logo_1.png" alt="LocalAGI Logo" width="220"/>
</p>
<h3 align="center"><em>Your AI. Your Hardware. Your Rules</em></h3>
<h3 align="center"><em>Your AI. Your Hardware. Your Rules.</em></h3>
<div align="center">
@@ -13,9 +13,9 @@
</div>
Create customizable AI assistants, automations, chat bots and agents that run 100% locally. No need for agentic Python libraries or cloud service keys, just bring your GPU (or even just CPU) and a web browser.
We empower you building AI Agents that you can run locally, without coding.
**LocalAGI** is a powerful, self-hostable AI Agent platform that allows you to design AI automations without writing code. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
**LocalAGI** is a powerful, self-hostable AI Agent platform designed for maximum privacy and flexibility. A complete drop-in replacement for OpenAI's Responses APIs with advanced agentic capabilities. No clouds. No data leaks. Just pure local AI that works on consumer-grade hardware (CPU and GPU).
## 🛡️ Take Back Your Privacy
@@ -37,7 +37,6 @@ LocalAGI ensures your data stays exactly where you want it—on your hardware. N
- 🖼 **Multimodal Support**: Ready for vision, text, and more.
- 🔧 **Extensible Custom Actions**: Easily script dynamic agent behaviors in Go (interpreted, no compilation!).
- 🛠 **Fully Customizable Models**: Use your own models or integrate seamlessly with [LocalAI](https://github.com/mudler/LocalAI).
- 📊 **Observability**: Monitor agent status and view detailed observable updates in real-time.
## 🛠️ Quickstart
@@ -69,11 +68,6 @@ Now you can access and manage your agents at [http://localhost:8080](http://loca
Still having issues? see this Youtube video: https://youtu.be/HtVwIxW3ePg
## Videos
[![Creating a basic agent](https://img.youtube.com/vi/HtVwIxW3ePg/mqdefault.jpg)](https://youtu.be/HtVwIxW3ePg)
[![Agent Observability](https://img.youtube.com/vi/v82rswGJt_M/mqdefault.jpg)](https://youtu.be/v82rswGJt_M)
## 📚🆕 Local Stack Family
🆕 LocalAI is now part of a comprehensive suite of AI tools designed to work together:
@@ -185,6 +179,14 @@ Good (relatively small) models that have been tested are:
- **✓ Effortless Setup**: Simple Docker compose setups and pre-built binaries.
- **✓ Feature-Rich**: From planning to multimodal capabilities, connectors for Slack, MCP support, LocalAGI has it all.
## 🌐 The Local Ecosystem
LocalAGI is part of the powerful Local family of privacy-focused AI tools:
- [**LocalAI**](https://github.com/mudler/LocalAI): Run Large Language Models locally.
- [**LocalRecall**](https://github.com/mudler/LocalRecall): Retrieval-Augmented Generation with local storage.
- [**LocalAGI**](https://github.com/mudler/LocalAGI): Deploy intelligent AI agents securely and privately.
## 🌟 Screenshots
### Powerful Web UI
@@ -192,8 +194,6 @@ Good (relatively small) models that have been tested are:
![Web UI Dashboard](https://github.com/user-attachments/assets/a40194f9-af3a-461f-8b39-5f4612fbf221)
![Web UI Agent Settings](https://github.com/user-attachments/assets/fb3c3e2a-cd53-4ca8-97aa-c5da51ff1f83)
![Web UI Create Group](https://github.com/user-attachments/assets/102189a2-0fba-4a1e-b0cb-f99268ef8062)
![Web UI Agent Observability](https://github.com/user-attachments/assets/f7359048-9d28-4cf1-9151-1f5556ce9235)
### Connectors Ready-to-Go

View File

@@ -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)

View File

@@ -180,12 +180,6 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult {
}()
if j.Obs != nil {
if len(j.ConversationHistory) > 0 {
m := j.ConversationHistory[len(j.ConversationHistory)-1]
j.Obs.Creation = &types.Creation{ ChatCompletionMessage: &m }
a.observer.Update(*j.Obs)
}
j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) {
j.Obs.Completion = &types.Completion{
Conversation: ccm,

View File

@@ -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"},
},

View File

@@ -6,7 +6,6 @@ import (
)
type Creation struct {
ChatCompletionMessage *openai.ChatCompletionMessage `json:"chat_completion_message,omitempty"`
ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"`
FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"`
FunctionParams ActionParams `json:"function_params,omitempty"`

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -1,4 +1,4 @@
package localoperator
package api
import (
"bytes"

View File

@@ -10,10 +10,6 @@ import (
"github.com/sashabaranov/go-openai/jsonschema"
)
const (
MetadataBrowserAgentHistory = "browser_agent_history"
)
type BrowserAgentRunner struct {
baseURL, customActionName string
client *api.Client
@@ -66,7 +62,7 @@ func (b *BrowserAgentRunner) Run(ctx context.Context, params types.ActionParams)
return types.ActionResult{
Result: fmt.Sprintf("Browser agent completed successfully. History:\n%s", historyStr),
Metadata: map[string]interface{}{MetadataBrowserAgentHistory: stateHistory},
Metadata: map[string]interface{}{"browser_agent_history": stateHistory},
}, nil
}

View File

@@ -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"],
}
}

View File

@@ -11,7 +11,6 @@ import (
"time"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/mudler/LocalAGI/pkg/localoperator"
"github.com/mudler/LocalAGI/pkg/xlog"
"github.com/mudler/LocalAGI/pkg/xstrings"
"github.com/mudler/LocalAGI/services/actions"
@@ -168,38 +167,8 @@ func replaceUserIDsWithNamesInMessage(api *slack.Client, message string) string
return message
}
func generateAttachmentsFromJobResponse(j *types.JobResult, api *slack.Client, channelID, ts string) (attachments []slack.Attachment) {
func generateAttachmentsFromJobResponse(j *types.JobResult) (attachments []slack.Attachment) {
for _, state := range j.State {
// coming from the browser agent
if history, exists := state.Metadata[actions.MetadataBrowserAgentHistory]; exists {
if historyStruct, ok := history.(*localoperator.StateHistory); ok {
state := historyStruct.States[len(historyStruct.States)-1]
// Decode base64 screenshot and upload to Slack
if state.Screenshot != "" {
screenshotData, err := base64.StdEncoding.DecodeString(state.Screenshot)
if err != nil {
xlog.Error(fmt.Sprintf("Error decoding screenshot: %v", err))
continue
}
data := string(screenshotData)
// Upload the file to Slack
_, err = api.UploadFileV2(slack.UploadFileV2Parameters{
Reader: bytes.NewReader(screenshotData),
FileSize: len(data),
ThreadTimestamp: ts,
Channel: channelID,
Filename: "screenshot.png",
InitialComment: "Browser Agent Screenshot",
})
if err != nil {
xlog.Error(fmt.Sprintf("Error uploading screenshot: %v", err))
continue
}
}
}
}
// coming from the search action
if urls, exists := state.Metadata[actions.MetadataUrls]; exists {
for _, url := range xstrings.UniqueSlice(urls.([]string)) {
@@ -406,7 +375,7 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(message, true),
slack.MsgOptionPostMessageParameters(postMessageParams),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, "")...),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
)
if err != nil {
xlog.Error(fmt.Sprintf("Error posting message: %v", err))
@@ -418,7 +387,7 @@ func replyWithPostMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(res.Response, true),
slack.MsgOptionPostMessageParameters(postMessageParams),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, "")...),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
// slack.MsgOptionTS(ts),
)
if err != nil {
@@ -439,7 +408,7 @@ func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionLinkNames(true),
slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(messages[0], true),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, msgTs)...),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
)
if err != nil {
xlog.Error(fmt.Sprintf("Error updating final message: %v", err))
@@ -466,7 +435,7 @@ func replyToUpdateMessage(finalResponse string, api *slack.Client, ev *slackeven
slack.MsgOptionLinkNames(true),
slack.MsgOptionEnableLinkUnfurl(),
slack.MsgOptionText(finalResponse, true),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res, api, ev.Channel, msgTs)...),
slack.MsgOptionAttachments(generateAttachmentsFromJobResponse(res)...),
)
if err != nil {
xlog.Error(fmt.Sprintf("Error updating final message: %v", err))

View File

@@ -23,7 +23,6 @@ oauth_config:
- commands
- groups:history
- files:read
- files:write
- im:history
- im:read
- im:write

View File

@@ -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 {

View File

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

View File

@@ -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}

View File

@@ -1,181 +1,4 @@
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 hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
@@ -184,7 +7,7 @@ import 'highlight.js/styles/monokai.css';
hljs.registerLanguage('json', json);
function AgentStatus() {
const [showStatus, setShowStatus] = useState(false);
const [showStatus, setShowStatus] = useState(true);
const { name } = useParams();
const [statusData, setStatusData] = useState(null);
const [loading, setLoading] = useState(true);
@@ -271,16 +94,17 @@ function AgentStatus() {
setObservableMap(prevMap => {
const prev = prevMap[data.id] || {};
const updated = {
...data,
...prev,
...data,
creation: data.creation,
progress: data.progress,
completion: data.completion,
};
// Events can be received out of order
if (data.creation)
updated.creation = data.creation;
if (data.completion)
updated.completion = data.completion;
if ((data.progress?.length ?? 0) > (prev.progress?.length ?? 0))
updated.progress = data.progress;
if (data.parent_id && !prevMap[data.parent_id])
prevMap[data.parent_id] = {
id: data.parent_id,
@@ -428,16 +252,11 @@ function AgentStatus() {
setExpandedCards(new Map(expandedCards).set(container.id, newExpanded));
}}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', maxWidth: '90%' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<i className={`fas fa-${container.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
<span style={{ width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
<span>
<span className='stat-label'>{container.name}</span>#<span className='stat-label'>{container.id}</span>
</span>
<ObservableSummary observable={container} />
</div>
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<i
@@ -460,22 +279,17 @@ function AgentStatus() {
const isExpanded = expandedCards.get(childKey);
return (
<div key={`${container.id}-child-${child.id}`} className='card' style={{ background: '#222', marginBottom: '0.5em' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'hand', maxWidth: '100%' }}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => {
const newExpanded = !expandedCards.get(childKey);
setExpandedCards(new Map(expandedCards).set(childKey, newExpanded));
}}
>
<div style={{ display: 'flex', maxWidth: '90%', gap: '10px', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<i className={`fas fa-${child.icon || 'robot'}`} style={{ verticalAlign: '-0.125em' }}></i>
<span style={{ width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
<span>
<span className='stat-label'>{child.name}</span>#<span className='stat-label'>{child.id}</span>
</span>
<ObservableSummary observable={child} />
</div>
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<i
@@ -489,14 +303,60 @@ function AgentStatus() {
</div>
</div>
<div style={{ display: isExpanded ? 'block' : 'none' }}>
<CollapsibleRawSections container={child} />
{child.creation && (
<div>
<h5>Creation:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.creation || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{child.progress && child.progress.length > 0 && (
<div>
<h5>Progress:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.progress || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{child.completion && (
<div>
<h5>Completion:</h5>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(child.completion || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
</div>
</div>
);
})}
</div>
)}
<CollapsibleRawSections container={container} />
{container.creation && (
<div>
<h4>Creation:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.creation || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{container.progress && container.progress.length > 0 && (
<div>
<h4>Progress:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.progress || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
{container.completion && (
<div>
<h4>Completion:</h4>
<pre className="hljs"><code>
<div dangerouslySetInnerHTML={{ __html: hljs.highlight(JSON.stringify(container.completion || {}, null, 2), { language: 'json' }).value }}></div>
</code></pre>
</div>
)}
</div>
</div>
</div>

View File

@@ -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)), {

View File

@@ -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

View File

@@ -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())