feat(copilot): move provider configuration to settings
This commit is contained in:
@@ -9,8 +9,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/llm"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/security"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/datatypes"
|
||||
@@ -30,14 +32,21 @@ func newCaptainPreferenceFixture(t *testing.T) *captainPreferenceFixture {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.CaptainPreference{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.CaptainPreference{}, &model.InstallationConfig{}))
|
||||
|
||||
account := &model.Account{Name: "Captain Preferences", Active: true, CaptainModels: datatypes.JSON(`{}`), CaptainFeatures: datatypes.JSON(`{}`)}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
|
||||
prefRepo := repository.NewCaptainPreferenceRepo(db)
|
||||
accountRepo := repository.NewAccountRepo(db)
|
||||
handler := NewCaptainPreferenceHandler(service.NewCaptainPreferenceService(prefRepo, accountRepo))
|
||||
installationConfigRepo := repository.NewInstallationConfigRepo(db)
|
||||
encryptor, err := security.NewEncryptor(security.DefaultEncryptionConfig())
|
||||
require.NoError(t, err)
|
||||
manager := llm.NewProviderManager()
|
||||
copilotConfigService := service.NewCopilotConfigService(installationConfigRepo, encryptor, manager)
|
||||
preferenceService := service.NewCaptainPreferenceService(prefRepo, accountRepo)
|
||||
preferenceService.SetCopilotConfigService(copilotConfigService)
|
||||
handler := NewCaptainPreferenceHandler(preferenceService)
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/accounts/:account_id/captain/preferences", handler.Get)
|
||||
router.PUT("/api/v1/accounts/:account_id/captain/preferences", func(c *gin.Context) {
|
||||
@@ -92,6 +101,7 @@ func TestCaptainPreferencesGetReturnsRawChatwootConfig(t *testing.T) {
|
||||
require.Contains(t, payload, "providers")
|
||||
require.Contains(t, payload, "models")
|
||||
require.Contains(t, payload, "features")
|
||||
require.Contains(t, payload, "provider_config")
|
||||
|
||||
features := payload["features"].(map[string]any)
|
||||
editor := features["editor"].(map[string]any)
|
||||
@@ -101,6 +111,27 @@ func TestCaptainPreferencesGetReturnsRawChatwootConfig(t *testing.T) {
|
||||
require.NotEmpty(t, editor["models"].([]any))
|
||||
}
|
||||
|
||||
func TestCaptainPreferencesUpdateProviderConfiguration(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
|
||||
w := f.request(http.MethodPut, f.path(""), map[string]any{
|
||||
"provider_config": map[string]any{
|
||||
"provider": "openai_compatible",
|
||||
"base_url": "https://llm.example.com/v1",
|
||||
"model": "example-model",
|
||||
"api_key": "secret-key-value",
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
||||
payload := decodeCaptainPreferencePayload(t, w)
|
||||
providerConfig := payload["provider_config"].(map[string]any)
|
||||
require.Equal(t, "openai_compatible", providerConfig["provider"])
|
||||
require.Equal(t, "https://llm.example.com/v1", providerConfig["base_url"])
|
||||
require.Equal(t, "example-model", providerConfig["model"])
|
||||
require.Equal(t, true, providerConfig["api_key_configured"])
|
||||
require.NotContains(t, w.Body.String(), "secret-key-value")
|
||||
}
|
||||
|
||||
func TestCaptainPreferencesUpdateMergesAccountModelsAndFeatures(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
require.NoError(t, f.db.Model(&model.Account{}).Where("id = ?", f.account.ID).Updates(map[string]any{
|
||||
|
||||
@@ -22,9 +22,9 @@ func NewCaptainTaskExtendedHandler(svc *service.CaptainTaskExtendedService) *Cap
|
||||
}
|
||||
|
||||
// LabelSuggestion returns AI-generated label suggestions for conversations.
|
||||
// GET /api/v1/accounts/:id/captain/tasks/label_suggestion
|
||||
// GET /api/v1/accounts/:account_id/captain/tasks/label_suggestion
|
||||
func (h *CaptainTaskExtendedHandler) LabelSuggestion(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
@@ -77,9 +77,9 @@ func (h *CaptainTaskExtendedHandler) LabelSuggestion(c *gin.Context) {
|
||||
}
|
||||
|
||||
// FollowUp returns AI-generated follow-up task suggestions for conversations.
|
||||
// GET /api/v1/accounts/:id/captain/tasks/follow_up
|
||||
// GET /api/v1/accounts/:account_id/captain/tasks/follow_up
|
||||
func (h *CaptainTaskExtendedHandler) FollowUp(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestCaptainTaskExtendedHandler_LabelSuggestion(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/captain/tasks/label_suggestion?conversation_ids="+fmt.Sprintf("%d", conv.ID), nil)
|
||||
|
||||
handler.LabelSuggestion(c)
|
||||
@@ -98,7 +98,7 @@ func TestCaptainTaskExtendedHandler_LabelSuggestion_MissingConversationIDs(t *te
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/captain/tasks/label_suggestion", nil)
|
||||
|
||||
handler.LabelSuggestion(c)
|
||||
@@ -121,7 +121,7 @@ func TestCaptainTaskExtendedHandler_LabelSuggestion_ChatwootPostRawPayload(t *te
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/label_suggestion", bytes.NewReader([]byte(`{"conversation_display_id":77}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestCaptainTaskExtendedHandler_FollowUp(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/captain/tasks/follow_up?conversation_ids="+fmt.Sprintf("%d", conv.ID), nil)
|
||||
|
||||
handler.FollowUp(c)
|
||||
@@ -173,7 +173,7 @@ func TestCaptainTaskExtendedHandler_FollowUp_MissingConversationIDs(t *testing.T
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/captain/tasks/follow_up", nil)
|
||||
|
||||
handler.FollowUp(c)
|
||||
@@ -204,7 +204,7 @@ func TestCaptainTaskExtendedHandler_FollowUp_ChatwootPostUpdatesContext(t *testi
|
||||
}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/follow_up", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func NewCaptainTaskHandler(svc *service.CaptainTaskService) *CaptainTaskHandler
|
||||
// ReplySuggestion generates AI-powered reply suggestions for a conversation.
|
||||
// POST /api/v1/accounts/:account_id/captain/tasks/reply_suggestion
|
||||
func (h *CaptainTaskHandler) ReplySuggestion(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
@@ -55,7 +55,7 @@ func (h *CaptainTaskHandler) ReplySuggestion(c *gin.Context) {
|
||||
// Summarize generates a concise summary of a conversation.
|
||||
// POST /api/v1/accounts/:account_id/captain/tasks/summarize
|
||||
func (h *CaptainTaskHandler) Summarize(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
@@ -80,7 +80,7 @@ func (h *CaptainTaskHandler) Summarize(c *gin.Context) {
|
||||
// Rewrite rewrites a draft message to improve tone, clarity, or language.
|
||||
// POST /api/v1/accounts/:account_id/captain/tasks/rewrite
|
||||
func (h *CaptainTaskHandler) Rewrite(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
@@ -107,9 +107,9 @@ func (h *CaptainTaskHandler) Rewrite(c *gin.Context) {
|
||||
// following the same SSE pattern as SSEStreamHandler.StreamCopilotMessage.
|
||||
|
||||
// StreamReplySuggestion streams AI-powered reply suggestions via SSE.
|
||||
// POST /api/v1/accounts/:id/captain/tasks/reply_suggestion/stream
|
||||
// POST /api/v1/accounts/:account_id/captain/tasks/reply_suggestion/stream
|
||||
func (h *CaptainTaskHandler) StreamReplySuggestion(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
captainWriteSSEError(c, http.StatusBadRequest, "invalid account_id")
|
||||
return
|
||||
@@ -149,9 +149,9 @@ func (h *CaptainTaskHandler) StreamReplySuggestion(c *gin.Context) {
|
||||
}
|
||||
|
||||
// StreamSummarize streams a conversation summary via SSE.
|
||||
// POST /api/v1/accounts/:id/captain/tasks/summarize/stream
|
||||
// POST /api/v1/accounts/:account_id/captain/tasks/summarize/stream
|
||||
func (h *CaptainTaskHandler) StreamSummarize(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
captainWriteSSEError(c, http.StatusBadRequest, "invalid account_id")
|
||||
return
|
||||
@@ -191,9 +191,9 @@ func (h *CaptainTaskHandler) StreamSummarize(c *gin.Context) {
|
||||
}
|
||||
|
||||
// StreamRewrite streams a rewritten message via SSE.
|
||||
// POST /api/v1/accounts/:id/captain/tasks/rewrite/stream
|
||||
// POST /api/v1/accounts/:account_id/captain/tasks/rewrite/stream
|
||||
func (h *CaptainTaskHandler) StreamRewrite(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
accountID, err := strconv.ParseUint(c.Param("account_id"), 10, 64)
|
||||
if err != nil {
|
||||
captainWriteSSEError(c, http.StatusBadRequest, "invalid account_id")
|
||||
return
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestCaptainTaskHandler_Summarize_ChatwootRawPayload(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/summarize", bytes.NewReader([]byte(`{"conversation_display_id":123}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -96,12 +96,33 @@ func TestCaptainTaskHandler_Summarize_ChatwootRawPayload(t *testing.T) {
|
||||
assert.NotContains(t, resp, "success")
|
||||
}
|
||||
|
||||
func TestCaptainTaskHandler_ReplySuggestion_UsesAccountIDRouteParam(t *testing.T) {
|
||||
provider := &mockCaptainTaskHandlerLLM{response: &llm.ChatResponse{Choices: []llm.ChatChoice{{Message: llm.ChatMessage{Role: "assistant", Content: "Happy to help"}}}}}
|
||||
handler, db := setupCaptainTaskHandlerTest(t, provider)
|
||||
displayID := uint(3)
|
||||
conv := &model.Conversation{AccountID: 1, DisplayID: &displayID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
||||
require.NoError(t, db.Create(conv).Error)
|
||||
require.NoError(t, db.Create(&model.Message{ConversationID: conv.ID, AccountID: 1, SenderType: "contact", MessageType: "incoming", Content: "Need help"}).Error)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/accounts/:account_id/captain/tasks/reply_suggestion", handler.ReplySuggestion)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/reply_suggestion", bytes.NewReader([]byte(`{"conversation_display_id":3}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, "Happy to help", resp["message"])
|
||||
}
|
||||
|
||||
func TestCaptainTaskHandler_Rewrite_NoProviderRawDisabled(t *testing.T) {
|
||||
handler, _ := setupCaptainTaskHandlerTest(t, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/rewrite", bytes.NewReader([]byte(`{"content":"hello","operation":"professional"}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -119,7 +140,7 @@ func TestCaptainTaskHandler_StreamRewrite_NoProviderDisabledSSE(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/rewrite/stream", bytes.NewReader([]byte(`{"content":"hello","operation":"professional"}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -146,7 +167,7 @@ func TestCaptainTaskHandler_StreamSummarize_ChatwootDisplayID(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/summarize/stream", bytes.NewReader([]byte(`{"conversation_display_id":456}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -165,7 +186,7 @@ func TestCaptainTaskHandler_StreamRewrite_InvalidOperationSSE(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Params = gin.Params{{Key: "account_id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/captain/tasks/rewrite/stream", strings.NewReader(`{"content":"hello","operation":"pirate"}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user