diff --git a/README.md b/README.md index ef02b84..e4808a5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ * [Non-Functional Features](#non-functional-features) * [Quick Start](#quick-start) * [Prerequisites](#prerequisites) + * [Install awscli and awscli-local](#install-awscli-and-awscli-local) * [1. Setup Environment](#1-setup-environment) * [2. Build and Run the Application](#2-build-and-run-the-application) * [Services](#services) @@ -46,7 +47,8 @@ This project is a notification management system that leverages AI for processin - Docker and Docker Compose (for LocalStack) - AWS CLI -#### If you don't have AWS CLI installed, you can install it using the following command: +#### Install awscli and awscli-local +If you don't have AWS CLI installed, you can install it using the following command: ```bash sudo apt-get install python3-pip -y pip3 install awscli awscli-local @@ -117,8 +119,7 @@ Now you can use the APIs in the collection. ## Future Plans -- Authentication in requests -- AI based todo setup -- Report generation based on periodic results -- Additional notification formats (e.g., PDF, Excel) -- Add OPENAI integration for AI processing +- Sandbox for running curl command +- Add Front End +- Generic Pages +- Complex Search screens diff --git a/config/config.go b/config/config.go index 8c93055..4d68add 100644 --- a/config/config.go +++ b/config/config.go @@ -3,9 +3,10 @@ package config import ( "NotificationManagement/config/helper" "fmt" - "github.com/spf13/viper" "os" "reflect" + + "github.com/spf13/viper" ) type Config struct { @@ -22,6 +23,7 @@ type Config struct { } type DevelopmentConfig struct { GeminiKey string `mapstructure:"geminikey"` + OpenAIKey string `mapstructure:"openaikey"` } type AppConfig struct { Name string `mapstructure:"name"` @@ -130,7 +132,7 @@ func LoadConfig() { fmt.Printf("Error unmarshaling config: %v\n", err) os.Exit(1) } - printAllVipers() + //printAllVipers() } func printAllVipers() { @@ -222,7 +224,7 @@ func loadDefaults() *Config { Format: "", }, Keycloak: KeycloakConfig{ - ServerURL: "http://localhost:8081", + ServerURL: "http://localhost:8081/keycloak/", Realm: "gocloak", ClientID: "gocloak", ClientSecret: "gocloak-secret", @@ -338,6 +340,7 @@ func loadFromEnv() *Config { }, Development: DevelopmentConfig{ GeminiKey: os.Getenv(EnvGeminiKey), + OpenAIKey: os.Getenv(EnvOpenaiKey), }, } setViperFields(c, "") diff --git a/config/env_config.go b/config/env_config.go index 893d708..a5f8727 100644 --- a/config/env_config.go +++ b/config/env_config.go @@ -16,6 +16,7 @@ const ( EnvAppDomain = "APP_DOMAIN" EnvGeminiKey = "GEMINI_KEY" + EnvOpenaiKey = "OPENAI_KEY" EnvDBHost = "DB_HOST" EnvDBPort = "DB_PORT" diff --git a/docker-compose.yml b/docker-compose.yml index 3823eb2..e079aff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,6 @@ services: - --import-realm environment: KC_HOSTNAME: $KEYCLOAK_SERVER_URL - KC_HOSTNAME_STRICT: true KEYCLOAK_USER: admin KEYCLOAK_PASSWORD: secret KEYCLOAK_ADMIN: admin diff --git a/domain/openai.go b/domain/openai.go new file mode 100644 index 0000000..ae7e2dc --- /dev/null +++ b/domain/openai.go @@ -0,0 +1,13 @@ +package domain + +import ( + "NotificationManagement/models" +) + +type OpenAIService interface { + AIService[models.OpenAIModel] +} + +type OpenAIModelRepository interface { + Repository[models.OpenAIModel, uint] +} diff --git a/env/app-config.json b/env/app-config.json index ee06b11..4029bef 100644 --- a/env/app-config.json +++ b/env/app-config.json @@ -65,7 +65,7 @@ } }, "keycloak": { - "serverUrl": "http://localhost:8081", + "serverUrl": "http://localhost:8081/keycloak/", "realm": "gocloak", "clientId": "gocloak", "clientSecret": "gocloak-secret" diff --git a/go.mod b/go.mod index ba6cc6c..b7247b0 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/labstack/echo-contrib v0.17.4 github.com/labstack/echo/v4 v4.13.4 github.com/labstack/gommon v0.4.2 + github.com/sashabaranov/go-openai v1.41.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 go.uber.org/fx v1.24.0 diff --git a/go.sum b/go.sum index 99f2a70..5213dd1 100644 --- a/go.sum +++ b/go.sum @@ -185,6 +185,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sashabaranov/go-openai v1.41.1 h1:zf5tM+GuxpyiyD9XZg8nCqu52eYFQg9OOew0gnIuDy4= +github.com/sashabaranov/go-openai v1.41.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= diff --git a/models/ai.go b/models/ai.go index 9de75ac..6f7efca 100644 --- a/models/ai.go +++ b/models/ai.go @@ -2,6 +2,7 @@ package models import ( "NotificationManagement/config" + "gorm.io/gorm" ) @@ -11,14 +12,21 @@ type AIModelInterface interface { type AIModel struct { gorm.Model - Type string `gorm:"size:10;check:type IN ('local','openai','gemini','deepseek')"` + Type string `gorm:"size:10;check:type IN ('local','openai','gemini','deepseek')"` + BaseURL *string `gorm:"size:500" json:"base_url,omitempty"` +} + +type OpenAIModel struct { + AIModel `mapper:"inherit"` + Name string `gorm:"size:255;not null" json:"name"` + ModelName string `gorm:"size:255;not null;check:model_name <> '';index:idx_ai_model_model_secret,unique" json:"model"` + APISecret EncryptedString `gorm:"size:500;index:idx_ai_model_model_secret,unique" json:"-"` } type DeepseekModel struct { AIModel `mapper:"inherit"` Name string `gorm:"size:255;not null" json:"name"` ModelName string `gorm:"size:255;not null;check:model_name <> '';index:idx_ai_model_model_url,unique" json:"model"` - BaseURL string `gorm:"size:500;index:idx_ai_model_model_url,unique" json:"base_url"` Size int64 `json:"size"` } @@ -33,9 +41,10 @@ func (d *AIModel) GetType() string { return d.Type } -func (d *GeminiModel) GetType() string { - return d.Type +func (*AIModel) TableName() string { + return "ai_models" } + func (d *GeminiModel) GetAPIKey() string { if config.IsDevelopment() && config.Development().GeminiKey != "" { return config.Development().GeminiKey @@ -43,16 +52,18 @@ func (d *GeminiModel) GetAPIKey() string { return string(d.APISecret) } -func (d *DeepseekModel) GetType() string { - return d.Type -} - -func (*DeepseekModel) TableName() string { - return "ai_models" +func (d *AIModel) GetBaseURL() string { + if d.BaseURL != nil && *d.BaseURL != "" { + return *d.BaseURL + } + return "" } -func (*GeminiModel) TableName() string { - return "ai_models" +func (d *OpenAIModel) GetAPIKey() string { + if config.IsDevelopment() && config.Development().OpenAIKey != "" { + return config.Development().OpenAIKey + } + return string(d.APISecret) } func (d *DeepseekModel) UpdateFromModel(source ModelInterface) { @@ -65,3 +76,9 @@ func (d *GeminiModel) UpdateFromModel(source ModelInterface) { copyFields(d, src) } } + +func (d *OpenAIModel) UpdateFromModel(source ModelInterface) { + if src, ok := source.(*OpenAIModel); ok { + copyFields(d, src) + } +} diff --git a/repositories/openai.go b/repositories/openai.go new file mode 100644 index 0000000..9451d91 --- /dev/null +++ b/repositories/openai.go @@ -0,0 +1,18 @@ +package repositories + +import ( + "NotificationManagement/domain" + "NotificationManagement/models" + + "gorm.io/gorm" +) + +type OpenAIModelRepositoryImpl struct { + domain.Repository[models.OpenAIModel, uint] +} + +func NewOpenAIModelRepository(db *gorm.DB) domain.OpenAIModelRepository { + return &OpenAIModelRepositoryImpl{ + Repository: NewSQLRepository[models.OpenAIModel](db), + } +} diff --git a/scripts/public/ai_models.sql b/scripts/public/ai_models.sql index 8c600c2..8f0bd1e 100644 --- a/scripts/public/ai_models.sql +++ b/scripts/public/ai_models.sql @@ -14,15 +14,15 @@ CREATE TABLE IF NOT EXISTS public.ai_models CONSTRAINT chk_ai_models_model_name CHECK ((model_name)::text <> ''::text), CONSTRAINT chk_ai_models_type - CHECK ((type)::text = ANY (ARRAY [('local'::character varying)::text, ('openai'::character varying)::text, ('gemini'::character varying)::text, ('deepseek'::character varying)::text])) + CHECK ((type)::text = ANY ((ARRAY ['local'::character varying, 'openai'::character varying, 'gemini'::character varying, 'deepseek'::character varying])::text[])) ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_model_model_secret - ON public.ai_models (model_name, api_secret); - CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_model_model_url ON public.ai_models (model_name, base_url); CREATE INDEX IF NOT EXISTS idx_ai_models_deleted_at ON public.ai_models (deleted_at); +CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_model_model_secret + ON public.ai_models (model_name, api_secret); + diff --git a/scripts/public/curl_requests.sql b/scripts/public/curl_requests.sql index 7481e65..a558532 100644 --- a/scripts/public/curl_requests.sql +++ b/scripts/public/curl_requests.sql @@ -10,7 +10,10 @@ CREATE TABLE IF NOT EXISTS public.curl_requests body text, raw_curl text, response_type varchar(10), - PRIMARY KEY (id) + user_id bigint, + PRIMARY KEY (id), + CONSTRAINT fk_curl_requests_user + FOREIGN KEY (user_id) REFERENCES public.users ); CREATE INDEX IF NOT EXISTS idx_curl_requests_deleted_at diff --git a/scripts/public/reminders.sql b/scripts/public/reminders.sql index 0033047..1422701 100644 --- a/scripts/public/reminders.sql +++ b/scripts/public/reminders.sql @@ -5,28 +5,28 @@ CREATE TABLE IF NOT EXISTS public.reminders updated_at timestamp with time zone, deleted_at timestamp with time zone, request_id bigint, - message text NOT NULL, + message text NOT NULL, triggered_time timestamp with time zone, next_trigger_time timestamp with time zone, occurrence bigint DEFAULT 0, - recurrence varchar(50), + recurrence varchar(50) NOT NULL, + after_every bigint NOT NULL, + task_id text, + upto timestamp with time zone, PRIMARY KEY (id), CONSTRAINT fk_curl_requests_reminders - FOREIGN KEY (request_id) REFERENCES public.curl_requests, - CONSTRAINT chk_reminders_recurrence - CHECK ((recurrence)::text = ANY - (ARRAY [('once'::character varying)::text, ('minutes'::character varying)::text, ('hour'::character varying)::text, ('daily'::character varying)::text, ('weekly'::character varying)::text])) + FOREIGN KEY (request_id) REFERENCES public.curl_requests ); -CREATE INDEX IF NOT EXISTS idx_reminders_deleted_at - ON public.reminders (deleted_at); +CREATE INDEX IF NOT EXISTS idx_reminders_upto + ON public.reminders (upto); CREATE INDEX IF NOT EXISTS idx_reminders_next_trigger_time ON public.reminders (next_trigger_time); -CREATE INDEX IF NOT EXISTS idx_reminders_request_id - ON public.reminders (request_id); - CREATE INDEX IF NOT EXISTS idx_reminders_triggered_time ON public.reminders (triggered_time); +CREATE INDEX IF NOT EXISTS idx_reminders_deleted_at + ON public.reminders (deleted_at); + diff --git a/scripts/public/request_ai_models.sql b/scripts/public/request_ai_models.sql index e6358c4..ca47a46 100644 --- a/scripts/public/request_ai_models.sql +++ b/scripts/public/request_ai_models.sql @@ -8,10 +8,10 @@ CREATE TABLE IF NOT EXISTS public.request_ai_models is_active boolean DEFAULT TRUE, ai_model_id bigint, PRIMARY KEY (id), - CONSTRAINT fk_curl_requests_models - FOREIGN KEY (request_id) REFERENCES public.curl_requests, CONSTRAINT fk_request_ai_models_ai_model - FOREIGN KEY (ai_model_id) REFERENCES public.ai_models + FOREIGN KEY (ai_model_id) REFERENCES public.ai_models, + CONSTRAINT fk_curl_requests_models + FOREIGN KEY (request_id) REFERENCES public.curl_requests ); CREATE UNIQUE INDEX IF NOT EXISTS idx_request_ai_model diff --git a/scripts/public/telegrams.sql b/scripts/public/telegrams.sql new file mode 100644 index 0000000..d2f07c4 --- /dev/null +++ b/scripts/public/telegrams.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS public.telegrams +( + id bigserial, + created_at timestamp with time zone, + updated_at timestamp with time zone, + deleted_at timestamp with time zone, + user_id bigint, + chat_id bigint NOT NULL, + otp varchar(255) NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fk_users_telegram + FOREIGN KEY (user_id) REFERENCES public.users +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_telegrams_chat_id + ON public.telegrams (chat_id); + +CREATE INDEX IF NOT EXISTS idx_telegrams_user_id + ON public.telegrams (user_id); + +CREATE INDEX IF NOT EXISTS idx_telegrams_deleted_at + ON public.telegrams (deleted_at); + diff --git a/server/server.go b/server/server.go index 3fdec1d..cba391c 100644 --- a/server/server.go +++ b/server/server.go @@ -75,12 +75,14 @@ var Module = fx.Options( repositories.NewReminderRepository, repositories.NewUserRepository, repositories.NewTelegramRepository, + repositories.NewOpenAIModelRepository, services.NewAIModelService, services.NewAsynqService, services.NewCurlService, services.NewDeepseekModelService, services.NewGeminiService, + services.NewOpenAIService, services.NewLLMService, services.NewReminderService, services.NewUserService, diff --git a/services/ai_dispatcher.go b/services/ai_dispatcher.go index e3c7344..8f9f50b 100644 --- a/services/ai_dispatcher.go +++ b/services/ai_dispatcher.go @@ -12,11 +12,12 @@ type AiDispatcherImpl struct { aiModel domain.AIModelService } -func NewAIDispatcher(geminiService domain.GeminiService, deepseekService domain.DeepseekService, ai domain.AIModelService) domain.AiDispatcher { +func NewAIDispatcher(geminiService domain.GeminiService, deepseekService domain.DeepseekService, openaiService domain.OpenAIService, ai domain.AIModelService) domain.AiDispatcher { return &AiDispatcherImpl{ services: &[]domain.DispatchableAIService{ geminiService, deepseekService, + openaiService, }, aiModel: ai, } diff --git a/services/deepseek.go b/services/deepseek.go index 2ba7c47..62accc7 100644 --- a/services/deepseek.go +++ b/services/deepseek.go @@ -92,7 +92,7 @@ func (s *DeepseekServiceImpl) PullModel(_ context.Context, model *models.Deepsee return errutil.NewAppError(errutil.ErrAIMarshalRequestFailed, err) } - url := fmt.Sprintf("%s/api/pull", model.BaseURL) + url := fmt.Sprintf("%s/api/pull", model.GetBaseURL()) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return errutil.NewAppError(errutil.ErrAICreateRequestFailed, err) @@ -166,7 +166,7 @@ func deepseekCall(model *models.DeepseekModel, response *types.CurlResponse, cur return nil, err } - url := fmt.Sprintf("%s/api/chat", model.BaseURL) + url := fmt.Sprintf("%s/api/chat", model.GetBaseURL()) client := &http.Client{} req, err := http.NewRequest("POST", url, strings.NewReader(string(reqBody))) if err != nil { diff --git a/services/gemini.go b/services/gemini.go index bc31774..f1e35e5 100644 --- a/services/gemini.go +++ b/services/gemini.go @@ -80,11 +80,13 @@ func geminiCall(ctx context.Context, model *models.GeminiModel, response *types. } client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: model.GetAPIKey(), + HTTPOptions: genai.HTTPOptions{ + BaseURL: model.GetBaseURL(), + }, }) if err != nil { return nil, err } - var parts []*genai.Part if req.ResponseType == types.ResponseTypeHTML { diff --git a/services/openai.go b/services/openai.go new file mode 100644 index 0000000..2311844 --- /dev/null +++ b/services/openai.go @@ -0,0 +1,193 @@ +package services + +import ( + "NotificationManagement/domain" + "NotificationManagement/models" + "NotificationManagement/repositories" + "NotificationManagement/types" + "NotificationManagement/utils/errutil" + "context" + "encoding/json" + + "github.com/sashabaranov/go-openai" +) + +type OpenAIServiceImpl struct { + domain.CommonService[models.OpenAIModel] + CurlService domain.CurlService +} + +func NewOpenAIService(repo domain.OpenAIModelRepository, curl domain.CurlService) domain.OpenAIService { + service := &OpenAIServiceImpl{ + CurlService: curl, + } + service.CommonService = NewCommonService(repo, service) + return service +} + +func (s *OpenAIServiceImpl) ProcessContext(ctx context.Context) context.Context { + if txContext, ok := repositories.GetTxContext(ctx); ok { + filters := append(txContext.Filter, repositories.NewFilter("type", "=", s.GetModelType())) + txContext.Filter = filters + } + return ctx +} + +func (s *OpenAIServiceImpl) MakeAIRequest(c context.Context, m *models.AIModel, requestId uint) (interface{}, error) { + curl, err := s.CurlService.GetModelById(c, requestId, nil) + if err != nil { + return nil, err + } + curlResponse, err := s.CurlService.ProcessCurlRequest(c, curl) + if err != nil { + return nil, err + } + model, err := s.GetModelById(c, m.ID, nil) + if err != nil { + return nil, err + } + respBody, err := openAICall(c, model, curlResponse, curl) + if err != nil { + return nil, err + } + return respBody, nil +} + +func (s *OpenAIServiceImpl) GetAIJsonResponse(c context.Context, m *models.AIModel, requestId uint) (map[string]interface{}, error) { + request, err := s.MakeAIRequest(c, m, requestId) + if err != nil { + return nil, err + } + resp, _ := request.(*openai.ChatCompletionResponse) + var aiResp map[string]interface{} + if len(resp.Choices) == 0 { + return map[string]interface{}{ + "comment": "Failed", + }, nil + } else if err := json.Unmarshal([]byte(resp.Choices[0].Message.Content), &aiResp); err != nil { + return map[string]interface{}{ + "comment": resp.Choices[0].Message.Content, + }, nil + } + return aiResp, nil +} + +func (s *OpenAIServiceImpl) GetModelType() string { + return "openai" +} + +// createJSONSchema creates a JSON schema from the CurlRequest additional fields +func createJSONSchema(req *models.CurlRequest) types.JSONSchema { + properties := make(map[string]types.JSONSchemaProperty) + required := []string{"IsCorrect"} + + properties["IsCorrect"] = types.JSONSchemaProperty{ + Type: "boolean", + Description: "Indicates whether the response is correct", + } + + // Add properties from additional fields + if req.AdditionalFields != nil { + for _, field := range *req.AdditionalFields { + var schemaType string + switch field.Type { + case "number": + schemaType = "number" + case "boolean": + schemaType = "boolean" + default: + schemaType = "string" + } + + properties[field.PropertyName] = types.JSONSchemaProperty{ + Type: schemaType, + Description: field.Description, + } + required = append(required, field.PropertyName) + } + } + + return types.JSONSchema{ + Type: "object", + Properties: properties, + Required: required, + AdditionalProperties: false, + } +} + +func openAICall(ctx context.Context, model *models.OpenAIModel, response *types.CurlResponse, req *models.CurlRequest) (*openai.ChatCompletionResponse, error) { + assistantContent, err := response.GetAssistantContent(req.ResponseType) + if err != nil { + return nil, err + } + + config := openai.DefaultConfig(model.GetAPIKey()) + if model.GetBaseURL() != "" { + config.BaseURL = model.GetBaseURL() + } + client := openai.NewClientWithConfig(config) + + messages := []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleAssistant, + Content: *assistantContent, + }, + { + Role: openai.ChatMessageRoleUser, + Content: req.Body, + }, + } + + // Create JSON schema from additional fields for structured output + jsonSchema := createJSONSchema(req) + + resp, err := client.CreateChatCompletion( + ctx, + openai.ChatCompletionRequest{ + Model: model.ModelName, + Messages: messages, + Stream: false, + ResponseFormat: &openai.ChatCompletionResponseFormat{ + Type: openai.ChatCompletionResponseFormatTypeJSONSchema, + JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{ + Name: "response_schema", + Description: "Structured response with required fields", + Schema: &jsonSchema, // Pass a pointer to jsonSchema + Strict: true, + }, + }, + }, + ) + + if err != nil { + return nil, errutil.NewAppError(errutil.ErrExternalServiceError, err) + } + + return &resp, nil +} + +func (s *OpenAIServiceImpl) CreateAIModel(c context.Context, model any) error { + openaiModel := (model).(*models.OpenAIModel) + return s.CreateModel(c, openaiModel) +} + +func (s *OpenAIServiceImpl) UpdateAIModel(c context.Context, model any) (any, error) { + openaiModel := (model).(*models.OpenAIModel) + return s.UpdateModel(c, openaiModel.ID, openaiModel) +} + +func (s *OpenAIServiceImpl) GetAIModelById(ctx context.Context, id uint) (any, error) { + return s.GetModelById(ctx, id, nil) +} + +func (s *OpenAIServiceImpl) GetAllAIModels(ctx context.Context) ([]any, error) { + allModels, err := s.GetAllModels(ctx, 100, 0) + if err != nil { + return nil, err + } + i := make([]any, len(allModels)) + for idx, model := range allModels { + i[idx] = model + } + return i, err +} diff --git a/types/ai.go b/types/ai.go index 06fde66..d696239 100644 --- a/types/ai.go +++ b/types/ai.go @@ -4,6 +4,7 @@ import ( "NotificationManagement/models" "NotificationManagement/utils/errutil" "fmt" + "gorm.io/gorm" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -44,7 +45,7 @@ func (r *AIModelRequest) Validate() error { rules = append(rules, validation.Field(&r.BaseURL, validation.Required, validation.Length(1, 500))) case "gemini", "openai": rules = append(rules, validation.Field(&r.APISecret, validation.Required, validation.Length(1, 500))) - + rules = append(rules, validation.Field(&r.BaseURL, validation.Length(0, 500))) // Optional BaseURL } return validation.ValidateStruct(r, rules...) @@ -59,7 +60,8 @@ func (dr *AIModelRequest) ToModel() (models.AIModelInterface, error) { Model: gorm.Model{ ID: dr.ID, }, - Type: dr.Type, + Type: dr.Type, + BaseURL: &dr.BaseURL, } switch dr.Type { case "deepseek", "local": @@ -67,30 +69,25 @@ func (dr *AIModelRequest) ToModel() (models.AIModelInterface, error) { AIModel: aiModel, Name: dr.Name, ModelName: dr.ModelName, - BaseURL: dr.BaseURL, Size: dr.Size, }, nil case "gemini": - return &models.GeminiModel{ + geminiModel := &models.GeminiModel{ AIModel: aiModel, Name: dr.Name, ModelName: dr.ModelName, APISecret: models.EncryptedString(dr.APISecret), - }, nil + } + return geminiModel, nil + case "openai": + openaiModel := &models.OpenAIModel{ + AIModel: aiModel, + Name: dr.Name, + ModelName: dr.ModelName, + APISecret: models.EncryptedString(dr.APISecret), + } + return openaiModel, nil default: return nil, errutil.NewAppError(errutil.ErrUnsupportedAIModelType, fmt.Errorf("unsupported AI model type: %s", dr.Type)) } } - -func FromDeepseekModel(model *models.DeepseekModel) *DeepseekModelResponse { - return &DeepseekModelResponse{ - ID: model.ID, - Name: model.Name, - Type: model.Type, - ModelName: model.ModelName, - BaseURL: model.BaseURL, - Size: model.Size, - CreatedAt: model.CreatedAt.Format(ResponseDateFormat), - UpdatedAt: model.UpdatedAt.Format(ResponseDateFormat), - } -} diff --git a/types/deepseek.go b/types/deepseek.go index 840a620..da035d9 100644 --- a/types/deepseek.go +++ b/types/deepseek.go @@ -1,5 +1,7 @@ package types +import "NotificationManagement/models" + type DeepseekModelResponse struct { ID uint `json:"id"` Name string `json:"name"` @@ -10,3 +12,16 @@ type DeepseekModelResponse struct { CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } + +func FromDeepseekModel(model *models.DeepseekModel) *DeepseekModelResponse { + return &DeepseekModelResponse{ + ID: model.ID, + Name: model.Name, + Type: model.Type, + ModelName: model.ModelName, + BaseURL: model.GetBaseURL(), + Size: model.Size, + CreatedAt: model.CreatedAt.Format(ResponseDateFormat), + UpdatedAt: model.UpdatedAt.Format(ResponseDateFormat), + } +} diff --git a/types/openai.go b/types/openai.go new file mode 100644 index 0000000..7055141 --- /dev/null +++ b/types/openai.go @@ -0,0 +1,58 @@ +package types + +import ( + "NotificationManagement/models" + "encoding/json" +) + +type OpenAIModelResponse struct { + ID uint `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + ModelName string `json:"model"` + APISecret models.EncryptedString `json:"api_secret"` + ModifiedAt string `json:"modified_at"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +func FromOpenAIModel(model *models.OpenAIModel) *OpenAIModelResponse { + return &OpenAIModelResponse{ + ID: model.ID, + Type: model.Type, + Name: model.Name, + ModelName: model.ModelName, + APISecret: model.APISecret, + CreatedAt: model.CreatedAt.Format(ResponseDateFormat), + UpdatedAt: model.UpdatedAt.Format(ResponseDateFormat), + } +} + +// JSONSchemaProperty represents a property in a JSON schema +type JSONSchemaProperty struct { + Type string `json:"type"` + Description string `json:"description,omitempty"` +} + +// JSONSchema represents a JSON schema structure +type JSONSchema struct { + Type string `json:"type"` + Properties map[string]JSONSchemaProperty `json:"properties"` + Required []string `json:"required"` + AdditionalProperties bool `json:"additionalProperties"` +} + +// MarshalJSON implements json.Marshaler interface +func (j *JSONSchema) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + Properties map[string]JSONSchemaProperty `json:"properties"` + Required []string `json:"required"` + AdditionalProperties bool `json:"additionalProperties"` + }{ + Type: j.Type, + Properties: j.Properties, + Required: j.Required, + AdditionalProperties: j.AdditionalProperties, + }) +}