Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
151 lines
4.5 KiB
Python
151 lines
4.5 KiB
Python
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
|
# For license information, please see license.txt
|
|
|
|
"""EnrichmentResult schema. Preserves the {value, source, method} provenance shape.
|
|
|
|
Every piece of evidence keeps a ``source`` URL so any field can be traced back to
|
|
the page it was extracted from. Dataclasses serialize to plain dicts/JSON via
|
|
``to_dict()`` so the result can be returned by the engine and stored by the mapper /
|
|
run-writer (Phase 4) without any framework coupling. This module imports no framework
|
|
services on purpose -- it is the pure, unit-testable result container.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass, field
|
|
|
|
|
|
# Canonical extraction-method labels -- every extracted value records which one
|
|
# produced it, so a reviewer can judge how much to trust it.
|
|
class Method:
|
|
JSON_LD = "JSON-LD"
|
|
META_TAG = "Meta Tag"
|
|
TITLE_TAG = "Title Tag"
|
|
BODY_TEXT = "Body Text"
|
|
FAVICON = "Favicon"
|
|
REGEX = "Regex"
|
|
TEL_LINK = "tel: link"
|
|
TEXT_HEURISTIC = "Text Heuristic"
|
|
KEYWORD_CLASSIFIER = "Keyword Classifier"
|
|
SOCIAL_RULE = "Social Rule"
|
|
|
|
|
|
@dataclass
|
|
class Email:
|
|
value: str
|
|
source: str = ""
|
|
method: str = Method.REGEX
|
|
|
|
def to_dict(self):
|
|
return {"value": self.value, "source": self.source, "method": self.method}
|
|
|
|
|
|
@dataclass
|
|
class Phone:
|
|
value: str # normalized / cleaned
|
|
raw: str = "" # exactly as found on the page
|
|
source: str = ""
|
|
method: str = Method.REGEX
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"value": self.value,
|
|
"raw": self.raw,
|
|
"source": self.source,
|
|
"method": self.method,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class Field:
|
|
"""A scalar value plus where/how it came from (company_name, description,
|
|
logo, industry, ...)."""
|
|
|
|
value: object = ""
|
|
source: str = ""
|
|
method: str = ""
|
|
|
|
def to_dict(self):
|
|
return {"value": self.value, "source": self.source, "method": self.method}
|
|
|
|
|
|
@dataclass
|
|
class SocialProfile:
|
|
value: str = ""
|
|
source: str = ""
|
|
method: str = Method.SOCIAL_RULE
|
|
|
|
def to_dict(self):
|
|
return {"value": self.value, "source": self.source, "method": self.method}
|
|
|
|
|
|
@dataclass
|
|
class EnrichmentResult:
|
|
website: str = ""
|
|
# Scalar fields carry their own provenance via Field.
|
|
company_name: Field = field(default_factory=Field)
|
|
description: Field = field(default_factory=Field)
|
|
logo: Field = field(default_factory=Field) # the company link icon (favicon / apple-touch)
|
|
image: Field = field(default_factory=Field) # larger social/JSON-LD image; kept, not the logo
|
|
industry: Field = field(default_factory=Field)
|
|
industry_confidence: float = 0.0
|
|
|
|
emails: list = field(default_factory=list) # list[Email]
|
|
phones: list = field(default_factory=list) # list[Phone]
|
|
social_profiles: dict = field(default_factory=dict) # network -> SocialProfile
|
|
|
|
# Diagnostics -- not part of the required schema but invaluable for debugging.
|
|
pages_crawled: list = field(default_factory=list)
|
|
errors: list = field(default_factory=list)
|
|
notes: list = field(default_factory=list) # human-readable warnings
|
|
|
|
def to_dict(self):
|
|
"""Render the canonical JSON schema. Every field is explainable: scalar
|
|
fields expose {value, source, method}; list items embed source + method;
|
|
social profiles are {value, source, method} per network."""
|
|
return {
|
|
"company_name": self.company_name.to_dict(),
|
|
"description": self.description.to_dict(),
|
|
"logo": self.logo.to_dict(),
|
|
"image": self.image.to_dict(),
|
|
"industry": self.industry.to_dict(),
|
|
"industry_confidence": round(self.industry_confidence, 2),
|
|
"emails": [e.to_dict() for e in self.emails],
|
|
"phones": [p.to_dict() for p in self.phones],
|
|
"social_profiles": {k: v.to_dict() for k, v in self.social_profiles.items()},
|
|
"_meta": {
|
|
"website": self.website,
|
|
"pages_crawled": self.pages_crawled,
|
|
"errors": self.errors,
|
|
"notes": self.notes,
|
|
},
|
|
}
|
|
|
|
def flat(self):
|
|
"""Convenience: collapse to plain scalar values (no provenance), useful
|
|
for storing the headline fields on a Frappe document."""
|
|
return {
|
|
"company_name": self.company_name.value,
|
|
"description": self.description.value,
|
|
"logo": self.logo.value,
|
|
"industry": self.industry.value,
|
|
"industry_confidence": round(self.industry_confidence, 2),
|
|
"social_profiles": {k: v.value for k, v in self.social_profiles.items()},
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class CrawledPage:
|
|
"""A single fetched page and its parsed artifacts."""
|
|
|
|
url: str
|
|
status_code: int = 0
|
|
html: str = ""
|
|
text: str = ""
|
|
title: str = ""
|
|
headings: list = field(default_factory=list)
|
|
error: str = ""
|
|
|
|
def to_dict(self):
|
|
return asdict(self)
|