Files
tm_back/pkg/ai_summarizer/entities.go
T
Mazyar 7d383c36c3 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.
2026-06-08 18:01:15 +03:30

258 lines
9.0 KiB
Go

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"`
}
// 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 <notice_id>/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)
}