package discovery import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/ultisuite/ulti-backend/internal/llm" ) const enrichSystemPrompt = `Tu es un assistant qui extrait des informations de contact structurées à partir de signatures d'emails. Réponds UNIQUEMENT en JSON valide, sans markdown ni texte autour. Privilégie les informations des signatures les plus récentes (listées en premier). Ne devine pas : omets les champs incertains. Format attendu: { "first_name": "", "last_name": "", "company": "", "department": "", "job_title": "", "emails": [{"value": "", "label": "work|home|other"}], "phones": [{"value": "", "label": "work|mobile|home|other"}], "addresses": [{"street": "", "city": "", "region": "", "postal_code": "", "country": "", "label": "work|home"}], "website": "", "social_profiles": [{"value": "https://...", "label": "linkedin|twitter|facebook|instagram|github|other"}], "notes": "" }` func buildEnrichPrompt(email, displayName string, signatures []SignatureEntry) string { var b strings.Builder fmt.Fprintf(&b, "Email principal: %s\n", email) if displayName != "" { fmt.Fprintf(&b, "Nom affiché: %s\n", displayName) } b.WriteString("\nSignatures (du plus récent au plus ancien):\n") for i, sig := range signatures { text := sig.SignatureText if len(text) > 1500 { text = text[:1500] } fmt.Fprintf(&b, "\n--- Signature %d (%s) ---\n%s\n", i+1, sig.MessageDate.Format("2006-01-02"), text) } return b.String() } func parseEnrichedData(raw string) (*EnrichedContactData, error) { raw = strings.TrimSpace(raw) raw = strings.TrimPrefix(raw, "```json") raw = strings.TrimPrefix(raw, "```") raw = strings.TrimSuffix(raw, "```") raw = strings.TrimSpace(raw) var data EnrichedContactData if err := json.Unmarshal([]byte(raw), &data); err != nil { return nil, err } return &data, nil } func enrichWithLLMTimeout(ctx context.Context, client *llm.Client, settings llm.Settings, email, displayName string, signatures []SignatureEntry, timeout time.Duration) (*EnrichedContactData, error) { enrichCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() resultCh := make(chan struct { data *EnrichedContactData err error }, 1) go func() { data, err := enrichWithLLM(enrichCtx, client, settings, email, displayName, signatures) resultCh <- struct { data *EnrichedContactData err error }{data, err} }() select { case <-ctx.Done(): return nil, ctx.Err() case <-enrichCtx.Done(): if err := enrichCtx.Err(); err != context.DeadlineExceeded { return nil, err } return nil, fmt.Errorf("llm enrichment timed out after %s", timeout) case res := <-resultCh: return res.data, res.err } } func enrichWithLLM(ctx context.Context, client *llm.Client, settings llm.Settings, email, displayName string, signatures []SignatureEntry) (*EnrichedContactData, error) { if client == nil || len(signatures) == 0 { return nil, fmt.Errorf("no signatures to enrich") } provider, model, err := llm.ResolveProvider(settings, "") if err != nil { return nil, err } prompt := buildEnrichPrompt(email, displayName, signatures) raw, err := client.Complete(ctx, provider, model, enrichSystemPrompt, prompt) if err != nil { return nil, err } return parseEnrichedData(raw) } func enrichedDataToSuggestions(userID, profileID string, data *EnrichedContactData) []Suggestion { if data == nil { return nil } var out []Suggestion addField := func(fieldPath, value, label, sugType string) { value = strings.TrimSpace(value) if value == "" { return } out = append(out, Suggestion{ ProfileID: profileID, SuggestionType: sugType, FieldPath: fieldPath, SuggestedValue: value, SuggestedLabel: label, Confidence: 0.75, Status: SuggestionPending, }) } addField("first_name", data.FirstName, "", "new_contact") addField("last_name", data.LastName, "", "new_contact") addField("company", data.Company, "", "new_contact") addField("department", data.Department, "", "new_contact") addField("job_title", data.JobTitle, "", "new_contact") addField("website", data.Website, "", "new_contact") addField("notes", data.Notes, "", "new_contact") for _, sp := range data.SocialProfiles { addField("social_profiles", sp.Value, sp.Label, "new_contact") } for _, e := range data.Emails { addField("emails", e.Value, e.Label, "new_contact") } for _, p := range data.Phones { addField("phones", p.Value, p.Label, "new_contact") } for _, a := range data.Addresses { parts := []string{a.Street, a.City, a.Region, a.PostalCode, a.Country} var nonEmpty []string for _, p := range parts { if strings.TrimSpace(p) != "" { nonEmpty = append(nonEmpty, strings.TrimSpace(p)) } } if len(nonEmpty) > 0 { addField("addresses", strings.Join(nonEmpty, ", "), a.Label, "new_contact") } } _ = userID return out }