665 lines
21 KiB
Go
665 lines
21 KiB
Go
package tender
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"tm/internal/company"
|
|
"tm/internal/customer"
|
|
"tm/pkg/ai_summarizer"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/response"
|
|
)
|
|
|
|
// AISummarizerClient defines the interface for on-demand AI summarization
|
|
type AISummarizerClient interface {
|
|
FetchSummaryOnDemand(ctx context.Context, reqBody ai_summarizer.SummarizeRequest) (*ai_summarizer.SummarizeResponse, error)
|
|
}
|
|
|
|
// AISummarizerStorage defines the interface for fetching summaries from MinIO storage
|
|
type AISummarizerStorage interface {
|
|
GetSummaryFromStorage(ctx context.Context, noticeID string) (string, error)
|
|
}
|
|
|
|
// TenderService interface defines tender business logic operations
|
|
type Service interface {
|
|
// Tender operations
|
|
GetByID(ctx context.Context, id string) (*TenderResponse, error)
|
|
Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
|
|
Delete(ctx context.Context, id string) error
|
|
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
|
Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
|
GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error)
|
|
|
|
// AI summarization operations
|
|
GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error)
|
|
TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error)
|
|
GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error)
|
|
|
|
// Maintenance operations
|
|
UpdateExpiredTenders(ctx context.Context) error
|
|
CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error
|
|
}
|
|
|
|
// tenderService implements TenderService interface
|
|
type tenderService struct {
|
|
repository TenderRepository
|
|
companyService company.Service
|
|
customerService customer.Service
|
|
logger logger.Logger
|
|
aiSummarizerClient AISummarizerClient
|
|
aiSummarizerStorage AISummarizerStorage
|
|
}
|
|
|
|
// NewService creates a new tender service
|
|
func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger, aiClient AISummarizerClient, aiStorage AISummarizerStorage) Service {
|
|
return &tenderService{
|
|
repository: repository,
|
|
companyService: companyService,
|
|
customerService: customerService,
|
|
logger: logger,
|
|
aiSummarizerClient: aiClient,
|
|
aiSummarizerStorage: aiStorage,
|
|
}
|
|
}
|
|
|
|
// GetTenderByID retrieves a tender by ID
|
|
func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse, error) {
|
|
s.logger.Info("Retrieving tender by ID", map[string]interface{}{
|
|
"tender_id": id,
|
|
})
|
|
|
|
tender, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
|
|
}
|
|
|
|
resp := tender.ToResponse()
|
|
|
|
// Enrich with AI summary from MinIO storage (best-effort, non-blocking)
|
|
s.enrichWithAISummary(ctx, resp)
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// enrichWithAISummary attempts to fetch the AI-generated overall summary from
|
|
// MinIO storage and attaches it to the response. Failures are logged but do not
|
|
// cause the request to fail — the field will simply be omitted.
|
|
func (s *tenderService) enrichWithAISummary(ctx context.Context, resp *TenderResponse) {
|
|
if s.aiSummarizerStorage == nil || resp.NoticePublicationID == "" {
|
|
return
|
|
}
|
|
|
|
summary, err := s.aiSummarizerStorage.GetSummaryFromStorage(ctx, resp.NoticePublicationID)
|
|
if err != nil {
|
|
s.logger.Debug("AI summary not available for tender", map[string]interface{}{
|
|
"tender_id": resp.ID,
|
|
"notice_id": resp.NoticePublicationID,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
resp.OverallSummary = summary
|
|
}
|
|
|
|
// UpdateTender updates an existing tender
|
|
func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) {
|
|
s.logger.Info("Updating tender", map[string]interface{}{
|
|
"tender_id": req.ID,
|
|
})
|
|
|
|
// Get existing tender
|
|
tender, err := s.repository.GetByID(ctx, req.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to find tender for update", map[string]interface{}{
|
|
"tender_id": req.ID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to find tender: %w", err)
|
|
}
|
|
|
|
// Update fields if provided
|
|
if req.Title != nil {
|
|
tender.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
tender.Description = *req.Description
|
|
}
|
|
if req.Status != nil {
|
|
tender.Status = *req.Status
|
|
}
|
|
if req.EstimatedValue != nil {
|
|
tender.EstimatedValue = *req.EstimatedValue
|
|
}
|
|
if req.Currency != nil {
|
|
tender.Currency = *req.Currency
|
|
}
|
|
if req.TenderDeadline != nil {
|
|
tender.TenderDeadline = *req.TenderDeadline
|
|
}
|
|
|
|
if err := s.repository.Update(ctx, tender); err != nil {
|
|
s.logger.Error("Failed to update tender", map[string]interface{}{
|
|
"tender_id": req.ID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to update tender: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Tender updated successfully", map[string]interface{}{
|
|
"tender_id": tender.ID,
|
|
})
|
|
|
|
return tender.ToResponse(), nil
|
|
}
|
|
|
|
// Delete deletes a tender
|
|
func (s *tenderService) Delete(ctx context.Context, id string) error {
|
|
s.logger.Info("Deleting tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
})
|
|
|
|
if err := s.repository.Delete(ctx, id); err != nil {
|
|
s.logger.Error("Failed to delete tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return fmt.Errorf("failed to delete tender: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Tender deleted successfully", map[string]interface{}{
|
|
"tender_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetDocumentSummaries retrieves document summaries for a specific tender
|
|
func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("tender ID cannot be empty")
|
|
}
|
|
|
|
// Get tender from repository
|
|
tender, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tender for document summaries", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to get tender: %w", err)
|
|
}
|
|
|
|
if tender == nil {
|
|
return nil, fmt.Errorf("tender not found")
|
|
}
|
|
|
|
// Return document summaries (empty slice if none exist)
|
|
if tender.DocumentSummaries == nil {
|
|
return []DocumentSummary{}, nil
|
|
}
|
|
|
|
s.logger.Info("Retrieved document summaries for tender", map[string]interface{}{
|
|
"tender_id": id,
|
|
"summaries_count": len(tender.DocumentSummaries),
|
|
})
|
|
|
|
return tender.DocumentSummaries, nil
|
|
}
|
|
|
|
// ListTenders retrieves tenders with pagination and filtering
|
|
func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
|
s.logger.Info("Listing tenders", map[string]interface{}{
|
|
"limit": pagination.Limit,
|
|
"offset": pagination.Offset,
|
|
"company_id": form.CompanyID,
|
|
"customer_id": form.CustomerID,
|
|
"from": form.DeadlineFrom,
|
|
"to": form.DeadlineTo,
|
|
})
|
|
|
|
// If customer ID is provided, get customer keywords and add to search
|
|
if form.CustomerID != nil && *form.CustomerID != "" {
|
|
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
|
|
if err == nil && customerResp != nil {
|
|
// Only use keywords if status is ready
|
|
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
|
|
// Combine customer keywords with existing search query
|
|
keywordsQuery := strings.Join(customerResp.Keywords, " ")
|
|
if form.Search != nil && *form.Search != "" {
|
|
combinedSearch := *form.Search + " " + keywordsQuery
|
|
form.Search = &combinedSearch
|
|
} else {
|
|
form.Search = &keywordsQuery
|
|
}
|
|
s.logger.Info("Using customer keywords for search", map[string]interface{}{
|
|
"customer_id": *form.CustomerID,
|
|
"keywords": customerResp.Keywords,
|
|
})
|
|
} else {
|
|
s.logger.Info("Customer keywords not ready, skipping keyword-based search", map[string]interface{}{
|
|
"customer_id": *form.CustomerID,
|
|
"keywords_status": customerResp.KeywordsStatus,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
tenders, total, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list tenders", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to list tenders: %w", err)
|
|
}
|
|
|
|
// If no company ID provided, return tenders without match percentage
|
|
if form.CompanyID == nil || *form.CompanyID == "" {
|
|
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
|
for _, tender := range tenders {
|
|
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
|
}
|
|
|
|
return &SearchResponse{
|
|
Tenders: tenderResponses,
|
|
Metadata: pagination.Response(total),
|
|
}, nil
|
|
}
|
|
|
|
// Get company CPV codes for match calculation
|
|
comp, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get company for match calculation", map[string]interface{}{
|
|
"company_id": *form.CompanyID,
|
|
"error": err.Error(),
|
|
})
|
|
// Continue without match percentage if company not found
|
|
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
|
for _, tender := range tenders {
|
|
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
|
}
|
|
|
|
return &SearchResponse{
|
|
Tenders: tenderResponses,
|
|
Metadata: pagination.Response(total),
|
|
},
|
|
nil
|
|
}
|
|
|
|
var companyCPVCodes []string
|
|
if comp.Tags != nil && comp.Tags.CPVCodes != nil {
|
|
companyCPVCodes = comp.Tags.CPVCodes
|
|
}
|
|
|
|
s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{
|
|
"company_id": *form.CompanyID,
|
|
"company_cpv_codes": len(companyCPVCodes),
|
|
"total_tenders": len(tenders),
|
|
})
|
|
|
|
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
|
for _, tender := range tenders {
|
|
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
|
}
|
|
|
|
return &SearchResponse{
|
|
Tenders: tenderResponses,
|
|
Metadata: pagination.Response(total),
|
|
},
|
|
nil
|
|
}
|
|
|
|
// Recommend retrieves tenders with pagination and filtering
|
|
func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
|
s.logger.Info("Recommend tenders", map[string]interface{}{
|
|
"company_id": form.CompanyID,
|
|
"customer_id": form.CustomerID,
|
|
})
|
|
|
|
form.Status = []string{string(TenderStatusActive)}
|
|
|
|
// Priority: Use customer keywords if available, otherwise use company CPV codes
|
|
if form.CustomerID != nil && *form.CustomerID != "" {
|
|
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
|
|
if err == nil && customerResp != nil {
|
|
// Only use keywords if status is ready
|
|
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
|
|
// Use customer keywords for recommendation
|
|
keywordsQuery := strings.Join(customerResp.Keywords, " ")
|
|
form.Search = &keywordsQuery
|
|
s.logger.Info("Using customer keywords for recommendation", map[string]interface{}{
|
|
"customer_id": *form.CustomerID,
|
|
"keywords": customerResp.Keywords,
|
|
})
|
|
} else {
|
|
s.logger.Info("Customer keywords not ready, falling back to company CPV codes", map[string]interface{}{
|
|
"customer_id": *form.CustomerID,
|
|
"keywords_status": customerResp.KeywordsStatus,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to company CPV codes if no customer keywords
|
|
if form.Search == nil || *form.Search == "" {
|
|
if form.CompanyID != nil && *form.CompanyID != "" {
|
|
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get company for CPV codes, skipping CPV-based filtering", map[string]interface{}{
|
|
"company_id": form.CompanyID,
|
|
"error": err.Error(),
|
|
})
|
|
// Continue without CPV-based filtering instead of failing
|
|
} else if company.Tags != nil && len(company.Tags.CPVCodes) > 0 {
|
|
form.Classifications = company.Tags.CPVCodes
|
|
}
|
|
}
|
|
}
|
|
|
|
tenders, total, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list tenders", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to list tenders: %w", err)
|
|
}
|
|
|
|
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
|
for _, tender := range tenders {
|
|
tenderResponses = append(tenderResponses, *tender.ToResponse())
|
|
}
|
|
|
|
return &SearchResponse{
|
|
Tenders: tenderResponses,
|
|
Metadata: pagination.Response(total),
|
|
}, nil
|
|
}
|
|
|
|
// Delete deletes a tender
|
|
|
|
// GetTenderStatistics retrieves tender statistics
|
|
func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) {
|
|
s.logger.Info("Retrieving tender statistics", map[string]interface{}{})
|
|
|
|
stats, err := s.repository.GetStatistics(ctx)
|
|
if err != nil {
|
|
s.logger.Error("Failed to retrieve tender statistics", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to retrieve statistics: %w", err)
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// UpdateExpiredTenders updates status of expired tenders
|
|
func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error {
|
|
s.logger.Info("Updating expired tenders", map[string]interface{}{})
|
|
|
|
// Get tenders that should be expired
|
|
expiredTenders, err := s.repository.GetExpiredTenders(ctx, time.Now().Unix())
|
|
if err != nil {
|
|
s.logger.Error("Failed to get expired tenders", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return fmt.Errorf("failed to get expired tenders: %w", err)
|
|
}
|
|
|
|
updatedCount := 0
|
|
for _, tender := range expiredTenders {
|
|
if tender.Status != TenderStatusExpired {
|
|
tender.UpdateStatus(TenderStatusExpired)
|
|
if err := s.repository.Update(ctx, &tender); err != nil {
|
|
s.logger.Error("Failed to update expired tender", map[string]interface{}{
|
|
"tender_id": tender.ID,
|
|
"error": err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
updatedCount++
|
|
}
|
|
}
|
|
|
|
s.logger.Info("Expired tenders updated", map[string]interface{}{
|
|
"total_expired": len(expiredTenders),
|
|
"updated": updatedCount,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupOldData removes old data based on retention policy
|
|
func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error {
|
|
s.logger.Info("Cleaning up old data", map[string]interface{}{
|
|
"older_than": olderThan.String(),
|
|
"limit": pagination.Limit,
|
|
"offset": pagination.Offset,
|
|
})
|
|
|
|
cutoffTime := time.Now().Add(-olderThan).Unix()
|
|
|
|
// For now, we'll just log what would be cleaned up
|
|
// In production, you might want to actually delete old data
|
|
form := &SearchForm{
|
|
Status: []string{string(TenderStatusExpired), string(TenderStatusCancelled), string(TenderStatusAwarded)},
|
|
}
|
|
|
|
oldTenders, _, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return fmt.Errorf("failed to find old tenders: %w", err)
|
|
}
|
|
|
|
cleanupCount := 0
|
|
for _, tender := range oldTenders {
|
|
if tender.CreatedAt < cutoffTime {
|
|
cleanupCount++
|
|
// Here you would delete the tender if implementing actual cleanup
|
|
// s.repository.Delete(ctx, tender.ID)
|
|
}
|
|
}
|
|
|
|
s.logger.Info("Old data cleanup completed", map[string]interface{}{
|
|
"found_old_tenders": cleanupCount,
|
|
"cutoff_time": cutoffTime,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAISummary retrieves the AI-generated summary for a tender.
|
|
// It first tries to fetch from MinIO storage (async pipeline result).
|
|
// If not found or not ready, it returns an appropriate error.
|
|
func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("tender ID cannot be empty")
|
|
}
|
|
|
|
// Get tender to find the notice_publication_id
|
|
t, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tender for AI summary", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to get tender: %w", err)
|
|
}
|
|
|
|
if t == nil {
|
|
return nil, fmt.Errorf("tender not found")
|
|
}
|
|
|
|
noticeID := t.NoticePublicationID
|
|
if noticeID == "" {
|
|
return nil, fmt.Errorf("tender has no notice publication ID")
|
|
}
|
|
|
|
// Try to get summary from MinIO storage (async pipeline)
|
|
if s.aiSummarizerStorage != nil {
|
|
summary, err := s.aiSummarizerStorage.GetSummaryFromStorage(ctx, noticeID)
|
|
if err == nil {
|
|
return &AISummaryResponse{
|
|
NoticeID: noticeID,
|
|
OverallSummary: summary,
|
|
Source: "storage",
|
|
}, nil
|
|
}
|
|
|
|
// Log but don't fail — the summary might just not be ready yet
|
|
s.logger.Info("AI summary not available from storage", map[string]interface{}{
|
|
"tender_id": id,
|
|
"notice_id": noticeID,
|
|
"error": err.Error(),
|
|
})
|
|
|
|
// Return specific error if summary not ready
|
|
if errors.Is(err, ai_summarizer.ErrSummaryNotReady) {
|
|
return nil, fmt.Errorf("AI summary is not ready yet, the pipeline is still processing")
|
|
}
|
|
if errors.Is(err, ai_summarizer.ErrObjectNotFound) {
|
|
return nil, fmt.Errorf("AI summary not found — this tender has not been processed by the AI pipeline yet")
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to get AI summary: %w", err)
|
|
}
|
|
|
|
return nil, fmt.Errorf("AI summarizer storage is not configured")
|
|
}
|
|
|
|
// TriggerAISummarize triggers on-demand AI summarization for a tender.
|
|
// It calls the external AI service HTTP API to generate summaries.
|
|
func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("tender ID cannot be empty")
|
|
}
|
|
|
|
if s.aiSummarizerClient == nil {
|
|
return nil, fmt.Errorf("AI summarizer service is not configured")
|
|
}
|
|
|
|
// Get tender to build the request
|
|
t, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tender for on-demand summarization", map[string]interface{}{
|
|
"tender_id": id,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to get tender: %w", err)
|
|
}
|
|
|
|
if t == nil {
|
|
return nil, fmt.Errorf("tender not found")
|
|
}
|
|
|
|
noticeID := t.NoticePublicationID
|
|
if noticeID == "" {
|
|
return nil, fmt.Errorf("tender has no notice publication ID")
|
|
}
|
|
|
|
// Build the AI summarizer request
|
|
reqBody := ai_summarizer.SummarizeRequest{
|
|
NoticeID: noticeID,
|
|
DocumentURL: t.DocumentURI,
|
|
Title: t.Title,
|
|
Description: t.Description,
|
|
Country: t.CountryCode,
|
|
SubmissionDeadline: time.UnixMilli(t.SubmissionDeadline).Format(time.RFC3339),
|
|
}
|
|
|
|
if t.EstimatedValue > 0 {
|
|
ev := t.EstimatedValue
|
|
reqBody.EstimatedValue = &ev
|
|
}
|
|
if t.Currency != "" {
|
|
reqBody.Currency = t.Currency
|
|
}
|
|
if t.BuyerOrganization != nil {
|
|
reqBody.AuthorityName = t.BuyerOrganization.Name
|
|
}
|
|
|
|
s.logger.Info("Triggering on-demand AI summarization", map[string]interface{}{
|
|
"tender_id": id,
|
|
"notice_id": noticeID,
|
|
})
|
|
|
|
// Call the AI service
|
|
resp, err := s.aiSummarizerClient.FetchSummaryOnDemand(ctx, reqBody)
|
|
if err != nil {
|
|
s.logger.Error("On-demand AI summarization failed", map[string]interface{}{
|
|
"tender_id": id,
|
|
"notice_id": noticeID,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("AI summarization request failed: %w", err)
|
|
}
|
|
|
|
// Map the response to our response type
|
|
summaries := make([]AIDocumentSummary, 0, len(resp.Summaries))
|
|
for _, s := range resp.Summaries {
|
|
summaries = append(summaries, AIDocumentSummary{
|
|
DocumentName: s.DocumentName,
|
|
DocumentType: s.DocumentType,
|
|
Summary: s.Summary,
|
|
SummaryModel: s.SummaryModel,
|
|
Error: s.Error,
|
|
})
|
|
}
|
|
|
|
s.logger.Info("On-demand AI summarization completed", map[string]interface{}{
|
|
"tender_id": id,
|
|
"notice_id": noticeID,
|
|
"summaries_count": len(summaries),
|
|
})
|
|
|
|
return &AISummarizeResponse{
|
|
NoticeID: resp.NoticeID,
|
|
Summaries: summaries,
|
|
OverallSummary: resp.OverallSummary,
|
|
}, nil
|
|
}
|
|
|
|
// GetAllAISummaries retrieves all AI-generated summaries from the MinIO storage.
|
|
// It lists all tender.json files in the bucket and returns summaries for tenders
|
|
// where the AI pipeline has completed processing.
|
|
func (s *tenderService) GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error) {
|
|
if s.aiSummarizerStorage == nil {
|
|
return nil, fmt.Errorf("AI summarizer storage is not configured")
|
|
}
|
|
|
|
s.logger.Info("Retrieving all AI summaries from storage", map[string]interface{}{})
|
|
|
|
// We need to access the underlying StorageClient to call GetAllSummaries.
|
|
// Since aiSummarizerStorage is an interface, we type-assert to access GetAllSummaries.
|
|
type allSummaries interface {
|
|
GetAllSummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error)
|
|
}
|
|
|
|
storage, ok := s.aiSummarizerStorage.(allSummaries)
|
|
if !ok {
|
|
return nil, fmt.Errorf("AI summarizer storage does not support batch retrieval")
|
|
}
|
|
|
|
results, err := storage.GetAllSummaries(ctx)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get all AI summaries", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, fmt.Errorf("failed to get all AI summaries: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Retrieved all AI summaries", map[string]interface{}{
|
|
"total_count": len(results),
|
|
})
|
|
|
|
return results, nil
|
|
}
|