mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
The previous commit was made on a stale base and so reverted work that landed on master before it. This commit puts every file back to the state of its parent's parent. The permission enforcement of the previous commit stays. Assisted-by: Claude:claude-sonnet-5-5 go
209 lines
8.3 KiB
Go
209 lines
8.3 KiB
Go
package studio
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"github.com/mudler/LocalAI/core/http/auth"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
studiosvc "github.com/mudler/LocalAI/core/services/studio"
|
|
"github.com/mudler/LocalAI/core/services/studio/agents"
|
|
)
|
|
|
|
// folderOut is the folder of an agent as the API shows it, or nil for an
|
|
// agent that works on all the user's files.
|
|
func folderOut(st studiosvc.AgentFolderState, ok bool) *schema.ChatsAgentFolder {
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &schema.ChatsAgentFolder{Path: st.Path, Name: st.Name, Mode: st.Mode, Effective: st.Effective, State: st.State}
|
|
}
|
|
|
|
// foldersIn is the folder choice of a write as a list: folders when sent, else
|
|
// the one folder. Nil means the body has none, which keeps the stored folders.
|
|
// {"folders": []} is an empty, non-nil list and clears them.
|
|
func foldersIn(req schema.ChatsAgentWrite) *[]studiosvc.AgentFolderWrite {
|
|
if req.Folders == nil && req.Folder == nil {
|
|
return nil
|
|
}
|
|
out := []studiosvc.AgentFolderWrite{}
|
|
if req.Folders != nil {
|
|
for _, f := range req.Folders {
|
|
out = append(out, studiosvc.AgentFolderWrite{Path: f.Path, Mode: f.Mode})
|
|
}
|
|
} else if req.Folder.Path != "" {
|
|
out = append(out, studiosvc.AgentFolderWrite{Path: req.Folder.Path, Mode: req.Folder.Mode})
|
|
}
|
|
return &out
|
|
}
|
|
|
|
// folderIn is the folder choice of a write, or nil when the body has none.
|
|
func folderIn(f *schema.ChatsAgentFolderWrite) *studiosvc.AgentFolderWrite {
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
return &studiosvc.AgentFolderWrite{Path: f.Path, Mode: f.Mode}
|
|
}
|
|
|
|
// agentFull is agentOut with the folder resolved for the user.
|
|
func (h *Handlers) agentFull(c echo.Context, a agents.Agent) schema.ChatsAgent {
|
|
out := agentOut(a)
|
|
h.addFolders(c, &out, a, h.svc.AgentFolderRows(c.Request().Context(), userID(c), []agents.Agent{a})[a.ID])
|
|
return out
|
|
}
|
|
|
|
// addFolders puts the folder rows of an agent, and the notice, in its output.
|
|
func (h *Handlers) addFolders(c echo.Context, out *schema.ChatsAgent, a agents.Agent, rows []studiosvc.AgentFolderState) {
|
|
if len(rows) == 0 {
|
|
return
|
|
}
|
|
out.Folder = folderOut(rows[0], true)
|
|
for _, r := range rows {
|
|
out.Folders = append(out.Folders, *folderOut(r, true))
|
|
}
|
|
if n := h.svc.SharedNotMounted(c.Request().Context(), userID(c), a); n > 0 {
|
|
out.FolderNotice = &schema.ChatsAgentFolderNotice{NotMounted: n}
|
|
}
|
|
}
|
|
|
|
func agentOut(a agents.Agent) schema.ChatsAgent {
|
|
nz := func(l []string) []string {
|
|
if l == nil {
|
|
return []string{}
|
|
}
|
|
return l
|
|
}
|
|
return schema.ChatsAgent{ID: a.ID, Name: a.Name, Description: a.Description, Instructions: a.Instructions, Model: a.Model,
|
|
MCPServers: nz(a.MCPServers), Addons: nz(a.Addons), Collections: nz(a.Collections), Skills: nz(a.Skills),
|
|
Network: networkOf(a.Network), BuiltinsOff: nz(a.BuiltinsOff), BuiltinsOn: nz(a.BuiltinsOn), Created: a.Created, Updated: a.Updated}
|
|
}
|
|
|
|
// agentIn reads the body. A model the user may not use is refused here, so a
|
|
// saved agent never names one the service would refuse at the first turn.
|
|
// When ok is false the refusal is already written and err is the write result,
|
|
// so the caller returns err without writing a second body.
|
|
func (h *Handlers) agentIn(c echo.Context) (a agents.Agent, folders *[]studiosvc.AgentFolderWrite, ok bool, err error) {
|
|
var req schema.ChatsAgentWrite
|
|
if err := c.Bind(&req); err != nil {
|
|
return a, nil, false, fail(c, http.StatusBadRequest, "invalid_request_error", "the body is not valid JSON")
|
|
}
|
|
if req.Model != "" && h.models != nil {
|
|
switch err := h.models.Check(c.Request().Context(), auth.GetUser(c), req.Model); {
|
|
case errors.Is(err, ErrUnknownModel):
|
|
return a, nil, false, fail(c, http.StatusBadRequest, "invalid_request_error", "unknown model")
|
|
case errors.Is(err, ErrModelNotAllowed):
|
|
return a, nil, false, fail(c, http.StatusForbidden, "authorization_error", "you may not use this model")
|
|
case err != nil:
|
|
return a, nil, false, failFor(c, err)
|
|
}
|
|
}
|
|
return agents.Agent{Name: req.Name, Description: req.Description, Instructions: req.Instructions, Model: req.Model,
|
|
MCPServers: req.MCPServers, Addons: req.Addons, Collections: req.Collections, Skills: req.Skills, Network: req.Network,
|
|
BuiltinsOff: req.BuiltinsOff, BuiltinsOn: req.BuiltinsOn},
|
|
foldersIn(req), true, nil
|
|
}
|
|
|
|
// networkOf is the network preset of an agent as the API shows it: off when
|
|
// none was set.
|
|
func networkOf(n string) string {
|
|
if n == "" {
|
|
return "off"
|
|
}
|
|
return n
|
|
}
|
|
|
|
// ListAgents lists the saved agents.
|
|
// @Summary List your saved agents.
|
|
// @Tags chats
|
|
// @Success 200 {object} schema.ChatsAgentList "Your agents, sorted by name"
|
|
// @Router /api/chats/agents [get]
|
|
func (h *Handlers) ListAgents(c echo.Context) error {
|
|
list, err := h.svc.Agents(c.Request().Context(), userID(c))
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
out := schema.ChatsAgentList{Agents: make([]schema.ChatsAgent, 0, len(list))}
|
|
rows := h.svc.AgentFolderRows(c.Request().Context(), userID(c), list)
|
|
for _, a := range list {
|
|
o := agentOut(a)
|
|
h.addFolders(c, &o, a, rows[a.ID])
|
|
out.Agents = append(out.Agents, o)
|
|
}
|
|
return c.JSON(http.StatusOK, out)
|
|
}
|
|
|
|
// CreateAgent saves a new agent.
|
|
// @Summary Save an agent.
|
|
// @Description An agent is a definition: instructions and exact lists of the MCP servers, add-ons, collections and skills a chat with it may use. A chat gets what the lists name and you may use at that moment, never more. Start a chat with it by passing agent_id to POST /api/chats. folders [{path, mode}] gives the agent up to 8 folders of your files, each read or write (folder {path, mode} is the same with one entry); without them the agent works on all your files.
|
|
// @Tags chats
|
|
// @Param request body schema.ChatsAgentWrite true "the agent"
|
|
// @Success 201 {object} schema.ChatsAgent "The saved agent"
|
|
// @Failure 400 {object} schema.ErrorResponse "The definition or the folder is not valid"
|
|
// @Router /api/chats/agents [post]
|
|
func (h *Handlers) CreateAgent(c echo.Context) error {
|
|
a, folders, ok, err := h.agentIn(c)
|
|
if !ok {
|
|
return err
|
|
}
|
|
saved, err := h.svc.SaveAgentWithFolders(c.Request().Context(), userID(c), a, folders)
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
return c.JSON(http.StatusCreated, h.agentFull(c, saved))
|
|
}
|
|
|
|
// GetAgent reads a saved agent.
|
|
// @Summary Read a saved agent.
|
|
// @Tags chats
|
|
// @Param id path string true "the agent's id"
|
|
// @Success 200 {object} schema.ChatsAgent "The agent"
|
|
// @Failure 404 {object} schema.ErrorResponse "No such conversation, or it is another user's"
|
|
// @Router /api/chats/agents/{id} [get]
|
|
func (h *Handlers) GetAgent(c echo.Context) error {
|
|
a, err := h.svc.Agent(c.Request().Context(), userID(c), c.Param("id"))
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, h.agentFull(c, a))
|
|
}
|
|
|
|
// UpdateAgent replaces a saved agent.
|
|
// @Summary Replace a saved agent.
|
|
// @Description The body replaces every field but folder: a body without folder or folders keeps the folders the agent has, so a client that does not know the fields cannot widen the agent, and folders [] (or folder {path: ""}) clears them. Chats that already run with the agent use the new definition from their next turn.
|
|
// @Tags chats
|
|
// @Param id path string true "the agent's id"
|
|
// @Param request body schema.ChatsAgentWrite true "the agent"
|
|
// @Success 200 {object} schema.ChatsAgent "The saved agent"
|
|
// @Failure 400 {object} schema.ErrorResponse "The definition or the folder is not valid"
|
|
// @Failure 404 {object} schema.ErrorResponse "No such conversation, or it is another user's"
|
|
// @Router /api/chats/agents/{id} [put]
|
|
func (h *Handlers) UpdateAgent(c echo.Context) error {
|
|
a, folders, ok, err := h.agentIn(c)
|
|
if !ok {
|
|
return err
|
|
}
|
|
a.ID = c.Param("id")
|
|
saved, err := h.svc.SaveAgentWithFolders(c.Request().Context(), userID(c), a, folders)
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, h.agentFull(c, saved))
|
|
}
|
|
|
|
// DeleteAgent deletes a saved agent.
|
|
// @Summary Delete a saved agent.
|
|
// @Description Chats that ran with it keep their history; their next turn runs without the agent's tools.
|
|
// @Tags chats
|
|
// @Param id path string true "the agent's id"
|
|
// @Success 204 "Deleted"
|
|
// @Failure 404 {object} schema.ErrorResponse "No such conversation, or it is another user's"
|
|
// @Router /api/chats/agents/{id} [delete]
|
|
func (h *Handlers) DeleteAgent(c echo.Context) error {
|
|
if err := h.svc.DeleteAgent(c.Request().Context(), userID(c), c.Param("id")); err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|