Skip to content

Commit caaf6e0

Browse files
committed
Let's start working on integrating this with other llms
1 parent 64c1aa9 commit caaf6e0

13 files changed

Lines changed: 2253 additions & 0 deletions

File tree

cmd/add.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/spf13/cobra"
11+
"github.com/streed/ml-notes/internal/autotag"
1112
interrors "github.com/streed/ml-notes/internal/errors"
1213
"github.com/streed/ml-notes/internal/logger"
1314
"github.com/streed/ml-notes/internal/models"
@@ -35,13 +36,15 @@ var (
3536
useEditor bool
3637
editorName string
3738
tags []string
39+
autoTag bool
3840
)
3941

4042
func init() {
4143
rootCmd.AddCommand(addCmd)
4244
addCmd.Flags().StringVarP(&title, "title", "t", "", "Note title (required)")
4345
addCmd.Flags().StringVarP(&content, "content", "c", "", "Note content")
4446
addCmd.Flags().StringSliceVarP(&tags, "tags", "T", []string{}, "Tags for the note (comma-separated)")
47+
addCmd.Flags().BoolVar(&autoTag, "auto-tag", false, "Automatically generate tags using AI")
4548
addCmd.Flags().BoolVarP(&useEditor, "editor", "e", false, "Use editor for content input")
4649
addCmd.Flags().StringVar(&editorName, "editor-cmd", "", "Specify editor to use (overrides $EDITOR)")
4750
_ = addCmd.MarkFlagRequired("title")
@@ -114,6 +117,43 @@ func runAdd(cmd *cobra.Command, args []string) error {
114117
return fmt.Errorf("failed to create note: %w", err)
115118
}
116119

120+
// Auto-tag if requested
121+
if autoTag {
122+
fmt.Println("🤖 Generating AI tags...")
123+
autoTagger := autotag.NewAutoTagger(appConfig)
124+
125+
if autoTagger.IsAvailable() {
126+
suggestedTags, err := autoTagger.SuggestTags(note)
127+
if err != nil {
128+
fmt.Printf("⚠️ Auto-tagging failed: %v\n", err)
129+
} else if len(suggestedTags) > 0 {
130+
// Merge with existing tags
131+
allTags := note.Tags
132+
tagSet := make(map[string]bool)
133+
for _, tag := range allTags {
134+
tagSet[tag] = true
135+
}
136+
for _, tag := range suggestedTags {
137+
if !tagSet[tag] {
138+
allTags = append(allTags, tag)
139+
}
140+
}
141+
142+
// Update note with auto-generated tags
143+
if err := noteRepo.UpdateTags(note.ID, allTags); err != nil {
144+
fmt.Printf("⚠️ Failed to apply auto-tags: %v\n", err)
145+
} else {
146+
note.Tags = allTags // Update for display
147+
fmt.Printf("🏷️ Auto-generated tags: %s\n", strings.Join(suggestedTags, ", "))
148+
}
149+
} else {
150+
fmt.Println("🏷️ No auto-tags generated")
151+
}
152+
} else {
153+
fmt.Printf("⚠️ Auto-tagging unavailable. Please ensure summarization is enabled and Ollama is running.\n")
154+
}
155+
}
156+
117157
// Index the note for vector search
118158
fullText := title + " " + content
119159
if err := vectorSearch.IndexNote(note.ID, fullText); err != nil {

cmd/autotag.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/streed/ml-notes/internal/autotag"
10+
"github.com/streed/ml-notes/internal/models"
11+
)
12+
13+
var autoTagCmd = &cobra.Command{
14+
Use: "auto-tag",
15+
Short: "Automatically generate tags for notes using AI",
16+
Long: `Use AI to analyze note content and automatically suggest or apply tags.
17+
This feature uses your configured Ollama instance to intelligently analyze
18+
note content and generate relevant organizational tags.
19+
20+
Examples:
21+
ml-notes auto-tag 123 # Suggest tags for note 123
22+
ml-notes auto-tag 123 456 789 # Suggest tags for multiple notes
23+
ml-notes auto-tag --all # Suggest tags for all notes
24+
ml-notes auto-tag --apply 123 # Apply suggested tags automatically
25+
ml-notes auto-tag --all --apply # Auto-tag all notes (use with caution!)
26+
ml-notes auto-tag --recent 10 --apply # Auto-tag 10 most recent notes`,
27+
RunE: runAutoTag,
28+
}
29+
30+
var (
31+
autoTagApply bool
32+
autoTagAll bool
33+
autoTagRecent int
34+
autoTagOverwrite bool
35+
autoTagDryRun bool
36+
)
37+
38+
func init() {
39+
rootCmd.AddCommand(autoTagCmd)
40+
autoTagCmd.Flags().BoolVar(&autoTagApply, "apply", false, "Automatically apply suggested tags to notes")
41+
autoTagCmd.Flags().BoolVar(&autoTagAll, "all", false, "Process all notes in the database")
42+
autoTagCmd.Flags().IntVar(&autoTagRecent, "recent", 0, "Process N most recent notes")
43+
autoTagCmd.Flags().BoolVar(&autoTagOverwrite, "overwrite", false, "Overwrite existing tags (default: merge with existing)")
44+
autoTagCmd.Flags().BoolVar(&autoTagDryRun, "dry-run", false, "Show what would be tagged without applying changes")
45+
}
46+
47+
func runAutoTag(_ *cobra.Command, args []string) error {
48+
// Create auto-tagger service
49+
autoTagger := autotag.NewAutoTagger(appConfig)
50+
51+
// Check if auto-tagging is available
52+
if !autoTagger.IsAvailable() {
53+
return fmt.Errorf("auto-tagging is not available. Please ensure:\n" +
54+
"1. Summarization is enabled (ml-notes config set enable-summarization true)\n" +
55+
"2. Ollama is running and accessible\n" +
56+
"3. A summarization model is configured")
57+
}
58+
59+
// Determine which notes to process
60+
var notes []*models.Note
61+
var err error
62+
63+
if autoTagAll {
64+
fmt.Println("🔍 Loading all notes for auto-tagging...")
65+
notes, err = noteRepo.List(0, 0) // Get all notes
66+
if err != nil {
67+
return fmt.Errorf("failed to load notes: %w", err)
68+
}
69+
} else if autoTagRecent > 0 {
70+
fmt.Printf("🔍 Loading %d most recent notes for auto-tagging...\n", autoTagRecent)
71+
notes, err = noteRepo.List(autoTagRecent, 0)
72+
if err != nil {
73+
return fmt.Errorf("failed to load recent notes: %w", err)
74+
}
75+
} else if len(args) > 0 {
76+
// Process specific note IDs
77+
fmt.Printf("🔍 Loading %d specified notes for auto-tagging...\n", len(args))
78+
for _, arg := range args {
79+
id, err := strconv.Atoi(arg)
80+
if err != nil {
81+
return fmt.Errorf("invalid note ID '%s': must be a number", arg)
82+
}
83+
84+
note, err := noteRepo.GetByID(id)
85+
if err != nil {
86+
return fmt.Errorf("failed to get note %d: %w", id, err)
87+
}
88+
notes = append(notes, note)
89+
}
90+
} else {
91+
return fmt.Errorf("must specify note IDs, --all, or --recent N")
92+
}
93+
94+
if len(notes) == 0 {
95+
fmt.Println("No notes found to process.")
96+
return nil
97+
}
98+
99+
fmt.Printf("🤖 Processing %d notes with AI auto-tagging...\n", len(notes))
100+
101+
if autoTagDryRun {
102+
fmt.Println("🧪 DRY RUN MODE - No changes will be applied\n")
103+
} else if autoTagApply {
104+
fmt.Println("⚡ APPLY MODE - Tags will be automatically applied\n")
105+
} else {
106+
fmt.Println("💡 SUGGESTION MODE - Tags will be suggested but not applied\n")
107+
}
108+
109+
// Process notes for auto-tagging
110+
successCount := 0
111+
errorCount := 0
112+
113+
for i, note := range notes {
114+
fmt.Printf("📝 Processing note %d/%d (ID: %d): %s\n", i+1, len(notes), note.ID, truncateTitle(note.Title, 50))
115+
116+
// Get suggested tags
117+
suggestedTags, err := autoTagger.SuggestTags(note)
118+
if err != nil {
119+
fmt.Printf(" ❌ Error: %v\n", err)
120+
errorCount++
121+
continue
122+
}
123+
124+
if len(suggestedTags) == 0 {
125+
fmt.Printf(" ⚠️ No tags suggested\n")
126+
continue
127+
}
128+
129+
// Show suggestions
130+
fmt.Printf(" 🏷️ Suggested tags: %s\n", strings.Join(suggestedTags, ", "))
131+
132+
// Show existing tags if any
133+
if len(note.Tags) > 0 {
134+
fmt.Printf(" 🏷️ Existing tags: %s\n", strings.Join(note.Tags, ", "))
135+
}
136+
137+
// Determine final tags
138+
var finalTags []string
139+
if autoTagOverwrite || len(note.Tags) == 0 {
140+
finalTags = suggestedTags
141+
} else {
142+
// Merge with existing tags, avoiding duplicates
143+
tagSet := make(map[string]bool)
144+
for _, tag := range note.Tags {
145+
tagSet[tag] = true
146+
finalTags = append(finalTags, tag)
147+
}
148+
for _, tag := range suggestedTags {
149+
if !tagSet[tag] {
150+
finalTags = append(finalTags, tag)
151+
}
152+
}
153+
}
154+
155+
// Apply tags if requested and not in dry-run mode
156+
if autoTagApply && !autoTagDryRun {
157+
if err := noteRepo.UpdateTags(note.ID, finalTags); err != nil {
158+
fmt.Printf(" ❌ Failed to apply tags: %v\n", err)
159+
errorCount++
160+
continue
161+
}
162+
163+
fmt.Printf(" ✅ Applied tags: %s\n", strings.Join(finalTags, ", "))
164+
} else if autoTagDryRun {
165+
fmt.Printf(" 🧪 Would apply tags: %s\n", strings.Join(finalTags, ", "))
166+
}
167+
168+
successCount++
169+
fmt.Println() // Empty line for readability
170+
}
171+
172+
// Summary
173+
fmt.Println(strings.Repeat("=", 60))
174+
fmt.Printf("📊 Auto-tagging Summary:\n")
175+
fmt.Printf(" ✅ Successfully processed: %d notes\n", successCount)
176+
if errorCount > 0 {
177+
fmt.Printf(" ❌ Errors encountered: %d notes\n", errorCount)
178+
}
179+
180+
if autoTagDryRun {
181+
fmt.Printf("\n🧪 This was a dry run. To apply the changes, run the same command without --dry-run\n")
182+
} else if !autoTagApply {
183+
fmt.Printf("\n💡 Tags were suggested but not applied. To apply automatically, use --apply flag\n")
184+
}
185+
186+
return nil
187+
}
188+
189+
// truncateTitle helper function to truncate long titles for display
190+
func truncateTitle(title string, maxLen int) string {
191+
if len(title) <= maxLen {
192+
return title
193+
}
194+
return title[:maxLen-3] + "..."
195+
}

cmd/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ func runConfigShow(cmd *cobra.Command, args []string) error {
8484
} else {
8585
fmt.Printf("editor: Auto-detect\n")
8686
}
87+
fmt.Printf("enable-auto-tagging: %v\n", cfg.EnableAutoTagging)
88+
if cfg.AutoTagModel != "" {
89+
fmt.Printf("auto-tag-model: %s\n", cfg.AutoTagModel)
90+
} else {
91+
fmt.Printf("auto-tag-model: %s (using summarization model)\n", cfg.SummarizationModel)
92+
}
93+
fmt.Printf("max-auto-tags: %d\n", cfg.MaxAutoTags)
8794
fmt.Println("SQLite-vec: Built-in (via Go bindings)")
8895
if cfg.VectorConfigVersion != "" {
8996
fmt.Printf("Vector config hash: %s\n", cfg.VectorConfigVersion)
@@ -171,6 +178,24 @@ func runConfigSet(cmd *cobra.Command, args []string) error {
171178
cfg.EnableSummarization = enable
172179
case "editor":
173180
cfg.Editor = value
181+
case "enable-auto-tagging":
182+
var enable bool
183+
if value == constants.BoolTrue || value == constants.BoolOne || value == constants.BoolYes {
184+
enable = true
185+
} else if value == constants.BoolFalse || value == constants.BoolZero || value == constants.BoolNo {
186+
enable = false
187+
} else {
188+
return fmt.Errorf("%w: %s", interrors.ErrInvalidBoolean, value)
189+
}
190+
cfg.EnableAutoTagging = enable
191+
case "auto-tag-model":
192+
cfg.AutoTagModel = value
193+
case "max-auto-tags":
194+
var maxTags int
195+
if _, err := fmt.Sscanf(value, "%d", &maxTags); err != nil || maxTags < 1 || maxTags > 20 {
196+
return fmt.Errorf("invalid max-auto-tags value: must be between 1 and 20")
197+
}
198+
cfg.MaxAutoTags = maxTags
174199
default:
175200
return fmt.Errorf("%w: %s", interrors.ErrUnknownConfigKey, key)
176201
}

0 commit comments

Comments
 (0)