Build an AI Supplier Risk Analyzer

Use Google Gemini to analyze supplier PDFs and photos for due diligence.

Introduction

30 Second Summary

Every year, small importers lose thousands of dollars to fraudulent or unreliable suppliers. Vetting a new supplier manually means hours of reading PDFs, cross-checking certifications, and scrutinizing factory photos for red flags.

In this project, you will build a Python command-line tool called SupplyLens that sends supplier documents and factory photos to Google's Gemini AI. Within seconds, it returns a structured risk card with extracted facts, red flags, and a clear pass/warn/fail verdict.

What You'll Build

You drop a supplier PDF into your terminal, hit Enter, and within seconds a color-coded risk card appears telling you exactly which claims look suspicious and whether to proceed.

By the end of this project, you'll have:

  • A working CLI tool that reads a supplier PDF and returns a structured risk assessment with extracted company details, certifications claimed, and payment terms.
  • An image analysis capability that flags red flags in factory photos covering equipment condition, cleanliness, safety compliance, and signs of stock imagery.
  • A color-coded pass/warn/fail verdict on a formatted JSON risk card you could share with a business partner.
  • Secret Mission: Combine PDF and factory photos in a single multimodal request to cross-reference document claims against visual evidence.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, and file I/O). No prior API experience is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment

Before SupplyLens can analyze a single supplier document, you need three things in place: Python to run the code, the Google GenAI SDK to communicate with Gemini, and an API key to authenticate your requests.

This step gets all three installed and wired together. By the end, you'll run a script that talks to Gemini and prints a response, proving the full connection works.

In this step, get ready to:
  • Verify Python 3.10+ is installed and install the Google GenAI SDK.
  • Get a free Gemini API key from Google AI Studio.
  • Create the project folder, write the connection script, and confirm it prints a Gemini response.
Verify Python and install the SDK

The Google GenAI SDK requires Python 3.10 or higher. Let's confirm you have the right version, then install the SDK.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).

Check your Python version by running the appropriate command for your operating system.

macOS

  • Check your Python version by running this command:
python3 --version

Windows

  • Check your Python version by running this command:
python --version

✔️ I see version 3.10 or higher

You're all set. Your output looks something like Python 3.10.x or higher. Move on to installing the SDK below.

ⓧ I see an older version

Your system has Python installed, but it is below 3.10. On macOS, the system default is often an older version even if you installed a newer one separately.

  • Download the latest Python from python.org/downloads.
  • Run the installer and follow the prompts.
  • After installing, close and reopen your VS Code terminal, then re-run the version check command.
  • If macOS still shows the old version with python3 --version, try running python3.10 --version (or whichever version you installed) to confirm the new install is available.

ⓧ Command not found

Python is not installed on your system yet.

  • Download Python 3.10+ from python.org/downloads.
  • Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
  • Close and reopen your VS Code terminal, then re-run the version check command to confirm.
  • Install the Google GenAI SDK by running this command:
pip install google-genai

What does this install?

The google-genai package is Google's official Python SDK for the Gemini API. It handles authentication, request formatting, and response parsing so you can focus on what you send to the model.

  • Verify the SDK installed correctly by running this command:
python -c "from google import genai; print('SDK ready')"

macOS and Linux users

If python does not work, use python3 instead. Use whichever command showed the correct version earlier.

You should see SDK ready printed in your terminal.

Seeing ModuleNotFoundError?

If you see ModuleNotFoundError: No module named 'google', your pip and python commands may point to different Python installations.

  • On macOS/Linux, try pip3 install google-genai instead, then re-run the verify command with python3.
  • On Windows, make sure you installed Python with Add Python to PATH checked.

help me fix this ModuleNotFoundError for google-genai

Get your Gemini API key

The Gemini API uses an API key to authenticate every request you make. Google AI Studio provides free-tier keys with no credit card required.

  • Open https://aistudio.google.com/app/apikey in your browser.
  • Sign in with your Google account.
  • Click Create API key.
  • If prompted to select a project, choose the default project that AI Studio created for you.
  • Copy your new API key and keep it in your clipboard. You will paste it into a file in the next substep.

Why is this free?

Gemini 2.5 Flash offers free-of-charge input and output tokens on the free tier. No billing account or credit card is needed. For a learning project with a small number of API calls, the free tier is more than enough.

Keep your API key private

Never share your API key publicly or commit it to a Git repository. In the next substep, you will store it in a .env file that stays on your machine only.

Create the project structure and verify the connection

Now you will create a project folder, store your API key safely, and write a short script that proves the full pipeline works.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir supplylens
cd supplylens
  • Confirm the folder exists by running:
ls

The folder is empty for now. Next, you will add two files to it.

  • Create the .env file to store your API key by running this command:
echo "GEMINI_API_KEY=your-api-key-here" > .env
  • Open the .env file and replace your-api-key-here with the actual API key you copied from Google AI Studio.
  • Save the file.
  • Verify the .env file was created by running:
ls -a

You should see .env listed in the output.

  • Open the supplylens folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the supplylens folder, and click Open.
  • If VS Code asks whether you trust the authors of this folder, click Yes, I trust the authors.
  • Open the terminal again with Ctrl+` if it closed.

Now create the main Python script.

  • Create a new file called supplylens.py by clicking the new file icon in the Explorer sidebar (or press Ctrl+N then Ctrl+S and name it supplylens.py).
  • Add the imports and the API key loading function by copying this code into supplylens.py:
import sys
import json
import base64
import pathlib
import os

from google import genai
from google.genai import types

# Load API key from .env file
def load_api_key():
    env_path = pathlib.Path(".env")
    if env_path.exists():
        for line in env_path.read_text().splitlines():
            if line.startswith("GEMINI_API_KEY="):
                return line.split("=", 1)[1].strip()
    key = os.environ.get("GEMINI_API_KEY")
    if not key:
        print("Error: No API key found. Set GEMINI_API_KEY in .env or environment.")
        sys.exit(1)
    return key

What does this code do?

  • The imports bring in libraries for file handling (pathlib, os), JSON parsing, and the Google GenAI SDK.
  • The load_api_key() function reads your .env file line by line, looking for GEMINI_API_KEY=. If it finds it, it returns the value after the equals sign.
  • If the .env file is missing or the key is not set, it falls back to checking the system environment variable before exiting with an error.
  • Add the client initialization and test code below the load_api_key function in supplylens.py:


# Initialize the Gemini client
api_key = load_api_key()
client = genai.Client(api_key=api_key)

MODEL = "gemini-2.5-flash"

if __name__ == "__main__":
    response = client.models.generate_content(
        model=MODEL,
        contents=["Say hello from SupplyLens!"]
    )
    print(response.text)

What does this code do?

  • The load_api_key() call retrieves your key from .env and passes it to genai.Client() to create an authenticated client.
  • The MODEL variable sets which Gemini model to use. gemini-2.5-flash is the free-tier model with multimodal capabilities.
  • The if __name__ == "__main__" block runs only when you execute the script directly. It sends a simple prompt to Gemini and prints whatever comes back.
  • Save supplylens.py.

Before you run the script, what do you think will happen if the API key is correct and the SDK is working?

  • Run the script by executing this command in your terminal:
python supplylens.py

macOS and Linux users

If python does not work, use python3 supplylens.py instead. Use whichever command showed the correct version earlier.

You should see Gemini respond with a greeting message. The exact wording varies each time, but it proves your API key, SDK, and network connection all work.

Seeing an API key error?

  • Make sure your .env file is in the same folder as supplylens.py (the supplylens folder on your Desktop).
  • Check that there are no extra spaces or quotes around the key in .env. The format should be exactly GEMINI_API_KEY=AIza... with no quotes.
  • Confirm you copied the full key from Google AI Studio.

help me fix my Gemini API key connection error in SupplyLens

✔️ Awesome, I've got everything!

Great. Double check you have saved both files.

ⓧ I'd like to double check the full code

Here is the full .env file:

GEMINI_API_KEY=your-api-key-here

For supplylens.py, scroll up and confirm both chunks are present in this order:

  • Chunk 1: the imports (import sys through from google.genai import types) followed by the load_api_key() function.
  • Chunk 2: the client initialization (api_key = load_api_key(), client = genai.Client(api_key=api_key), MODEL = "gemini-2.5-flash") and the if __name__ == "__main__" block that sends a test prompt.

Your environment is locked in and Gemini is responding. Next up, you will feed it a real supplier PDF and watch it extract structured facts in seconds.

Analyze Your First Supplier PDF

Your connection to Gemini is live and responding. Now it's time to put it to real work.

The core value of SupplyLens is turning a 20-page supplier document into structured facts in seconds. In this step, you'll feed a supplier PDF to the Gemini API and get back a structured risk assessment with extracted company details, certifications, and red flags.

In this step, get ready to:
  • Create a sample supplier PDF for testing.
  • Build the PDF analysis function with structured due-diligence prompts.
  • Wire up the command-line interface to accept a file path and display results.
Create a sample supplier PDF for testing

Before you can analyze a supplier document, you need one to test with. You'll create a short fake supplier brochure with enough detail for Gemini to extract facts and spot red flags.

  • Open any word processor (Google Docs, Microsoft Word, or Apple Pages).
  • Create a new document and paste in the following supplier information:
GOLDEN DRAGON MANUFACTURING CO., LTD.

Address: Building 7, Industrial Zone, Guangzhou, China
Email: sales@gmail.com
Established: 2019
Business License: GZ-2023-88412

Certifications: ISO 9001, CE Marking, RoHS Compliant

Product Categories:
- Consumer electronics (Bluetooth speakers, earbuds, power banks)
- Home appliances (air purifiers, humidifiers)
- Automotive accessories (dash cams, LED lights)
- Pet products (automatic feeders, GPS collars)

Production Capacity: 500,000 units/month
Minimum Order: 100 pieces
Payment Terms: 100% T/T in advance for first order

We have 15 years of experience serving customers worldwide.

Why this content?

This fake brochure contains several deliberate red flags that real-world due diligence would catch. The Gmail address doesn't match the company domain. The business license is from 2023 but claims 15 years of experience. Payment is 100% upfront. The product range spans unrelated categories.

  • Export or save the document as a PDF file named sample_supplier.pdf.
  • Move sample_supplier.pdf into your supplylens project folder.
  • Confirm the file is in the right place by running:
ls sample_supplier.pdf

You should see sample_supplier.pdf listed in the output.

Don't see the file?

Make sure you saved/moved the PDF into the supplylens folder, not your Downloads folder or Desktop. Run pwd to confirm your terminal is inside the supplylens directory.

help me find where my sample_supplier.pdf ended up

Define the due-diligence criteria

A general prompt like "analyze this PDF" would return vague summaries. For actionable supplier vetting, the AI needs specific criteria to check against.

  • In supplylens.py, find the line that says MODEL = "gemini-2.5-flash".
  • Below that line, add the RISK_CRITERIA constant by pasting this code:
# Due diligence criteria encoded as system context
RISK_CRITERIA = """
You are SupplyLens, an AI supplier vetting assistant for small import businesses.

Analyze supplier documents and factory photos using these due-diligence criteria:

RED FLAGS TO CHECK:
- Missing or unverifiable ISO/quality certifications
- Vague or incomplete business address (no street number, PO box only, residential address)
- Payment terms requesting 100% upfront or payment to personal bank accounts
- Prices significantly below market (25%+ below comparable quotes)
- Business license less than 12 months old but claiming years of experience
- Email domain not matching company domain (Gmail/Yahoo/QQ for business communications)
- Claims of manufacturing capability across too many unrelated product categories
- No evidence of quality control processes or testing equipment
- Stock photos used instead of actual facility images
- Refusal or inability to provide certificate numbers for verification

FACTORY PHOTO ASSESSMENT:
- Equipment condition and maintenance (rust, damage, outdated machinery)
- Organization and cleanliness (cluttered floors, poor material storage)
- Safety compliance (fire exits blocked, no safety gear, missing signage)
- Signs of active production (raw materials, work in progress, finished goods)
- Scale consistency (does facility size match claimed production capacity?)
- QC evidence (inspection stations, measurement tools, testing equipment)
"""

What does this code do?

RISK_CRITERIA is a long string that acts as the AI's "expertise" in supplier vetting. It encodes real due-diligence red flags that import professionals check for: mismatched email domains, 100% upfront payment demands, and suspiciously broad product ranges.

By embedding these criteria directly in the prompt, you turn a general-purpose AI into a domain-specific vetting assistant.

  • Directly below RISK_CRITERIA, add the RISK_CARD_SCHEMA constant that tells Gemini exactly what JSON shape to return:
RISK_CARD_SCHEMA = """
Respond ONLY with valid JSON matching this exact schema:
{
    "verdict": "PASS" or "WARN" or "FAIL",
    "risk_score": <integer 1-10, where 1 is lowest risk and 10 is highest risk>,
    "supplier_name": "<extracted company name or 'Unknown'>",
    "extracted_facts": {
        "company_name": "<name or null>",
        "address": "<full address or null>",
        "certifications_claimed": ["<list of certifications mentioned>"],
        "payment_terms": "<stated payment terms or null>",
        "production_claims": "<key production/capacity claims or null>",
        "years_in_business": "<stated years or null>"
    },
    "red_flags": [
        {
            "flag": "<short description of the red flag>",
            "severity": "high" or "medium" or "low",
            "evidence": "<what in the document/photo triggered this flag>"
        }
    ],
    "positive_signals": ["<list of positive indicators found>"],
    "recommendations": ["<list of recommended next steps for the buyer>"]
}
"""

What does this schema do?

RISK_CARD_SCHEMA is a structured prompt engineering technique. Instead of getting back a free-form paragraph, you tell the model exactly what JSON structure to return.

This makes the output machine-readable. Later steps will parse this JSON to display a formatted risk card with color-coded verdicts.

  • Save supplylens.py and verify the constants don't break anything by running:
python supplylens.py

You should still see a response from Gemini (the old "Say hello" test). This confirms your new constants have no syntax errors.

Seeing a SyntaxError?

Check that both triple-quote strings (""") have matching opening and closing quotes. A missing closing """ is the most common mistake with multi-line strings.

help me fix a SyntaxError in my supplylens.py after adding the RISK_CRITERIA and RISK_CARD_SCHEMA constants

Wire up the command-line interface

The criteria and schema are ready. Now you need a function that reads a PDF, sends it to Gemini with those prompts, and a CLI entry point that ties it all together.

  • In supplylens.py, add the analyze_pdf function below RISK_CARD_SCHEMA:
def analyze_pdf(filepath):
    """Analyze a supplier PDF document and return raw text analysis."""
    pdf_path = pathlib.Path(filepath)
    if not pdf_path.exists():
        print(f"Error: File not found: {filepath}")
        sys.exit(1)

    pdf_bytes = pdf_path.read_bytes()

    prompt = f"""{RISK_CRITERIA}

Analyze the following supplier document. Extract key facts and identify any red flags.

{RISK_CARD_SCHEMA}
"""

    response = client.models.generate_content(
        model=MODEL,
        contents=[
            types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf"),
            prompt,
        ],
    )

    return response.text

What does this function do?

pdf_path.read_bytes() loads the entire PDF as raw bytes. Gemini can natively understand PDF content when you send it as a types.Part.from_bytes with the application/pdf MIME type.

The prompt combines your risk criteria and the JSON schema, so Gemini knows both what to look for and how to format the answer. This is a multimodal API call because you're sending both a document and text in a single request.

  • Now replace the entire if __name__ == "__main__": block at the bottom of the file with this main() function:
def main():
    if len(sys.argv) < 2:
        print("Usage: python supplylens.py <file.pdf> [--image <photo.jpg>]")
        print("       python supplylens.py --image <photo.jpg>")
        sys.exit(1)

    pdf_path = sys.argv[1]
    print(f"\nAnalyzing supplier document: {pdf_path}")
    print("This may take 10-30 seconds...\n")
    raw = analyze_pdf(pdf_path)
    print(raw)


if __name__ == "__main__":
    main()

What does this code do?

sys.argv[1] grabs the first argument the learner passes on the command line (the PDF file path). If no argument is provided, the script prints usage instructions and exits.

The main() function calls analyze_pdf() and prints the raw JSON response from Gemini. Later steps will parse and format this output into a color-coded risk card.

  • Save supplylens.py.

Before you run this, what do you think the output will look like? Will it be plain English paragraphs, or the JSON structure you requested in RISK_CARD_SCHEMA?

  • Run SupplyLens against your sample PDF:
python supplylens.py sample_supplier.pdf

After 10-30 seconds, you should see JSON output containing the supplier name (Golden Dragon Manufacturing), the address, certifications claimed, and red flags like the Gmail email domain and 100% upfront payment terms.

Seeing an error about the file not being found?

Make sure sample_supplier.pdf is in the same folder as supplylens.py. Run ls to confirm both files are listed together.

If you see a rate limit error from the API, wait 60 seconds and try again. The free tier processes requests on a best-effort basis.

help me debug why my supplylens.py can't find or analyze my sample PDF

Use your own supplier documents

You can replace sample_supplier.pdf with any real supplier brochure, audit report, or product catalog you have. Gemini handles PDFs up to 50MB using the inline method.

✔️ Awesome, I've got everything!

Great. Double check you've saved supplylens.py and that your terminal showed JSON output with extracted facts and red flags.

ⓧ I'd like to double check the full code

Your supplylens.py should now contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (sys, json, base64, pathlib, os, genai, types).
  • load_api_key() function.
  • Client initialization (api_key, client, MODEL).
  • RISK_CRITERIA multi-line string with red flags and factory photo criteria.
  • RISK_CARD_SCHEMA multi-line string with the JSON schema.
  • analyze_pdf() function that reads bytes and calls client.models.generate_content.
  • main() function with sys.argv argument parsing.
  • if __name__ == "__main__": main() at the very bottom.

You've just turned a PDF into a structured risk assessment in under 30 seconds. Next up, you'll add factory photo analysis so SupplyLens can spot red flags that documents alone can't reveal.

Add Factory Photo Analysis

Your tool can already read a supplier PDF and pull out key facts. That's powerful, but documents only tell one side of the story.

Factory photos reveal ground truth that text can hide. A cluttered floor, missing safety gear, or obvious stock photos are red flags that no PDF analysis catches. This is where multimodal AI earns its keep. In this step, you'll teach SupplyLens to see.

In this step, get ready to:
  • Build an image analysis function that sends photos to Gemini for facility assessment.
  • Update the CLI to accept an --image flag for photo analysis.
  • Test with contrasting factory photos and compare what the AI catches.
Build the image analysis function

The Gemini API accepts images the same way it accepts PDFs. You read the file as bytes and send it with types.Part.from_bytes. The difference is you need to tell the API what image format you're sending (JPEG or PNG).

  • In supplylens.py, find the analyze_pdf() function.
  • Add the following analyze_image() function directly below it:
def analyze_image(filepath):
    """Analyze a factory/facility photo and return assessment."""
    img_path = pathlib.Path(filepath)
    if not img_path.exists():
        print(f"Error: File not found: {filepath}")
        sys.exit(1)

    img_bytes = img_path.read_bytes()

    # Determine MIME type from extension
    suffix = img_path.suffix.lower()
    mime_map = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png"}
    mime_type = mime_map.get(suffix, "image/jpeg")

    prompt = f"""{RISK_CRITERIA}

Analyze this factory/facility photo. Assess the facility condition and identify any red flags.

{RISK_CARD_SCHEMA}
"""

    response = client.models.generate_content(
        model=MODEL,
        contents=[
            types.Part.from_bytes(data=img_bytes, mime_type=mime_type),
            prompt,
        ],
    )

    return response.text

What does this code do?

This function mirrors analyze_pdf() but handles images instead of documents.

  • mime_map maps file extensions to the correct MIME type so the API knows whether it's receiving a JPEG or PNG.
  • The prompt reuses your existing RISK_CRITERIA (which already includes factory photo assessment rules) and RISK_CARD_SCHEMA for structured output.
  • The fallback default of "image/jpeg" handles any unrecognized extension gracefully.

Now you need to update main() so the learner can actually trigger image analysis from the command line.

  • Find the existing main() function at the bottom of supplylens.py.
  • Replace the entire main() function with this updated version that parses a --image flag:
def main():
    if len(sys.argv) < 2:
        print("Usage: python supplylens.py <file.pdf> [--image <photo.jpg>]")
        print("       python supplylens.py --image <photo.jpg>")
        sys.exit(1)

    pdf_path = None
    image_path = None

    # Parse arguments
    args = sys.argv[1:]
    i = 0
    while i < len(args):
        if args[i] == "--image" and i + 1 < len(args):
            image_path = args[i + 1]
            i += 2
        else:
            pdf_path = args[i]
            i += 1

    if pdf_path and not image_path:
        print(f"\nAnalyzing supplier document: {pdf_path}")
        print("This may take 10-30 seconds...\n")
        raw = analyze_pdf(pdf_path)
        print(raw)
    elif image_path and not pdf_path:
        print(f"\nAnalyzing facility photo: {image_path}")
        print("This may take 10-30 seconds...\n")
        raw = analyze_image(image_path)
        print(raw)
    else:
        print("Error: Provide a PDF path and/or --image flag.")
        sys.exit(1)

What does this code do?

The updated main() uses a while loop to parse CLI arguments flexibly.

  • When it finds --image, it grabs the next argument as the image file path and skips ahead by two positions.
  • Any other argument is treated as the PDF path.
  • The branching logic at the bottom routes to either PDF analysis, image analysis, or an error depending on what was provided.
  • Save supplylens.py.
  • Verify the PDF path still works by running:
python supplylens.py sample_supplier.pdf

You should see the same JSON risk analysis output as before. The argument parsing now supports both modes without breaking existing behaviour.

Getting a syntax error?

  • Make sure the old main() function is fully replaced. You should not have two def main(): lines in the file.
  • Check that analyze_image() is defined before main() in the file. Python reads top to bottom.
  • Confirm the if __name__ == "__main__": block at the very end still calls main().

Help me fix a syntax error in my supplylens.py file

Test with good and bad factory photos

To see how well the AI evaluates facilities, you need two contrasting images. One should look professional and organized. The other should look messy, low-quality, or like an obvious stock photo.

  • Find or take two test images for your supplylens folder:
  • A clean image: a photo of a tidy workspace, organized shelving, or professional facility. Save it as factory_clean.jpg.
  • A messy image: a cluttered space, generic stock photo of a factory, or disorganized work area. Save it as factory_messy.jpg.

Where to find test images

Search for "clean factory floor" and "messy warehouse" on any free stock photo site. You can also photograph your own workspace or desk as a quick test. Any JPEG or PNG file works.

  • Confirm both files are in your project folder by running:
ls

You should see factory_clean.jpg and factory_messy.jpg listed alongside supplylens.py and your other files.

Now run the clean image through SupplyLens:

  • Analyze your clean factory image by running:
python supplylens.py --image factory_clean.jpg

This may take 10-30 seconds. You should see a JSON response describing an organized, well-maintained facility with few or no red flags.

  • Now analyze the messy image by running:
python supplylens.py --image factory_messy.jpg

Compare the two outputs. The AI should flag concerns in the messy image (cluttered floors, poor organization, potential safety issues) while giving the clean image a lower risk score.

Why does the same prompt produce different risk assessments?

Your RISK_CRITERIA prompt includes specific factory photo assessment rules (equipment condition, cleanliness, safety compliance, signs of active production). Gemini applies these rules to whatever visual evidence it finds in each image. The criteria stay constant. The evidence changes.

Getting a rate limit error?

The Gemini free tier has usage limits. If you see a rate limit error, wait 60 seconds and try again. For a learning project with a handful of calls, you should stay well within the daily allowance.

Help me troubleshoot a Gemini API rate limit error

Before you run the final verification, what do you think will happen if you pass an image file that doesn't exist?

  • Test the error handling by running:
python supplylens.py --image nonexistent.jpg

You should see Error: File not found: nonexistent.jpg printed. The error handling in your analyze_image() function catches missing files before making an API call.

✔️ Awesome, I've got everything!

Great. Double check you've saved supplylens.py and both image analysis calls returned JSON results.

ⓧ I'd like to double check the full code

Your supplylens.py is now over 30 lines, so confirm these sections exist in order:

  • Imports (sys, json, base64, pathlib, os, genai, types).
  • load_api_key() function.
  • Client initialization (api_key, client, MODEL).
  • RISK_CRITERIA and RISK_CARD_SCHEMA string constants.
  • analyze_pdf() function (unchanged from Step 2).
  • analyze_image() function with mime_map dictionary (new in this step).
  • main() function with argument parsing loop and --image branch (updated in this step).
  • if __name__ == "__main__": block at the very end.

SupplyLens can now see. You've given it the ability to analyze factory photos and flag real visual red flags. Next up, you'll transform this raw output into a structured risk card with a clear pass/warn/fail verdict that a busy importer can act on in seconds.

Build the Structured Risk Scoring Engine

Your PDF and image analysis functions can pull insights from supplier documents and factory photos. The problem is that they dump raw text to your terminal.

A busy importer doesn't want to read paragraphs. They need a single pass/warn/fail verdict with specific red flags called out. In this step, you'll parse the AI's response into structured JSON and display a formatted risk card that makes the decision obvious at a glance.

In this step, get ready to:
  • Parse the model's JSON response into a structured risk card.
  • Display a color-coded verdict with extracted facts and red flags.
  • Save the risk card to a JSON file for record-keeping.
Review the risk card JSON schema

Your RISK_CARD_SCHEMA constant already tells Gemini exactly what JSON structure to return. It requests a verdict, a risk_score, red_flags with severity levels, extracted_facts, and recommendations.

Why prompt-based JSON instead of a formal schema API?

Gemini supports a structured outputs feature, but prompt-based JSON instruction works reliably with gemini-2.5-flash on the free tier. You ask for JSON in the prompt, and the model returns it. The trade-off is that the model might occasionally wrap the JSON in markdown code fences or return malformed output, so you need a parser that handles both cases.

The schema and due-diligence criteria are already wired into your prompts. What's missing is the code that takes the raw response and turns it into something useful.

Add the JSON parsing function

Gemini often wraps its JSON output in markdown code fences like ```json. You need a function that strips those fences before parsing.

  • In supplylens.py, find the end of the analyze_image() function. Add the following function below it:
def parse_risk_card(raw_response):
    """Parse the JSON risk card from the model response."""
    # Strip markdown code fences if present
    text = raw_response.strip()
    if text.startswith("```json"):
        text = text[7:]
    if text.startswith("```"):
        text = text[3:]
    if text.endswith("```"):
        text = text[:-3]
    text = text.strip()

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return None

What does this code do?

  • Strips any ```json or ``` fences that Gemini wraps around its JSON responses.
  • Attempts to parse the cleaned text as JSON using json.loads().
  • Returns None if the JSON is malformed, so your display function can handle the failure gracefully.
Build the risk card display

Now you need a function that takes the parsed JSON and prints a formatted, color-coded risk card to the terminal. This function is longer, so you'll add it in two parts.

  • Below parse_risk_card(), add the first part of the display function that prints the header and extracted facts:
def display_risk_card(card):
    """Display the risk card with color-coded verdict."""
    if card is None:
        print("\n[!] Could not parse risk card. Raw response may be malformed.")
        return

    # ANSI color codes
    colors = {"PASS": "\033[92m", "WARN": "\033[93m", "FAIL": "\033[91m"}
    reset = "\033[0m"

    verdict = card.get("verdict", "UNKNOWN")
    color = colors.get(verdict, "")

    print("\n" + "=" * 60)
    print(f"  SUPPLYLENS RISK CARD")
    print("=" * 60)
    print(f"  Supplier: {card.get('supplier_name', 'Unknown')}")
    print(f"  Verdict:  {color}{verdict}{reset}")
    print(f"  Risk Score: {card.get('risk_score', '?')}/10")
    print("-" * 60)

    # Extracted facts
    facts = card.get("extracted_facts", {})
    print("\n  EXTRACTED FACTS:")
    print(f"    Company:        {facts.get('company_name', 'N/A')}")
    print(f"    Address:        {facts.get('address', 'N/A')}")
    print(f"    Certifications: {', '.join(facts.get('certifications_claimed', [])) or 'None found'}")
    print(f"    Payment Terms:  {facts.get('payment_terms', 'N/A')}")
    print(f"    Production:     {facts.get('production_claims', 'N/A')}")
    print(f"    Years Active:   {facts.get('years_in_business', 'N/A')}")

What does this section do?

  • Returns early with an error message if parsing failed (card is None).
  • Uses ANSI escape codes to color the verdict green (PASS), yellow (WARN), or red (FAIL).
  • Prints a structured header with the supplier name, verdict, and risk score, followed by all extracted facts in a clean table layout.
  • Immediately below the last line of the facts section (still inside display_risk_card), add the red flags, positive signals, and recommendations sections:
    # Red flags
    red_flags = card.get("red_flags", [])
    if red_flags:
        print(f"\n  RED FLAGS ({len(red_flags)}):")
        for flag in red_flags:
            severity = flag.get("severity", "unknown").upper()
            sev_color = {
                "HIGH": "\033[91m",
                "MEDIUM": "\033[93m",
                "LOW": "\033[33m",
            }.get(severity, "")
            print(f"    {sev_color}[{severity}]{reset} {flag.get('flag', '')}")
            print(f"           Evidence: {flag.get('evidence', '')}")
    else:
        print("\n  RED FLAGS: None detected")

    # Positive signals
    positives = card.get("positive_signals", [])
    if positives:
        print(f"\n  POSITIVE SIGNALS:")
        for signal in positives:
            print(f"    [+] {signal}")

    # Recommendations
    recs = card.get("recommendations", [])
    if recs:
        print(f"\n  RECOMMENDATIONS:")
        for rec in recs:
            print(f"    -> {rec}")

    print("\n" + "=" * 60)

What does this section do?

  • Loops through each red flag and prints it with a severity-colored label (red for HIGH, yellow for MEDIUM, dim yellow for LOW).
  • Shows the evidence that triggered each flag, so the reader knows exactly what the AI spotted.
  • Lists positive signals and actionable recommendations at the end of the card.
  • Save supplylens.py.
Wire the parsing and display into main()

Your new functions exist, but main() still just prints the raw response. You need to replace those print(raw) calls with parsing, display, and file-saving logic.

  • In the main() function, find the if pdf_path and not image_path: branch. Locate these lines:
    if pdf_path and not image_path:
        print(f"\nAnalyzing supplier document: {pdf_path}")
        print("This may take 10-30 seconds...\n")
        raw = analyze_pdf(pdf_path)
        print(raw)
  • Replace print(raw) with the parsing, display, and save logic so the branch looks like this:
    # Run analysis
    if pdf_path and not image_path:
        print(f"\nAnalyzing supplier document: {pdf_path}")
        print("This may take 10-30 seconds...\n")
        raw = analyze_pdf(pdf_path)
        card = parse_risk_card(raw)
        display_risk_card(card)

        # Save JSON output
        output_path = pathlib.Path(pdf_path).stem + "_risk_card.json"
        if card:
            pathlib.Path(output_path).write_text(json.dumps(card, indent=2))
            print(f"\n  Risk card saved to: {output_path}")
  • Now find the elif image_path and not pdf_path: branch. Locate these lines:
    elif image_path and not pdf_path:
        print(f"\nAnalyzing facility photo: {image_path}")
        print("This may take 10-30 seconds...\n")
        raw = analyze_image(image_path)
        print(raw)
  • Replace print(raw) with parsing and display so the branch looks like this:
    elif image_path and not pdf_path:
        print(f"\nAnalyzing facility photo: {image_path}")
        print("This may take 10-30 seconds...\n")
        raw = analyze_image(image_path)
        card = parse_risk_card(raw)
        display_risk_card(card)
  • Finally, replace the existing else: block at the bottom of main() with a combined-analysis placeholder and the else. The end of your main() should look like this:
    elif pdf_path and image_path:
        print(f"\nAnalyzing document: {pdf_path}")
        print(f"Analyzing photo:    {image_path}")
        print("Running combined analysis... This may take 10-30 seconds.\n")
        print("[!] Combined analysis requires Secret Mission implementation.")
        print("    Run with just --image or just a PDF path for now.")
        sys.exit(1)

    else:
        print("Error: Provide a PDF path and/or --image flag.")
        sys.exit(1)

What changed in main()?

  • Both analysis branches now pipe the raw response through parse_risk_card() then display_risk_card() instead of printing raw text.
  • The PDF branch saves the parsed JSON to a _risk_card.json file for record-keeping.
  • A new combined branch handles the case where both a PDF and image are provided. It's a placeholder for the Secret Mission.
  • Save supplylens.py.

Before you run this, what do you think the output will look like? Will it be raw JSON, or will you see a formatted card with colors and sections?

  • Run your updated script against the sample PDF:
python supplylens.py sample_supplier.pdf

After 10-30 seconds, you should see a formatted risk card with a color-coded verdict (PASS, WARN, or FAIL), a risk score out of 10, extracted facts in a table, and red flags with severity labels. You'll also see a line confirming the JSON was saved to sample_supplier_risk_card.json.

  • Confirm the saved JSON file exists by running:
ls

You should see sample_supplier_risk_card.json listed alongside your other project files.

Seeing "Could not parse risk card"?

  • The model occasionally returns malformed JSON on the free tier. Run the command again. The retry usually succeeds.
  • If it fails repeatedly, check that your RISK_CARD_SCHEMA constant starts with the line Respond ONLY with valid JSON. That instruction is what tells the model to output pure JSON.
  • Make sure parse_risk_card() is defined above main() in your file. Python reads top to bottom, so functions must be defined before they're called.

Why is my risk card not parsing correctly?

✔️ Awesome, I've got everything!

Great. Double check you've saved supplylens.py and that your risk card displays with colored verdict, extracted facts, red flags, and recommendations.

ⓧ I'd like to double check the full code

Your supplylens.py is long at this point. Scroll through your file and confirm these sections exist in order:

  • Imports (sys, json, base64, pathlib, os, genai, types) at the top.
  • load_api_key() function, then client initialization and MODEL constant.
  • RISK_CRITERIA and RISK_CARD_SCHEMA constants.
  • analyze_pdf() then analyze_image() functions.
  • parse_risk_card() then display_risk_card() functions.
  • main() function with three branches: PDF-only (parses, displays, saves), image-only (parses, displays), combined (placeholder message).
  • if __name__ == "__main__": main() at the very bottom.

You've turned raw AI text into a machine-readable, color-coded risk card that any importer could scan in seconds. The JSON file means you can build a history of vetted suppliers over time. Next up, you'll tackle the Secret Mission: combining PDF and photo analysis in a single request to catch contradictions between what suppliers claim and what their factory actually shows.

Secret mission

Cross-Reference PDF Claims Against Photos

A supplier's PDF can say anything. But when you cross-reference those claims against actual factory photos, contradictions light up like red flags on a radar. Can you combine both inputs into a single multimodal request and catch the lies?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses the Gemini API free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • supplylens project folder (contains supplylens.py, .env, test PDFs, test images, and generated risk card JSON files).
  • google-genai Python package installed via pip.
  • Gemini API key created in Google AI Studio.

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend SupplyLens with more features.

  • Your supplylens folder stays on your Desktop ready to use.
  • Your Gemini API key remains active in Google AI Studio. The free tier has no charges, so there is nothing to worry about.
  • The google-genai package stays installed in your Python environment.

Pause - I'll come back to this later

Free up a small amount of disk space while keeping your project intact for later.

  • Close VS Code if you still have it open.
  • Your supplylens folder and all files inside it will remain on your Desktop. Nothing to pause since there are no running processes or servers.
  • Your Gemini API key remains active but costs nothing on the free tier.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete your project folder by running this command in your terminal:
cd ~/Desktop
rm -rf supplylens

On Windows, run this instead:

cd ~\Desktop
rmdir /s /q supplylens
  • Uninstall the Google GenAI SDK by running:
pip uninstall google-genai

That's a Wrap!

That's a Wrap!

Nice work. You just built a production-ready CLI tool that performs AI-powered supplier due diligence in under 60 seconds.

You've learned how to:

  • Make multimodal API calls combining PDFs and images in a single request to Google Gemini.
  • Design domain-specific prompts encoding real supplier vetting criteria like certifications, payment terms, factory evidence, and pricing signals.
  • Implement structured JSON output parsing to produce actionable risk cards with color-coded verdicts.
  • Secret Mission: Cross-referenced document claims against visual evidence to detect contradictions between what a supplier says and what their factory photos show.

Ready to quiz yourself?