added AI auto-generated keywords to customer profile
This commit is contained in:
@@ -32,6 +32,16 @@ const (
|
||||
CustomerRoleAnalyst CustomerRole = "analyst"
|
||||
)
|
||||
|
||||
// KeywordsStatus represents the status of keyword processing
|
||||
type KeywordsStatus string
|
||||
|
||||
const (
|
||||
KeywordsStatusPending KeywordsStatus = "pending"
|
||||
KeywordsStatusInProgress KeywordsStatus = "in_progress"
|
||||
KeywordsStatusReady KeywordsStatus = "ready"
|
||||
KeywordsStatusFailed KeywordsStatus = "failed"
|
||||
)
|
||||
|
||||
// Customer represents a customer in the tender management system
|
||||
// A customer can have an associated company for login purposes
|
||||
type Customer struct {
|
||||
@@ -44,8 +54,10 @@ type Customer struct {
|
||||
Type CustomerType `bson:"type"`
|
||||
Role CustomerRole `bson:"role"`
|
||||
Phone *string `bson:"phone"`
|
||||
Companies []string `bson:"companies"`
|
||||
DeviceToken []string `bson:"device_token"`
|
||||
Companies []string `bson:"companies"`
|
||||
Keywords []string `bson:"keywords,omitempty"`
|
||||
KeywordsStatus KeywordsStatus `bson:"keywords_status,omitempty"`
|
||||
DeviceToken []string `bson:"device_token"`
|
||||
LastLoginAt *int64 `bson:"last_login_at"`
|
||||
UpdatedAt int64 `bson:"updated_at"`
|
||||
CreatedAt int64 `bson:"created_at"`
|
||||
|
||||
+27
-14
@@ -28,6 +28,7 @@ type UpdateCustomerForm struct {
|
||||
Username string `json:"username" valid:"required,username,length(3|30)" example:"user"`
|
||||
Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
||||
}
|
||||
|
||||
// CustomerStatusForm represents the form for suspending a customer
|
||||
@@ -63,25 +64,37 @@ type CustomerResponse struct {
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
LastLoginAt *int64 `json:"last_login_at"`
|
||||
Companies []*CompanySummary `json:"companies"`
|
||||
DeviceToken []string `json:"-"`
|
||||
Companies []*CompanySummary `json:"companies"`
|
||||
Keywords []string `json:"keywords"`
|
||||
KeywordsStatus string `json:"keywords_status"`
|
||||
DeviceToken []string `json:"-"`
|
||||
}
|
||||
|
||||
// ToResponse converts Customer to CustomerResponse
|
||||
func (c *Customer) ToResponse(companies []*CompanySummary) *CustomerResponse {
|
||||
keywords := c.Keywords
|
||||
if keywords == nil {
|
||||
keywords = []string{}
|
||||
}
|
||||
keywordsStatus := string(c.KeywordsStatus)
|
||||
if keywordsStatus == "" {
|
||||
keywordsStatus = string(KeywordsStatusReady)
|
||||
}
|
||||
return &CustomerResponse{
|
||||
ID: c.ID.Hex(),
|
||||
FullName: c.FullName,
|
||||
Username: c.Username,
|
||||
Email: c.Email,
|
||||
Type: string(c.Type),
|
||||
Status: string(c.Status),
|
||||
Role: string(c.Role),
|
||||
Phone: c.Phone,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedAt: c.CreatedAt,
|
||||
LastLoginAt: c.LastLoginAt,
|
||||
Companies: companies,
|
||||
ID: c.ID.Hex(),
|
||||
FullName: c.FullName,
|
||||
Username: c.Username,
|
||||
Email: c.Email,
|
||||
Type: string(c.Type),
|
||||
Status: string(c.Status),
|
||||
Role: string(c.Role),
|
||||
Phone: c.Phone,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedAt: c.CreatedAt,
|
||||
LastLoginAt: c.LastLoginAt,
|
||||
Companies: companies,
|
||||
Keywords: keywords,
|
||||
KeywordsStatus: keywordsStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tm/internal/company"
|
||||
"tm/pkg/glm"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/notification"
|
||||
"tm/pkg/redis"
|
||||
@@ -61,10 +62,11 @@ type customerService struct {
|
||||
redisClient redis.Client
|
||||
notification notification.SDK
|
||||
validator ValidationService
|
||||
glmSDK *glm.SDK
|
||||
}
|
||||
|
||||
// New creates a new customer service
|
||||
func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService) Service {
|
||||
func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService, glmSDK *glm.SDK) Service {
|
||||
return &customerService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
@@ -73,6 +75,7 @@ func NewService(repository Repository, logger logger.Logger, authService authori
|
||||
redisClient: redisClient,
|
||||
notification: notificationSDK,
|
||||
validator: validator,
|
||||
glmSDK: glmSDK,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +130,21 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
||||
Phone: form.Phone,
|
||||
}
|
||||
|
||||
// Generate keywords using GLM AI
|
||||
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||
keywords, err := s.generateKeywords(ctx, customer)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to generate keywords, continuing without keywords", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
keywords = []string{}
|
||||
customer.KeywordsStatus = KeywordsStatusFailed
|
||||
} else {
|
||||
customer.Keywords = keywords
|
||||
customer.KeywordsStatus = KeywordsStatusReady
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err = s.repository.Register(ctx, customer)
|
||||
if err != nil {
|
||||
@@ -335,7 +353,24 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
||||
|
||||
// Handle company assignments
|
||||
companies := make([]*CompanySummary, 0)
|
||||
companiesChanged := false
|
||||
if len(form.Companies) > 0 {
|
||||
// Check if companies changed
|
||||
if len(form.Companies) != len(customer.Companies) {
|
||||
companiesChanged = true
|
||||
} else {
|
||||
companyMap := make(map[string]bool)
|
||||
for _, id := range customer.Companies {
|
||||
companyMap[id] = true
|
||||
}
|
||||
for _, id := range form.Companies {
|
||||
if !companyMap[id] {
|
||||
companiesChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
customer.Companies = form.Companies
|
||||
companies, err = s.loadCompaniesForCustomer(ctx, customer)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
|
||||
@@ -345,17 +380,101 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
||||
}
|
||||
}
|
||||
|
||||
if form.FullName != nil {
|
||||
infoChanged := false
|
||||
if form.FullName != nil && (customer.FullName == nil || *form.FullName != *customer.FullName) {
|
||||
customer.FullName = form.FullName
|
||||
infoChanged = true
|
||||
}
|
||||
|
||||
if form.Phone != nil {
|
||||
if form.Phone != nil && (customer.Phone == nil || *form.Phone != *customer.Phone) {
|
||||
customer.Phone = form.Phone
|
||||
infoChanged = true
|
||||
}
|
||||
|
||||
// Update role if provided
|
||||
if form.Role != "" {
|
||||
if form.Role != "" && customer.Role != CustomerRole(form.Role) {
|
||||
customer.Role = CustomerRole(form.Role)
|
||||
infoChanged = true
|
||||
}
|
||||
|
||||
// Handle keywords
|
||||
if form.Keywords != nil {
|
||||
// Keywords explicitly provided by customer, set status to in_progress
|
||||
// Customer's manual keywords are saved, but we'll enhance them with AI
|
||||
customer.Keywords = form.Keywords
|
||||
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||
|
||||
// Save customer with in_progress status first, then process asynchronously
|
||||
customerID := customer.ID.Hex()
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
// Get fresh customer data for processing
|
||||
cust, err := s.repository.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get customer for keyword processing", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If GLM SDK is not available, keep manual keywords and set status to ready
|
||||
if s.glmSDK == nil {
|
||||
s.logger.Info("GLM SDK not available, keeping manual keywords", map[string]interface{}{
|
||||
"customer_id": customerID,
|
||||
})
|
||||
cust.KeywordsStatus = KeywordsStatusReady
|
||||
} else {
|
||||
// Regenerate keywords based on updated customer info
|
||||
keywords, err := s.generateKeywords(ctx, cust)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to process keywords after manual update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID,
|
||||
})
|
||||
// Keep manual keywords but set status to failed
|
||||
cust.KeywordsStatus = KeywordsStatusFailed
|
||||
} else if len(keywords) > 0 {
|
||||
// Update with AI-generated keywords
|
||||
cust.Keywords = keywords
|
||||
cust.KeywordsStatus = KeywordsStatusReady
|
||||
} else {
|
||||
// Empty keywords from GLM, keep manual keywords and set status to ready
|
||||
s.logger.Warn("GLM returned empty keywords, keeping manual keywords", map[string]interface{}{
|
||||
"customer_id": customerID,
|
||||
})
|
||||
cust.KeywordsStatus = KeywordsStatusReady
|
||||
}
|
||||
}
|
||||
|
||||
// Update customer with new status
|
||||
err = s.repository.Update(ctx, cust)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update customer keywords status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID,
|
||||
})
|
||||
} else {
|
||||
s.logger.Info("Keywords processed successfully", map[string]interface{}{
|
||||
"customer_id": customerID,
|
||||
"status": cust.KeywordsStatus,
|
||||
})
|
||||
}
|
||||
}()
|
||||
} else if infoChanged || companiesChanged {
|
||||
// Customer info changed, regenerate keywords
|
||||
customer.KeywordsStatus = KeywordsStatusInProgress
|
||||
keywords, err := s.generateKeywords(ctx, customer)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to regenerate keywords, keeping existing keywords", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
customer.KeywordsStatus = KeywordsStatusFailed
|
||||
} else {
|
||||
customer.Keywords = keywords
|
||||
customer.KeywordsStatus = KeywordsStatusReady
|
||||
}
|
||||
}
|
||||
|
||||
// Update in database
|
||||
@@ -1244,3 +1363,92 @@ func (s *customerService) AssignRole(ctx context.Context, customerID string, for
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateKeywords generates keywords for a customer using GLM AI based on their information
|
||||
func (s *customerService) generateKeywords(ctx context.Context, customer *Customer) ([]string, error) {
|
||||
// If GLM SDK is not available, return empty keywords
|
||||
if s.glmSDK == nil {
|
||||
s.logger.Warn("GLM SDK not available, skipping keyword generation", map[string]interface{}{
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Build customer information string for AI
|
||||
var customerInfo strings.Builder
|
||||
if customer.FullName != nil {
|
||||
customerInfo.WriteString(fmt.Sprintf("Name: %s\n", *customer.FullName))
|
||||
}
|
||||
customerInfo.WriteString(fmt.Sprintf("Type: %s\n", customer.Type))
|
||||
customerInfo.WriteString(fmt.Sprintf("Role: %s\n", customer.Role))
|
||||
if customer.Phone != nil {
|
||||
customerInfo.WriteString(fmt.Sprintf("Phone: %s\n", *customer.Phone))
|
||||
}
|
||||
|
||||
// Get company information if available
|
||||
if len(customer.Companies) > 0 {
|
||||
customerInfo.WriteString("Companies: ")
|
||||
for i, companyID := range customer.Companies {
|
||||
comp, err := s.companyService.GetByID(ctx, companyID)
|
||||
if err == nil {
|
||||
if i > 0 {
|
||||
customerInfo.WriteString(", ")
|
||||
}
|
||||
customerInfo.WriteString(comp.Name)
|
||||
if comp.Industry != "" {
|
||||
customerInfo.WriteString(fmt.Sprintf(" (Industry: %s)", comp.Industry))
|
||||
}
|
||||
}
|
||||
}
|
||||
customerInfo.WriteString("\n")
|
||||
}
|
||||
|
||||
systemPrompt := `You are a keyword extraction assistant for a tender management system.
|
||||
Analyze the customer information and generate 5-10 relevant keywords that would help match them with appropriate tenders.
|
||||
Keywords should be:
|
||||
- Relevant to tender/procurement categories
|
||||
- Based on customer type, role, and company information
|
||||
- Professional and specific
|
||||
- Suitable for matching with tender descriptions
|
||||
|
||||
Return ONLY a comma-separated list of keywords, no explanations or additional text.`
|
||||
|
||||
userMessage := fmt.Sprintf("Generate keywords for this customer:\n\n%s", customerInfo.String())
|
||||
|
||||
s.logger.Info("Generating keywords using GLM AI", map[string]interface{}{
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
|
||||
response, err := s.glmSDK.QuickChatWithSystem(ctx, systemPrompt, userMessage)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate keywords using GLM AI", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
return []string{}, fmt.Errorf("failed to generate keywords: %w", err)
|
||||
}
|
||||
|
||||
// Parse comma-separated keywords
|
||||
keywordsStr := strings.TrimSpace(response)
|
||||
if keywordsStr == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// Split by comma and clean up
|
||||
keywordList := strings.Split(keywordsStr, ",")
|
||||
keywords := make([]string, 0, len(keywordList))
|
||||
for _, kw := range keywordList {
|
||||
kw = strings.TrimSpace(kw)
|
||||
if kw != "" {
|
||||
keywords = append(keywords, kw)
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Keywords generated successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID,
|
||||
"keywords": keywords,
|
||||
"count": len(keywords),
|
||||
})
|
||||
|
||||
return keywords, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user