ultisuite-backend/internal/contacts/discovery/enrich.go
R3D347HR4Y 556d5f416d Enhance API and configuration for contact discovery and public sharing
- Introduced new endpoints for contact discovery, including scanning, listing, and managing discovered contacts.
- Implemented retry logic for handling missing DAV credentials during contact operations.
- Added public share functionality for drive API, allowing users to manage public shares, including upload, delete, and rename operations.
- Updated Nextcloud configuration to support public share links and improved error handling for public share permissions.
- Enhanced logging and validation across contact and drive APIs for better error tracking and user feedback.
- Added tests for new contact matching and ranking functionalities to ensure accuracy and reliability.
2026-06-06 20:27:02 +02:00

163 lines
4.8 KiB
Go

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
}