Give tracker agents a browser to read setters

The tracker-mapping and common-pattern enrichment agents only had
web search, which returns title/url/snippet, so they could never
open a cookie-database or cookie-policy page to read which vendor
actually sets a tracker. This mis-attributed setters whose snippet
is misleading (e.g. _li_* read as LinkedIn rather than LiveIntent).

Wire the read-only headless-browser toolset into both agents, gated
on a configured Chrome endpoint, mirroring the common-third-party
enrichment worker: agent construction moves into the run path so each
run can carry a per-run browser that is closed when the run returns.
The prompts now direct the agent to open a promising result and read
the named setter from the full page text. Both agents stay unchanged
when no Chrome endpoint is configured.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-12 16:11:17 +02:00
parent dbfd191bc5
commit b5bf63b436
8 changed files with 99 additions and 36 deletions

View File

@@ -25,6 +25,7 @@ import (
"go.gearno.de/kit/pg"
"go.gearno.de/kit/worker"
"go.probo.inc/probo/pkg/agent"
"go.probo.inc/probo/pkg/agent/tools/browser"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/llm"
@@ -42,7 +43,8 @@ const defaultMappingStaleAfter = 10 * time.Minute
type trackerMappingHandler struct {
pg *pg.Client
logger *log.Logger
mappingAgent *agent.Agent
mappingCfg TrackerMappingAgentConfig
mappingEnabled bool
disambiguationAgent *agent.Agent
agentTimeout time.Duration
disambiguationTimeout time.Duration
@@ -69,15 +71,13 @@ func NewTrackerMappingWorker(
h := &trackerMappingHandler{
pg: pgClient,
logger: logger,
mappingCfg: mappingCfg,
mappingEnabled: mappingCfg.LLMClient != nil,
agentTimeout: agentTimeout,
disambiguationTimeout: disambiguationCfg.Timeout,
staleAfter: staleAfter,
}
if mappingCfg.LLMClient != nil {
h.mappingAgent = buildTrackerMappingAgent(mappingCfg, pgClient, logger)
}
if disambiguationCfg.LLMClient != nil {
h.disambiguationAgent = thirdparty.BuildDisambiguationAgent(disambiguationCfg, logger)
}
@@ -191,7 +191,7 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker
// the deterministic signals could not resolve a catalog third party.
// The LLM and web-search calls happen outside any transaction; the
// result is persisted in its own short transaction.
if commonThirdPartyID == nil && h.mappingAgent != nil {
if commonThirdPartyID == nil && h.mappingEnabled {
ident, err := h.identifyWithAgent(ctx, tp, det.origin)
if err != nil {
return fmt.Errorf("cannot identify with agent: %w", err)
@@ -622,12 +622,27 @@ func (h *trackerMappingHandler) identifyWithAgent(
prompt := buildAgentPrompt(tp, domains, siteDomain)
// Build the mapping agent per run so it can carry a per-run browser
// when a Chrome endpoint is configured. The browser lets the agent
// open cookie-database and cookie-policy pages to read the true
// setter; it is closed when this run returns.
var browserTools []agent.Tool
if h.mappingCfg.ChromeAddr != "" {
webBrowser := browser.NewBrowser(ctx, h.mappingCfg.ChromeAddr)
defer webBrowser.Close()
browserTools = browser.NewReadOnlyToolset(webBrowser).Tools()
}
mappingAgent := buildTrackerMappingAgent(h.mappingCfg, h.pg, h.logger, browserTools)
agentCtx, cancel := context.WithTimeout(ctx, h.agentTimeout)
defer cancel()
result, err := agent.RunTyped[TrackerMappingAgentResult](
agentCtx,
h.mappingAgent,
mappingAgent,
[]llm.Message{
{
Role: llm.RoleUser,