Files
LocalAI/core/http/endpoints/studio/draft.go
T
Ettore Di Giacinto e0d77e410c Report the mode of the built-in bash in the chat add-on list
A chat with no add-on that runs commands could not tell a person
whether the server's built-in bash was off or simply not selected.
The add-on list now carries commands_host_exec (off, ask or on), a
read-only copy of the chats_host_exec setting as it applies now.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:19:45 +00:00

115 lines
5.1 KiB
Go

package studio
import (
"errors"
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/http/auth"
studiosvc "github.com/mudler/LocalAI/core/services/studio"
"github.com/mudler/LocalAI/core/services/studio/connsets"
)
// draftQuery reads the model and the agent of a chat that does not exist yet,
// and refuses what Create would refuse: an agent without the agents feature,
// a model the user may not use.
func (h *Handlers) draftQuery(c echo.Context) (model, agentID string, ok bool) {
model, agentID = c.QueryParam("model"), c.QueryParam("agent_id")
if agentID != "" && !auth.FeatureAllowed(c, auth.FeatureAgents) {
_ = fail(c, http.StatusForbidden, "authorization_error", "feature not enabled for your account: "+auth.FeatureAgents)
return "", "", false
}
if model != "" && h.models != nil {
switch err := h.models.Check(c.Request().Context(), auth.GetUser(c), model); {
case errors.Is(err, ErrUnknownModel):
_ = fail(c, http.StatusBadRequest, "invalid_request_error", "unknown model")
return "", "", false
case errors.Is(err, ErrModelNotAllowed):
_ = fail(c, http.StatusForbidden, "authorization_error", "you may not use this model")
return "", "", false
case err != nil:
_ = failFor(c, err)
return "", "", false
}
}
return model, agentID, true
}
// DraftTools lists the tools a new chat could switch.
// @Summary List the tools a new chat could switch on and off.
// @Description What GET /api/chats/{id}/tools lists, for a chat that does not exist yet: a chat on model, with the saved agent agent_id when it is set (the agent's model, when it has one, replaces model). Every switch is in its default state. The composer of a new chat shows it and sends the person's choices in the switches of POST /api/chats.
// @Tags chats
// @Param model query string false "the model of the new chat"
// @Param agent_id query string false "a saved agent of yours"
// @Success 200 {object} schema.ChatsToolsPanel "The tools and their default switches"
// @Failure 400 {object} schema.ErrorResponse "The request is not valid"
// @Failure 403 {object} schema.ErrorResponse "Refused"
// @Failure 404 {object} schema.ErrorResponse "No such agent"
// @Router /api/chats/draft/tools [get]
func (h *Handlers) DraftTools(c echo.Context) error {
model, agentID, ok := h.draftQuery(c)
if !ok {
return nil
}
p, err := h.svc.DraftTools(c.Request().Context(), userID(c), model, agentID)
if err != nil {
return failFor(c, err)
}
return writeToolsPanel(c, p)
}
// DraftAddons lists the add-ons a new chat could use.
// @Summary List the add-ons a new chat could use.
// @Description What GET /api/chats/{id}/addons lists, for a chat that does not exist yet, as GET /api/chats/draft/tools takes it: the add-ons with their switch and whether the chat could run commands at its first turn. The draft's current switches may be passed (addons_off, builtins_off, builtins_on: comma-separated lists, as the create request takes them) so the answer is the one the chat would get: with the shell group off, commands is switched_off.
// @Tags chats
// @Param model query string false "the model of the new chat"
// @Param agent_id query string false "a saved agent of yours"
// @Param addons_off query string false "comma-separated add-on IDs the draft switched off"
// @Param builtins_off query string false "comma-separated built-in groups the draft switched off"
// @Param builtins_on query string false "comma-separated opt-in built-in groups the draft switched on"
// @Success 200 {object} schema.ChatsAddonList "The add-ons and the commands state, with the draft switches applied"
// @Failure 400 {object} schema.ErrorResponse "The request is not valid"
// @Failure 403 {object} schema.ErrorResponse "Refused"
// @Failure 404 {object} schema.ErrorResponse "No such agent"
// @Router /api/chats/draft/addons [get]
func (h *Handlers) DraftAddons(c echo.Context) error {
model, agentID, ok := h.draftQuery(c)
if !ok {
return nil
}
list, cmds, err := h.svc.DraftAddons(c.Request().Context(), userID(c), model, agentID, draftSwitches(c))
if err != nil {
return failFor(c, err)
}
return writeAddonList(c, h, list, cmds)
}
// draftSwitches reads the switches of a draft from the query: comma-separated
// lists, also when the parameter is repeated. The service checks the names.
func draftSwitches(c echo.Context) studiosvc.Switches {
list := func(name string) []string {
var out []string
for _, v := range c.QueryParams()[name] {
for _, item := range strings.Split(v, ",") {
if item = strings.TrimSpace(item); item != "" {
out = append(out, item)
}
}
}
return out
}
sw := studiosvc.Switches{AddonsOff: list("addons_off"), BuiltinsOff: list("builtins_off"), BuiltinsOn: list("builtins_on")}
// connections=<addon>:<binding>, comma-separated; an add-on appears once
// per binding it may use. The service checks them.
for _, pair := range list("connections") {
if addon, binding, ok := strings.Cut(pair, ":"); ok && addon != "" && binding != "" {
if sw.Connections == nil {
sw.Connections = connsets.Sets{}
}
sw.Connections[addon] = append(sw.Connections[addon], binding)
}
}
return sw
}