From f81168bc188e7654db2e66fe55eee4d22ef5b37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 19 May 2026 10:59:16 +0400 Subject: [PATCH] Build tracker identification prompt categories dynamically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generate the category list from coredata.ThirdPartyCategories() at runtime instead of hardcoding it in the prompt text. Type the TrackerIdentification.Category field as coredata.ThirdPartyCategory so JSON unmarshaling validates values automatically. Also documents the .txt.tmpl template file naming convention. Signed-off-by: Émile Ré --- AGENTS.md | 1 + contrib/claude/agent.md | 21 ++++ contrib/claude/file-naming.md | 29 +++++ ...on.txt => tracker_identification.txt.tmpl} | 4 +- pkg/cookiebanner/tracker_identification.go | 14 +-- pkg/cookiebanner/tracker_mapping_worker.go | 107 ++++++------------ pkg/coredata/common_third_party.go | 1 + 7 files changed, 96 insertions(+), 81 deletions(-) create mode 100644 contrib/claude/file-naming.md rename pkg/cookiebanner/prompts/{tracker_identification.txt => tracker_identification.txt.tmpl} (86%) diff --git a/AGENTS.md b/AGENTS.md index 003cceeaf..ffcad1f57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,7 @@ Detailed guides for specific subsystems live in `contrib/claude/`: - [`contrib/claude/react-components.md`](contrib/claude/react-components.md) — React component shape (file/export, props, configure vs data via hooks) - [`contrib/claude/ui.md`](contrib/claude/ui.md) — @probo/ui, Tailwind, tailwind-variants, folders, skeletons, compound components - [`contrib/claude/config.md`](contrib/claude/config.md) — Configuration propagation (all files to update when config changes) +- [`contrib/claude/file-naming.md`](contrib/claude/file-naming.md) — File naming conventions (template files, extensions) - [`contrib/claude/commit.md`](contrib/claude/commit.md) — Commit message conventions - [`contrib/claude/license.md`](contrib/claude/license.md) — ISC license header (all file types) - [`contrib/claude/release.md`](contrib/claude/release.md) — Release process (version bump, changelog, tag, push) diff --git a/contrib/claude/agent.md b/contrib/claude/agent.md index a1e26fae2..cbd6fcde0 100644 --- a/contrib/claude/agent.md +++ b/contrib/claude/agent.md @@ -72,6 +72,27 @@ shutdown broadcast onto a per-run `cancelRun(agent.ErrSuspendForCheckpoint)`, so the same contract drives both the public Go API and the worker infrastructure path. +## Prompt templates + +Prompt files with placeholders use `.txt.tmpl` (see general template naming +convention in `.cursor/rules/template-files.mdc`). + +Use `agent.WithInstructionsFunc` to build prompts dynamically at runtime: + +```go +//go:embed prompts/tracker_identification.txt.tmpl +var trackerIdentificationPrompt string + +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) +} +``` + ## Limits - Max turns: 10 (default) diff --git a/contrib/claude/file-naming.md b/contrib/claude/file-naming.md new file mode 100644 index 000000000..0c4214434 --- /dev/null +++ b/contrib/claude/file-naming.md @@ -0,0 +1,29 @@ +# File naming conventions + +## Template files + +Template files use the extension pattern `..tmpl`: + +``` +pkg/trust/sitemap.xml.tmpl +pkg/server/mailactions/templates/page.html.tmpl +pkg/cookiebanner/prompts/tracker_identification.txt.tmpl +pkg/probo/templates/risk_list.json.tmpl +compose/keycloak/probo-realm.json.tmpl +pkg/esign/certificate.html.tmpl +``` + +The `` indicates the format produced after rendering (`.xml`, `.html`, `.txt`, `.json`). The final `.tmpl` suffix marks the file as a template requiring substitution before use. + +### Dynamic values over hardcoded lists + +Never hardcode enum values or source-of-truth lists in template text. Use a +placeholder and substitute at runtime so the template stays in sync with the +code: + +``` +Use one of: {{.Categories}}. +``` + +For single-placeholder templates, `strings.Replace` is sufficient. For multiple +placeholders, use `text/template`. diff --git a/pkg/cookiebanner/prompts/tracker_identification.txt b/pkg/cookiebanner/prompts/tracker_identification.txt.tmpl similarity index 86% rename from pkg/cookiebanner/prompts/tracker_identification.txt rename to pkg/cookiebanner/prompts/tracker_identification.txt.tmpl index ace804bc5..519ad046c 100644 --- a/pkg/cookiebanner/prompts/tracker_identification.txt +++ b/pkg/cookiebanner/prompts/tracker_identification.txt.tmpl @@ -45,6 +45,6 @@ Return a structured JSON response with: 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. +8. For the category field, use one of: {{.Categories}}. + Most cookies fall under ANALYTICS or MARKETING. diff --git a/pkg/cookiebanner/tracker_identification.go b/pkg/cookiebanner/tracker_identification.go index 9eaf53da4..83629440c 100644 --- a/pkg/cookiebanner/tracker_identification.go +++ b/pkg/cookiebanner/tracker_identification.go @@ -14,13 +14,13 @@ package cookiebanner +import "go.probo.inc/probo/pkg/coredata" + // 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. +// agent returns. 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."` + 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."` } diff --git a/pkg/cookiebanner/tracker_mapping_worker.go b/pkg/cookiebanner/tracker_mapping_worker.go index b7eb48b78..28972c8bd 100644 --- a/pkg/cookiebanner/tracker_mapping_worker.go +++ b/pkg/cookiebanner/tracker_mapping_worker.go @@ -40,7 +40,7 @@ const ( agentMaxPatternConfidence = 0.8 ) -//go:embed prompts/tracker_identification.txt +//go:embed prompts/tracker_identification.txt.tmpl var trackerIdentificationPrompt string type trackerMappingHandler struct { @@ -100,7 +100,7 @@ func buildTrackerMappingAgent( return agent.New( "tracker-mapping", cfg.LLMClient, - agent.WithInstructions(trackerIdentificationPrompt), + agent.WithInstructionsFunc(trackerMappingInstructions), agent.WithModel(cfg.Model), agent.WithTools(tools...), agent.WithOutputType(outputType), @@ -109,6 +109,21 @@ func buildTrackerMappingAgent( ) } +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 (h *trackerMappingHandler) Claim(ctx context.Context) (coredata.TrackerPattern, error) { var tp coredata.TrackerPattern @@ -373,10 +388,7 @@ func (h *trackerMappingHandler) resolveOrCreateCommonThirdParty( return &party.ID, nil } - category := coredata.ThirdPartyCategoryOther - if parsed := parseThirdPartyCategory(identification.Category); parsed != "" { - category = parsed - } + category := identification.Category now := time.Now() party = coredata.CommonThirdParty{ @@ -411,7 +423,7 @@ func (h *trackerMappingHandler) resolveOrCreateCommonThirdParty( ctx, "created common third party from agent identification", log.String("name", identification.ThirdPartyName), - log.String("category", string(category)), + log.String("category", category.String()), ) return &party.ID, nil @@ -473,75 +485,26 @@ func (h *trackerMappingHandler) resolveThirdParty( } 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) - + maxAge := "session" if tp.MaxAgeSeconds != nil { - fmt.Fprintf(&b, "- Max age: %d seconds\n", *tp.MaxAgeSeconds) - } else { - fmt.Fprintf(&b, "- Max age: session\n") + 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 { - fmt.Fprintf(&b, "- Observed on domains: %s\n", strings.Join(domains, ", ")) + prompt += fmt.Sprintf(" %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 "" - } + return prompt } diff --git a/pkg/coredata/common_third_party.go b/pkg/coredata/common_third_party.go index c58883d1f..41f66857d 100644 --- a/pkg/coredata/common_third_party.go +++ b/pkg/coredata/common_third_party.go @@ -214,6 +214,7 @@ LIMIT 1; if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } + return fmt.Errorf("cannot collect common third party by slug: %w", err) }