Improve common third-party enrichment quality
Give the company-profile agent (Agent A) the read-only browser toolset and build it per-run, so it can read footer, imprint, about, and legal pages and follow a product domain to the corporate one to resolve the legal name and headquarters address rather than failing cold. Make the website the hard precondition: when Agent A cannot resolve a canonical website, skip the compliance-docs agent and logo step instead of running them blind, which previously produced inconsistent cross-domain document URLs. Fall back to the catalog display name for the legal name when nothing better is found, recorded with a distinct provenance status so a later real find overwrites it. Rewrite both enrichment prompts in the project's role/task/instructions XML style, add a domain-consistency rule for document URLs and a tool-budget directive, and document the prompt style as a rule and guide. Fix the find_links_matching browser tool, which double-encoded its pattern and made JSON.parse fail on every keyword, starving any agent that used it until it hit the turn cap. Salvage output when an agent exhausts its turn budget while still exploring with a pending structured output by forcing one final synthesis turn instead of failing outright. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -132,11 +132,10 @@ func resolveEnrichmentMaxTokens(configured *int) int {
|
||||
}
|
||||
|
||||
type enrichmentHandler struct {
|
||||
pg *pg.Client
|
||||
logger *log.Logger
|
||||
cfg EnrichmentConfig
|
||||
companyAgent *agent.Agent
|
||||
httpClient *http.Client
|
||||
pg *pg.Client
|
||||
logger *log.Logger
|
||||
cfg EnrichmentConfig
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewCommonThirdPartyEnrichmentWorker builds the worker that enriches
|
||||
@@ -158,13 +157,6 @@ func NewCommonThirdPartyEnrichmentWorker(
|
||||
httpClient: newEnrichmentHTTPClient(),
|
||||
}
|
||||
|
||||
// Agent A has no browser, so it is built once and reused. Agent B is
|
||||
// built per Process because it needs a per-run browser bound to the
|
||||
// process context.
|
||||
if cfg.LLMClient != nil {
|
||||
h.companyAgent = buildCompanyProfileAgent(cfg, logger)
|
||||
}
|
||||
|
||||
return worker.New(
|
||||
"common-third-party-enrichment-worker",
|
||||
h,
|
||||
@@ -203,7 +195,7 @@ func (h *enrichmentHandler) Claim(ctx context.Context) (coredata.CommonThirdPart
|
||||
// transaction. Process always writes an enrichment payload, even on a
|
||||
// no-result run, so stale recovery does not re-queue the row.
|
||||
func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonThirdParty) error {
|
||||
if h.companyAgent == nil {
|
||||
if h.cfg.LLMClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -226,6 +218,33 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh
|
||||
}
|
||||
|
||||
website := effectiveWebsiteURL(party, company, h.cfg.ConfidenceThreshold)
|
||||
|
||||
meta := make(map[string]EnrichmentFieldMeta)
|
||||
|
||||
// Website is the hard precondition: Agent B and the logo step both
|
||||
// depend on it, and an Agent B run without a domain scope produces
|
||||
// inconsistent cross-domain results. When no website is resolved,
|
||||
// persist what Agent A found and stop here rather than running Agent
|
||||
// B blind.
|
||||
if website == "" {
|
||||
for _, field := range scalarFields(company, ComplianceDocsResult{}) {
|
||||
applyScalarField(&party, meta, prior, field, h.cfg.ConfidenceThreshold, now)
|
||||
}
|
||||
|
||||
applyCertifications(&party, meta, prior, CertificationsField{}, h.cfg.ConfidenceThreshold, now)
|
||||
applyLegalNameFallback(&party, meta, now)
|
||||
|
||||
runErrors = append(runErrors, "website_url unresolved: skipped compliance docs")
|
||||
|
||||
return h.persist(ctx, party, EnrichmentMetadata{
|
||||
Model: h.cfg.Model,
|
||||
AttemptedAt: now,
|
||||
Status: enrichmentStatusFailed,
|
||||
Error: strings.Join(runErrors, "; "),
|
||||
Fields: meta,
|
||||
}, nil, now)
|
||||
}
|
||||
|
||||
legalName := effectiveLegalName(party, company, h.cfg.ConfidenceThreshold)
|
||||
|
||||
// Agent B: compliance documents and trust pages.
|
||||
@@ -241,13 +260,12 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh
|
||||
// transaction; the File row is inserted below.
|
||||
logoFile := h.prepareLogo(ctx, party, website)
|
||||
|
||||
meta := make(map[string]EnrichmentFieldMeta)
|
||||
|
||||
for _, field := range scalarFields(company, compliance) {
|
||||
applyScalarField(&party, meta, prior, field, h.cfg.ConfidenceThreshold, now)
|
||||
}
|
||||
|
||||
applyCertifications(&party, meta, prior, compliance.Certifications, h.cfg.ConfidenceThreshold, now)
|
||||
applyLegalNameFallback(&party, meta, now)
|
||||
|
||||
status := enrichmentStatusDone
|
||||
|
||||
@@ -266,6 +284,20 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh
|
||||
Fields: meta,
|
||||
}
|
||||
|
||||
return h.persist(ctx, party, payload, logoFile, now)
|
||||
}
|
||||
|
||||
// persist marshals the enrichment payload onto the row and writes it in a
|
||||
// single transaction, inserting the logo File row and linking it when one
|
||||
// was prepared. It always writes an enrichment payload, even on a
|
||||
// no-result run, so stale recovery does not re-queue the row.
|
||||
func (h *enrichmentHandler) persist(
|
||||
ctx context.Context,
|
||||
party coredata.CommonThirdParty,
|
||||
payload EnrichmentMetadata,
|
||||
logoFile *coredata.File,
|
||||
now time.Time,
|
||||
) error {
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal enrichment metadata: %w", err)
|
||||
@@ -298,7 +330,7 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh
|
||||
"enriched common third party",
|
||||
log.String("common_third_party_id", party.ID.String()),
|
||||
log.String("name", party.Name),
|
||||
log.String("status", status),
|
||||
log.String("status", payload.Status),
|
||||
log.Bool("logo_stored", logoFile != nil),
|
||||
)
|
||||
|
||||
@@ -323,10 +355,27 @@ func (h *enrichmentHandler) RecoverStale(ctx context.Context) error {
|
||||
)
|
||||
}
|
||||
|
||||
// runCompanyProfile builds Agent A with a per-run browser when a Chrome
|
||||
// endpoint is configured, then runs it. The browser is closed when the
|
||||
// run returns. It is not pinned to a domain so the agent can follow a
|
||||
// product site to the legal entity's corporate domain (where the legal
|
||||
// name and headquarters address live); SSRF protection still blocks
|
||||
// non-public hosts.
|
||||
func (h *enrichmentHandler) runCompanyProfile(
|
||||
ctx context.Context,
|
||||
party coredata.CommonThirdParty,
|
||||
) (CompanyProfileResult, error) {
|
||||
var browserTools []agent.Tool
|
||||
|
||||
if h.cfg.ChromeAddr != "" {
|
||||
webBrowser := browser.NewBrowser(ctx, h.cfg.ChromeAddr)
|
||||
defer webBrowser.Close()
|
||||
|
||||
browserTools = browser.NewReadOnlyToolset(webBrowser).Tools()
|
||||
}
|
||||
|
||||
companyAgent := buildCompanyProfileAgent(h.cfg, h.logger, browserTools)
|
||||
|
||||
prompt := buildCompanyProfilePrompt(party)
|
||||
|
||||
agentCtx, cancel := context.WithTimeout(ctx, h.cfg.AgentTimeout)
|
||||
@@ -334,7 +383,7 @@ func (h *enrichmentHandler) runCompanyProfile(
|
||||
|
||||
result, err := agent.RunTyped[CompanyProfileResult](
|
||||
agentCtx,
|
||||
h.companyAgent,
|
||||
companyAgent,
|
||||
[]llm.Message{
|
||||
{
|
||||
Role: llm.RoleUser,
|
||||
|
||||
Reference in New Issue
Block a user