Integrated AI summary

This commit is contained in:
Mazyar
2026-05-05 17:47:31 +03:30
parent ddbf1ca3d5
commit 65a19d1514
12 changed files with 989 additions and 11 deletions
+125
View File
@@ -0,0 +1,125 @@
package ai_summarizer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"tm/pkg/logger"
)
// Client is the HTTP client for the AI summarization on-demand API.
type Client struct {
config *Config
httpClient *http.Client
logger logger.Logger
}
// NewClient creates a new AI summarizer HTTP client.
func NewClient(config *Config, log logger.Logger) (*Client, error) {
if config == nil {
config = DefaultConfig()
}
if err := config.ValidateAPI(); err != nil {
return nil, fmt.Errorf("ai_summarizer: invalid config: %w", err)
}
return &Client{
config: config,
httpClient: &http.Client{
Timeout: config.APITimeout,
},
logger: log,
}, nil
}
// FetchSummaryOnDemand calls the external AI service POST /ai/summarize endpoint
// to generate a summary on demand. It retries on transient failures up to the
// configured number of attempts.
func (c *Client) FetchSummaryOnDemand(ctx context.Context, reqBody SummarizeRequest) (*SummarizeResponse, error) {
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("ai_summarizer: failed to marshal request body: %w", err)
}
url := c.config.APIBaseURL + "/ai/summarize"
var lastErr error
for attempt := 0; attempt <= c.config.APIRetryCount; attempt++ {
if attempt > 0 {
c.logger.Warn("Retrying AI summarize request", map[string]interface{}{
"notice_id": reqBody.NoticeID,
"attempt": attempt,
})
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(c.config.APIRetryDelay):
}
}
resp, err := c.doPost(ctx, url, jsonBody)
if err != nil {
lastErr = err
c.logger.Error("AI summarize request failed", map[string]interface{}{
"notice_id": reqBody.NoticeID,
"attempt": attempt,
"error": err.Error(),
})
continue
}
return resp, nil
}
return nil, fmt.Errorf("ai_summarizer: all %d attempts failed, last error: %w", c.config.APIRetryCount+1, lastErr)
}
// doPost performs a single POST request and parses the response.
func (c *Client) doPost(ctx context.Context, url string, jsonBody []byte) (*SummarizeResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("ai_summarizer: failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
c.logger.Debug("Sending AI summarize request", map[string]interface{}{
"url": url,
})
httpResp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("ai_summarizer: request error: %w", err)
}
defer httpResp.Body.Close()
bodyBytes, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("ai_summarizer: failed to read response body: %w", err)
}
if httpResp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("%w: status 404, body: %s", ErrObjectNotFound, string(bodyBytes))
}
if httpResp.StatusCode >= 400 {
return nil, fmt.Errorf("%w: status %d, body: %s", ErrAPINonSuccess, httpResp.StatusCode, string(bodyBytes))
}
var result SummarizeResponse
if err := json.Unmarshal(bodyBytes, &result); err != nil {
return nil, fmt.Errorf("ai_summarizer: failed to decode response JSON: %w", err)
}
c.logger.Info("AI summarize request succeeded", map[string]interface{}{
"notice_id": result.NoticeID,
"summaries_count": len(result.Summaries),
})
return &result, nil
}