- 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.
135 lines
5.2 KiB
Go
135 lines
5.2 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/ai"
|
|
"github.com/ultisuite/ulti-backend/internal/llm"
|
|
"github.com/ultisuite/ulti-backend/internal/websearch"
|
|
)
|
|
|
|
const improveContactSystemPrompt = `Tu es un assistant qui nettoie, structure et enrichit une fiche contact existante.
|
|
Réponds UNIQUEMENT en JSON valide, sans markdown ni texte autour.
|
|
Corrige les problèmes de formatage : URLs avec slash final, accents/casse des noms, téléphones, adresses mal remplies ou incomplètes, champs vides inutiles.
|
|
Réorganise les informations de façon cohérente. Ne supprime pas d'emails ou téléphones valides.
|
|
Tu peux compléter poste, entreprise, site web, réseaux sociaux ou notes à partir des résultats de recherche en ligne fournis, uniquement si tu es raisonnablement confiant qu'ils concernent cette personne (profils LinkedIn, pages entreprise, etc.). En cas de doute ou d'homonymie, n'ajoute rien de nouveau.
|
|
Quand des résultats de recherche mentionnent des profils LinkedIn ou d'autres réseaux sociaux pertinents, ajoute-les dans social_profiles avec l'URL complète. Ne mets pas LinkedIn dans website : website = site personnel ou entreprise, social_profiles = profils sociaux.
|
|
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": ""
|
|
}`
|
|
|
|
type ImproveContactInput struct {
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
MiddleName string `json:"middle_name,omitempty"`
|
|
Company string `json:"company,omitempty"`
|
|
Department string `json:"department,omitempty"`
|
|
JobTitle string `json:"job_title,omitempty"`
|
|
Website string `json:"website,omitempty"`
|
|
Notes string `json:"notes,omitempty"`
|
|
Emails []FieldWithLabel `json:"emails,omitempty"`
|
|
Phones []FieldWithLabel `json:"phones,omitempty"`
|
|
Addresses []AddressField `json:"addresses,omitempty"`
|
|
SocialProfiles []FieldWithLabel `json:"social_profiles,omitempty"`
|
|
Birthday string `json:"birthday,omitempty"`
|
|
RawVCard string `json:"raw_vcard,omitempty"`
|
|
}
|
|
|
|
func contactCityFromAddresses(addresses []AddressField) string {
|
|
for _, a := range addresses {
|
|
if city := strings.TrimSpace(a.City); city != "" {
|
|
return city
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func buildImproveContactPrompt(input ImproveContactInput, searchSection string) string {
|
|
var b strings.Builder
|
|
b.WriteString("Fiche contact actuelle:\n")
|
|
payload, _ := json.MarshalIndent(input, "", " ")
|
|
b.Write(payload)
|
|
if searchSection != "" {
|
|
b.WriteString("\n\n")
|
|
b.WriteString(searchSection)
|
|
b.WriteString("\n\nÀ partir de ces résultats, complète social_profiles (LinkedIn en priorité, puis X/Twitter, Facebook, Instagram, GitHub) lorsque l'URL correspond clairement à cette personne.")
|
|
}
|
|
b.WriteString("\n\nPropose une version nettoyée, mieux structurée et enrichie si les sources le permettent.")
|
|
return b.String()
|
|
}
|
|
|
|
func (s *Service) fetchContactSearchResults(ctx context.Context, externalUserID string, input ImproveContactInput) string {
|
|
if s.websearch == nil {
|
|
return ""
|
|
}
|
|
searchSettings, err := s.loadSearchSettings(ctx, externalUserID)
|
|
if err != nil || !searchSettingsConfigured(searchSettings) {
|
|
return ""
|
|
}
|
|
provider, err := websearch.ResolveProvider(searchSettings)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
query := websearch.BuildContactSearchQuery(
|
|
input.FirstName,
|
|
input.LastName,
|
|
input.MiddleName,
|
|
input.Company,
|
|
input.JobTitle,
|
|
contactCityFromAddresses(input.Addresses),
|
|
)
|
|
if query == "" {
|
|
return ""
|
|
}
|
|
results, err := s.websearch.Search(ctx, provider, query, 5)
|
|
if err != nil {
|
|
s.logger.Warn("contact improve web search failed", "error", err)
|
|
return ""
|
|
}
|
|
return websearch.FormatResultsForPrompt(results)
|
|
}
|
|
|
|
func (s *Service) ImproveContact(ctx context.Context, externalUserID string, input ImproveContactInput) (*EnrichedContactData, error) {
|
|
if s.llm == nil {
|
|
return nil, ErrLLMNotConfigured
|
|
}
|
|
llmSettings, err := s.loadLLMSettings(ctx, externalUserID)
|
|
if err != nil || !llmSettingsHasProvider(llmSettings) {
|
|
return nil, ErrLLMNotConfigured
|
|
}
|
|
provider, model, err := llm.ResolveProvider(llmSettings, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
improveCtx, cancel := context.WithTimeout(ctx, llmEnrichTimeout)
|
|
defer cancel()
|
|
|
|
searchSection := s.fetchContactSearchResults(improveCtx, externalUserID, input)
|
|
prompt := buildImproveContactPrompt(input, searchSection)
|
|
raw, err := s.llm.CompleteWithUsage(improveCtx, provider, model, improveContactSystemPrompt, prompt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ai.RecordFeatureUsage(ctx, s.db, externalUserID, "contact_discovery", raw.Model, provider, raw.Usage)
|
|
data, err := parseEnrichedData(raw.Content)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse improved contact: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|