Skip to main content

A local-first fact gate for tool-using AI agents.

Project description

GroundGuard

GroundGuard logo

Assertions for AI agent answers - deterministic, local, no LLM judge.

Stop tool-using agents from ignoring the facts they already fetched. Important numeric claims must trace back to facts recorded from this run, and required tool facts cannot disappear silently.

Works with OpenAI | LangChain | LangGraph | PydanticAI | CrewAI | AutoGen | FastAPI | promptfoo | DeepEval | Langfuse | Phoenix | GitHub Actions

CI PyPI Python License Status Discussions

PyPI | Docs | Examples | Benchmark | Releases | Roadmap | Discussions | Contributing

English | Chinese README

Benchmark signal: GroundGuard catches 71/71 expected failures in a 200-case bilingual realistic dataset, with 0 false positives and 0 false negatives, without using an LLM judge.

from groundguard import FactGate

gate = FactGate()
gate.record_tool_result("q3_revenue", "5.2", "billion_usd")  # from a tool call
report = gate.check("Q3 revenue came in at $4.8 billion [fact:q3_revenue].", required=["q3_revenue"])
print(report.output_claims[0].status)  # contradicted

The answer says 4.8B; the ledger says 5.2B, so the claim is stopped before release.

GroundGuard demo: omitted required facts blocked in red, corrected answer verified in green

Why GroundGuard?

Tool-using agents often fail in ways that look normal:

  • A tool returns the right data, but the model says the data was unavailable.
  • A tool returns no data, but the model invents a confident-looking number.
  • A final answer cites numbers that cannot be traced to the current tool run.

The dangerous version is mundane: your agent fetched Q3 revenue, then writes last year's number into a report for leadership. Nothing crashes. The answer just looks professional enough to pass human review.

Tracing tools show what happened. LLM-as-judge tools score an answer after it is written. GroundGuard fills a smaller gap: it gives the final answer a deterministic, testable fact gate before you let it pass.

GroundGuard started from a problem I kept seeing in my own workflow: tools had already returned the data, but the model's final answer still contained numbers that were not reliably checked. Sometimes the model rewrote a number incorrectly. Sometimes it omitted a key fact that the tool had already returned. I wanted the path from tool data to final answer to be transparent and traceable, with a ledger check that can confirm whether the generated numbers match the facts that were actually retrieved.

In plain terms, GroundGuard works like bookkeeping for agent facts:

  1. When a tool returns an important value, you explicitly record it in a local ledger.
  2. When the model writes the final answer, GroundGuard extracts the numeric claims it made.
  3. GroundGuard compares the answer against the ledger and reports what was verified, missing, invented, or contradicted.
  4. Your policy decides whether to only flag the issue, strip unsafe claims, or block the answer before it reaches the user.

That means the model can still write naturally, but the key numbers have to match the facts your tools actually returned.

Quick Start

python -m pip install groundguard-ai
groundguard-init --template github-action
groundguard-demo
groundguard-benchmark

The PyPI distribution name is groundguard-ai; the Python import name remains groundguard. Latest PyPI release: 0.3.1.

10-Second Demo

python examples/financial_report_demo/run.py

The bundled demo reproduces the failure mode GroundGuard is built for:

Before GroundGuard correction
-----------------------------
passed: False
verified: 0
unverified: 0
contradicted: 0
omitted_required: 2
policy_reason: omitted_required_count=2 > max_omitted_required=0

After fact-key correction
-------------------------
passed: True
verified: 2
unverified: 0
contradicted: 0
omitted_required: 0

What Works Today

  • In-memory Ledger with TTL filtering and JSONL persistence.
  • Explicit tool_call(...).record_facts(...) registration.
  • Deterministic numeric claim extraction with [fact:key] markers, Chinese amounts, percentages, compact English magnitudes, and common English USD formats.
  • Extraction transparency: CoverageReport.suspected_numbers, uncovered_numbers, and extraction_coverage show which numeric-looking spans were seen but not covered by extractors.
  • Pluggable extractor registry through register_extractor(...) / unregister_extractor(...), plus request-scoped extractor lists for multi-tenant services that must not mutate global process state.
  • Matching statuses: verified, candidate_match, unverified, contradicted, and ambiguous.
  • Required fact coverage checks for "tool had data, model omitted it" failures.
  • CoverageReport and configurable Policy evaluation.
  • grounded_generate() with report return, blocking, conservative stripping, optional tagged-claim repair, and one-shot reask.
  • @grounded(...) decorator for framework-free Python functions.
  • FactGate high-level runtime API for config-driven record/check flows.
  • groundguard-init starter templates for GitHub Actions, OpenAI, promptfoo, LangGraph, PydanticAI, CrewAI, AutoGen, and FastAPI.
  • groundguard-report CLI with native and assertion-style JSON schemas, including per-claim componentResults, Markdown, HTML, and GitHub comment renderers.
  • Built-in extractor packs for finance, SaaS, ecommerce, and ops metrics.
  • Dedicated promptfoo and DeepEval adapter helpers.
  • Dependency-free OpenTelemetry-style event export.
  • OpenAI-compatible wrapper, LangChain-compatible callback, and LangGraph-style node examples.
  • A composite GitHub Action for CI fact gates.
  • A tiny dependency-free HTTP server entrypoint for gateway-style checks.

Installation

Install the GitHub tag directly:

python -m pip install groundguard-ai

For local development:

git clone https://github.com/chasen2041maker/GroundGuard.git
cd GroundGuard
python -m pip install -e ".[dev]"
python -m pytest

For the live OpenAI SDK example, install the optional SDK extra after cloning:

python -m pip install -e ".[openai]"

Python API

from decimal import Decimal

from groundguard import Ledger, Policy, grounded_generate, tool_call


def fetch_financials(ticker: str) -> dict[str, str]:
    return {
        "ticker": ticker,
        "net_profit": "823200000",
        "revenue": "3830000000",
    }


def fake_llm(prompt: str) -> str:
    return (
        "Revenue was $3.83 billion [fact:revenue_2025], "
        "and net profit was $823.2 million [fact:net_profit_2025]."
    )


with Ledger(session_id="req_001") as ledger:
    with tool_call("get_company_financials", {"ticker": "ACME"}, ledger) as call:
        result = fetch_financials("ACME")
        call.record_facts(
            {
                "net_profit_2025": (Decimal(result["net_profit"]), "USD"),
                "revenue_2025": (Decimal(result["revenue"]), "USD"),
            },
            raw=result,
        )

    result = grounded_generate(
        prompt="Summarize the latest financial performance.",
        llm_call=fake_llm,
        ledger=ledger,
        required_fact_keys=["net_profit_2025", "revenue_2025"],
        policy=Policy(on_unverified="flag"),
        return_report=True,
    )

print(result.answer)
print(result.report.passed)

Core Concepts

Concept What It Means Why It Matters
Fact A verifiable value explicitly registered from a tool call. The only source GroundGuard treats as evidence.
RequiredFact A fact key the current answer must cover. Catches cases where the tool had data but the model ignored it.
OutputClaim A numeric claim extracted from the final answer. Lets GroundGuard verify what the model actually wrote.
CoverageReport The final reconciliation report. Shows verified, candidate, unverified, contradicted, omitted facts, and uncovered numeric-looking spans.
Policy Pass/fail thresholds and handling behavior. Lets you flag, strip, block, repair tagged contradictions, or reask once.

How It Works

flowchart LR
    A["Tool call"] --> B["Explicit fact registration"]
    B --> C["Ledger"]
    D["Model final answer"] --> E["Output claim extraction"]
    C --> F["Matcher"]
    E --> F
    F --> G["CoverageReport"]
    H["Required fact keys"] --> G
    G --> I["Policy: flag / strip / block / fix / reask"]

GroundGuard v1 is deterministic by design: no hosted service, no database, no second LLM, and no token-level generation control claims.

Current claim extraction is intentionally narrow: it extracts numeric claims that include a unit or magnitude marker, such as $3.83 billion, USD 10.25M, 1.2M, 3,830 million dollars, 21.5%, or 21.5 percent. Bare numbers without units are not treated as verified claims to avoid false positives. They are still exposed in CoverageReport.uncovered_numbers, so users can see what the deterministic extractors did not cover.

Registered numeric facts are normalized before matching. For example, (Decimal("3.83"), "usd_b"), (Decimal("3830"), "usd_m"), and (Decimal("3830000000"), "USD") all compare as the same USD value.

Custom extractors can be registered when your domain needs additional claim types:

from groundguard import OutputClaim, register_extractor


@register_extractor("ticker_entity")
def extract_tickers(text: str) -> list[OutputClaim]:
    return []

For multi-tenant services, prefer request-scoped extractors so one tenant's rules do not affect another tenant in the same Python process:

from groundguard import extract_output_claims, registered_extractors

claims = extract_output_claims(
    answer,
    extractors=registered_extractors() | {"ticker_entity": extract_tickers},
)

report = ledger.coverage_report(
    answer,
    extractors=registered_extractors() | {"ticker_entity": extract_tickers},
)

register_extractor(...) is process-global and is best used at application startup, not dynamically per request.

Where It Fits

Tool family What it is great at Where GroundGuard fits
Langfuse / Phoenix Traces, sessions, observability, debugging timelines. Adds a deterministic final-answer gate before release.
promptfoo / DeepEval Test suites, eval assertions, scorecards. Emits assertion-style JSON so fact coverage can be one metric in those suites.
LLM-as-judge evaluators Semantic quality checks and subjective grading. Avoids a second model for facts that already came from tools.
Constrained decoding Token-level format or grammar control. Checks post-generation factual coverage; it does not claim token-level control.

GroundGuard is deliberately narrow: it asks whether the final answer covered and cited the facts your tools already returned.

Benchmark

groundguard-benchmark

The bundled benchmark now runs two deterministic suites:

  • a 25-case smoke suite for the core fact-gate contract,
  • a 200-case bilingual realistic dataset covering English and Chinese outputs, currencies, percentages, basis points, users, orders, tickets, latency, storage units, candidate matches, omissions, contradictions, ambiguity, and bare-number extraction limits.

Expected signal:

smoke.cases_total: 25
smoke.expected_failures: 14
smoke.detected_failures: 14
smoke.false_positives: 0
realistic_dataset.cases_total: 200
realistic_dataset.expected_failures: 71
realistic_dataset.detected_failures: 71
realistic_dataset.false_positives: 0
realistic_dataset.false_negatives: 0

CLI

For application code, the shortest stable entrypoint is FactGate:

from decimal import Decimal

from groundguard import FactGate

gate = FactGate.from_config("groundguard.yml")
gate.record_tool_result("revenue_2025", Decimal("3.83"), "billion_usd")

report = gate.check(
    "Revenue was $3.83 billion.",
    required=["revenue_2025"],
)

assert report.passed

Generate a JSON coverage report from a ledger JSONL file and an answer file:

groundguard-report \
  --ledger-jsonl facts.jsonl \
  --answer-file answer.txt \
  --required-fact net_profit_2025 \
  --required-fact revenue_2025 \
  --fail-on-policy

Without an installed console script:

python -m groundguard.cli.report --ledger-jsonl facts.jsonl --answer-file answer.txt

Use a config file for CI and repeatable eval runs:

cp groundguard.example.yml groundguard.yml
groundguard-report \
  --config groundguard.yml \
  --ledger-jsonl facts.jsonl \
  --answer-file answer.txt

Emit a promptfoo/DeepEval-friendly assertion payload:

groundguard-report \
  --ledger-jsonl facts.jsonl \
  --answer-file answer.txt \
  --required-fact net_profit_2025 \
  --schema assertion

The assertion schema includes pass, success, score, reason, namedScores, top-level claims, per-claim componentResults, and the full GroundGuard report under metadata.groundguard. Each claim exposes text_span, start, end, status, matched_fact_key, ledger_value, answer_value, and diff for downstream UI highlighting.

Render human-readable artifacts for PRs and audits:

groundguard-report \
  --ledger-jsonl facts.jsonl \
  --answer-file answer.txt \
  --required-fact revenue_2025 \
  --format markdown \
  --output groundguard-report.md

groundguard-report \
  --ledger-jsonl facts.jsonl \
  --answer-file answer.txt \
  --format html \
  --output groundguard-report.html

groundguard.yml can also select scoped extractor packs:

extractors:
  packs:
    - finance
    - saas
    - ops
units:
  tolerance: 0.005
report:
  schema: assertion
  format: github

Schema Compatibility

GroundGuard treats Fact, OutputClaim, CoverageReport, Policy, AssertionReport, and DatasetCase as public protocol objects. Each object includes schema_version. GroundGuard will not remove or rename fields within a major version; new fields may be added with defaults so existing integrations continue to read older payloads.

Published schema files:

GitHub Action

Use the composite action in another repository:

- name: Run GroundGuard
  uses: chasen2041maker/GroundGuard@v0.3.1
  with:
    ledger-jsonl: groundguard-ledger.jsonl
    answer-file: answer.txt
    config: groundguard.yml
    required-facts: net_profit_2025,revenue_2025
    schema: assertion
    fail-on-policy: "true"

For a PR comment example, see docs/examples/github-actions/pr-comment.yml.

Adapters

OpenAI-compatible chat wrapper:

from groundguard.adapters import openai_chat_llm

llm_call = openai_chat_llm(
    client.chat.completions.create,
    model="gpt-4.1-mini",
)

LangChain-compatible callback handler:

from decimal import Decimal

from groundguard.adapters import GroundGuardCallbackHandler

handler = GroundGuardCallbackHandler(
    ledger=ledger,
    fact_mapper=lambda output, context: {
        "net_profit_2025": (Decimal(output["net_profit"]), "CNY"),
    },
)

The callback handler intentionally requires an explicit fact_mapper; v1 does not guess which arbitrary JSON fields are evidence.

promptfoo / DeepEval helper functions:

from groundguard.integrations.deepeval import to_deepeval_result
from groundguard.integrations.promptfoo import to_promptfoo_assertion

promptfoo_payload = to_promptfoo_assertion(report)
deepeval_payload = to_deepeval_result(report)

Runnable Examples

Packaged commands:

groundguard-demo
groundguard-demo --json
groundguard-benchmark

Repository examples after cloning:

python examples/financial_report_demo/run.py
python examples/decorator_demo/run.py
python examples/langgraph_node/run.py
python examples/openai_demo/run.py
python examples/promptfoo_groundguard/run.py
python examples/deepeval_groundguard/run.py

examples/openai_demo/run.py records tool facts, shows an answer blocked for omitting those required facts, then shows the corrected fact-key answer passing.

For a live OpenAI SDK call:

export OPENAI_API_KEY=...
python examples/openai_demo/run.py --live-openai

What GroundGuard Is Not

  • Not a tracing or observability platform; export reports to Langfuse, Phoenix, or OpenTelemetry instead.
  • Not an LLM-as-judge or general hallucination detector; it checks deterministic ledger-vs-answer assertions.
  • Not a database or hosted service; it runs locally, and important numbers need a unit, magnitude, or fact marker to be checked.

Roadmap

  • Milestone 1: Core library - Ledger, claim extraction, matching, policy, grounded_generate, and the financial report demo. Implemented.
  • Milestone 2: Framework adapters - OpenAI wrapper, LangChain callback, LangGraph-style node example, and native @grounded(...) decorator. Starter coverage is implemented; more framework recipes are welcome.
  • Milestone 3: CI integration - Assertion-style JSON, composite GitHub Action, PR comment workflow example, promptfoo/DeepEval helper adapters, and per-claim component results. Implemented.
  • Milestone 4: Visualization - Deferred until the core library and distribution path are stable.

Documentation

Security

Ledger data may contain prompts, tool outputs, and sensitive business data. GroundGuard is local-first and does not upload data by default. Always anonymize fixtures, reports, and examples before sharing them publicly.

Contributing

Contributions are welcome, especially:

  • anonymized "tool had data, model missed it" examples
  • claim extraction and matching improvements
  • framework integration examples
  • API design feedback

Please read CONTRIBUTING.md before opening a larger pull request.

License

GroundGuard is released under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

groundguard_ai-0.3.1.tar.gz (70.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

groundguard_ai-0.3.1-py3-none-any.whl (60.9 kB view details)

Uploaded Python 3

File details

Details for the file groundguard_ai-0.3.1.tar.gz.

File metadata

  • Download URL: groundguard_ai-0.3.1.tar.gz
  • Upload date:
  • Size: 70.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for groundguard_ai-0.3.1.tar.gz
Algorithm Hash digest
SHA256 8e35032ab37c8387a5fcd82e813f4c8459f572efda17fac765ddf06e08c554a2
MD5 1b771826e4b3901f66fd8b2bf8d6f345
BLAKE2b-256 41fac063aa7eade627bfbc4cbf5424aca077f59e344a550c5f746ed86ba88033

See more details on using hashes here.

Provenance

The following attestation bundles were made for groundguard_ai-0.3.1.tar.gz:

Publisher: publish.yml on chasen2041maker/GroundGuard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file groundguard_ai-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: groundguard_ai-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 60.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for groundguard_ai-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 167eb9b5a4cb2f52dec4908f33842b4a9d27ca073e93d8d3f5635d3ac5b5a541
MD5 214e84495b3fc0f1eb5242d85ea4f29a
BLAKE2b-256 5e883e9b994ab4aa96282a8ff55a2d5d8b44fc0d2593171d48a08c1449995105

See more details on using hashes here.

Provenance

The following attestation bundles were made for groundguard_ai-0.3.1-py3-none-any.whl:

Publisher: publish.yml on chasen2041maker/GroundGuard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page