Metadata-Version: 2.5
Name: enrichfold
Version: 0.4.0
Summary: Provider-neutral, provenance-first entity enrichment for people and companies.
Project-URL: Homepage, https://github.com/Mihailorama/enrichfold
Project-URL: Repository, https://github.com/Mihailorama/enrichfold
Project-URL: Issues, https://github.com/Mihailorama/enrichfold/issues
Author: Mihail R.
License: MIT
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# Enrichfold

Provider-neutral, provenance-first entity enrichment for people and companies.

`enrichfold` is an offline-first core, not a scraping product: applications supply
their own discovery providers and credentials. An optional Keenable adapter provides
web search when explicitly configured. Every accepted attribute retains its source
URL, observation time, and confidence, so downstream systems can decide whether a
result is suitable for an automated action or requires review.

```python
from enrichfold import Entity, EnrichmentPipeline, Evidence

class CompanyProvider:
    def discover(self, entity):
        return [Evidence(
            source_url="https://example.com/team",
            observed_at="2026-08-20T12:00:00Z",
            confidence=0.94,
            attributes={"industry": "software", "company_size": "51-200"},
        )]

company = Entity.company(domain="example.com")
result = EnrichmentPipeline([CompanyProvider()]).enrich(company)
print(result.attributes["industry"].value)  # software
```

## Evidence, conflicts, and review gates

`enrichfold` keeps provider I/O outside the library. Its core turns supplied
evidence into deterministic decisions while retaining disagreements for a human
approval flow. A conflicting value is never silently accepted:

```python
from enrichfold import Claim, Evidence, reconcile_claims

result = reconcile_claims([
    Claim(
        field="industry",
        value="software",
        evidence=Evidence(
            source_url="https://acme.example/about",
            observed_at="2026-08-20T12:00:00Z",
            confidence=0.91,
        ),
    ),
    Claim(
        field="industry",
        value="retail",
        evidence=Evidence(
            source_url="https://directory.example/acme",
            observed_at="2026-08-20T12:00:00Z",
            confidence=0.88,
        ),
    ),
])

industry = result.fields["industry"]
assert industry.value == "software"          # stable suggested value
assert industry.status == "needs_review"     # do not automate an action
assert result.requires_review is True
```

`Claim(kind="inferred", ...)` also requires review even with no competing
claim. This distinction makes it possible to keep model-produced hypotheses
without presenting them as observed facts.

## Multi-provider research runs

`ResearchEngine` is the boundary for applications that call several research
providers. It runs caller-owned adapters concurrently, reserves generic units
before starting work, retains each provider outcome, and returns an explicit
coverage/review state. It does not make network calls itself.

```python
from enrichfold import (
    Claim,
    Evidence,
    Entity,
    ProviderOutput,
    ProviderSpec,
    ResearchBudget,
    ResearchEngine,
)

def official_site(entity):
    # The host application owns this adapter, its HTTP client, and credentials.
    return ProviderOutput(
        claims=(Claim(
            field="industry",
            value="software",
            evidence=Evidence(
                source_url="https://example.com/about",
                observed_at="2026-08-20T12:00:00Z",
                confidence=0.9,
            ),
        ),),
        usage_units=2,
    )

result = ResearchEngine(
    [ProviderSpec("official-site", official_site, reserved_units=2)],
    budget=ResearchBudget(max_units=5),
).run(Entity.company(domain="example.com"), requested_fields=("industry",))

assert result.status in {"completed", "partial", "needs_review", "failed"}
assert result.budget.reserved_units == 2
```

An optional `EvidenceValidator` can return `EvidenceVerdict("needs_review",
reason)` for a weak source or `EvidenceVerdict("rejected", reason)` to keep
it out of resolution. In either case, the result preserves the original claim,
source URL, and verdict in `evidence_assessments`.

## Keenable search provider

`KeenableProvider` uses the same REST search contract as Scrapefold's Keenable
engine. It reads `KEENABLE_API_KEY` by default and performs one search per entity.
Because search results are sources rather than verified attributes, the caller maps
them to claims explicitly:

```python
from enrichfold import (
    Claim,
    Entity,
    Evidence,
    KeenableProvider,
    ProviderSpec,
    ResearchEngine,
)

def claims(entity, results):
    for result in results:
        yield Claim(
            field="summary",
            value=result["description"],
            kind="inferred",
            evidence=Evidence(
                source_url=result["url"],
                observed_at=result["acquired_at"],
                confidence=0.8,
                provider="keenable",
            ),
        )

provider = KeenableProvider(
    lambda entity: f'{entity.identifiers["domain"]} company',
    claims,
)
result = ResearchEngine([
    ProviderSpec("keenable", provider, reserved_units=1),
]).run(Entity.company(domain="example.com"), requested_fields=("summary",))
```

## Company identity gate

Before a caller enriches or acts on a company, use the offline identity gate.
It is deliberately conservative: free mailboxes, invalid sites, domain
conflicts, and corporate domains that do not exactly match the name receive a
review status. Applications can pass separately verified site metadata when
they have it.

```python
from enrichfold import derive_company_identity

identity = derive_company_identity(
    email="hello@acme.example",
    company_name="Acme",
    website="https://www.acme.example/about",
)

assert identity.status == "verified"
assert identity.canonical_domain == "acme.example"
```

## Design boundaries

- Core reconciliation and identity APIs make no network calls; explicit provider
  adapters such as `KeenableProvider` may do so.
- No inferred facts: a field is returned only when a provider supplies evidence.
- Conflicts have a deterministic suggested value but are marked `needs_review`.
- Inferred claims are always marked `needs_review`.
- Multi-provider runs reserve caller-defined generic units before execution and
  expose partial coverage rather than hiding failed or skipped providers.
- Optional source-policy hooks can accept, reject, or route evidence to review;
  the package never fetches or validates URLs on its own.
- Company identity is verified only through an exact name/domain match or
  caller-supplied, independently verified same-domain site metadata.
- Bring your own providers for search engines, public data APIs, browser tools, or
  internal approved sources.

The package intentionally does not decide whether a review is approved or run
an action after one; persistence, permissions, UI, and provider-specific claim
extraction stay with the host application.

## Installation

### Python

```bash
pip install enrichfold
```

### TypeScript / Node.js

The TypeScript companion currently exposes the same offline company identity
gate. It is intentionally a normal npm dependency, rather than a Python
subprocess hidden inside a web application:

```bash
npm install @mihailorama/enrichfold
```

Its provider runtime will follow as a compatible TypeScript surface; Python and
TypeScript package versions are released independently.

## Development

```bash
uv run --with pytest pytest -q
python -m build
```

## License

MIT.
