Add LLM agent fallback for unmapped tracker patterns

When both pattern matching and domain matching fail to identify a
tracker, an opt-in LLM agent can now attempt identification using
internal database searches and optional web search. The agent returns
structured output (third party name, category, description, confidence)
and the worker auto-creates CommonThirdParty records when needed.

The feature is gated behind the `llm.tracker-mapping.provider` config
field; when unset the worker behaves exactly as before.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-15 15:57:06 +04:00
parent 80237eb39c
commit 0cc2b62cf2
9 changed files with 683 additions and 1 deletions

View File

@@ -344,6 +344,60 @@ LIMIT 1;
return &pattern, nil
}
type CommonTrackerPatternSearchResult struct {
Pattern string `db:"pattern"`
Description string `db:"description"`
TrackerType TrackerType `db:"tracker_type"`
ThirdPartyName *string `db:"third_party_name"`
Confidence float32 `db:"confidence"`
}
func (ps *CommonTrackerPatterns) FindByKeyword(
ctx context.Context,
conn pg.Querier,
fragment string,
limit int,
) ([]CommonTrackerPatternSearchResult, error) {
if limit <= 0 || limit > 20 {
limit = 10
}
q := `
SELECT
ctp.pattern,
ctp.description,
ctp.tracker_type,
ct.name AS third_party_name,
ctp.confidence
FROM
common_tracker_patterns ctp
LEFT JOIN common_third_parties ct ON ct.id = ctp.common_third_party_id
WHERE
ctp.pattern ILIKE '%' || @fragment || '%'
OR ctp.description ILIKE '%' || @fragment || '%'
ORDER BY
ctp.confidence DESC
LIMIT @limit;
`
args := pgx.StrictNamedArgs{
"fragment": fragment,
"limit": limit,
}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return nil, fmt.Errorf("cannot search common tracker patterns: %w", err)
}
results, err := pgx.CollectRows(rows, pgx.RowToStructByName[CommonTrackerPatternSearchResult])
if err != nil {
return nil, fmt.Errorf("cannot collect common tracker pattern search results: %w", err)
}
return results, nil
}
func (ps *CommonTrackerPatterns) LoadByCommonThirdPartyID(
ctx context.Context,
conn pg.Querier,

View File

@@ -179,6 +179,34 @@ LIMIT 1;
return &commonThirdPartyID, nil
}
func (dts *DetectedTrackers) LoadInitiatorDomainsByTrackerPatternID(
ctx context.Context,
conn pg.Querier,
trackerPatternID gid.GID,
) ([]string, error) {
q := `
SELECT DISTINCT initiator_domain
FROM detected_trackers
WHERE tracker_pattern_id = @tracker_pattern_id
AND initiator_domain IS NOT NULL
LIMIT 5;
`
args := pgx.StrictNamedArgs{"tracker_pattern_id": trackerPatternID}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return nil, fmt.Errorf("cannot load initiator domains: %w", err)
}
domains, err := pgx.CollectRows(rows, pgx.RowTo[string])
if err != nil {
return nil, fmt.Errorf("cannot collect initiator domains: %w", err)
}
return domains, nil
}
func (dts *DetectedTrackers) RelinkByTrackerPatternID(
ctx context.Context,
tx pg.Tx,