package ai_summarizer import ( "strings" ) // SummarizeRequest is the payload for POST /ai/summarize. type SummarizeRequest struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` } // NewSummarizeRequest builds a SummarizeRequest for POST /ai/summarize. func NewSummarizeRequest(contractFolderID, noticePublicationID string) SummarizeRequest { return SummarizeRequest{ ContractFolderID: strings.TrimSpace(contractFolderID), NoticePublicationID: strings.TrimSpace(noticePublicationID), } } // SummarizeResponse represents the response from the AI summarization API. type SummarizeResponse struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` 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. func (r *SummarizeResponse) ResolvedNoticePublicationID() string { if r == nil { return "" } if id := strings.TrimSpace(r.NoticePublicationID); id != "" { return id } return strings.TrimSpace(r.NoticeID) } // DocumentSummary represents a summary for a single document within the AI response. type DocumentSummary struct { DocumentName string `json:"document_name"` DocumentType string `json:"document_type"` Summary string `json:"summary"` SummaryModel string `json:"summary_model"` Error string `json:"error"` // empty when no error } // ProcedureDocumentsSummary reports scraped documents stored under one procedure folder in MinIO. type ProcedureDocumentsSummary struct { ContractFolderID string `json:"contract_folder_id"` DocumentCount int `json:"document_count"` LatestModified int64 `json:"latest_modified"` // Unix seconds } // StoredDocument represents a document object stored for a tender in MinIO. type StoredDocument struct { ObjectName string `json:"-"` DocumentName string `json:"document_name"` Size int64 `json:"size"` LastModified int64 `json:"last_modified"` DocumentType string `json:"document_type,omitempty"` } // TranslateRequest is the payload for POST /ai/translate. type TranslateRequest struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` Language string `json:"language"` RequestSource string `json:"-"` // internal: daily_job | manual_trigger } const ( TranslationRequestSourceDailyJob = "daily_job" TranslationRequestSourceManualTrigger = "manual_trigger" ) // TranslateResponse represents the translation payload from POST /ai/translate. type TranslateResponse struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` Language string `json:"language"` TranslatedTitle string `json:"translated_title"` 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"` } // PipelineTranslateResponse represents the response from POST /pipeline/translate. type PipelineTranslateResponse struct { Status string `json:"status"` Languages []string `json:"languages"` } // PipelineActionResponse is returned by fire-and-forget pipeline endpoints. type PipelineActionResponse struct { Status string `json:"status"` } // OnboardingRequest is the payload for POST /onboarding. type OnboardingRequest struct { CompanyID string `json:"company_id"` Documents string `json:"documents"` WebsiteURL string `json:"website_url"` } // OnboardingResponse is returned by POST /onboarding. type OnboardingResponse struct { Status string `json:"status"` } // RecommendRequest is the payload for POST /recommend. type RecommendRequest struct { CompanyID string `json:"company_id"` } // RecommendedTender is one ranked tender from POST /recommend. type RecommendedTender struct { Rank string `json:"rank"` TenderID string `json:"tender_id"` Analysis string `json:"analysis"` } // StoredTranslation is one language entry inside tender.json translations map. type StoredTranslation struct { Title string `json:"title"` Description string `json:"description"` } // TranslationStatus tracks which languages the async pipeline has finished. type TranslationStatus struct { Done []string `json:"done"` DoneUpper []string `json:"Done"` } func (s TranslationStatus) languagesDone() []string { if len(s.Done) > 0 { return s.Done } return s.DoneUpper } func languageInDone(done []string, language string) bool { lang := strings.ToLower(strings.TrimSpace(language)) for _, item := range done { if strings.ToLower(strings.TrimSpace(item)) == lang { return true } } return false } // TenderJSON represents the structure of the /tender.json file // stored in the AI service's MinIO bucket. type TenderJSON struct { // Summary is the current pipeline field for the tender-level summary text. 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"` TranslationStatus TranslationStatus `json:"translation_status"` TranslationStatusCamel TranslationStatus `json:"translationStatus"` } // summaryText returns the best-effort tender-level summary (may be empty). func (t *TenderJSON) summaryText() string { if t == nil { return "" } for _, candidate := range []*string{t.OverallSummary, t.OverallSummaryCamel} { if candidate == nil { continue } if text := strings.TrimSpace(*candidate); text != "" { return text } } return strings.TrimSpace(t.Summary) } // resolved returns whether summarization is marked done and the summary text // (may be empty when the pipeline finished without text yet). func (t *TenderJSON) resolved() (done bool, overall string) { if t == nil { return false, "" } done = t.SummarizeStatus.isDone() || t.SummarizeStatusCamel.isDone() return done, t.summaryText() } // SummarizeStatus represents the summarization status inside tender.json. type SummarizeStatus struct { Done bool `json:"done"` // true when the pipeline has finished DoneUpper bool `json:"Done"` // some writers emit PascalCase } func (s SummarizeStatus) isDone() bool { return s.Done || s.DoneUpper } // translationsDone returns languages marked complete by the translation pipeline. func (t *TenderJSON) translationsDone() []string { done := t.TranslationStatus.languagesDone() if len(done) > 0 { return done } return t.TranslationStatusCamel.languagesDone() } // 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 } return t.storedTranslationText(language) }