diff --git a/internal/llm/llm.go b/internal/llm/llm.go index 7aa7bc7..031f88a 100644 --- a/internal/llm/llm.go +++ b/internal/llm/llm.go @@ -3,11 +3,17 @@ package llm import ( "context" "errors" + "log" + "strings" + "time" "google.golang.org/genai" ) -const GeminiModel = "gemini-1.5-flash" +const ( + GeminiModel = "gemini-1.5-flash" + maxRetries = 3 +) type modelClient interface { GenerateContent(ctx context.Context, model string, parts []*genai.Content, config *genai.GenerateContentConfig) (*genai.GenerateContentResponse, error) @@ -33,15 +39,41 @@ func NewGeminiService(apiKey string) (*GeminiService, error) { return &GeminiService{client: client.Models, modelName: GeminiModel}, nil } +func (s *GeminiService) generateContentWithRetry(ctx context.Context, prompt string, schema *genai.Schema) (*genai.GenerateContentResponse, error) { + var resp *genai.GenerateContentResponse + var err error + + for i := 0; i < maxRetries; i++ { + resp, err = s.client.GenerateContent(ctx, s.modelName, genai.Text(prompt), &genai.GenerateContentConfig{ + ResponseMIMEType: "application/json", + ResponseSchema: schema, + }) + if err == nil { + return resp, nil + } + + var apiError *genai.APIError + if errors.As(err, &apiError) { + log.Printf("LLM API error: Code=%d, Status=%s, Message=%s", apiError.Code, apiError.Status, apiError.Message) + if apiError.Code == 503 && strings.Contains(apiError.Message, "overloaded") { + log.Printf("Retrying LLM call (%d/%d)...", i+1, maxRetries-1) + time.Sleep(time.Second * time.Duration(i+1)) // Wait 1, 2 seconds + continue + } + } + + return nil, err + } + + return nil, err +} + func (s *GeminiService) GenerateContent(ctx context.Context, prompt string, schema *genai.Schema) (string, error) { if s.client == nil { return "", errors.New("LLM service is disabled: no API key provided") } - resp, err := s.client.GenerateContent(ctx, s.modelName, genai.Text(prompt), &genai.GenerateContentConfig{ - ResponseMIMEType: "application/json", - ResponseSchema: schema, - }) + resp, err := s.generateContentWithRetry(ctx, prompt, schema) if err != nil { return "", err } diff --git a/internal/templates/entries.gohtml b/internal/templates/entries.gohtml index 62e5395..266ff33 100644 --- a/internal/templates/entries.gohtml +++ b/internal/templates/entries.gohtml @@ -29,7 +29,7 @@

Summary

-

{{.Summary}}

+

{{textToHTML .Summary}}

diff --git a/internal/templates/funcs.go b/internal/templates/funcs.go index 476fa71..f461bd5 100644 --- a/internal/templates/funcs.go +++ b/internal/templates/funcs.go @@ -13,7 +13,9 @@ var staticFS embed.FS func FuncMap() template.FuncMap { return template.FuncMap{ - "include": include, + "include": include, + "textToHTML": textToHTML, + "htmlEscape": htmlEscape, } } @@ -32,3 +34,11 @@ func include(filename string) (any, error) { return template.HTML(b), nil } } + +func textToHTML(s string) template.HTML { + return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(s), "\n", "
")) +} + +func htmlEscape(s string) template.HTML { + return template.HTML(s) +} \ No newline at end of file diff --git a/internal/templates/templates.go b/internal/templates/templates.go index 4caeddf..5ca0762 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -27,11 +27,7 @@ func init() { archiveTemplateName := "entries.gohtml" emailTemplateName := "email.gotxt" - ArchiveTemplate, err = htmlTemplate.New(archiveTemplateName).Funcs(FuncMap()).Funcs(htmlTemplate.FuncMap{ - "htmlEscape": func(s string) htmlTemplate.HTML { - return htmlTemplate.HTML(s) - }, - }).ParseFS(embedFS, archiveTemplateName) + ArchiveTemplate, err = htmlTemplate.New(archiveTemplateName).Funcs(FuncMap()).ParseFS(embedFS, archiveTemplateName) if err != nil { log.Fatalf("Error parsing archive template: %v", err)