package main import ( "bytes" "context" "fmt" "log" "strings" "sync" "github.com/tmc/langchaingo/llms" ) // getSuggestedTags generates suggested tags for a document using the LLM func (app *App) getSuggestedTags(ctx context.Context, content string, suggestedTitle string, availableTags []string) ([]string, error) { likelyLanguage := getLikelyLanguage() templateMutex.RLock() defer templateMutex.RUnlock() var promptBuffer bytes.Buffer err := tagTemplate.Execute(&promptBuffer, map[string]interface{}{ "Language": likelyLanguage, "AvailableTags": availableTags, "Title": suggestedTitle, "Content": content, }) if err != nil { return nil, fmt.Errorf("error executing tag template: %v", err) } prompt := promptBuffer.String() log.Printf("Tag suggestion prompt: %s", prompt) completion, err := app.LLM.GenerateContent(ctx, []llms.MessageContent{ { Parts: []llms.ContentPart{ llms.TextContent{ Text: prompt, }, }, Role: llms.ChatMessageTypeHuman, }, }) if err != nil { return nil, fmt.Errorf("error getting response from LLM: %v", err) } response := strings.TrimSpace(completion.Choices[0].Content) suggestedTags := strings.Split(response, ",") for i, tag := range suggestedTags { suggestedTags[i] = strings.TrimSpace(tag) } // Filter out tags that are not in the available tags list filteredTags := []string{} for _, tag := range suggestedTags { for _, availableTag := range availableTags { if strings.EqualFold(tag, availableTag) { filteredTags = append(filteredTags, availableTag) break } } } return filteredTags, nil } func (app *App) doOCRViaLLM(ctx context.Context, jpegBytes []byte) (string, error) { // Convert the image to text completion, err := app.VisionLLM.GenerateContent(ctx, []llms.MessageContent{ { Parts: []llms.ContentPart{ llms.BinaryPart("image/jpeg", jpegBytes), llms.TextPart("Just transcribe the text in this image and preserve the formatting and layout (high quality OCR). Do that for ALL the text in the image. Be thorough and pay attention. This is very important. The image is from a text document so be sure to continue until the bottom of the page. Thanks a lot! You tend to forget about some text in the image so please focus! Use markdown format."), }, Role: llms.ChatMessageTypeHuman, }, }) if err != nil { return "", fmt.Errorf("error getting response from LLM: %v", err) } result := completion.Choices[0].Content fmt.Println(result) return result, nil } // getSuggestedTitle generates a suggested title for a document using the LLM func (app *App) getSuggestedTitle(ctx context.Context, content string) (string, error) { likelyLanguage := getLikelyLanguage() templateMutex.RLock() defer templateMutex.RUnlock() var promptBuffer bytes.Buffer err := titleTemplate.Execute(&promptBuffer, map[string]interface{}{ "Language": likelyLanguage, "Content": content, }) if err != nil { return "", fmt.Errorf("error executing title template: %v", err) } prompt := promptBuffer.String() log.Printf("Title suggestion prompt: %s", prompt) completion, err := app.LLM.GenerateContent(ctx, []llms.MessageContent{ { Parts: []llms.ContentPart{ llms.TextContent{ Text: prompt, }, }, Role: llms.ChatMessageTypeHuman, }, }) if err != nil { return "", fmt.Errorf("error getting response from LLM: %v", err) } return strings.TrimSpace(strings.Trim(completion.Choices[0].Content, "\"")), nil } // generateDocumentSuggestions generates suggestions for a set of documents func (app *App) generateDocumentSuggestions(ctx context.Context, suggestionRequest GenerateSuggestionsRequest) ([]DocumentSuggestion, error) { // Fetch all available tags from paperless-ngx availableTagsMap, err := app.Client.GetAllTags(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch available tags: %v", err) } // Prepare a list of tag names availableTagNames := make([]string, 0, len(availableTagsMap)) for tagName := range availableTagsMap { if tagName == manualTag { continue } availableTagNames = append(availableTagNames, tagName) } documents := suggestionRequest.Documents documentSuggestions := []DocumentSuggestion{} var wg sync.WaitGroup var mu sync.Mutex errorsList := make([]error, 0) for i := range documents { wg.Add(1) go func(doc Document) { defer wg.Done() documentID := doc.ID log.Printf("Processing Document ID %d...", documentID) content := doc.Content if len(content) > 5000 { content = content[:5000] } var suggestedTitle string var suggestedTags []string if suggestionRequest.GenerateTitles { suggestedTitle, err = app.getSuggestedTitle(ctx, content) if err != nil { mu.Lock() errorsList = append(errorsList, fmt.Errorf("Document %d: %v", documentID, err)) mu.Unlock() log.Printf("Error processing document %d: %v", documentID, err) return } } if suggestionRequest.GenerateTags { suggestedTags, err = app.getSuggestedTags(ctx, content, suggestedTitle, availableTagNames) if err != nil { mu.Lock() errorsList = append(errorsList, fmt.Errorf("Document %d: %v", documentID, err)) mu.Unlock() log.Printf("Error generating tags for document %d: %v", documentID, err) return } } mu.Lock() suggestion := DocumentSuggestion{ ID: documentID, OriginalDocument: doc, } // Titles if suggestionRequest.GenerateTitles { suggestion.SuggestedTitle = suggestedTitle } else { suggestion.SuggestedTitle = doc.Title } // Tags if suggestionRequest.GenerateTags { suggestion.SuggestedTags = suggestedTags } else { suggestion.SuggestedTags = removeTagFromList(doc.Tags, manualTag) } documentSuggestions = append(documentSuggestions, suggestion) mu.Unlock() log.Printf("Document %d processed successfully.", documentID) }(documents[i]) } wg.Wait() if len(errorsList) > 0 { return nil, errorsList[0] // Return the first error encountered } return documentSuggestions, nil }