2024-03-26 17:54:35 +00:00
package openai
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"sync/atomic"
"time"
2024-03-29 21:29:33 +00:00
"github.com/gofiber/fiber/v2"
2024-06-23 08:24:36 +00:00
"github.com/mudler/LocalAI/core/config"
2024-07-18 04:38:41 +00:00
"github.com/mudler/LocalAI/core/schema"
2024-07-10 13:28:39 +00:00
"github.com/mudler/LocalAI/core/services"
2024-06-23 08:24:36 +00:00
model "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/utils"
2024-03-29 21:29:33 +00:00
"github.com/rs/zerolog/log"
2024-03-26 17:54:35 +00:00
)
// 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" `
}
2024-03-29 21:29:33 +00:00
// 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]
2024-03-26 17:54:35 +00:00
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" } )
}
2024-07-10 13:28:39 +00:00
if ! modelExists ( cl , ml , request . Model ) {
2024-03-26 17:54:35 +00:00
log . Warn ( ) . Msgf ( "Model: %s was not found in list of models." , request . Model )
return c . Status ( fiber . StatusBadRequest ) . SendString ( "Model " + request . Model + " not found" )
}
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 ( & currentId , 1 )
return currentId
}
2024-07-18 04:38:41 +00:00
// 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]
2024-03-26 17:54:35 +00:00
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 ( 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
}
2024-07-10 13:28:39 +00:00
func modelExists ( cl * config . BackendConfigLoader , ml * model . ModelLoader , modelName string ) ( found bool ) {
2024-03-26 17:54:35 +00:00
found = false
2024-07-10 13:28:39 +00:00
models , err := services . ListModels ( cl , ml , "" , true )
2024-03-26 17:54:35 +00:00
if err != nil {
return
}
for _ , model := range models {
if model == modelName {
found = true
return
}
}
return
}
2024-07-18 04:38:41 +00:00
// 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]
2024-03-26 17:54:35 +00:00
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 )
2024-07-18 04:38:41 +00:00
return c . Status ( fiber . StatusOK ) . JSON ( schema . DeleteAssistantResponse {
2024-03-26 17:54:35 +00:00
ID : assistantID ,
Object : "assistant.deleted" ,
Deleted : true ,
} )
}
}
log . Warn ( ) . Msgf ( "Unable to find assistant %s for deletion" , assistantID )
2024-07-18 04:38:41 +00:00
return c . Status ( fiber . StatusNotFound ) . JSON ( schema . DeleteAssistantResponse {
2024-03-26 17:54:35 +00:00
ID : assistantID ,
Object : "assistant.deleted" ,
Deleted : false ,
} )
}
}
2024-07-18 04:38:41 +00:00
// 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]
2024-03-26 17:54:35 +00:00
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 ( 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 {
2024-07-18 04:38:41 +00:00
request := new ( schema . AssistantFileRequest )
2024-03-26 17:54:35 +00:00
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 ( fmt . Sprintf ( "Unable to find file_id: %s" , request . FileID ) )
}
}
2024-06-24 06:34:36 +00:00
return c . Status ( fiber . StatusNotFound ) . SendString ( fmt . Sprintf ( "Unable to find %q" , assistantID ) )
2024-03-26 17:54:35 +00:00
}
}
func ListAssistantFilesEndpoint ( cl * config . BackendConfigLoader , ml * model . ModelLoader , appConfig * config . ApplicationConfig ) func ( c * fiber . Ctx ) error {
type ListAssistantFiles struct {
2024-07-18 04:38:41 +00:00
Data [ ] schema . File
2024-03-26 17:54:35 +00:00
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 ( 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 {
2024-04-23 16:43:00 +00:00
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 )
2024-07-18 04:38:41 +00:00
return c . Status ( fiber . StatusOK ) . JSON ( schema . DeleteAssistantFileResponse {
2024-04-23 16:43:00 +00:00
ID : fileId ,
Object : "assistant.file.deleted" ,
Deleted : true ,
} )
2024-03-26 17:54:35 +00:00
}
}
}
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 )
2024-07-18 04:38:41 +00:00
return c . Status ( fiber . StatusNotFound ) . JSON ( schema . DeleteAssistantFileResponse {
2024-03-26 17:54:35 +00:00
ID : fileId ,
Object : "assistant.file.deleted" ,
Deleted : true ,
} )
}
}
}
}
log . Warn ( ) . Msgf ( "Unable to find assistant: %s" , assistantID )
2024-07-18 04:38:41 +00:00
return c . Status ( fiber . StatusNotFound ) . JSON ( schema . DeleteAssistantFileResponse {
2024-03-26 17:54:35 +00:00
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 ( fmt . Sprintf ( "Unable to find assistant file with file_id: %s" , fileId ) )
}
}
return c . Status ( fiber . StatusNotFound ) . SendString ( fmt . Sprintf ( "Unable to find assistant file with assistant_id: %s" , assistantID ) )
}
}