ai路由完成

This commit is contained in:
Liujian
2024-09-25 20:28:01 +08:00
parent cb9f048f15
commit ea62e9cd3f
21 changed files with 786 additions and 16 deletions
+40
View File
@@ -0,0 +1,40 @@
package ai_api_dto
type CreateAPI struct {
Id string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Description string `json:"description"`
Disable bool `json:"disable"`
AiPrompt *AiPrompt `json:"ai_prompt"`
AiModel *AiModel `json:"ai_model"`
Timeout int `json:"timeout"`
Retry int `json:"retry"`
}
type AiPrompt struct {
Variables []*AiPromptVariable `json:"variables"`
Prompt string `json:"prompt"`
}
type AiPromptVariable struct {
Key string `json:"key"`
Description string `json:"description"`
Require bool `json:"require"`
}
type AiModel struct {
Id string `json:"id"`
Config string `json:"config"`
}
type EditAPI struct {
Name *string `json:"name"`
Path *string `json:"path"`
Description *string `json:"description"`
Disable *bool `json:"disable"`
AiPrompt *AiPrompt `json:"ai_prompt"`
AiModel *AiModel `json:"ai_model"`
Timeout *int `json:"timeout"`
Retry *int `json:"retry"`
}
+35
View File
@@ -0,0 +1,35 @@
package ai_api_dto
import (
"github.com/eolinker/go-common/auto"
)
type API struct {
Id string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Description string `json:"description"`
Disable bool `json:"disable"`
AiPrompt *AiPrompt `json:"ai_prompt"`
AiModel *AiModel `json:"ai_model"`
Timeout int `json:"timeout"`
Retry int `json:"retry"`
}
type APIItem struct {
Id string `json:"id"`
Name string `json:"name"`
RequestPath string `json:"request_path"`
Description string `json:"description"`
Disable bool `json:"disable"`
Creator auto.Label `json:"creator" aolabel:"user"`
Updater auto.Label `json:"updater" aolabel:"user"`
CreateTime auto.TimeLabel `json:"create_time"`
UpdateTime auto.TimeLabel `json:"update_time"`
Model ModelItem `json:"model"`
}
type ModelItem struct {
Id string `json:"id"`
Logo string `json:"logo"`
}
+278
View File
@@ -0,0 +1,278 @@
package ai_api
import (
"context"
"encoding/json"
"errors"
"fmt"
ai_api_dto "github.com/APIParkLab/APIPark/module/ai-api/dto"
"github.com/APIParkLab/APIPark/module/ai/provider"
ai_api "github.com/APIParkLab/APIPark/service/ai-api"
api_doc "github.com/APIParkLab/APIPark/service/api-doc"
"github.com/APIParkLab/APIPark/service/service"
"github.com/eolinker/go-common/auto"
"github.com/eolinker/go-common/store"
"github.com/eolinker/go-common/utils"
"github.com/getkin/kin-openapi/openapi3"
"github.com/google/uuid"
"gorm.io/gorm"
"net/http"
)
var _ IAPIModule = (*imlAPIModule)(nil)
var (
openapi3Loader = openapi3.NewLoader()
)
type imlAPIModule struct {
serviceService service.IServiceService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
aiAPIService ai_api.IAPIService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (i *imlAPIModule) getAPIDoc(ctx context.Context, serviceId string) (*openapi3.T, error) {
doc, err := i.apiDocService.GetDoc(ctx, serviceId)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return nil, fmt.Errorf("get service info error:%v", err)
}
return genOpenAPI3Template(info.Name, info.Description), nil
}
return openapi3Loader.LoadFromData([]byte(doc.Content))
}
func (i *imlAPIModule) updateAPIDoc(ctx context.Context, serviceId string, path string, description string, aiPrompt *ai_api_dto.AiPrompt) error {
doc, err := i.getAPIDoc(ctx, serviceId)
if err != nil {
return err
}
var variables []*ai_api_dto.AiPromptVariable
if aiPrompt != nil {
variables = aiPrompt.Variables
}
doc.AddOperation(path, http.MethodPost, genOperation(description, variables))
result, err := doc.MarshalJSON()
if err != nil {
return err
}
return i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: uuid.New().String(),
Content: string(result),
})
}
func (i *imlAPIModule) deleteAPIDoc(ctx context.Context, serviceId string, path string) error {
doc, err := i.getAPIDoc(ctx, serviceId)
if err != nil {
return err
}
doc.Paths.Delete(path)
result, err := doc.MarshalJSON()
if err != nil {
return err
}
return i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: uuid.New().String(),
Content: string(result),
})
}
func (i *imlAPIModule) Create(ctx context.Context, serviceId string, input *ai_api_dto.CreateAPI) (*ai_api_dto.API, error) {
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return nil, err
}
if info.Kind != service.AIService {
return nil, fmt.Errorf("service kind is not ai service")
}
if input.Id == "" {
input.Id = uuid.New().String()
}
err = i.transaction.Transaction(ctx, func(txCtx context.Context) error {
err := i.updateAPIDoc(ctx, serviceId, input.Path, input.Description, input.AiPrompt)
if err != nil {
return err
}
return i.aiAPIService.Create(ctx, &ai_api.Create{
ID: input.Id,
Name: input.Name,
Service: serviceId,
Path: input.Path,
Description: input.Description,
Timeout: input.Timeout,
Retry: input.Retry,
Model: input.AiModel.Id,
AdditionalConfig: map[string]interface{}{
"ai_prompt": input.AiPrompt,
"ai_model": input.AiModel,
},
})
})
if err != nil {
return nil, err
}
return i.Get(ctx, serviceId, input.Id)
}
func (i *imlAPIModule) Edit(ctx context.Context, serviceId string, apiId string, input *ai_api_dto.EditAPI) (*ai_api_dto.API, error) {
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return nil, err
}
if info.Kind != service.AIService {
return nil, fmt.Errorf("service kind is not ai service")
}
err = i.transaction.Transaction(ctx, func(txCtx context.Context) error {
apiInfo, err := i.aiAPIService.Get(ctx, apiId)
if err != nil {
return err
}
if input.Path != nil {
apiInfo.Path = *input.Path
}
if input.Description != nil {
apiInfo.Description = *input.Description
}
err = i.updateAPIDoc(ctx, serviceId, apiInfo.Path, apiInfo.Description, input.AiPrompt)
if err != nil {
return err
}
var modelId *string
if input.AiModel != nil {
modelId = &input.AiModel.Id
}
if input.AiPrompt != nil {
apiInfo.AdditionalConfig["ai_prompt"] = input.AiPrompt
}
if input.AiModel != nil {
apiInfo.AdditionalConfig["ai_model"] = input.AiModel
}
return i.aiAPIService.Save(ctx, apiId, &ai_api.Edit{
Name: input.Name,
Path: input.Path,
Description: input.Description,
Timeout: input.Timeout,
Retry: input.Retry,
Model: modelId,
AdditionalConfig: &apiInfo.AdditionalConfig,
})
})
if err != nil {
return nil, err
}
return i.Get(ctx, serviceId, apiId)
}
func (i *imlAPIModule) Delete(ctx context.Context, serviceId string, apiId string) error {
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return err
}
if info.Kind != service.AIService {
return fmt.Errorf("service kind is not ai service")
}
return i.transaction.Transaction(ctx, func(txCtx context.Context) error {
err = i.deleteAPIDoc(ctx, serviceId, apiId)
if err != nil {
return err
}
return i.aiAPIService.Delete(ctx, apiId)
})
}
func (i *imlAPIModule) List(ctx context.Context, keyword string, serviceId string) ([]*ai_api_dto.APIItem, error) {
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return nil, err
}
if info.Kind != service.AIService {
return nil, fmt.Errorf("service kind is not ai service")
}
apis, err := i.aiAPIService.Search(ctx, keyword, map[string]interface{}{
"service": serviceId,
}, "update_at desc")
if err != nil {
return nil, err
}
p, has := provider.GetProvider(info.AdditionalConfig["provider"])
if !has {
return nil, fmt.Errorf("provider not found")
}
return utils.SliceToSlice(apis, func(t *ai_api.API) *ai_api_dto.APIItem {
modelItem := ai_api_dto.ModelItem{
Id: t.Model,
}
model, has := p.LLM(t.Model)
if has {
modelItem.Logo = model.Logo
}
return &ai_api_dto.APIItem{
Id: t.ID,
Name: t.Name,
RequestPath: t.Path,
Description: t.Description,
Disable: t.Disable,
Creator: auto.UUID(t.Creator),
Updater: auto.UUID(t.Updater),
CreateTime: auto.TimeLabel(t.CreateAt),
UpdateTime: auto.TimeLabel(t.UpdateAt),
Model: modelItem,
}
}), nil
}
func (i *imlAPIModule) Get(ctx context.Context, serviceId string, apiId string) (*ai_api_dto.API, error) {
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return nil, err
}
if info.Kind != service.AIService {
return nil, fmt.Errorf("service kind is not ai service")
}
apiInfo, err := i.aiAPIService.Get(ctx, apiId)
if err != nil {
return nil, err
}
prompt, err := ConvertStruct[ai_api_dto.AiPrompt](apiInfo.AdditionalConfig["ai_prompt"])
if err != nil {
return nil, err
}
aiModel, err := ConvertStruct[ai_api_dto.AiModel](apiInfo.AdditionalConfig["ai_model"])
if err != nil {
return nil, err
}
return &ai_api_dto.API{
Id: apiInfo.ID,
Name: apiInfo.Name,
Path: apiInfo.Path,
Description: apiInfo.Description,
Disable: apiInfo.Disable,
AiPrompt: prompt,
AiModel: aiModel,
Timeout: apiInfo.Timeout,
Retry: apiInfo.Retry,
}, nil
}
func ConvertStruct[T any](data interface{}) (*T, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
var t T
err = json.Unmarshal(b, &t)
if err != nil {
return nil, err
}
return &t, nil
}
+23
View File
@@ -0,0 +1,23 @@
package ai_api
import (
"context"
ai_api_dto "github.com/APIParkLab/APIPark/module/ai-api/dto"
"github.com/eolinker/go-common/autowire"
"reflect"
)
type IAPIModule interface {
Create(ctx context.Context, serviceId string, input *ai_api_dto.CreateAPI) (*ai_api_dto.API, error)
Edit(ctx context.Context, serviceId string, apiId string, input *ai_api_dto.EditAPI) (*ai_api_dto.API, error)
Delete(ctx context.Context, serviceId string, apiId string) error
List(ctx context.Context, keyword, serviceId string) ([]*ai_api_dto.APIItem, error)
Get(ctx context.Context, serviceId string, apiId string) (*ai_api_dto.API, error)
}
func init() {
autowire.Auto[IAPIModule](func() reflect.Value {
m := new(imlAPIModule)
return reflect.ValueOf(m)
})
}
+105
View File
@@ -0,0 +1,105 @@
package ai_api
import (
ai_api_dto "github.com/APIParkLab/APIPark/module/ai-api/dto"
"github.com/getkin/kin-openapi/openapi3"
)
func genOpenAPI3Template(title string, description string) *openapi3.T {
result := new(openapi3.T)
result.OpenAPI = "3.1.0"
result.Info = &openapi3.Info{
Title: title,
Description: description,
Version: "beta",
}
result.Components = genComponents()
result.Paths = new(openapi3.Paths)
return result
}
func genOperation(description string, variables []*ai_api_dto.AiPromptVariable) *openapi3.Operation {
operation := openapi3.NewOperation()
operation.RequestBody = genRequestBody(variables)
operation.Responses = &openapi3.Responses{}
operation.Responses.Set("200", genResponse())
operation.Description = description
return operation
}
func genRequestBody(variables []*ai_api_dto.AiPromptVariable) *openapi3.RequestBodyRef {
requestBody := openapi3.NewRequestBody()
requestBody.Content = openapi3.NewContentWithSchema(genRequestBodySchema(variables), []string{"application/json"})
return &openapi3.RequestBodyRef{
Value: requestBody,
}
}
func genResponse() *openapi3.ResponseRef {
response := openapi3.NewResponse()
response.Content = openapi3.NewContentWithSchema(genResponseSchema(), []string{"application/json"})
description := "Response from the server"
response.Description = &description
return &openapi3.ResponseRef{
Value: response,
}
}
func genRequestBodySchema(variables []*ai_api_dto.AiPromptVariable) *openapi3.Schema {
result := openapi3.NewObjectSchema()
variableSchema := openapi3.NewObjectSchema()
required := make([]string, 0, len(variables))
for _, v := range variables {
val := openapi3.NewStringSchema()
val.Description = v.Description
if v.Require {
required = append(required, v.Key)
}
variableSchema.WithProperty(v.Key, val)
}
result.WithProperty("variables", variableSchema.WithRequired(required))
result.WithProperty("messages", genMessageSchema())
result.WithRequired([]string{"variables", "messages"})
return result
}
func genComponents() *openapi3.Components {
components := openapi3.NewComponents()
components.Schemas = make(openapi3.Schemas)
components.Schemas["Message"] = genMessageSchema().NewRef()
components.Schemas["Response"] = genResponseSchema().NewRef()
return &components
}
func genMessageSchema() *openapi3.Schema {
messageSchema := openapi3.NewObjectSchema()
messageSchema.Title = "Message"
messageSchema.Description = "Chat Message"
roleSchema := openapi3.NewStringSchema()
roleSchema.Description = "Role of the message sender"
roleSchema.Example = "assistant"
contentSchema := openapi3.NewStringSchema()
contentSchema.Description = "The message content"
contentSchema.Example = "Hello, how can I help you?"
messageSchema.WithProperties(map[string]*openapi3.Schema{
"role": roleSchema,
"content": contentSchema,
})
return messageSchema
}
func genResponseSchema() *openapi3.Schema {
responseSchema := openapi3.NewObjectSchema()
responseSchema.Description = "Response from the server"
responseSchema.WithPropertyRef("message", openapi3.NewSchemaRef("#/components/schemas/Message", genMessageSchema()))
responseSchema.WithProperty("code", openapi3.NewInt32Schema().WithMin(0))
responseSchema.WithProperty("error", openapi3.NewStringSchema())
responseSchema.WithProperty("finish_reason", openapi3.NewStringSchema().WithEnum([]string{
"stop",
"length",
"function_call",
"content_filter",
"null",
}))
return responseSchema
}
+6 -5
View File
@@ -8,11 +8,12 @@ type Provider struct {
}
type ProviderItem struct {
Id string `json:"id"`
Name string `json:"name"`
DefaultLLM string `json:"default_llm"`
Logo string `json:"logo"`
Configured bool `json:"configured"`
Id string `json:"id"`
Name string `json:"name"`
DefaultLLM string `json:"default_llm"`
DefaultLLMLogo string `json:"default_llm_logo"`
Logo string `json:"logo"`
Configured bool `json:"configured"`
}
type SimpleProviderItem struct {
+5
View File
@@ -69,11 +69,16 @@ func (i *imlProviderModule) Providers(ctx context.Context) ([]*ai_dto.ProviderIt
DefaultLLM: v.Info().DefaultLLM,
}
if info, has := providerMap[v.Info().Id]; has {
llm, has := v.LLM(info.DefaultLLM)
if !has {
continue
}
err = v.GlobalConfig().CheckConfig(info.Config)
if err == nil {
item.Configured = true
}
item.DefaultLLM = info.DefaultLLM
item.DefaultLLMLogo = llm.Logo
}
items = append(items, item)
}
+22 -7
View File
@@ -4,6 +4,7 @@ import (
_ "embed"
"encoding/json"
"github.com/APIParkLab/APIPark/module/ai/provider"
"github.com/eolinker/eosc"
"sync"
)
@@ -27,19 +28,23 @@ type ModelData struct {
type Model struct {
globalConfig provider.IAIConfig
invokeConfig provider.IAIConfig
models []*ModelData
models eosc.Untyped[string, *ModelData]
locker sync.RWMutex
}
func NewModel() provider.IAIProvider {
modelData := make([]*ModelData, 0)
json.Unmarshal(modelJson, &modelData)
return &Model{
m := &Model{
globalConfig: NewGlobalConfigDriver(),
invokeConfig: NewInvokeConfigDriver(),
models: modelData,
models: eosc.BuildUntyped[string, *ModelData](),
locker: sync.RWMutex{},
}
for _, v := range modelData {
m.models.Set(v.Id, v)
}
return m
}
func (m *Model) Index() int {
@@ -83,11 +88,9 @@ func (m *Model) UpdateLLMs() error {
}
func (m *Model) LLMs() []*provider.LLM {
m.locker.RLock()
models := m.models
m.locker.RUnlock()
result := make([]*provider.LLM, 0, len(models))
for _, model := range models {
result := make([]*provider.LLM, 0, models.Count())
for _, model := range models.List() {
llm := &provider.LLM{
Id: model.Id,
Logo: model.Logo,
@@ -97,3 +100,15 @@ func (m *Model) LLMs() []*provider.LLM {
}
return result
}
func (m *Model) LLM(id string) (*provider.LLM, bool) {
model, has := m.models.Get(id)
if !has {
return nil, false
}
return &provider.LLM{
Id: model.Id,
Logo: model.Logo,
Scopes: model.Scopes,
}, true
}
+1
View File
@@ -19,6 +19,7 @@ type IAIProvider interface {
InvokeConfig() IAIConfig
UpdateLLMs() error
LLMs() []*LLM
LLM(id string) (*LLM, bool)
}
type IAIConfig interface {
-1
View File
@@ -29,7 +29,6 @@ func (i *imlAPIDocModule) UpdateDoc(ctx context.Context, serviceId string, input
}
err = i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: input.Id,
Service: serviceId,
Content: input.Content,
})
if err != nil {