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 }