Build tracker identification prompt categories dynamically
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é <emile@probo.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
29
contrib/claude/file-naming.md
Normal file
29
contrib/claude/file-naming.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# File naming conventions
|
||||
|
||||
## Template files
|
||||
|
||||
Template files use the extension pattern `<name>.<output-ext>.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 `<output-ext>` 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`.
|
||||
@@ -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.
|
||||
</instructions>
|
||||
@@ -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."`
|
||||
}
|
||||
|
||||
@@ -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"+
|
||||
"<pattern> %s </pattern>\n"+
|
||||
"<type> %s </type>\n"+
|
||||
"<match_type> %s </match_type>\n"+
|
||||
"<max_age> %s </max_age>\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("<observed_domains> %s </observed_domains>\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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user