mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-05 20:13:23 -05:00
The proposal card lets the owner correct the text before confirming.
POST /api/chats/changes/{id}/confirm now accepts an optional JSON body
{"content": "..."}. The text has the same size limit and text check as a
proposal, the file and the audit hash hold what was written, and a
refused text leaves the proposal waiting.
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
167 lines
7.0 KiB
Go
167 lines
7.0 KiB
Go
package studio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
studiosvc "github.com/mudler/LocalAI/core/services/studio"
|
|
"github.com/mudler/LocalAI/core/services/studio/proposals"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
)
|
|
|
|
var changeRules = []errRule{
|
|
fixed(http.StatusNotFound, typeNotFound, "change not found", proposals.ErrNotFound),
|
|
verbatim(http.StatusBadRequest, typeInvalid, studiosvc.ErrBadChange.Error()+": ", studiosvc.ErrBadChange),
|
|
verbatim(http.StatusConflict, typeConflict, "studio: ", studiosvc.ErrChangeExpired, studiosvc.ErrChangeAnswered,
|
|
studiosvc.ErrTargetChanged, studiosvc.ErrNotApplied, studiosvc.ErrCannotRevert, studiosvc.ErrNothingToRevert),
|
|
fixed(http.StatusConflict, typeFilesOff, "changes need files, which are off on this server", studiosvc.ErrChangesOff),
|
|
}
|
|
|
|
func failChange(c echo.Context, err error) error { return mapErr(c, err, changeRules, failFor) }
|
|
|
|
// ListChanges lists the change proposals.
|
|
// @Summary List the changes that a chat proposed.
|
|
// @Description A model proposes a change to your memory or an agent profile with the propose_change tool, from a Studio chat or a connector chat. Nothing is written until you confirm. A proposal expires after 24 hours. Newest first; status keeps only one status.
|
|
// @Tags chats
|
|
// @Param status query string false "pending, applied, dismissed, expired, reverted or stale"
|
|
// @Success 200 {array} contract.ChangeProposal "Your proposals"
|
|
// @Failure 400 {object} schema.ErrorResponse "The status is not known"
|
|
// @Router /api/chats/changes [get]
|
|
func (h *Handlers) ListChanges(c echo.Context) error {
|
|
var statuses []contract.ProposalStatus
|
|
if s := c.QueryParam("status"); s != "" {
|
|
st := contract.ProposalStatus(s)
|
|
switch st {
|
|
case contract.ProposalPending, contract.ProposalApplied, contract.ProposalDismissed, contract.ProposalExpired, contract.ProposalReverted, contract.ProposalStale:
|
|
statuses = append(statuses, st)
|
|
default:
|
|
return fail(c, http.StatusBadRequest, typeInvalid, "unknown status")
|
|
}
|
|
}
|
|
list, err := h.svc.Changes().List(c.Request().Context(), userID(c), statuses...)
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
if list == nil {
|
|
list = []contract.ChangeProposal{}
|
|
}
|
|
return c.JSON(http.StatusOK, list)
|
|
}
|
|
|
|
// GetChange returns one change proposal.
|
|
// @Summary Get a proposed change.
|
|
// @Tags chats
|
|
// @Param id path string true "the proposal"
|
|
// @Success 200 {object} contract.ChangeProposal "The proposal, with the text it would write"
|
|
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
|
|
// @Router /api/chats/changes/{id} [get]
|
|
func (h *Handlers) GetChange(c echo.Context) error {
|
|
p, err := h.svc.Changes().Get(c.Request().Context(), userID(c), c.Param("id"))
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// ConfirmChange writes a proposed change.
|
|
// @Summary Confirm a proposed change.
|
|
// @Description Writes the change to your files as one change set that Undo can take back. A JSON body {"content": "..."} writes your own text in place of the proposed one; it has the same size limit. It is refused with 409 when the file changed since the proposal, or when the proposal expired or was answered.
|
|
// @Tags chats
|
|
// @Param id path string true "the proposal"
|
|
// @Success 200 {object} contract.ChangeProposal "The applied proposal"
|
|
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
|
|
// @Failure 409 {object} schema.ErrorResponse "Stale, expired or answered"
|
|
// @Router /api/chats/changes/{id}/confirm [post]
|
|
func (h *Handlers) ConfirmChange(c echo.Context) error {
|
|
var body struct {
|
|
Content *string `json:"content"`
|
|
}
|
|
if c.Request().ContentLength != 0 {
|
|
if err := json.NewDecoder(io.LimitReader(c.Request().Body, 2*contract.MaxChangeBytes)).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
|
return fail(c, http.StatusBadRequest, typeInvalid, "the body must be JSON")
|
|
}
|
|
}
|
|
var p contract.ChangeProposal
|
|
var err error
|
|
if body.Content != nil {
|
|
p, err = h.svc.Changes().ConfirmEdited(c.Request().Context(), userID(c), c.Param("id"), *body.Content)
|
|
} else {
|
|
p, err = h.svc.Changes().Confirm(c.Request().Context(), userID(c), c.Param("id"))
|
|
}
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// DismissChange answers a proposed change with no.
|
|
// @Summary Dismiss a proposed change.
|
|
// @Tags chats
|
|
// @Param id path string true "the proposal"
|
|
// @Success 200 {object} contract.ChangeProposal "The dismissed proposal"
|
|
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
|
|
// @Failure 409 {object} schema.ErrorResponse "Not waiting"
|
|
// @Router /api/chats/changes/{id}/dismiss [post]
|
|
func (h *Handlers) DismissChange(c echo.Context) error {
|
|
p, err := h.svc.Changes().Dismiss(c.Request().Context(), userID(c), c.Param("id"))
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// UndoChange reverts an applied change.
|
|
// @Summary Undo an applied change.
|
|
// @Description Restores the file with the change set the confirmation wrote. It is refused with 409 when the file was edited since.
|
|
// @Tags chats
|
|
// @Param id path string true "the proposal"
|
|
// @Success 200 {object} contract.ChangeProposal "The reverted proposal"
|
|
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
|
|
// @Failure 409 {object} schema.ErrorResponse "Not applied, or edited since"
|
|
// @Router /api/chats/changes/{id}/undo [post]
|
|
func (h *Handlers) UndoChange(c echo.Context) error {
|
|
p, err := h.svc.Changes().Revert(c.Request().Context(), userID(c), c.Param("id"))
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// ChangeAudit lists the audit of the changes.
|
|
// @Summary List the audit of proposed changes.
|
|
// @Description Who proposed, confirmed, dismissed or reverted what, from which source, newest first. It holds a hash of the text, never the text.
|
|
// @Tags chats
|
|
// @Param limit query int false "most lines (default 50, at most 200)"
|
|
// @Success 200 {array} proposals.Event "Your audit lines"
|
|
// @Router /api/chats/changes/audit [get]
|
|
func (h *Handlers) ChangeAudit(c echo.Context) error {
|
|
limit := 50
|
|
if v, err := strconv.Atoi(c.QueryParam("limit")); err == nil && v > 0 {
|
|
limit = min(v, 200)
|
|
}
|
|
evs, err := h.svc.Changes().Events(c.Request().Context(), userID(c), limit)
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, evs)
|
|
}
|
|
|
|
// ListContextFiles lists the notes the host puts in the prompt of a chat.
|
|
// @Summary List your context files.
|
|
// @Description The short index files that the host loads into every chat (your memory, an agent's notes, a conversation's notes), with the topic files next to them, the agent profiles and the caps that keep a turn small. It only reads; change a file on the Files page or confirm a proposal.
|
|
// @Tags chats
|
|
// @Success 200 {object} studiosvc.ContextFiles "Your context files"
|
|
// @Router /api/chats/context-files [get]
|
|
func (h *Handlers) ListContextFiles(c echo.Context) error {
|
|
list, err := h.svc.ListContextFiles(c.Request().Context(), userID(c))
|
|
if err != nil {
|
|
return failChange(c, err)
|
|
}
|
|
return c.JSON(http.StatusOK, list)
|
|
}
|