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

@@ -0,0 +1,45 @@
<role>
You are a cookie and web tracker identification expert. Your job is to identify which company or service sets a given cookie or tracker, based on its name, type, and the domains it was observed on.
</role>
<task>
Given a tracker pattern (cookie name, local storage key, etc.), its type, max-age, and the domains where it was observed, identify the company or service responsible for setting it.
Return a structured JSON response with:
- third_party_name: the canonical company/service name (e.g. "Google Analytics", not "google" or "GA")
- category: the business category of the third party
- description: a one-sentence description of what this tracker does
- confidence: how confident you are in the identification (0.0 to 1.0)
</task>
<instructions>
1. First use the search_tracker_patterns tool to look for similar known patterns in the database. Strip variable parts (IDs, UUIDs, timestamps) from the pattern name and search for the fixed prefix or root (e.g. for "_gat_UA-12345678-1", search for "_gat").
2. If search_tracker_patterns returns results with a third party name, use the search_third_parties tool to confirm the exact name in the database and get its category.
3. Only use web_search as a last resort if the internal tools return nothing useful. Search for "what is cookie [name]" or "[name] cookie tracker".
4. Common cookie naming conventions to recognize:
- _ga*, _gid, _gat*: Google Analytics
- _fbp, _fbc, fr: Meta / Facebook
- _pk_*: Matomo (formerly Piwik)
- _hj*: Hotjar
- _gcl_*: Google Ads
- __cf*: Cloudflare
- _tt_*: TikTok
- hubspot*: HubSpot
- _cls_*: Clarity (Microsoft)
5. The observed domains are strong signals. If the cookie comes from a well-known tracking domain (e.g. doubleclick.net, facebook.com, analytics.google.com), that is strong evidence of the third party.
6. Be conservative with confidence:
- 0.9-1.0: exact pattern match found in database or unmistakable naming convention + matching domain
- 0.7-0.8: strong signal from naming convention or domain, but not a database match
- 0.5-0.6: reasonable guess based on partial naming patterns
- Below 0.5: uncertain, speculative
7. If you truly cannot identify the tracker, set third_party_name to an empty string and confidence below 0.3.
8. For the category field, use one of: ANALYTICS, ADVERTISING, CLOUD_MONITORING, CLOUD_PROVIDER, COLLABORATION, CUSTOMER_SUPPORT, DATA_STORAGE_AND_PROCESSING, DOCUMENT_MANAGEMENT, EMPLOYEE_MANAGEMENT, ENGINEERING, FINANCE, IDENTITY_PROVIDER, IT, MARKETING, OFFICE_OPERATIONS, OTHER, PASSWORD_MANAGEMENT, PRODUCT_AND_DESIGN, PROFESSIONAL_SERVICES, RECRUITING, SALES, SECURITY, VERSION_CONTROL.
Most cookies fall under ANALYTICS, ADVERTISING, or MARKETING.
</instructions>

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cookiebanner
import (
"context"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/agent"
"go.probo.inc/probo/pkg/coredata"
)
type (
searchPatternsParams struct {
Query string `json:"query" jsonschema:"Search fragment to match against known cookie/tracker pattern names and descriptions (e.g. '_ga', 'matomo', 'facebook')"`
}
searchPatternsResult struct {
Pattern string `json:"pattern"`
Description string `json:"description"`
TrackerType string `json:"tracker_type"`
ThirdPartyName string `json:"third_party_name,omitempty"`
Confidence float32 `json:"confidence"`
}
searchThirdPartiesParams struct {
Query string `json:"query" jsonschema:"Search fragment to match against known third party names (e.g. 'Google', 'Meta', 'Hotjar')"`
}
searchThirdPartiesResult struct {
Name string `json:"name"`
Category string `json:"category"`
WebsiteURL string `json:"website_url,omitempty"`
}
)
func searchTrackerPatternsTool(pgClient *pg.Client) agent.Tool {
return agent.FunctionTool(
"search_tracker_patterns",
"Search the internal database of known cookie and tracker patterns by name fragment or description keyword. Returns matching patterns with their linked third party name and confidence score. Use this first to find similar known patterns before resorting to web search.",
func(ctx context.Context, p searchPatternsParams) (agent.ToolResult, error) {
if p.Query == "" {
return agent.ResultError("query is required"), nil
}
var out []searchPatternsResult
if err := pgClient.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
var patterns coredata.CommonTrackerPatterns
results, err := patterns.FindByKeyword(ctx, conn, p.Query, 10)
if err != nil {
return err
}
out = make([]searchPatternsResult, len(results))
for i, r := range results {
out[i] = searchPatternsResult{
Pattern: r.Pattern,
Description: r.Description,
TrackerType: string(r.TrackerType),
Confidence: r.Confidence,
}
if r.ThirdPartyName != nil {
out[i].ThirdPartyName = *r.ThirdPartyName
}
}
return nil
},
); err != nil {
return agent.ResultErrorf("search failed: %s", err), nil
}
return agent.ResultJSON(out), nil
},
)
}
func searchThirdPartiesTool(pgClient *pg.Client) agent.Tool {
return agent.FunctionTool(
"search_third_parties",
"Search the internal database of known third parties (companies/services) by name fragment. Returns matching third party names, categories, and website URLs. Use this to find the exact name of a known third party to link the tracker to.",
func(ctx context.Context, p searchThirdPartiesParams) (agent.ToolResult, error) {
if p.Query == "" {
return agent.ResultError("query is required"), nil
}
var out []searchThirdPartiesResult
if err := pgClient.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
var parties coredata.CommonThirdParties
if err := parties.LoadAll(
ctx,
conn,
coredata.NewCommonThirdPartyFilter(&p.Query),
); err != nil {
return err
}
out = make([]searchThirdPartiesResult, len(parties))
for i, tp := range parties {
out[i] = searchThirdPartiesResult{
Name: tp.Name,
Category: string(tp.Category),
}
if tp.WebsiteURL != nil {
out[i].WebsiteURL = *tp.WebsiteURL
}
}
return nil
},
); err != nil {
return agent.ResultErrorf("search failed: %s", err), nil
}
return agent.ResultJSON(out), nil
},
)
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cookiebanner
// TrackerIdentification is the structured output the tracker-mapping
// agent returns. The Category field uses the same values as the
// third_party_category PostgreSQL enum so auto-created CommonThirdParty
// rows get a valid category without mapping.
type TrackerIdentification struct {
ThirdPartyName string `json:"third_party_name" jsonschema:"Name of the company or service that sets this tracker (e.g. 'Google Analytics', 'Meta Pixel'). Empty string if truly unknown."`
Category string `json:"category" jsonschema:"Third party category. One of: ANALYTICS, ADVERTISING, CLOUD_MONITORING, CLOUD_PROVIDER, COLLABORATION, CUSTOMER_SUPPORT, DATA_STORAGE_AND_PROCESSING, DOCUMENT_MANAGEMENT, EMPLOYEE_MANAGEMENT, ENGINEERING, FINANCE, IDENTITY_PROVIDER, IT, MARKETING, OFFICE_OPERATIONS, OTHER, PASSWORD_MANAGEMENT, PRODUCT_AND_DESIGN, PROFESSIONAL_SERVICES, RECRUITING, SALES, SECURITY, VERSION_CONTROL"`
Description string `json:"description" jsonschema:"What this tracker does in one sentence"`
Confidence float64 `json:"confidence" jsonschema:"Confidence level from 0.0 to 1.0. Set below 0.5 if unsure."`
}

View File

@@ -16,25 +16,50 @@ package cookiebanner
import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
"go.gearno.de/kit/worker"
"go.probo.inc/probo/pkg/agent"
"go.probo.inc/probo/pkg/agent/tools/search"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/llm"
"go.probo.inc/probo/pkg/slug"
)
const (
agentTimeout = 60 * time.Second
agentMaxTurns = 5
agentConfidenceThreshold = 0.6
agentMaxPatternConfidence = 0.8
)
//go:embed prompts/tracker_identification.txt
var trackerIdentificationPrompt string
type trackerMappingHandler struct {
pg *pg.Client
logger *log.Logger
agent *agent.Agent
}
type TrackerMappingConfig struct {
LLMClient *llm.Client
Model string
SearchEndpoint string
}
func NewTrackerMappingWorker(
pgClient *pg.Client,
logger *log.Logger,
cfg TrackerMappingConfig,
opts ...worker.Option,
) *worker.Worker[coredata.TrackerPattern] {
h := &trackerMappingHandler{
@@ -42,6 +67,16 @@ func NewTrackerMappingWorker(
logger: logger,
}
if cfg.LLMClient != nil {
h.agent = buildTrackerMappingAgent(
cfg.LLMClient,
cfg.Model,
cfg.SearchEndpoint,
pgClient,
logger,
)
}
return worker.New(
"tracker-mapping-worker",
h,
@@ -50,6 +85,39 @@ func NewTrackerMappingWorker(
)
}
func buildTrackerMappingAgent(
llmClient *llm.Client,
model string,
searchEndpoint string,
pgClient *pg.Client,
logger *log.Logger,
) *agent.Agent {
tools := []agent.Tool{
searchTrackerPatternsTool(pgClient),
searchThirdPartiesTool(pgClient),
}
if searchEndpoint != "" {
tools = append(tools, search.WebSearchTool(searchEndpoint))
}
outputType, err := agent.NewOutputType[TrackerIdentification]("tracker_identification")
if err != nil {
panic(fmt.Sprintf("cookiebanner: cannot build tracker identification output type: %s", err))
}
return agent.New(
"tracker-mapping",
llmClient,
agent.WithInstructions(trackerIdentificationPrompt),
agent.WithModel(model),
agent.WithTools(tools...),
agent.WithOutputType(outputType),
agent.WithMaxTurns(agentMaxTurns),
agent.WithLogger(logger),
)
}
func (h *trackerMappingHandler) Claim(ctx context.Context) (coredata.TrackerPattern, error) {
var tp coredata.TrackerPattern
@@ -85,6 +153,10 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker
commonPatternID, thirdPartyID = h.matchByDomain(ctx, tx, tp)
}
if commonPatternID == nil && h.agent != nil {
commonPatternID, thirdPartyID = h.identifyWithAgent(ctx, tx, tp)
}
if commonPatternID == nil {
commonPatternID = h.createUnmatchedPattern(ctx, tx, tp)
}
@@ -183,6 +255,195 @@ func (h *trackerMappingHandler) matchByDomain(
return &commonPattern.ID, thirdPartyID
}
func (h *trackerMappingHandler) identifyWithAgent(
ctx context.Context,
tx pg.Tx,
tp coredata.TrackerPattern,
) (*gid.GID, *gid.GID) {
var trackers coredata.DetectedTrackers
domains, err := trackers.LoadInitiatorDomainsByTrackerPatternID(ctx, tx, tp.ID)
if err != nil {
h.logger.WarnCtx(ctx, "cannot load initiator domains for agent", log.Error(err))
}
prompt := buildAgentPrompt(tp, domains)
agentCtx, cancel := context.WithTimeout(ctx, agentTimeout)
defer cancel()
result, err := h.agent.Run(
agentCtx,
[]llm.Message{
{
Role: llm.RoleUser,
Parts: []llm.Part{llm.TextPart{Text: prompt}},
},
},
)
if err != nil {
h.logger.WarnCtx(
ctx,
"agent identification failed",
log.Error(err),
log.String("pattern", tp.Pattern),
)
return nil, nil
}
var identification TrackerIdentification
if err := json.Unmarshal([]byte(result.FinalMessage().Text()), &identification); err != nil {
h.logger.WarnCtx(
ctx,
"cannot parse agent identification output",
log.Error(err),
log.String("pattern", tp.Pattern),
)
return nil, nil
}
if identification.Confidence < agentConfidenceThreshold {
h.logger.InfoCtx(
ctx,
"agent identification below confidence threshold",
log.String("pattern", tp.Pattern),
log.Float64("confidence", identification.Confidence),
)
return nil, nil
}
confidence := float32(identification.Confidence)
if confidence > agentMaxPatternConfidence {
confidence = agentMaxPatternConfidence
}
var commonThirdPartyID *gid.GID
if identification.ThirdPartyName != "" {
commonThirdPartyID = h.resolveOrCreateCommonThirdParty(
ctx,
tx,
identification,
domains,
)
}
now := time.Now()
commonPattern := coredata.CommonTrackerPattern{
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
CommonThirdPartyID: commonThirdPartyID,
TrackerType: tp.TrackerType,
Pattern: tp.Pattern,
MatchType: tp.MatchType,
Description: identification.Description,
MaxAgeSeconds: tp.MaxAgeSeconds,
Confidence: confidence,
CreatedAt: now,
UpdatedAt: now,
}
actualID, _, err := commonPattern.Upsert(ctx, tx)
if err != nil {
h.logger.ErrorCtx(
ctx,
"cannot upsert common tracker pattern from agent",
log.Error(err),
log.String("pattern", tp.Pattern),
)
return nil, nil
}
commonPattern.ID = actualID
thirdPartyID, err := h.resolveThirdParty(ctx, tx, tp, &commonPattern)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot resolve third party from agent match", log.Error(err))
return &commonPattern.ID, nil
}
h.logger.InfoCtx(
ctx,
"agent identified tracker pattern",
log.String("pattern", tp.Pattern),
log.String("third_party", identification.ThirdPartyName),
log.Float64("confidence", identification.Confidence),
)
return &commonPattern.ID, thirdPartyID
}
func (h *trackerMappingHandler) resolveOrCreateCommonThirdParty(
ctx context.Context,
tx pg.Tx,
identification TrackerIdentification,
domains []string,
) *gid.GID {
var party coredata.CommonThirdParty
if err := party.LoadByName(ctx, tx, identification.ThirdPartyName); err == nil {
return &party.ID
}
partySlug := slug.Make(identification.ThirdPartyName)
if partySlug == "" {
return nil
}
if err := party.LoadBySlug(ctx, tx, partySlug); err == nil {
return &party.ID
}
category := coredata.ThirdPartyCategoryOther
if parsed := parseThirdPartyCategory(identification.Category); parsed != "" {
category = parsed
}
now := time.Now()
party = coredata.CommonThirdParty{
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType),
Name: identification.ThirdPartyName,
Slug: partySlug,
Category: category,
Certifications: []string{},
CreatedAt: now,
UpdatedAt: now,
}
if err := party.Insert(ctx, tx); err != nil {
h.logger.WarnCtx(
ctx,
"cannot create common third party from agent",
log.Error(err),
log.String("name", identification.ThirdPartyName),
)
return nil
}
for _, domain := range domains {
domainRecord := coredata.CommonThirdPartyDomain{
ID: gid.New(gid.NilTenant, coredata.CommonThirdPartyDomainEntityType),
CommonThirdPartyID: party.ID,
Domain: domain,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := domainRecord.Upsert(ctx, tx); err != nil {
h.logger.WarnCtx(
ctx,
"cannot create common third party domain from agent",
log.Error(err),
log.String("domain", domain),
)
}
}
h.logger.InfoCtx(
ctx,
"created common third party from agent identification",
log.String("name", identification.ThirdPartyName),
log.String("category", string(category)),
)
return &party.ID
}
func (h *trackerMappingHandler) createUnmatchedPattern(
ctx context.Context,
tx pg.Tx,
@@ -242,3 +503,77 @@ func (h *trackerMappingHandler) resolveThirdParty(
return &t.ID, nil
}
func buildAgentPrompt(tp coredata.TrackerPattern, domains []string) string {
var b strings.Builder
fmt.Fprintf(&b, "Identify the following tracker:\n\n")
fmt.Fprintf(&b, "- Pattern: %s\n", tp.Pattern)
fmt.Fprintf(&b, "- Type: %s\n", tp.TrackerType)
fmt.Fprintf(&b, "- Match type: %s\n", tp.MatchType)
if tp.MaxAgeSeconds != nil {
fmt.Fprintf(&b, "- Max age: %d seconds\n", *tp.MaxAgeSeconds)
} else {
fmt.Fprintf(&b, "- Max age: session\n")
}
if len(domains) > 0 {
fmt.Fprintf(&b, "- Observed on domains: %s\n", strings.Join(domains, ", "))
}
return b.String()
}
func parseThirdPartyCategory(s string) coredata.ThirdPartyCategory {
switch s {
case "ANALYTICS":
return coredata.ThirdPartyCategoryAnalytics
case "ADVERTISING":
return coredata.ThirdPartyCategoryMarketing
case "CLOUD_MONITORING":
return coredata.ThirdPartyCategoryCloudMonitoring
case "CLOUD_PROVIDER":
return coredata.ThirdPartyCategoryCloudProvider
case "COLLABORATION":
return coredata.ThirdPartyCategoryCollaboration
case "CUSTOMER_SUPPORT":
return coredata.ThirdPartyCategoryCustomerSupport
case "DATA_STORAGE_AND_PROCESSING":
return coredata.ThirdPartyCategoryDataStorageAndProcessing
case "DOCUMENT_MANAGEMENT":
return coredata.ThirdPartyCategoryDocumentManagement
case "EMPLOYEE_MANAGEMENT":
return coredata.ThirdPartyCategoryEmployeeManagement
case "ENGINEERING":
return coredata.ThirdPartyCategoryEngineering
case "FINANCE":
return coredata.ThirdPartyCategoryFinance
case "IDENTITY_PROVIDER":
return coredata.ThirdPartyCategoryIdentityProvider
case "IT":
return coredata.ThirdPartyCategoryIT
case "MARKETING":
return coredata.ThirdPartyCategoryMarketing
case "OFFICE_OPERATIONS":
return coredata.ThirdPartyCategoryOfficeOperations
case "OTHER":
return coredata.ThirdPartyCategoryOther
case "PASSWORD_MANAGEMENT":
return coredata.ThirdPartyCategoryPasswordManagement
case "PRODUCT_AND_DESIGN":
return coredata.ThirdPartyCategoryProductAndDesign
case "PROFESSIONAL_SERVICES":
return coredata.ThirdPartyCategoryProfessionalServices
case "RECRUITING":
return coredata.ThirdPartyCategoryRecruiting
case "SALES":
return coredata.ThirdPartyCategorySales
case "SECURITY":
return coredata.ThirdPartyCategorySecurity
case "VERSION_CONTROL":
return coredata.ThirdPartyCategoryVersionControl
default:
return ""
}
}

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,

View File

@@ -304,6 +304,11 @@ func (impl *Implm) Run(
return err
}
trackerMappingCfg, err := impl.buildTrackerMappingConfig(l, tp, r)
if err != nil {
return err
}
fileManagerService := filemanager.NewService(s3Client)
var samlCert *x509.Certificate
@@ -683,7 +688,7 @@ func (impl *Implm) Run(
},
)
trackerMappingWorker := cookiebanner.NewTrackerMappingWorker(pgClient, l)
trackerMappingWorker := cookiebanner.NewTrackerMappingWorker(pgClient, l, trackerMappingCfg)
trackerMappingWorkerCtx, stopTrackerMappingWorker := context.WithCancel(context.Background())
wg.Go(
func() {

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package probod
import (
"github.com/prometheus/client_golang/prometheus"
"go.gearno.de/kit/log"
"go.opentelemetry.io/otel/trace"
"go.probo.inc/probo/pkg/cookiebanner"
)
// buildTrackerMappingConfig wires the tracker-mapping agent. It is opt-in:
// deployments that do not set `llm.tracker-mapping.provider` get a zero
// config (nil LLM client) so the worker runs without agent fallback.
func (impl *Implm) buildTrackerMappingConfig(
l *log.Logger,
tp trace.TracerProvider,
r prometheus.Registerer,
) (cookiebanner.TrackerMappingConfig, error) {
if impl.cfg.Agents.TrackerMapping.Provider == "" {
return cookiebanner.TrackerMappingConfig{}, nil
}
agentCfg, llmClient, err := impl.resolveAgentClient(
"tracker-mapping",
impl.cfg.Agents.TrackerMapping,
l,
tp,
r,
)
if err != nil {
return cookiebanner.TrackerMappingConfig{}, err
}
return cookiebanner.TrackerMappingConfig{
LLMClient: llmClient,
Model: agentCfg.ModelName,
SearchEndpoint: impl.cfg.SearchEndpoint,
}, nil
}

View File

@@ -49,6 +49,7 @@ type (
Probo LLMAgentConfig `json:"probo"`
EvidenceDescriber LLMAgentConfig `json:"evidence-describer"`
ThirdPartyAssessor LLMAgentConfig `json:"third-party-assessor"`
TrackerMapping LLMAgentConfig `json:"tracker-mapping"`
}
)