Implement AI onboarding and recommendation features in company service
- Added new AI onboarding and recommendation endpoints in the company handler for starting onboarding and retrieving ranked tender recommendations. - Introduced `StartAIOnboarding` and `GetAIRecommendations` methods in the company service to handle AI interactions. - Updated the company service constructor to include the AI recommendation client. - Enhanced the AI summarizer client with methods for onboarding and fetching recommendations. - Added response structures for onboarding and recommended tenders in the company form. This update enhances the tender management system by integrating AI capabilities for onboarding and tender recommendations, improving user experience and operational efficiency.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package company
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"tm/pkg/ai_summarizer"
|
||||
)
|
||||
|
||||
var ErrAIRecommendationNotConfigured = errors.New("AI recommendation service is not configured")
|
||||
|
||||
// AIRecommendationClient defines the interface for company tender recommendation AI operations.
|
||||
type AIRecommendationClient interface {
|
||||
StartOnboarding(ctx context.Context, reqBody ai_summarizer.OnboardingRequest) (*ai_summarizer.OnboardingResponse, error)
|
||||
FetchRecommendations(ctx context.Context, reqBody ai_summarizer.RecommendRequest) ([]ai_summarizer.RecommendedTender, error)
|
||||
}
|
||||
|
||||
type onboardingDocumentPayload struct {
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// StartAIOnboarding sends company profile data to the AI team to start recommendation onboarding.
|
||||
func (s *companyService) StartAIOnboarding(ctx context.Context, companyID string) (*OnboardingResponse, error) {
|
||||
if s.aiRecommendationClient == nil {
|
||||
return nil, ErrAIRecommendationNotConfigured
|
||||
}
|
||||
if strings.TrimSpace(companyID) == "" {
|
||||
return nil, errors.New("company ID is required")
|
||||
}
|
||||
|
||||
company, err := s.repository.GetByID(ctx, companyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load company for AI onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
|
||||
documents, err := s.buildOnboardingDocuments(company.DocumentFileIDs)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to prepare company documents for AI onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to prepare company documents: %w", err)
|
||||
}
|
||||
|
||||
websiteURL := ""
|
||||
if company.Website != nil {
|
||||
websiteURL = strings.TrimSpace(*company.Website)
|
||||
}
|
||||
|
||||
s.logger.Info("Starting AI company onboarding", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"documents_count": len(company.DocumentFileIDs),
|
||||
"has_website_url": websiteURL != "",
|
||||
})
|
||||
|
||||
result, err := s.aiRecommendationClient.StartOnboarding(ctx, ai_summarizer.OnboardingRequest{
|
||||
CompanyID: companyID,
|
||||
Documents: documents,
|
||||
WebsiteURL: websiteURL,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("AI onboarding request failed", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to start AI onboarding: %w", err)
|
||||
}
|
||||
|
||||
return &OnboardingResponse{Status: result.Status}, nil
|
||||
}
|
||||
|
||||
// GetAIRecommendations retrieves ranked tender recommendations from the AI team.
|
||||
func (s *companyService) GetAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) {
|
||||
if s.aiRecommendationClient == nil {
|
||||
return nil, ErrAIRecommendationNotConfigured
|
||||
}
|
||||
if strings.TrimSpace(companyID) == "" {
|
||||
return nil, errors.New("company ID is required")
|
||||
}
|
||||
|
||||
if _, err := s.repository.GetByID(ctx, companyID); err != nil {
|
||||
s.logger.Error("Failed to load company for AI recommendations", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
|
||||
s.logger.Info("Fetching AI tender recommendations", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
})
|
||||
|
||||
items, err := s.aiRecommendationClient.FetchRecommendations(ctx, ai_summarizer.RecommendRequest{
|
||||
CompanyID: companyID,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error("AI recommend request failed", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to fetch AI recommendations: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]RecommendedTenderResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
responses = append(responses, RecommendedTenderResponse{
|
||||
Rank: item.Rank,
|
||||
TenderID: item.TenderID,
|
||||
Analysis: item.Analysis,
|
||||
})
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) {
|
||||
if len(fileIDs) == 0 {
|
||||
return "[]", nil
|
||||
}
|
||||
if s.fileStore == nil {
|
||||
return "", errors.New("file storage is not configured")
|
||||
}
|
||||
|
||||
payloads := make([]onboardingDocumentPayload, 0, len(fileIDs))
|
||||
for _, fileID := range fileIDs {
|
||||
if strings.TrimSpace(fileID) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
reader, info, err := s.fileStore.Download(fileID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Skipping company document for AI onboarding", map[string]interface{}{
|
||||
"file_id": fileID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
content, readErr := io.ReadAll(reader)
|
||||
closeErr := reader.Close()
|
||||
if readErr != nil {
|
||||
return "", fmt.Errorf("failed to read document %s: %w", fileID, readErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
s.logger.Warn("Failed to close company document reader", map[string]interface{}{
|
||||
"file_id": fileID,
|
||||
"error": closeErr.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
filename := fileID
|
||||
contentType := "application/octet-stream"
|
||||
if info != nil {
|
||||
if info.Filename != "" {
|
||||
filename = info.Filename
|
||||
}
|
||||
if info.ContentType != "" {
|
||||
contentType = info.ContentType
|
||||
}
|
||||
}
|
||||
|
||||
payloads = append(payloads, onboardingDocumentPayload{
|
||||
Filename: filename,
|
||||
ContentType: contentType,
|
||||
Content: base64.StdEncoding.EncodeToString(content),
|
||||
})
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(payloads)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode company documents: %w", err)
|
||||
}
|
||||
|
||||
return string(encoded), nil
|
||||
}
|
||||
Reference in New Issue
Block a user