ultisuite-backend/internal/contacts/discovery/enrich.go
R3D347HR4Y 3978622050
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
refactor(ai): update AI gateway and cost management features
- Refactored AI gateway to utilize new cost management structures for usage tracking.
- Replaced deprecated token extraction methods with a unified cost parsing approach.
- Enhanced usage fallback mechanisms and introduced detailed usage metrics in responses.
- Added new metering functionality to record AI usage and costs effectively.
- Updated tests to reflect changes in usage parsing and cost calculations.
- Introduced new API endpoints for retrieving AI usage summaries and pricing information.
2026-06-16 10:46:33 +02:00

167 lines
5.1 KiB
Go

package discovery
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/ultisuite/ulti-backend/internal/ai"
"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, db *pgxpool.Pool, externalUserID string, 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, db, externalUserID, 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, db *pgxpool.Pool, externalUserID string, 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)
result, err := client.CompleteWithUsage(ctx, provider, model, enrichSystemPrompt, prompt)
if err != nil {
return nil, err
}
ai.RecordFeatureUsage(ctx, db, externalUserID, "contact_discovery", result.Model, provider, result.Usage)
return parseEnrichedData(result.Content)
}
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
}