// Copyright (c) 2026 Probo Inc . // // 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" _ "embed" "fmt" "strings" "time" "go.gearno.de/kit/log" "go.gearno.de/kit/pg" "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/llm" ) const ( agentTimeout = 60 * time.Second agentMaxTurns = 5 agentConfidenceThreshold = 0.6 agentMaxPatternConfidence = 0.8 ) //go:embed prompts/tracker_identification.txt.tmpl var trackerIdentificationPrompt string // TrackerMappingAgentResult is the structured output the tracker-mapping // agent returns. type TrackerMappingAgentResult 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 coredata.ThirdPartyCategory `json:"category" jsonschema:"Third party category"` 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."` } type TrackerMappingConfig struct { LLMClient *llm.Client Model string FirecrawlAPIKey string } func buildTrackerMappingAgent( cfg TrackerMappingConfig, pgClient *pg.Client, logger *log.Logger, ) *agent.Agent { tools := []agent.Tool{ searchTrackerPatternsTool(pgClient), searchThirdPartiesTool(pgClient), } if cfg.FirecrawlAPIKey != "" { tools = append(tools, search.FirecrawlSearchTool(cfg.FirecrawlAPIKey)) } outputType, err := agent.NewOutputType[TrackerMappingAgentResult]("tracker_identification") if err != nil { panic(fmt.Sprintf("cookiebanner: cannot build tracker identification output type: %s", err)) } return agent.New( "tracker-mapping", cfg.LLMClient, agent.WithInstructionsFunc(trackerMappingInstructions), agent.WithModel(cfg.Model), agent.WithTools(tools...), agent.WithOutputType(outputType), agent.WithMaxTurns(agentMaxTurns), agent.WithLogger(logger), ) } func trackerMappingInstructions(_ context.Context, _ *agent.Agent) string { categories := coredata.ThirdPartyCategories() parts := make([]string, len(categories)) for i, c := range categories { parts[i] = string(c) } return strings.Replace( trackerIdentificationPrompt, "{{.Categories}}", strings.Join(parts, ", "), 1, ) } func buildAgentPrompt(tp coredata.TrackerPattern, domains []string) string { maxAge := "session" if tp.MaxAgeSeconds != nil { maxAge = fmt.Sprintf("%d seconds", *tp.MaxAgeSeconds) } prompt := fmt.Sprintf( "Identify the following tracker:\n\n"+ " %s \n"+ " %s \n"+ " %s \n"+ " %s \n", tp.Pattern, tp.TrackerType, tp.MatchType, maxAge, ) if len(domains) > 0 { prompt += fmt.Sprintf(" %s \n", strings.Join(domains, ", ")) } return prompt }