- 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.
167 lines
5.1 KiB
Go
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
|
|
}
|