Implement AI analysis trigger endpoint and refactor related components
- Added a new endpoint to trigger on-demand agentic analysis for tenders via the AI service. - Introduced `TriggerAIAnalyze` method in the `TenderHandler` to handle requests and responses for AI analysis. - Updated the `tenderService` to include `TriggerAIAnalyze` method, which validates input and interacts with the AI summarizer client. - Enhanced the `AIAnalyzeResponse` and `AIAnalyzeDocument` structures to support the new analysis feature. - Refactored the `DocumentSummarizationWorker` and `TranslationWorker` to remove deprecated MinIO dependencies, streamlining the AI service interactions. This update improves the functionality of the tender management system by allowing users to trigger AI analysis on-demand, enhancing the overall user experience and system capabilities.
This commit is contained in:
@@ -2,58 +2,20 @@ package ai_summarizer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SummarizeRequest represents the request payload sent to the AI service
|
||||
// POST /ai/summarize endpoint for on-demand summarization.
|
||||
// SummarizeRequest is the payload for POST /ai/summarize.
|
||||
type SummarizeRequest struct {
|
||||
ContractFolderID string `json:"contract_folder_id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
DocumentURL string `json:"document_url,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
EstimatedValue *float64 `json:"estimated_value,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
SubmissionDeadline string `json:"submission_deadline,omitempty"`
|
||||
AuthorityName string `json:"authority_name,omitempty"`
|
||||
}
|
||||
|
||||
// SummarizeRequestInput collects fields used to build SummarizeRequest.
|
||||
type SummarizeRequestInput struct {
|
||||
ContractFolderID string
|
||||
NoticePublicationID string
|
||||
DocumentURL string
|
||||
Title string
|
||||
Description string
|
||||
Country string
|
||||
Currency string
|
||||
SubmissionDeadline int64 // Unix seconds; omitted from request when zero
|
||||
EstimatedValue float64
|
||||
AuthorityName string
|
||||
ContractFolderID string `json:"contract_folder_id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
}
|
||||
|
||||
// NewSummarizeRequest builds a SummarizeRequest for POST /ai/summarize.
|
||||
func NewSummarizeRequest(in SummarizeRequestInput) SummarizeRequest {
|
||||
req := SummarizeRequest{
|
||||
ContractFolderID: strings.TrimSpace(in.ContractFolderID),
|
||||
NoticePublicationID: strings.TrimSpace(in.NoticePublicationID),
|
||||
DocumentURL: strings.TrimSpace(in.DocumentURL),
|
||||
Title: in.Title,
|
||||
Description: in.Description,
|
||||
Country: in.Country,
|
||||
Currency: in.Currency,
|
||||
AuthorityName: strings.TrimSpace(in.AuthorityName),
|
||||
func NewSummarizeRequest(contractFolderID, noticePublicationID string) SummarizeRequest {
|
||||
return SummarizeRequest{
|
||||
ContractFolderID: strings.TrimSpace(contractFolderID),
|
||||
NoticePublicationID: strings.TrimSpace(noticePublicationID),
|
||||
}
|
||||
if in.EstimatedValue > 0 {
|
||||
v := in.EstimatedValue
|
||||
req.EstimatedValue = &v
|
||||
}
|
||||
if in.SubmissionDeadline > 0 {
|
||||
req.SubmissionDeadline = time.Unix(in.SubmissionDeadline, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// SummarizeResponse represents the response from the AI summarization API.
|
||||
@@ -63,6 +25,8 @@ type SummarizeResponse struct {
|
||||
NoticeID string `json:"notice_id"` // legacy field name
|
||||
Summaries []DocumentSummary `json:"summaries"`
|
||||
OverallSummary *string `json:"overall_summary"`
|
||||
RollupS float64 `json:"rollup_s"`
|
||||
TotalS float64 `json:"total_s"`
|
||||
}
|
||||
|
||||
// ResolvedNoticePublicationID returns the notice publication ID from the response.
|
||||
@@ -82,7 +46,7 @@ type DocumentSummary struct {
|
||||
DocumentType string `json:"document_type"`
|
||||
Summary string `json:"summary"`
|
||||
SummaryModel string `json:"summary_model"`
|
||||
Error string `json:"error"` // empty string when no error
|
||||
Error string `json:"error"` // empty when no error
|
||||
}
|
||||
|
||||
// ProcedureDocumentsSummary reports scraped documents stored under one procedure folder in MinIO.
|
||||
@@ -101,13 +65,10 @@ type StoredDocument struct {
|
||||
DocumentType string `json:"document_type,omitempty"`
|
||||
}
|
||||
|
||||
// TranslateRequest represents the request payload sent to the AI service
|
||||
// POST /ai/translate endpoint.
|
||||
// TranslateRequest is the payload for POST /ai/translate.
|
||||
type TranslateRequest struct {
|
||||
ContractFolderID string `json:"contract_folder_id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Language string `json:"language"`
|
||||
RequestSource string `json:"-"` // internal: daily_job | manual_trigger
|
||||
}
|
||||
@@ -126,6 +87,41 @@ type TranslateResponse struct {
|
||||
TranslatedDescription string `json:"translated_description"`
|
||||
}
|
||||
|
||||
// AnalyzeRequest is the payload for POST /ai/analyze.
|
||||
type AnalyzeRequest struct {
|
||||
ContractFolderID string `json:"contract_folder_id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
}
|
||||
|
||||
// NewAnalyzeRequest builds an AnalyzeRequest for POST /ai/analyze.
|
||||
func NewAnalyzeRequest(contractFolderID, noticePublicationID string) AnalyzeRequest {
|
||||
return AnalyzeRequest{
|
||||
ContractFolderID: strings.TrimSpace(contractFolderID),
|
||||
NoticePublicationID: strings.TrimSpace(noticePublicationID),
|
||||
}
|
||||
}
|
||||
|
||||
// AnalyzeDocumentStats describes one document touched during agentic analysis.
|
||||
type AnalyzeDocumentStats struct {
|
||||
Filename string `json:"filename"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
TimesRead int `json:"times_read"`
|
||||
TimesQueried int `json:"times_queried"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// AnalyzeResponse is the payload from POST /ai/analyze.
|
||||
type AnalyzeResponse struct {
|
||||
ContractFolderID string `json:"contract_folder_id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
Summary string `json:"summary"`
|
||||
Documents []AnalyzeDocumentStats `json:"documents"`
|
||||
Scratchpad map[string]interface{} `json:"scratchpad"`
|
||||
ToolCallsLog []string `json:"tool_calls_log"`
|
||||
Iterations int `json:"iterations"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
// PipelineTranslateRequest represents the request payload for POST /pipeline/translate.
|
||||
type PipelineTranslateRequest struct {
|
||||
Languages []string `json:"languages"`
|
||||
@@ -137,6 +133,11 @@ type PipelineTranslateResponse struct {
|
||||
Languages []string `json:"languages"`
|
||||
}
|
||||
|
||||
// PipelineActionResponse is returned by fire-and-forget pipeline endpoints.
|
||||
type PipelineActionResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// StoredTranslation is one language entry inside tender.json translations map.
|
||||
type StoredTranslation struct {
|
||||
Title string `json:"title"`
|
||||
@@ -173,6 +174,7 @@ type TenderJSON struct {
|
||||
Summary string `json:"summary"`
|
||||
OverallSummary *string `json:"overall_summary"`
|
||||
OverallSummaryCamel *string `json:"overallSummary"`
|
||||
Summaries []DocumentSummary `json:"summaries"`
|
||||
SummarizeStatus SummarizeStatus `json:"summarize_status"`
|
||||
SummarizeStatusCamel SummarizeStatus `json:"summarizeStatus"`
|
||||
Translations map[string]StoredTranslation `json:"translations"`
|
||||
@@ -225,23 +227,31 @@ func (t *TenderJSON) translationsDone() []string {
|
||||
return t.TranslationStatusCamel.languagesDone()
|
||||
}
|
||||
|
||||
// translationForLanguage returns stored translation text when the pipeline marked
|
||||
// the language done and non-empty title exists.
|
||||
// storedTranslationText returns translation text from the translations map when
|
||||
// title is non-empty. It does not require translation_status.done — on-demand
|
||||
// writes may persist translations before updating status.
|
||||
func (t *TenderJSON) storedTranslationText(language string) (StoredTranslation, bool) {
|
||||
if t == nil || t.Translations == nil {
|
||||
return StoredTranslation{}, false
|
||||
}
|
||||
lang := strings.ToLower(strings.TrimSpace(language))
|
||||
for key, entry := range t.Translations {
|
||||
if strings.ToLower(strings.TrimSpace(key)) != lang {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(entry.Title) == "" {
|
||||
continue
|
||||
}
|
||||
return entry, true
|
||||
}
|
||||
return StoredTranslation{}, false
|
||||
}
|
||||
|
||||
// translationForLanguage returns stored translation when the pipeline marked the
|
||||
// language done and non-empty title exists.
|
||||
func (t *TenderJSON) translationForLanguage(language string) (StoredTranslation, bool) {
|
||||
if t == nil || !languageInDone(t.translationsDone(), language) {
|
||||
return StoredTranslation{}, false
|
||||
}
|
||||
lang := strings.ToLower(strings.TrimSpace(language))
|
||||
if t.Translations != nil {
|
||||
for key, entry := range t.Translations {
|
||||
if strings.ToLower(strings.TrimSpace(key)) != lang {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(entry.Title) == "" {
|
||||
continue
|
||||
}
|
||||
return entry, true
|
||||
}
|
||||
}
|
||||
return StoredTranslation{}, false
|
||||
return t.storedTranslationText(language)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user