Files
LocalAI/core/http/endpoints/localai/agents.go
Ettore Di Giacinto 89076bab92 fix(ui): wrap struct to pass metadata fields
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-10 08:48:25 +00:00

376 lines
12 KiB
Go

package localai
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/application"
"github.com/mudler/LocalAI/core/services"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/LocalAGI/core/state"
coreTypes "github.com/mudler/LocalAGI/core/types"
agiServices "github.com/mudler/LocalAGI/services"
)
func ListAgentsEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
statuses := svc.ListAgents()
agents := make([]string, 0, len(statuses))
for name := range statuses {
agents = append(agents, name)
}
sort.Strings(agents)
resp := map[string]any{
"agents": agents,
"agentCount": len(agents),
"actions": len(agiServices.AvailableActions),
"connectors": len(agiServices.AvailableConnectors),
"statuses": statuses,
}
if hubURL := svc.AgentHubURL(); hubURL != "" {
resp["agent_hub_url"] = hubURL
}
return c.JSON(http.StatusOK, resp)
}
}
func CreateAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
var cfg state.AgentConfig
if err := c.Bind(&cfg); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
if err := svc.CreateAgent(&cfg); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
}
}
func GetAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
ag := svc.GetAgent(name)
if ag == nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Agent not found"})
}
return c.JSON(http.StatusOK, map[string]any{
"active": !ag.Paused(),
})
}
}
func UpdateAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
var cfg state.AgentConfig
if err := c.Bind(&cfg); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
if err := svc.UpdateAgent(name, &cfg); err != nil {
if strings.Contains(err.Error(), "not found") {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
}
func DeleteAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
if err := svc.DeleteAgent(name); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
}
func GetAgentConfigEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
cfg := svc.GetAgentConfig(name)
if cfg == nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Agent not found"})
}
return c.JSON(http.StatusOK, cfg)
}
}
func PauseAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
if err := svc.PauseAgent(c.Param("name")); err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
}
func ResumeAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
if err := svc.ResumeAgent(c.Param("name")); err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
}
func GetAgentStatusEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
history := svc.GetAgentStatus(name)
if history == nil {
history = &state.Status{ActionResults: []coreTypes.ActionState{}}
}
entries := []string{}
for i := len(history.Results()) - 1; i >= 0; i-- {
h := history.Results()[i]
actionName := ""
if h.ActionCurrentState.Action != nil {
actionName = h.ActionCurrentState.Action.Definition().Name.String()
}
entries = append(entries, fmt.Sprintf("Reasoning: %s\nAction taken: %s\nParameters: %+v\nResult: %s",
h.Reasoning,
actionName,
h.ActionCurrentState.Params,
h.Result))
}
return c.JSON(http.StatusOK, map[string]any{
"Name": name,
"History": entries,
})
}
}
func GetAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
history, err := svc.GetAgentObservables(name)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]any{
"Name": name,
"History": history,
})
}
}
func ClearAgentObservablesEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
if err := svc.ClearAgentObservables(name); err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]any{"Name": name, "cleared": true})
}
}
func ChatWithAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
var payload struct {
Message string `json:"message"`
}
if err := c.Bind(&payload); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request format"})
}
message := strings.TrimSpace(payload.Message)
if message == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Message cannot be empty"})
}
messageID, err := svc.Chat(name, message)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusAccepted, map[string]any{
"status": "message_received",
"message_id": messageID,
})
}
}
func AgentSSEEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
manager := svc.GetSSEManager(name)
if manager == nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Agent not found"})
}
return services.HandleSSE(c, manager)
}
}
type agentConfigMetaResponse struct {
state.AgentConfigMeta
OutputsDir string `json:"OutputsDir"`
}
func GetAgentConfigMetaEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
return c.JSON(http.StatusOK, agentConfigMetaResponse{
AgentConfigMeta: svc.GetConfigMeta(),
OutputsDir: svc.OutputsDir(),
})
}
}
func ExportAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
name := c.Param("name")
data, err := svc.ExportAgent(name)
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
}
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.json", name))
return c.JSONBlob(http.StatusOK, data)
}
}
func ImportAgentEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
// Try multipart form file first
file, err := c.FormFile("file")
if err == nil {
src, err := file.Open()
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to open file"})
}
defer src.Close()
data, err := io.ReadAll(src)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to read file"})
}
if err := svc.ImportAgent(data); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
}
// Try JSON body
var cfg state.AgentConfig
if err := json.NewDecoder(c.Request().Body).Decode(&cfg); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request: provide a file or JSON body"})
}
data, err := json.Marshal(&cfg)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
if err := svc.ImportAgent(data); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
}
}
// --- Actions ---
func ListActionsEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
return c.JSON(http.StatusOK, map[string]any{
"actions": svc.ListAvailableActions(),
})
}
}
func GetActionDefinitionEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
actionName := c.Param("name")
var payload struct {
Config map[string]string `json:"config"`
}
if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil {
payload.Config = map[string]string{}
}
def, err := svc.GetActionDefinition(actionName, payload.Config)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, def)
}
}
func ExecuteActionEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
actionName := c.Param("name")
var payload struct {
Config map[string]string `json:"config"`
Params coreTypes.ActionParams `json:"params"`
}
if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"})
}
result, err := svc.ExecuteAction(c.Request().Context(), actionName, payload.Config, payload.Params)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, result)
}
}
func AgentFileEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
svc := app.AgentPoolService()
requestedPath := c.QueryParam("path")
if requestedPath == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "no file path specified"})
}
// Resolve the real path (follows symlinks, eliminates ..)
resolved, err := filepath.EvalSymlinks(filepath.Clean(requestedPath))
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
}
// Only serve files from the outputs subdirectory
outputsDir, _ := filepath.EvalSymlinks(filepath.Clean(svc.OutputsDir()))
if utils.InTrustedRoot(resolved, outputsDir) != nil {
return c.JSON(http.StatusForbidden, map[string]string{"error": "access denied"})
}
info, err := os.Stat(resolved)
if err != nil || info.IsDir() {
return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
}
return c.File(resolved)
}
}