From 1756f3462bbb6a6b064a64be0e2ac192c684ca61 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Thu, 29 May 2025 19:47:39 +0200 Subject: [PATCH] feat: Enhance AI response generation and improve email content extraction with detailed logging --- internal/ai/ai.go | 11 +++++-- internal/imap/imap.go | 72 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/internal/ai/ai.go b/internal/ai/ai.go index 6893aba..550927b 100644 --- a/internal/ai/ai.go +++ b/internal/ai/ai.go @@ -89,7 +89,8 @@ func (a *AI) GenerateReply(emailContent string, contextContent map[string]string // Prepare the system message with language-specific instruction systemMsg := fmt.Sprintf(`You are a helpful assistant who responds to emails. -Your primary goal is to answer the user's query based on the provided email and context. +Your primary goal is to answer the user's query (found in the 'Email Body') by primarily using the information available in the 'Additional Context' and your general knowledge. +While the 'Email Body' provides the question, your answer should be synthesized from the context and your understanding, not by directly repeating or solely relying on unverified information from the 'Email Body' itself. Instructions: - Language: Your response must be entirely in %s, regardless of the language used in the email content or context. @@ -108,7 +109,13 @@ Instructions: {Role: "user", Content: userMsg}, } - return a.makeAPIRequest(messages) + aiResponse, err := a.makeAPIRequest(messages) + if err != nil { + // Error already logged by makeAPIRequest, just propagate + return "", err + } + logger.WithField("rawAIResponse", aiResponse).Debug("Received raw response from AI") + return aiResponse, nil } func (a *AI) makeAPIRequest(messages []Message) (string, error) { diff --git a/internal/imap/imap.go b/internal/imap/imap.go index fa8be0e..929e270 100644 --- a/internal/imap/imap.go +++ b/internal/imap/imap.go @@ -6,6 +6,7 @@ import ( "io" "mime" "mime/multipart" + "mime/quotedprintable" "net/mail" "paraclub-ai-mailer/config" "paraclub-ai-mailer/internal/logger" @@ -73,6 +74,7 @@ type Email struct { ID string Subject string From string + Date time.Time Body string } @@ -194,6 +196,7 @@ func (ic *IMAPClient) FetchUnprocessedEmails() ([]Email, error) { ID: msg.Envelope.MessageId, Subject: msg.Envelope.Subject, From: from, + Date: msg.Envelope.Date, Body: bodyBuilder.String(), }) } @@ -259,7 +262,7 @@ func (ic *IMAPClient) SaveDraft(email Email, response string) (err error) { response, email.From, email.Subject, - time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700"), + email.Date.Format("Mon, 02 Jan 2006 15:04:05 -0700"), extractMessageContent(email.Body)) literal := &MessageLiteral{ @@ -285,6 +288,7 @@ func (ic *IMAPClient) SaveDraft(email Email, response string) (err error) { // by removing email headers and MIME boundaries func extractMessageContent(body string) string { logger.WithField("bodyLength", len(body)).Debug("Starting message content extraction") + logger.WithField("rawInputBody", body).Debug("extractMessageContent: Raw input body") msg, err := mail.ReadMessage(strings.NewReader(body)) if err != nil { logger.WithFields(logrus.Fields{ @@ -296,6 +300,7 @@ func extractMessageContent(body string) string { contentTypeHeader := msg.Header.Get("Content-Type") logger.WithField("contentTypeHeader", contentTypeHeader).Debug("Got Content-Type header") + logger.WithField("parsedContentTypeHeader", contentTypeHeader).Debug("extractMessageContent: Parsed Content-Type header") mediaType, params, err := mime.ParseMediaType(contentTypeHeader) if err != nil { @@ -310,27 +315,50 @@ func extractMessageContent(body string) string { "mediaType": mediaType, "params": params, }).Debug("Parsed message Content-Type") + logger.WithFields(logrus.Fields{ + "mediaType": mediaType, + "params": params, + }).Debug("extractMessageContent: Parsed mediaType and params") var content string if strings.HasPrefix(mediaType, "multipart/") { + // For multipart, the handling of Content-Transfer-Encoding will be done within handleMultipartMessage for each part + logger.Debug("extractMessageContent: Handling as multipart message") content = handleMultipartMessage(msg.Body, params["boundary"]) + logger.WithField("contentFromMultipart", content).Debug("extractMessageContent: Content after handleMultipartMessage") } else { - content = handleSinglePartMessage(msg.Body) + // For single part, handle Content-Transfer-Encoding here + var partReader io.Reader = msg.Body + transferEncoding := strings.ToLower(msg.Header.Get("Content-Transfer-Encoding")) + if transferEncoding == "quoted-printable" { + partReader = quotedprintable.NewReader(msg.Body) + } + // Add handling for "base64" if needed in the future + // else if transferEncoding == "base64" { + // partReader = base64.NewDecoder(base64.StdEncoding, msg.Body) + // } + logger.Debug("extractMessageContent: Handling as single part message") + content = handleSinglePartMessage(partReader) + logger.WithField("contentFromSinglePart", content).Debug("extractMessageContent: Content after handleSinglePartMessage") } if content == "" { logger.Debug("No content extracted, falling back to simple extraction") + logger.Debug("extractMessageContent: No content from primary extraction, falling back.") return fallbackExtractContent(body) } // Clean up the content + logger.WithField("contentBeforeClean", content).Debug("extractMessageContent: Content before cleanMessageContent") content = cleanMessageContent(content) + logger.WithField("contentAfterClean", content).Debug("extractMessageContent: Content after cleanMessageContent") logger.WithField("contentLength", len(content)).Debug("Successfully extracted and cleaned message content") return content } func handleMultipartMessage(reader io.Reader, boundary string) string { + logger.WithField("boundary", boundary).Debug("handleMultipartMessage: Starting with boundary") if boundary == "" { logger.Debug("No boundary found in multipart message") return "" @@ -351,30 +379,55 @@ func handleMultipartMessage(reader io.Reader, boundary string) string { } contentType := part.Header.Get("Content-Type") + contentTransferEncoding := strings.ToLower(part.Header.Get("Content-Transfer-Encoding")) + logger.WithFields(logrus.Fields{ + "partIndex": partIndex, + "partContentType": contentType, + "partTransferEncoding": contentTransferEncoding, + "partHeaders": part.Header, + }).Debug("handleMultipartMessage: Processing part") + if strings.HasPrefix(contentType, "text/plain") { + var partReader io.Reader = part + if contentTransferEncoding == "quoted-printable" { + partReader = quotedprintable.NewReader(part) + } + // Add handling for "base64" if needed in the future + // else if contentTransferEncoding == "base64" { + // partReader = base64.NewDecoder(base64.StdEncoding, part) + // } + buf := new(bytes.Buffer) - if _, err := buf.ReadFrom(part); err != nil { - continue + if _, err := buf.ReadFrom(partReader); err != nil { + logger.WithError(err).WithField("partIndex", partIndex).Debug("Failed to read from partReader in multipart") + continue // Or handle error more robustly } textContent = buf.String() + // Assuming we only care about the first text/plain part found + // If multiple text/plain parts could exist and need concatenation, this logic would need adjustment. break } partIndex++ } + logger.WithField("textContentResult", textContent).Debug("handleMultipartMessage: Returning textContent") return textContent } func handleSinglePartMessage(reader io.Reader) string { + logger.Debug("handleSinglePartMessage: Starting") buf := new(bytes.Buffer) if _, err := buf.ReadFrom(reader); err != nil { logger.WithError(err).Debug("Failed to read message body") return "" } - return buf.String() + content := buf.String() + logger.WithField("readContent", content).Debug("handleSinglePartMessage: Content read from reader") + return content } func cleanMessageContent(content string) string { + logger.WithField("inputContentLength", len(content)).Debug("cleanMessageContent: Starting") // Remove any remaining email headers that might be in the body lines := strings.Split(content, "\n") var cleanLines []string @@ -401,21 +454,26 @@ func cleanMessageContent(content string) string { } content = strings.Join(cleanLines, "\n") + logger.WithField("contentAfterHeaderStripLength", len(content)).Debug("cleanMessageContent: Content after header stripping") // Convert newlines to HTML breaks for display content = strings.ReplaceAll(content, "\r\n", "
\n") content = strings.ReplaceAll(content, "\n", "
\n") + logger.WithField("contentAfterNewlineConversionLength", len(content)).Debug("cleanMessageContent: Content after newline conversion") // Remove any remaining email signature markers content = strings.Split(content, "\n-- ")[0] content = strings.Split(content, "
-- ")[0] - return strings.TrimSpace(content) + finalContent := strings.TrimSpace(content) + logger.WithField("finalCleanedContentLength", len(finalContent)).Debug("cleanMessageContent: Returning final cleaned content") + return finalContent } // fallbackExtractContent is the previous implementation used as fallback func fallbackExtractContent(body string) string { logger.WithField("bodyLength", len(body)).Debug("Using fallback content extraction method") + logger.WithField("rawInputBodyFallbackLength", len(body)).Debug("fallbackExtractContent: Raw input body") parts := strings.Split(body, "\r\n\r\n") if len(parts) > 1 { content := strings.Join(parts[1:], "\r\n\r\n") @@ -425,6 +483,7 @@ func fallbackExtractContent(body string) string { "contentLength": len(content), "partsCount": len(parts), }).Debug("Successfully extracted content using fallback method") + logger.WithField("extractedContentFallbackLength", len(content)).Debug("fallbackExtractContent: Content from splitting parts") return content } content := body @@ -434,6 +493,7 @@ func fallbackExtractContent(body string) string { "contentLength": len(content), "fullBody": true, }).Debug("Using full body as content in fallback method") + logger.WithField("finalContentFallbackLength", len(content)).Debug("fallbackExtractContent: Final content from full body") return content }