X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/0e57dcfc96cd95268d5dd00af7ce0594721d3ded..8707a09fee94c97e29d668d24fb9713373913e0b:/whatsapp_import.go?ds=inline diff --git a/whatsapp_import.go b/whatsapp_import.go index df871c1..f293ebd 100644 --- a/whatsapp_import.go +++ b/whatsapp_import.go @@ -34,6 +34,88 @@ var ( whatsappImportUnknown = regexp.MustCompile(`^\[WhatsApp\] (.*), [A-Za-z]+ (\d{4}-\d{2}-\d{2})$`) ) +// WhatsAppImportPlan holds the new chat-day batches and the size of the merged +// log. Its estimate uses the measured cost of 1.3 calls per message, not a +// promise about a particular export or compaction retry. +type WhatsAppImportPlan struct { + agent string + cutoffs map[string]time.Time + added []DatedMessage + chats []string + cutoff time.Time + first time.Time + last time.Time + bytes int + messages int + skipped int + ownerKnown bool +} + +// WhatsAppImportOptions selects the target, exports, and plan-only mode. +type WhatsAppImportOptions struct { + planOnly bool + agent string + paths []string +} + +// Write prints the plan without chat text or the owner's profile name. +func (p WhatsAppImportPlan) Write(out io.Writer) error { + cutoff := "none (no live WhatsApp batch)" + if !p.cutoff.IsZero() { + cutoff = p.cutoff.Format(time.RFC3339) + } + + dateRange := "none" + if !p.first.IsZero() { + dateRange = p.first.Format(time.RFC3339) + " to " + p.last.Format(time.RFC3339) + } + + owner := "known (mapped to You)" + if !p.ownerKnown { + owner = "unknown (sender names kept as written)" + } + + _, err := fmt.Fprintf(out, "agent: %s\nchats: %d\n", p.agent, len(p.chats)) + if err != nil { + return fmt.Errorf("write import plan: %w", err) + } + + for _, chat := range p.chats { + line := " " + chat + cutoff, found := p.cutoffs[chat] + if found { + line += " (cutoff: " + cutoff.Format(time.RFC3339) + ")" + } + + _, err = fmt.Fprintln(out, line) + if err != nil { + return fmt.Errorf("write import plan: %w", err) + } + } + + estimate := 1.3 * float64(p.messages) + if len(p.added) == 0 { + estimate = 0 + } + + lines := []string{ + fmt.Sprintf("batches: %d (%d chat-days skipped)", len(p.added), p.skipped), + "dates: " + dateRange, + "cutoff: " + cutoff, + "owner: " + owner, + fmt.Sprintf("bytes: %d (new batch text)", p.bytes), + fmt.Sprintf("messages: %d (merged log)", p.messages), + fmt.Sprintf("estimated calls: %.0f (about 1.3 per message, limit %d)", estimate, rebuildCallLimit), + } + + _, err = io.WriteString(out, strings.Join(lines, "\n")+"\n") + if err != nil { + return fmt.Errorf("write import plan: %w", err) + } + + return nil +} + // whatsappImport runs the offline import command. It holds the same token lock // as the daemon and probe, even for a plan. The plan reads no credentials for a // model and makes no network call. Only a completed rebuild replaces the agent @@ -143,7 +225,7 @@ func whatsappImport(ctx context.Context, args []string, out io.Writer) (err erro } client := newModelClient(address, auth, agent.CacheKey(), usagePath) - backup, err := rebuild(ctx, agent, plan.added, client, newCompactor(client, agent.Prompt, config.Compactor, agent.Tools()), out) + backup, err := rebuild(ctx, agent, plan.added, client, newCompactor(client, agent.Prompt, config.Compactor, agent.Tools(), agent.Images()), out) if err != nil { return fmt.Errorf("rebuild imported memory: %w", err) } @@ -156,88 +238,6 @@ func whatsappImport(ctx context.Context, args []string, out io.Writer) (err erro return nil } -// WhatsAppImportPlan holds the new chat-day batches and the size of the merged -// log. Its estimate uses the measured cost of 1.3 calls per message, not a -// promise about a particular export or compaction retry. -type WhatsAppImportPlan struct { - agent string - cutoffs map[string]time.Time - added []DatedMessage - chats []string - cutoff time.Time - first time.Time - last time.Time - bytes int - messages int - skipped int - ownerKnown bool -} - -// Write prints the plan without chat text or the owner's profile name. -func (p WhatsAppImportPlan) Write(out io.Writer) error { - cutoff := "none (no live WhatsApp batch)" - if !p.cutoff.IsZero() { - cutoff = p.cutoff.Format(time.RFC3339) - } - - dateRange := "none" - if !p.first.IsZero() { - dateRange = p.first.Format(time.RFC3339) + " to " + p.last.Format(time.RFC3339) - } - - owner := "known (mapped to You)" - if !p.ownerKnown { - owner = "unknown (sender names kept as written)" - } - - _, err := fmt.Fprintf(out, "agent: %s\nchats: %d\n", p.agent, len(p.chats)) - if err != nil { - return fmt.Errorf("write import plan: %w", err) - } - - for _, chat := range p.chats { - line := " " + chat - cutoff, found := p.cutoffs[chat] - if found { - line += " (cutoff: " + cutoff.Format(time.RFC3339) + ")" - } - - _, err = fmt.Fprintln(out, line) - if err != nil { - return fmt.Errorf("write import plan: %w", err) - } - } - - estimate := 1.3 * float64(p.messages) - if len(p.added) == 0 { - estimate = 0 - } - - lines := []string{ - fmt.Sprintf("batches: %d (%d chat-days skipped)", len(p.added), p.skipped), - "dates: " + dateRange, - "cutoff: " + cutoff, - "owner: " + owner, - fmt.Sprintf("bytes: %d (new batch text)", p.bytes), - fmt.Sprintf("messages: %d (merged log)", p.messages), - fmt.Sprintf("estimated calls: %.0f (about 1.3 per message, limit %d)", estimate, rebuildCallLimit), - } - - _, err = io.WriteString(out, strings.Join(lines, "\n")+"\n") - if err != nil { - return fmt.Errorf("write import plan: %w", err) - } - - return nil -} - -// WhatsAppImportOptions selects the target, exports, and plan-only mode. -type WhatsAppImportOptions struct { - planOnly bool - agent string - paths []string -} - // whatsappImportParse accepts GNU-style flags, including --agent=NAME and -- // before paths that start with a dash. func whatsappImportParse(args []string) (WhatsAppImportOptions, error) { @@ -452,16 +452,6 @@ func whatsappImportNames(ctx context.Context, dir string) (names map[string]What return names, owner, nil } -// whatsappImportName adds an exact match unless distinct IDs share its name. -func whatsappImportName(names map[string]WhatsAppChat, chat WhatsAppChat) { - old, found := names[chat.Name] - if found && old.ID != chat.ID { - chat.ID = "" - } - - names[chat.Name] = chat -} - // whatsappImportPlan filters each chat at its earliest live message across the // target and root logs. Only imported days in the target are skipped. Each // remaining chat-day becomes a work message dated at its last exported message. @@ -570,6 +560,16 @@ func whatsappImportPlan(ctx context.Context, paths []string, old, rootLog []Date return plan, nil } +// whatsappImportName adds an exact match unless distinct IDs share its name. +func whatsappImportName(names map[string]WhatsAppChat, chat WhatsAppChat) { + old, found := names[chat.Name] + if found && old.ID != chat.ID { + chat.ID = "" + } + + names[chat.Name] = chat +} + // whatsappImportHistory reads batch names and live cutoffs from both logs. // Imported chat-days belong only to the target log, which is first. func whatsappImportHistory(target, root []DatedMessage, names map[string]WhatsAppChat, cutoffs map[string]time.Time, location *time.Location) (map[string]bool, error) {