Spaces:
Configuration error
Configuration error
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))) | |
} | |
} | |