Files
tm_back/internal/tender/form.go
T
Mazyar 2538747768
continuous-integration/drone/push Build is passing
Enhance tender response structure and update recommendation handler
- Added new fields `Rank` and `Analysis` to the `TenderResponse` struct for improved data representation.
- Updated the `RecommendTenders` handler to reflect changes in API documentation, including a new summary and description for AI-ranked tender recommendations.
- Improved error handling in the recommendation process, ensuring appropriate responses for missing company IDs and unavailable AI services.

This update enhances the tender response capabilities and improves the clarity of the recommendation API, aligning with the overall goal of providing better insights for users.
2026-06-21 12:29:37 +03:30

604 lines
26 KiB
Go

package tender
import (
"strings"
"time"
"tm/pkg/response"
)
// UpdateTenderRequest represents a request to update an existing tender
type UpdateTenderRequest struct {
ID string `json:"id" valid:"required~Tender ID is required"`
Title *string `json:"title,omitempty" valid:"stringlength(1|500)~Title must be between 1 and 500 characters"`
Description *string `json:"description,omitempty" valid:"stringlength(0|5000)~Description must not exceed 5000 characters"`
Status *TenderStatus `json:"status,omitempty"`
EstimatedValue *float64 `json:"estimated_value,omitempty" valid:"range(0|999999999999)~Estimated value must be between 0 and 999999999999"`
Currency *string `json:"currency,omitempty" valid:"stringlength(3|3)~Currency must be 3 characters"`
TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds
}
// SearchForm represents a request to list tenders with pagination and filtering
type SearchForm struct {
Search *string `query:"q" valid:"optional"`
Title *string `query:"title" valid:"optional"`
Description *string `query:"description" valid:"optional"`
NoticeType *string `query:"notice_type" valid:"optional"`
NoticeTypes []string `query:"notice_types" valid:"optional"`
FormTypes []string `query:"form_types" valid:"optional"`
ProcurementType *string `query:"procurement_type" valid:"optional"`
Country *string `query:"country" valid:"optional"`
CountryCode *string `query:"country_code" valid:"optional"`
CountryCodes []string `query:"country_codes" valid:"optional"`
RegionCodes []string `query:"region_codes" valid:"optional"`
CompanyID *string `query:"company_id" valid:"optional"`
CustomerID *string `query:"customer_id" valid:"optional"`
Status []string `query:"status" valid:"optional"`
MainClassification *string `query:"main_classification" valid:"optional"`
Classifications []string `query:"classifications" valid:"optional"`
CpvCodes []string `query:"cpv_codes" valid:"optional"`
MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"`
MaxEstimatedValue *float64 `query:"max_estimated_value" valid:"optional"`
Currency string `query:"currency" valid:"optional"`
CreatedAt *int64 `query:"created_at" valid:"optional"`
CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"`
CreatedAtTo *int64 `query:"created_at_to" valid:"optional"`
TenderDeadline *int64 `query:"tender_deadline" valid:"optional"`
TenderDeadlineFrom *int64 `query:"tender_deadline_from" valid:"optional"`
TenderDeadlineTo *int64 `query:"tender_deadline_to" valid:"optional"`
DeadlineFrom *int64 `query:"deadline_from" valid:"optional"`
DeadlineTo *int64 `query:"deadline_to" valid:"optional"`
PublicationDate *int64 `query:"publication_date" valid:"optional"`
PublicationDateFrom *int64 `query:"publication_date_from" valid:"optional"`
PublicationDateTo *int64 `query:"publication_date_to" valid:"optional"`
SubmissionDeadline *int64 `query:"submission_deadline" valid:"optional"`
SubmissionDateAliasFrom *int64 `query:"submission_deadline_from" valid:"optional"`
SubmissionDateAliasTo *int64 `query:"submission_deadline_to" valid:"optional"`
SubmissionDateFrom *int64 `query:"submission_date_from" valid:"optional"`
SubmissionDateTo *int64 `query:"submission_date_to" valid:"optional"`
Source []string `query:"source" valid:"optional"`
BuyerOrganizationID *string `query:"buyer_organization_id" valid:"optional"`
NoticePublicationID *string `query:"notice_publication_id" valid:"optional"`
Languages []string `query:"languages" valid:"optional"`
OnlyActiveDeadlines bool `query:"-" valid:"optional"`
Language *string `query:"lang" valid:"optional"`
DocumentsScraped bool `query:"documents_scraped" valid:"optional"`
// ContractFolderIDsWithDocuments is populated by the service from MinIO when DocumentsScraped is true.
ContractFolderIDsWithDocuments []string `query:"-" valid:"optional"`
}
// SearchResponse represents the response for listing tenders
type SearchResponse struct {
Tenders []TenderResponse `json:"tenders"`
Metadata *response.Meta `json:"-"`
}
// TenderScrapedDocumentsItem is a tender that has scraped documents stored in the database.
type TenderScrapedDocumentsItem struct {
TenderID string `json:"tender_id"`
NoticePublicationID string `json:"notice_publication_id"`
ContractFolderID string `json:"contract_folder_id"`
Title string `json:"title"`
DocumentsScrapedAt int64 `json:"documents_scraped_at,omitempty"` // Unix timestamp (seconds)
DocumentCount int `json:"document_count"`
}
// TendersWithScrapedDocumentsResponse is the paginated list of tenders with scraped documents.
type TendersWithScrapedDocumentsResponse struct {
Tenders []TenderScrapedDocumentsItem `json:"tenders"`
Metadata *response.Meta `json:"-"`
}
// ToTenderScrapedDocumentsItem maps a tender entity to a list item.
func (t *Tender) ToTenderScrapedDocumentsItem() TenderScrapedDocumentsItem {
if t == nil {
return TenderScrapedDocumentsItem{}
}
return TenderScrapedDocumentsItem{
TenderID: t.ID.Hex(),
NoticePublicationID: strings.TrimSpace(t.NoticePublicationID),
ContractFolderID: strings.TrimSpace(t.ContractFolderID),
Title: strings.TrimSpace(t.Title),
DocumentsScrapedAt: t.ProcessingMetadata.DocumentsScrapedAt,
DocumentCount: len(t.ScrapedDocuments),
}
}
// AddressResponse is postal / NUTS address data for API consumers.
type AddressResponse struct {
StreetName string `json:"street_name,omitempty"`
CityName string `json:"city_name,omitempty"`
PostalZone string `json:"postal_zone,omitempty"`
CountrySubentityCode string `json:"country_subentity_code,omitempty"`
Department string `json:"department,omitempty"`
Region string `json:"region,omitempty"`
CountryCode string `json:"country_code"`
}
// OrganizationResponse is an organization (buyer, winner, etc.) in tender API payloads.
type OrganizationResponse struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
CompanyID string `json:"company_id,omitempty"`
WebsiteURI string `json:"website_uri,omitempty"`
ContactName string `json:"contact_name,omitempty"`
ContactTelephone string `json:"contact_telephone,omitempty"`
ContactEmail string `json:"contact_email,omitempty"`
Address AddressResponse `json:"address,omitempty"`
}
// TenderLotResponse is one procurement lot for list/detail tender APIs.
type TenderLotResponse struct {
LotID string `json:"lot_id,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
MainNatureOfContract string `json:"main_nature_of_contract,omitempty"`
MainClassification string `json:"main_classification,omitempty"`
MainClassificationDescription string `json:"main_classification_description,omitempty"`
MainClassificationDisplay string `json:"main_classification_display,omitempty"`
AdditionalClassifications []string `json:"additional_classifications,omitempty"`
Duration string `json:"duration,omitempty"`
EstimatedValue float64 `json:"estimated_value,omitempty"`
Currency string `json:"currency,omitempty"`
}
// TenderWinnerResponse is a contract winner for awarded tenders.
type TenderWinnerResponse struct {
OfficialName string `json:"official_name"`
RegistrationNumber string `json:"registration_number,omitempty"`
PostalAddress string `json:"postal_address,omitempty"`
Town string `json:"town,omitempty"`
Postcode string `json:"postcode,omitempty"`
CountrySubdivisionNuts string `json:"country_subdivision_nuts,omitempty"`
Country string `json:"country,omitempty"`
ContactPoint string `json:"contact_point,omitempty"`
Email string `json:"email,omitempty"`
Telephone string `json:"telephone,omitempty"`
LotID string `json:"lot_id,omitempty"`
Amount float64 `json:"amount,omitempty"`
Currency string `json:"currency,omitempty"`
}
// TenderResponse represents a tender in API responses (search, list, and detail).
type TenderResponse struct {
ID string `json:"id"`
NoticePublicationID string `json:"notice_publication_id"`
RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"`
ContractFolderID string `json:"contract_folder_id,omitempty"`
ProcurementProjectID string `json:"procurement_project_id,omitempty"`
ContractNoticeID string `json:"contract_notice_id,omitempty"`
NoticeTypeCode string `json:"notice_type_code"`
FormType string `json:"form_type,omitempty"`
NoticeSubTypeCode string `json:"notice_sub_type_code,omitempty"`
NoticeIdentifier string `json:"notice_identifier,omitempty"`
NoticeVersion string `json:"notice_version,omitempty"`
NoticeLanguageCode string `json:"notice_language_code,omitempty"`
GazetteID string `json:"gazette_id,omitempty"`
NoticeDispatchAt int64 `json:"notice_dispatch_at,omitempty"`
NoticeDispatchDateWithTimezone string `json:"notice_dispatch_date_with_timezone,omitempty"`
ESenderDispatchAt int64 `json:"esender_dispatch_at,omitempty"`
IssueDate int64 `json:"issue_date,omitempty"`
IssueTime int64 `json:"issue_time,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
ProcurementTypeCode string `json:"procurement_type_code"`
ProcedureCode string `json:"procedure_code"`
MainClassification string `json:"main_classification"`
MainClassificationDescription string `json:"main_classification_description,omitempty"`
MainClassificationDisplay string `json:"main_classification_display,omitempty"`
AdditionalClassifications []string `json:"additional_classifications,omitempty"`
EstimatedValue float64 `json:"estimated_value"`
Currency string `json:"currency"`
EstimatedValueVATBasis string `json:"estimated_value_vat_basis,omitempty"`
Duration string `json:"duration"`
DurationUnit string `json:"duration_unit"`
PublicationDate int64 `json:"publication_date"`
TenderDeadline int64 `json:"tender_deadline"`
SubmissionDeadline int64 `json:"submission_deadline"`
ApplicationDeadline int64 `json:"application_deadline"`
SubmissionURL string `json:"submission_url"`
PlaceOfPerformance string `json:"place_of_performance,omitempty"`
CountryCode string `json:"country_code"`
RegionCode string `json:"region_code,omitempty"`
CityName string `json:"city_name,omitempty"`
PostalCode string `json:"postal_code,omitempty"`
DocumentURI string `json:"document_uri,omitempty"`
DocumentURL string `json:"document_url,omitempty"`
TenderURL string `json:"tender_url,omitempty"`
BuyerProfileURL string `json:"buyer_profile_url,omitempty"`
BuyerOrganization *OrganizationResponse `json:"buyer_organization,omitempty"`
WinningTenderer *OrganizationResponse `json:"winning_tenderer,omitempty"`
Lots []TenderLotResponse `json:"lots,omitempty"`
Winners []TenderWinnerResponse `json:"winners,omitempty"`
OfficialLanguages []string `json:"official_languages,omitempty"`
AwardDate int64 `json:"award_date,omitempty"`
AwardedValue float64 `json:"awarded_value,omitempty"`
ContractNumber string `json:"contract_number,omitempty"`
Status TenderStatus `json:"status"`
Source TenderSource `json:"source,omitempty"`
TenderID string `json:"tender_id"`
ProjectName string `json:"project_name,omitempty"`
CreatedAt int64 `json:"created_at"`
OverallSummary string `json:"overall_summary,omitempty"`
Language string `json:"language,omitempty"`
Rank string `json:"rank,omitempty"`
Analysis string `json:"analysis,omitempty"`
}
// TenderDocumentResponse represents a scraped tender document available for download.
type TenderDocumentResponse struct {
Filename string `json:"filename"`
Size int64 `json:"size"`
LastModified int64 `json:"last_modified"`
DocumentType string `json:"document_type,omitempty"`
}
// TenderDocumentsResponse represents scraped documents for a tender.
type TenderDocumentsResponse struct {
TenderID string `json:"tender_id"`
Documents []TenderDocumentResponse `json:"documents"`
}
const (
enrichmentMainCPVDescriptionKey = "main_classification_description"
vatBasisUnspecified = "unspecified"
)
// ToTenderResponse converts a Tender entity to TenderResponse
func (t *Tender) ToResponse() *TenderResponse {
return t.ToResponseWithLanguage("")
}
// ToResponseWithLanguage converts a Tender entity to TenderResponse using Mongo fields.
// Detail endpoints overlay MinIO translations in the service layer.
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
title := t.Title
description := t.Description
_ = language
mainDesc := firstNonEmpty(
strings.TrimSpace(t.ProcessingMetadata.EnrichmentData[enrichmentMainCPVDescriptionKey]),
strings.TrimSpace(t.MainClassificationDescription),
)
mainCode := strings.TrimSpace(t.MainClassification)
documentURL := strings.TrimSpace(t.DocumentURI)
if documentURL == "" {
documentURL = strings.TrimSpace(t.TenderURL)
}
estimatedValue, currency := t.ResolvedEstimatedValueAndCurrency()
response := &TenderResponse{
ID: t.ID.Hex(),
NoticePublicationID: t.NoticePublicationID,
RelatedNoticePublicationIDs: append([]string(nil), t.RelatedNoticePublicationIDs...),
ContractFolderID: strings.TrimSpace(t.ContractFolderID),
ProcurementProjectID: strings.TrimSpace(t.ProcurementProjectID),
ContractNoticeID: strings.TrimSpace(t.ContractNoticeID),
NoticeTypeCode: strings.TrimSpace(t.NoticeTypeCode),
FormType: strings.TrimSpace(t.FormType),
NoticeSubTypeCode: strings.TrimSpace(t.NoticeSubTypeCode),
NoticeIdentifier: strings.TrimSpace(t.NoticeIdentifier),
NoticeVersion: strings.TrimSpace(t.NoticeVersion),
NoticeLanguageCode: strings.TrimSpace(t.NoticeLanguageCode),
GazetteID: strings.TrimSpace(t.GazetteID),
NoticeDispatchAt: t.NoticeDispatchAt,
NoticeDispatchDateWithTimezone: formatNoticeDispatchRFC3339(t),
ESenderDispatchAt: t.ESenderDispatchAt,
IssueDate: t.IssueDate,
IssueTime: t.IssueTime,
Title: title,
Description: description,
ProcurementTypeCode: strings.TrimSpace(t.ProcurementTypeCode),
ProcedureCode: strings.TrimSpace(t.ProcedureCode),
MainClassification: mainCode,
MainClassificationDescription: mainDesc,
MainClassificationDisplay: classificationDisplay(mainCode, mainDesc),
AdditionalClassifications: append([]string(nil), t.AdditionalClassifications...),
EstimatedValue: estimatedValue,
Currency: currency,
EstimatedValueVATBasis: vatBasisUnspecified,
Duration: t.Duration,
DurationUnit: t.DurationUnit,
PublicationDate: t.PublicationDate,
TenderDeadline: t.TenderDeadline,
SubmissionDeadline: t.SubmissionDeadline,
ApplicationDeadline: t.ApplicationDeadline,
SubmissionURL: strings.TrimSpace(t.SubmissionURL),
PlaceOfPerformance: strings.TrimSpace(t.PlaceOfPerformance),
CountryCode: strings.TrimSpace(t.CountryCode),
RegionCode: strings.TrimSpace(t.RegionCode),
CityName: strings.TrimSpace(t.CityName),
PostalCode: strings.TrimSpace(t.PostalCode),
DocumentURI: strings.TrimSpace(t.DocumentURI),
DocumentURL: documentURL,
TenderURL: strings.TrimSpace(t.TenderURL),
BuyerProfileURL: strings.TrimSpace(t.BuyerProfileURL),
BuyerOrganization: organizationToResponse(t.BuyerOrganization),
WinningTenderer: organizationToResponse(t.WinningTenderer),
Lots: buildTenderLotsResponse(t, mainDesc),
Winners: buildTenderWinnersResponse(t),
OfficialLanguages: append([]string(nil), t.OfficialLanguages...),
AwardDate: t.AwardDate,
AwardedValue: t.AwardedValue,
ContractNumber: strings.TrimSpace(t.ContractNumber),
Status: t.Status,
Source: t.Source,
TenderID: t.TenderID,
ProjectName: strings.TrimSpace(t.ProjectName),
CreatedAt: t.CreatedAt,
OverallSummary: strings.TrimSpace(t.AIOverallSummary),
}
return response
}
func organizationToResponse(o *Organization) *OrganizationResponse {
if o == nil {
return nil
}
return &OrganizationResponse{
ID: strings.TrimSpace(o.ID),
Name: strings.TrimSpace(o.Name),
CompanyID: strings.TrimSpace(o.CompanyID),
WebsiteURI: strings.TrimSpace(o.WebsiteURI),
ContactName: strings.TrimSpace(o.ContactName),
ContactTelephone: strings.TrimSpace(o.ContactTelephone),
ContactEmail: strings.TrimSpace(o.ContactEmail),
Address: AddressResponse{
StreetName: strings.TrimSpace(o.Address.StreetName),
CityName: strings.TrimSpace(o.Address.CityName),
PostalZone: strings.TrimSpace(o.Address.PostalZone),
CountrySubentityCode: strings.TrimSpace(o.Address.CountrySubentityCode),
Department: strings.TrimSpace(o.Address.Department),
Region: strings.TrimSpace(o.Address.Region),
CountryCode: strings.TrimSpace(o.Address.CountryCode),
},
}
}
func formatNoticeDispatchRFC3339(t *Tender) string {
if t == nil {
return ""
}
sec := t.NoticeDispatchAt
if sec == 0 {
sec = t.ESenderDispatchAt
}
if sec == 0 {
return ""
}
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if s := strings.TrimSpace(v); s != "" {
return s
}
}
return ""
}
func classificationDisplay(code, description string) string {
code = strings.TrimSpace(code)
description = strings.TrimSpace(description)
if code != "" && description != "" {
return code + " - " + description
}
if code != "" {
return code
}
return description
}
func buildTenderLotsResponse(t *Tender, globalMainDesc string) []TenderLotResponse {
if t == nil || len(t.ProcurementLots) == 0 {
return nil
}
out := make([]TenderLotResponse, 0, len(t.ProcurementLots))
mainCode := strings.TrimSpace(t.MainClassification)
for i := range t.ProcurementLots {
lot := t.ProcurementLots[i]
lotCode := strings.TrimSpace(lot.MainClassification)
perLotDesc := strings.TrimSpace(lot.MainClassificationDescription)
if perLotDesc == "" && lotCode != "" && lotCode == mainCode {
perLotDesc = globalMainDesc
}
out = append(out, TenderLotResponse{
LotID: strings.TrimSpace(lot.LotID),
Title: strings.TrimSpace(lot.Title),
Description: strings.TrimSpace(lot.Description),
MainNatureOfContract: strings.TrimSpace(lot.MainNatureOfContract),
MainClassification: lotCode,
MainClassificationDescription: perLotDesc,
MainClassificationDisplay: classificationDisplay(lotCode, perLotDesc),
AdditionalClassifications: append([]string(nil), lot.AdditionalClassifications...),
Duration: strings.TrimSpace(lot.Duration),
EstimatedValue: lot.EstimatedValue,
Currency: strings.TrimSpace(lot.Currency),
})
}
return out
}
func buildTenderWinnersResponse(t *Tender) []TenderWinnerResponse {
if t == nil {
return nil
}
if len(t.AwardedEntities) > 0 {
out := make([]TenderWinnerResponse, 0, len(t.AwardedEntities))
for _, a := range t.AwardedEntities {
w := TenderWinnerResponse{
OfficialName: strings.TrimSpace(a.Name),
RegistrationNumber: strings.TrimSpace(a.CompanyID),
PostalAddress: strings.TrimSpace(a.Address),
Country: strings.TrimSpace(a.Country),
LotID: strings.TrimSpace(a.LotID),
Amount: a.Amount,
Currency: strings.TrimSpace(a.Currency),
Town: "",
Postcode: "",
CountrySubdivisionNuts: "",
ContactPoint: "",
Email: "",
Telephone: "",
}
if org := organizationByID(t, a.OrganizationID); org != nil {
mergeWinnerFromOrganization(&w, org)
}
out = append(out, w)
}
return out
}
if t.WinningTenderer == nil || strings.TrimSpace(t.WinningTenderer.Name) == "" {
return nil
}
w := t.WinningTenderer
addr := w.Address
return []TenderWinnerResponse{{
OfficialName: strings.TrimSpace(w.Name),
RegistrationNumber: strings.TrimSpace(w.CompanyID),
PostalAddress: strings.TrimSpace(addr.StreetName),
Town: strings.TrimSpace(addr.CityName),
Postcode: strings.TrimSpace(addr.PostalZone),
CountrySubdivisionNuts: strings.TrimSpace(addr.CountrySubentityCode),
Country: strings.TrimSpace(addr.CountryCode),
ContactPoint: strings.TrimSpace(w.ContactName),
Email: strings.TrimSpace(w.ContactEmail),
Telephone: strings.TrimSpace(w.ContactTelephone),
}}
}
func organizationByID(t *Tender, id string) *Organization {
id = strings.TrimSpace(id)
if id == "" || t == nil {
return nil
}
for i := range t.Organizations {
if strings.TrimSpace(t.Organizations[i].ID) == id {
return &t.Organizations[i]
}
}
return nil
}
func mergeWinnerFromOrganization(w *TenderWinnerResponse, org *Organization) {
if w == nil || org == nil {
return
}
if w.OfficialName == "" {
w.OfficialName = strings.TrimSpace(org.Name)
}
if w.RegistrationNumber == "" {
w.RegistrationNumber = strings.TrimSpace(org.CompanyID)
}
if w.PostalAddress == "" {
w.PostalAddress = strings.TrimSpace(org.Address.StreetName)
}
if w.Town == "" {
w.Town = strings.TrimSpace(org.Address.CityName)
}
if w.Postcode == "" {
w.Postcode = strings.TrimSpace(org.Address.PostalZone)
}
if w.CountrySubdivisionNuts == "" {
w.CountrySubdivisionNuts = strings.TrimSpace(org.Address.CountrySubentityCode)
}
if w.Country == "" {
w.Country = strings.TrimSpace(org.Address.CountryCode)
}
if w.ContactPoint == "" {
w.ContactPoint = strings.TrimSpace(org.ContactName)
}
if w.Email == "" {
w.Email = strings.TrimSpace(org.ContactEmail)
}
if w.Telephone == "" {
w.Telephone = strings.TrimSpace(org.ContactTelephone)
}
}
// AITranslateRequest represents a request for on-demand AI translation.
type AITranslateRequest struct {
Language string `json:"language" valid:"required,stringlength(2|5)~Language must be between 2 and 5 characters"`
}
// AITranslateResponse represents the response for AI translation trigger.
type AITranslateResponse 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"`
}
// AISummaryResponse represents the response for getting an AI-generated summary
type AISummaryResponse struct {
NoticeID string `json:"notice_id"`
OverallSummary string `json:"overall_summary"`
Source string `json:"source"` // storage | pending | unavailable
}
// AISummarizeResponse represents the response for triggering on-demand AI summarization
type AISummarizeResponse struct {
ContractFolderID string `json:"contract_folder_id"`
NoticePublicationID string `json:"notice_publication_id"`
Summaries []AIDocumentSummary `json:"summaries"`
OverallSummary *string `json:"overall_summary"`
}
// AIDocumentSummary represents a single document summary from the AI service
type AIDocumentSummary struct {
DocumentName string `json:"document_name"`
DocumentType string `json:"document_type"`
Summary string `json:"summary"`
SummaryModel string `json:"summary_model"`
Error string `json:"error,omitempty"`
}
// AIAnalyzeResponse represents the response for on-demand agentic analysis.
type AIAnalyzeResponse struct {
ContractFolderID string `json:"contract_folder_id"`
NoticePublicationID string `json:"notice_publication_id"`
Summary string `json:"summary"`
Documents []AIAnalyzeDocument `json:"documents"`
ToolCallsLog []string `json:"tool_calls_log"`
Iterations int `json:"iterations"`
Model string `json:"model"`
}
// AIAnalyzeDocument describes one document referenced during analysis.
type AIAnalyzeDocument struct {
Filename string `json:"filename"`
SizeBytes int64 `json:"size_bytes"`
TimesRead int `json:"times_read"`
TimesQueried int `json:"times_queried"`
Description string `json:"description"`
}