mirror of
https://github.com/mudler/LocalAI.git
synced 2024-12-27 00:01:07 +00:00
a1634b219a
* initial pass: roll out bluemonday sanitization more widely Signed-off-by: Dave Lee <dave@gray101.com> * add one additional sanitize - the overall modelslist used by the docs site Signed-off-by: Dave Lee <dave@gray101.com> --------- Signed-off-by: Dave Lee <dave@gray101.com>
523 lines
18 KiB
Go
523 lines
18 KiB
Go
package openai
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/microcosm-cc/bluemonday"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
"github.com/mudler/LocalAI/core/services"
|
|
model "github.com/mudler/LocalAI/pkg/model"
|
|
"github.com/mudler/LocalAI/pkg/utils"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// ToolType defines a type for tool options
|
|
type ToolType string
|
|
|
|
const (
|
|
CodeInterpreter ToolType = "code_interpreter"
|
|
Retrieval ToolType = "retrieval"
|
|
Function ToolType = "function"
|
|
|
|
MaxCharacterInstructions = 32768
|
|
MaxCharacterDescription = 512
|
|
MaxCharacterName = 256
|
|
MaxToolsSize = 128
|
|
MaxFileIdSize = 20
|
|
MaxCharacterMetadataKey = 64
|
|
MaxCharacterMetadataValue = 512
|
|
)
|
|
|
|
type Tool struct {
|
|
Type ToolType `json:"type"`
|
|
}
|
|
|
|
// Assistant represents the structure of an assistant object from the OpenAI API.
|
|
type Assistant struct {
|
|
ID string `json:"id"` // The unique identifier of the assistant.
|
|
Object string `json:"object"` // Object type, which is "assistant".
|
|
Created int64 `json:"created"` // The time at which the assistant was created.
|
|
Model string `json:"model"` // The model ID used by the assistant.
|
|
Name string `json:"name,omitempty"` // The name of the assistant.
|
|
Description string `json:"description,omitempty"` // The description of the assistant.
|
|
Instructions string `json:"instructions,omitempty"` // The system instructions that the assistant uses.
|
|
Tools []Tool `json:"tools,omitempty"` // A list of tools enabled on the assistant.
|
|
FileIDs []string `json:"file_ids,omitempty"` // A list of file IDs attached to this assistant.
|
|
Metadata map[string]string `json:"metadata,omitempty"` // Set of key-value pairs attached to the assistant.
|
|
}
|
|
|
|
var (
|
|
Assistants = []Assistant{} // better to return empty array instead of "null"
|
|
AssistantsConfigFile = "assistants.json"
|
|
)
|
|
|
|
type AssistantRequest struct {
|
|
Model string `json:"model"`
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Instructions string `json:"instructions,omitempty"`
|
|
Tools []Tool `json:"tools,omitempty"`
|
|
FileIDs []string `json:"file_ids,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// CreateAssistantEndpoint is the OpenAI Assistant API endpoint https://platform.openai.com/docs/api-reference/assistants/createAssistant
|
|
// @Summary Create an assistant with a model and instructions.
|
|
// @Param request body AssistantRequest true "query params"
|
|
// @Success 200 {object} Assistant "Response"
|
|
// @Router /v1/assistants [post]
|
|
func CreateAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
request := new(AssistantRequest)
|
|
if err := c.BodyParser(request); err != nil {
|
|
log.Warn().AnErr("Unable to parse AssistantRequest", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
|
|
}
|
|
|
|
if !modelExists(cl, ml, request.Model) {
|
|
log.Warn().Msgf("Model: %s was not found in list of models.", request.Model)
|
|
return c.Status(fiber.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Model %q not found", request.Model)))
|
|
}
|
|
|
|
if request.Tools == nil {
|
|
request.Tools = []Tool{}
|
|
}
|
|
|
|
if request.FileIDs == nil {
|
|
request.FileIDs = []string{}
|
|
}
|
|
|
|
if request.Metadata == nil {
|
|
request.Metadata = make(map[string]string)
|
|
}
|
|
|
|
id := "asst_" + strconv.FormatInt(generateRandomID(), 10)
|
|
|
|
assistant := Assistant{
|
|
ID: id,
|
|
Object: "assistant",
|
|
Created: time.Now().Unix(),
|
|
Model: request.Model,
|
|
Name: request.Name,
|
|
Description: request.Description,
|
|
Instructions: request.Instructions,
|
|
Tools: request.Tools,
|
|
FileIDs: request.FileIDs,
|
|
Metadata: request.Metadata,
|
|
}
|
|
|
|
Assistants = append(Assistants, assistant)
|
|
utils.SaveConfig(appConfig.ConfigsDir, AssistantsConfigFile, Assistants)
|
|
return c.Status(fiber.StatusOK).JSON(assistant)
|
|
}
|
|
}
|
|
|
|
var currentId int64 = 0
|
|
|
|
func generateRandomID() int64 {
|
|
atomic.AddInt64(¤tId, 1)
|
|
return currentId
|
|
}
|
|
|
|
// ListAssistantsEndpoint is the OpenAI Assistant API endpoint to list assistents https://platform.openai.com/docs/api-reference/assistants/listAssistants
|
|
// @Summary List available assistents
|
|
// @Param limit query int false "Limit the number of assistants returned"
|
|
// @Param order query string false "Order of assistants returned"
|
|
// @Param after query string false "Return assistants created after the given ID"
|
|
// @Param before query string false "Return assistants created before the given ID"
|
|
// @Success 200 {object} []Assistant "Response"
|
|
// @Router /v1/assistants [get]
|
|
func ListAssistantsEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
// Because we're altering the existing assistants list we should just duplicate it for now.
|
|
returnAssistants := Assistants
|
|
// Parse query parameters
|
|
limitQuery := c.Query("limit", "20")
|
|
orderQuery := c.Query("order", "desc")
|
|
afterQuery := c.Query("after")
|
|
beforeQuery := c.Query("before")
|
|
|
|
// Convert string limit to integer
|
|
limit, err := strconv.Atoi(limitQuery)
|
|
if err != nil {
|
|
return c.Status(http.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Invalid limit query value: %s", limitQuery)))
|
|
}
|
|
|
|
// Sort assistants
|
|
sort.SliceStable(returnAssistants, func(i, j int) bool {
|
|
if orderQuery == "asc" {
|
|
return returnAssistants[i].Created < returnAssistants[j].Created
|
|
}
|
|
return returnAssistants[i].Created > returnAssistants[j].Created
|
|
})
|
|
|
|
// After and before cursors
|
|
if afterQuery != "" {
|
|
returnAssistants = filterAssistantsAfterID(returnAssistants, afterQuery)
|
|
}
|
|
if beforeQuery != "" {
|
|
returnAssistants = filterAssistantsBeforeID(returnAssistants, beforeQuery)
|
|
}
|
|
|
|
// Apply limit
|
|
if limit < len(returnAssistants) {
|
|
returnAssistants = returnAssistants[:limit]
|
|
}
|
|
|
|
return c.JSON(returnAssistants)
|
|
}
|
|
}
|
|
|
|
// FilterAssistantsBeforeID filters out those assistants whose ID comes before the given ID
|
|
// We assume that the assistants are already sorted
|
|
func filterAssistantsBeforeID(assistants []Assistant, id string) []Assistant {
|
|
idInt, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return assistants // Return original slice if invalid id format is provided
|
|
}
|
|
|
|
var filteredAssistants []Assistant
|
|
|
|
for _, assistant := range assistants {
|
|
aid, err := strconv.Atoi(strings.TrimPrefix(assistant.ID, "asst_"))
|
|
if err != nil {
|
|
continue // Skip if invalid id in assistant
|
|
}
|
|
|
|
if aid < idInt {
|
|
filteredAssistants = append(filteredAssistants, assistant)
|
|
}
|
|
}
|
|
|
|
return filteredAssistants
|
|
}
|
|
|
|
// FilterAssistantsAfterID filters out those assistants whose ID comes after the given ID
|
|
// We assume that the assistants are already sorted
|
|
func filterAssistantsAfterID(assistants []Assistant, id string) []Assistant {
|
|
idInt, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return assistants // Return original slice if invalid id format is provided
|
|
}
|
|
|
|
var filteredAssistants []Assistant
|
|
|
|
for _, assistant := range assistants {
|
|
aid, err := strconv.Atoi(strings.TrimPrefix(assistant.ID, "asst_"))
|
|
if err != nil {
|
|
continue // Skip if invalid id in assistant
|
|
}
|
|
|
|
if aid > idInt {
|
|
filteredAssistants = append(filteredAssistants, assistant)
|
|
}
|
|
}
|
|
|
|
return filteredAssistants
|
|
}
|
|
|
|
func modelExists(cl *config.BackendConfigLoader, ml *model.ModelLoader, modelName string) (found bool) {
|
|
found = false
|
|
models, err := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, model := range models {
|
|
if model == modelName {
|
|
found = true
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// DeleteAssistantEndpoint is the OpenAI Assistant API endpoint to delete assistents https://platform.openai.com/docs/api-reference/assistants/deleteAssistant
|
|
// @Summary Delete assistents
|
|
// @Success 200 {object} schema.DeleteAssistantResponse "Response"
|
|
// @Router /v1/assistants/{assistant_id} [delete]
|
|
func DeleteAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
assistantID := c.Params("assistant_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required")
|
|
}
|
|
|
|
for i, assistant := range Assistants {
|
|
if assistant.ID == assistantID {
|
|
Assistants = append(Assistants[:i], Assistants[i+1:]...)
|
|
utils.SaveConfig(appConfig.ConfigsDir, AssistantsConfigFile, Assistants)
|
|
return c.Status(fiber.StatusOK).JSON(schema.DeleteAssistantResponse{
|
|
ID: assistantID,
|
|
Object: "assistant.deleted",
|
|
Deleted: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
log.Warn().Msgf("Unable to find assistant %s for deletion", assistantID)
|
|
return c.Status(fiber.StatusNotFound).JSON(schema.DeleteAssistantResponse{
|
|
ID: assistantID,
|
|
Object: "assistant.deleted",
|
|
Deleted: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetAssistantEndpoint is the OpenAI Assistant API endpoint to get assistents https://platform.openai.com/docs/api-reference/assistants/getAssistant
|
|
// @Summary Get assistent data
|
|
// @Success 200 {object} Assistant "Response"
|
|
// @Router /v1/assistants/{assistant_id} [get]
|
|
func GetAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
assistantID := c.Params("assistant_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required")
|
|
}
|
|
|
|
for _, assistant := range Assistants {
|
|
if assistant.ID == assistantID {
|
|
return c.Status(fiber.StatusOK).JSON(assistant)
|
|
}
|
|
}
|
|
|
|
return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant with id: %s", assistantID)))
|
|
}
|
|
}
|
|
|
|
type AssistantFile struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
AssistantID string `json:"assistant_id"`
|
|
}
|
|
|
|
var (
|
|
AssistantFiles []AssistantFile
|
|
AssistantsFileConfigFile = "assistantsFile.json"
|
|
)
|
|
|
|
func CreateAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
request := new(schema.AssistantFileRequest)
|
|
if err := c.BodyParser(request); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
|
|
}
|
|
|
|
assistantID := c.Params("assistant_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required")
|
|
}
|
|
|
|
for _, assistant := range Assistants {
|
|
if assistant.ID == assistantID {
|
|
if len(assistant.FileIDs) > MaxFileIdSize {
|
|
return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("Max files %d for assistant %s reached.", MaxFileIdSize, assistant.Name))
|
|
}
|
|
|
|
for _, file := range UploadedFiles {
|
|
if file.ID == request.FileID {
|
|
assistant.FileIDs = append(assistant.FileIDs, request.FileID)
|
|
assistantFile := AssistantFile{
|
|
ID: file.ID,
|
|
Object: "assistant.file",
|
|
CreatedAt: time.Now().Unix(),
|
|
AssistantID: assistant.ID,
|
|
}
|
|
AssistantFiles = append(AssistantFiles, assistantFile)
|
|
utils.SaveConfig(appConfig.ConfigsDir, AssistantsFileConfigFile, AssistantFiles)
|
|
return c.Status(fiber.StatusOK).JSON(assistantFile)
|
|
}
|
|
}
|
|
|
|
return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find file_id: %s", request.FileID)))
|
|
}
|
|
}
|
|
|
|
return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find %q", assistantID)))
|
|
}
|
|
}
|
|
|
|
func ListAssistantFilesEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
type ListAssistantFiles struct {
|
|
Data []schema.File
|
|
Object string
|
|
}
|
|
|
|
return func(c *fiber.Ctx) error {
|
|
assistantID := c.Params("assistant_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required")
|
|
}
|
|
|
|
limitQuery := c.Query("limit", "20")
|
|
order := c.Query("order", "desc")
|
|
limit, err := strconv.Atoi(limitQuery)
|
|
if err != nil || limit < 1 || limit > 100 {
|
|
limit = 20 // Default to 20 if there's an error or the limit is out of bounds
|
|
}
|
|
|
|
// Sort files by CreatedAt depending on the order query parameter
|
|
if order == "asc" {
|
|
sort.Slice(AssistantFiles, func(i, j int) bool {
|
|
return AssistantFiles[i].CreatedAt < AssistantFiles[j].CreatedAt
|
|
})
|
|
} else { // default to "desc"
|
|
sort.Slice(AssistantFiles, func(i, j int) bool {
|
|
return AssistantFiles[i].CreatedAt > AssistantFiles[j].CreatedAt
|
|
})
|
|
}
|
|
|
|
// Limit the number of files returned
|
|
var limitedFiles []AssistantFile
|
|
hasMore := false
|
|
if len(AssistantFiles) > limit {
|
|
hasMore = true
|
|
limitedFiles = AssistantFiles[:limit]
|
|
} else {
|
|
limitedFiles = AssistantFiles
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"object": "list",
|
|
"data": limitedFiles,
|
|
"first_id": func() string {
|
|
if len(limitedFiles) > 0 {
|
|
return limitedFiles[0].ID
|
|
}
|
|
return ""
|
|
}(),
|
|
"last_id": func() string {
|
|
if len(limitedFiles) > 0 {
|
|
return limitedFiles[len(limitedFiles)-1].ID
|
|
}
|
|
return ""
|
|
}(),
|
|
"has_more": hasMore,
|
|
}
|
|
|
|
return c.Status(fiber.StatusOK).JSON(response)
|
|
}
|
|
}
|
|
|
|
func ModifyAssistantEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
request := new(AssistantRequest)
|
|
if err := c.BodyParser(request); err != nil {
|
|
log.Warn().AnErr("Unable to parse AssistantRequest", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
|
|
}
|
|
|
|
assistantID := c.Params("assistant_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id is required")
|
|
}
|
|
|
|
for i, assistant := range Assistants {
|
|
if assistant.ID == assistantID {
|
|
newAssistant := Assistant{
|
|
ID: assistantID,
|
|
Object: assistant.Object,
|
|
Created: assistant.Created,
|
|
Model: request.Model,
|
|
Name: request.Name,
|
|
Description: request.Description,
|
|
Instructions: request.Instructions,
|
|
Tools: request.Tools,
|
|
FileIDs: request.FileIDs, // todo: should probably verify fileids exist
|
|
Metadata: request.Metadata,
|
|
}
|
|
|
|
// Remove old one and replace with new one
|
|
Assistants = append(Assistants[:i], Assistants[i+1:]...)
|
|
Assistants = append(Assistants, newAssistant)
|
|
utils.SaveConfig(appConfig.ConfigsDir, AssistantsConfigFile, Assistants)
|
|
return c.Status(fiber.StatusOK).JSON(newAssistant)
|
|
}
|
|
}
|
|
return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant with id: %s", assistantID)))
|
|
}
|
|
}
|
|
|
|
func DeleteAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
assistantID := c.Params("assistant_id")
|
|
fileId := c.Params("file_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id and file_id are required")
|
|
}
|
|
// First remove file from assistant
|
|
for i, assistant := range Assistants {
|
|
if assistant.ID == assistantID {
|
|
for j, fileId := range assistant.FileIDs {
|
|
Assistants[i].FileIDs = append(Assistants[i].FileIDs[:j], Assistants[i].FileIDs[j+1:]...)
|
|
|
|
// Check if the file exists in the assistantFiles slice
|
|
for i, assistantFile := range AssistantFiles {
|
|
if assistantFile.ID == fileId {
|
|
// Remove the file from the assistantFiles slice
|
|
AssistantFiles = append(AssistantFiles[:i], AssistantFiles[i+1:]...)
|
|
utils.SaveConfig(appConfig.ConfigsDir, AssistantsFileConfigFile, AssistantFiles)
|
|
return c.Status(fiber.StatusOK).JSON(schema.DeleteAssistantFileResponse{
|
|
ID: fileId,
|
|
Object: "assistant.file.deleted",
|
|
Deleted: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Warn().Msgf("Unable to locate file_id: %s in assistants: %s. Continuing to delete assistant file.", fileId, assistantID)
|
|
for i, assistantFile := range AssistantFiles {
|
|
if assistantFile.AssistantID == assistantID {
|
|
|
|
AssistantFiles = append(AssistantFiles[:i], AssistantFiles[i+1:]...)
|
|
utils.SaveConfig(appConfig.ConfigsDir, AssistantsFileConfigFile, AssistantFiles)
|
|
|
|
return c.Status(fiber.StatusNotFound).JSON(schema.DeleteAssistantFileResponse{
|
|
ID: fileId,
|
|
Object: "assistant.file.deleted",
|
|
Deleted: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
log.Warn().Msgf("Unable to find assistant: %s", assistantID)
|
|
|
|
return c.Status(fiber.StatusNotFound).JSON(schema.DeleteAssistantFileResponse{
|
|
ID: fileId,
|
|
Object: "assistant.file.deleted",
|
|
Deleted: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
func GetAssistantFileEndpoint(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(c *fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
assistantID := c.Params("assistant_id")
|
|
fileId := c.Params("file_id")
|
|
if assistantID == "" {
|
|
return c.Status(fiber.StatusBadRequest).SendString("parameter assistant_id and file_id are required")
|
|
}
|
|
|
|
for _, assistantFile := range AssistantFiles {
|
|
if assistantFile.AssistantID == assistantID {
|
|
if assistantFile.ID == fileId {
|
|
return c.Status(fiber.StatusOK).JSON(assistantFile)
|
|
}
|
|
return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant file with file_id: %s", fileId)))
|
|
}
|
|
}
|
|
return c.Status(fiber.StatusNotFound).SendString(bluemonday.StrictPolicy().Sanitize(fmt.Sprintf("Unable to find assistant file with assistant_id: %s", assistantID)))
|
|
}
|
|
}
|