Files
doczyai-pipelines/src/pipelines/shared/extraction/tin_npi_funcs.py
T
2026-02-04 00:34:53 -05:00

754 lines
28 KiB
Python

import json
import logging
import re
import src.constants.regex_patterns as regex_patterns
import src.config as config
import src.prompts.prompt_templates as prompt_templates
from src.utils import llm_utils, string_utils
from src.pipelines.saas.prompts import prompt_calls
from src.prompts.fieldset import Field, FieldSet
def get_all_matches(text: str, pattern: str) -> list[str]:
"""
Extracts all unique matches of a given pattern from the provided text.
Args:
text (str): The input text to search for matches.
pattern (str): The regular expression pattern to match against the text.
Returns:
list[str]: A list of unique matches found in the text. If the pattern is None or empty, returns an empty list.
"""
if string_utils.is_empty(pattern): # check if pattern is None or empty
return []
matches = re.findall(pattern, text)
return list(set(matches)) # Remove duplicates
def get_all_matches_with_ocr(
text: str, exact_pattern: str, ocr_pattern: str, identifier_type: str = "TIN"
) -> list[tuple]:
"""Extracts all matches for a specific identifier type from the text, using both exact and OCR patterns.
Args:
text (str): The input text to search for matches.
exact_pattern (str): The regular expression pattern for exact matches.
ocr_pattern (str): The regular expression pattern for OCR-correctable matches.
identifier_type (str, optional): The type of identifier being searched for (e.g., "TIN" or "NPI"). Defaults to "TIN".
Returns:
list[tuple]: A list of (value, match_quality) tuples where match_quality is "EXACT" or "OCR_CORRECTED".
"""
results = []
seen_values = set()
# Get exact matches and normalize them
exact_matches = get_all_matches(text, exact_pattern)
for match in exact_matches:
# Normalize: remove hyphens/dots, keep only digits
normalized = "".join(c for c in match if c.isdigit())
# Validate length based on identifier type
if identifier_type == "TIN":
expected_length = 9
elif identifier_type == "NPI":
expected_length = 10
if len(normalized) == expected_length and normalized not in seen_values:
results.append((normalized, "EXACT"))
seen_values.add(normalized)
# Get OCR-correctable candidates (even if we found exact matches)
ocr_candidates = get_all_matches(text, ocr_pattern)
for candidate in ocr_candidates:
# Checks if length follows pattern: 10*N + (N-1) -> (len + 1) % 11 == 0
if (
identifier_type == "NPI"
and len(candidate) > 10
and (len(candidate) + 1) % 11 == 0
):
# Split into chunks of 10, skipping every 11th character (the separator)
chunks = [candidate[i : i + 10] for i in range(0, len(candidate), 11)]
for chunk in chunks:
corrected, is_valid = attempt_ocr_correction(chunk, identifier_type)
if is_valid and corrected not in seen_values:
results.append((corrected, "OCR_CORRECTED"))
seen_values.add(corrected)
continue
logging.debug(f"OCR candidate for {identifier_type}: {candidate}")
corrected, is_valid = attempt_ocr_correction(candidate, identifier_type)
if is_valid and corrected not in seen_values:
results.append((corrected, "OCR_CORRECTED"))
seen_values.add(corrected)
return results
def attempt_ocr_correction(candidate: str, identifier_type: str) -> tuple[str, bool]:
"""Attempt OCR correction on a candidate string
Args:
candidate (str): The candidate string that may contain OCR errors.
identifier_type (str): The type of identifier (e.g., "TIN" or "NPI").
Returns:
tuple[str, bool]: A tuple containing the corrected string and a boolean
indicating if the correction was successful:
(corrected_value, is_valid_after_correction)
"""
if identifier_type not in ["TIN", "NPI"]:
return (
candidate,
False,
) # Currently only TIN and NPI OCR correction is supported
corrected = candidate
corrections_made = []
# Apply OCR substitutions
for bad_char, good_char in regex_patterns.OCR_SUBSTITUTIONS.items():
if bad_char in corrected:
corrected = corrected.replace(bad_char, good_char)
corrections_made.append(f"{bad_char}{good_char}")
# Remove formatting (hyphens, dots) and keep only digits
digits_only = "".join(c for c in corrected if c.isdigit())
# Validate based on identifier type
if identifier_type == "TIN":
# TIN must be exactly 9 digits
expected_length = 9
elif identifier_type == "NPI":
# NPI must be exactly 10 digits
expected_length = 10
else:
return candidate, False # Unsupported identifier type
if len(digits_only) == expected_length:
if corrections_made:
logging.debug(
f"OCR correction applied: {candidate} -> {corrected} ({', '.join(corrections_made)})"
)
return digits_only, True
# If not valid, return original candidate
return candidate, False
def chunk_on_matches(matches: list[str], text_dict: dict) -> str:
"""
Extracts and concatenates text from a dictionary of pages where any of the specified matches are found.
Args:
matches (list[str]): A list of strings to search for in the text.
text_dict (dict): A dictionary where keys are identifiers (e.g., page numbers) and values are strings of text.
Returns:
str: A single string containing the concatenated text from all pages where at least one match is found,
separated by newline characters.
"""
valid_pages = []
for page_text in text_dict.values():
if any(match in page_text for match in matches):
valid_pages.append(page_text)
return "\n".join(valid_pages)
def clean_provider_info(provider_info: list[dict]) -> list[dict]:
"""Cleans provider information by standardizing Name/TIN/NPI formats.
Args:
provider_info (list[dict]): A list of provider information dictionaries. Each
dictionary should be keyed by TIN, NPI, NAME
Returns:
list: Cleaned provider information
"""
cleaned_providers = []
for provider in provider_info:
cleaned_provider = {}
# ---- Clean TIN (list, digits only, 9 chars) ----
cleaned_tins = []
if isinstance(provider.get("TIN"), list):
for tin in provider["TIN"]:
if not tin:
continue
tin_str = str(tin).replace("-", "").replace(".", "")
if tin_str.isdigit() and len(tin_str) == 9:
cleaned_tins.append(tin_str)
cleaned_provider["TIN"] = cleaned_tins if cleaned_tins else ["UNKNOWN"]
# ---- Clean NPI (list, digits only, 10 chars) ----
cleaned_npis = []
if isinstance(provider.get("NPI"), list):
for npi in provider["NPI"]:
if not npi:
continue
npi_str = str(npi).replace("-", "").replace(".", "")
if npi_str.isdigit() and len(npi_str) == 10:
cleaned_npis.append(npi_str)
cleaned_provider["NPI"] = cleaned_npis if cleaned_npis else ["UNKNOWN"]
# ---- Clean NAME (list) ----
cleaned_names = []
if isinstance(provider.get("NAME"), list):
for name in provider["NAME"]:
if not name:
continue
name_str = " ".join(str(name).split()).strip().rstrip(".")
if name_str:
cleaned_names.append(name_str)
cleaned_provider["NAME"] = cleaned_names if cleaned_names else ["UNKNOWN"]
cleaned_providers.append(cleaned_provider)
return cleaned_providers
def merge_provider_info_with_hybrid_smart_chunking(one_to_one_results, filename):
"""
Merges provider_info into one_to_one_results.
Args:
one_to_one_results (dict): Dictionary of one-to-one field results.
provider_info (list[dict]): List of dictionaries containing provider information.
Each dictionary is keyed by TIN, NPI, NAME, IS_GROUP.
Returns:
dict: Updated one_to_one_results with merged values from provider_info.
"""
# Extract group and non-group providers
group_tins = set()
group_npis = set()
group_names = set()
other_tins = []
other_npis = []
other_names = []
provider_name = one_to_one_results.get("PROVIDER_NAME") # Can be List or string
# Normalize provider_name to list: if string, convert to list to avoid character splitting
if isinstance(provider_name, str):
provider_name = [provider_name]
elif not isinstance(provider_name, list):
provider_name = []
provider_info = one_to_one_results[
"PROV_INFO_JSON"
] # Returns string representation of list
prov_info_json_list = (
json.loads(provider_info) if isinstance(provider_info, str) else provider_info
)
# Track if we found any name matches
found_match = False
for provider in prov_info_json_list:
# Ensure list format (FORMAT FIX ONLY)
tins = provider.get("TIN") or []
npis = provider.get("NPI") or []
names = provider.get("NAME") or []
# Normalize to lists: if string, convert to list to avoid character splitting
if isinstance(tins, str):
tins = [tins]
elif not isinstance(tins, list):
tins = []
if isinstance(npis, str):
npis = [npis]
elif not isinstance(npis, list):
npis = []
if isinstance(names, str):
names = [names]
elif not isinstance(names, list):
names = []
name_matches = prompt_calls.provider_name_match_check(
names, provider_name, filename
) # Returns Boolean
if name_matches:
found_match = True
provider["IS_GROUP"] = "Y"
for tin in tins:
group_tins.add(tin)
for npi in npis:
group_npis.add(npi)
for name in names:
group_names.add(name)
else:
provider["IS_GROUP"] = "N"
other_tins.extend(tins)
other_npis.extend(npis)
other_names.extend(names)
# If provider_name is not null and no match was found, add a new record
if provider_name and not found_match:
new_provider = {
"NAME": provider_name,
"TIN": [],
"NPI": [],
"IS_GROUP": "Y",
}
prov_info_json_list.append(new_provider)
for name in provider_name:
group_names.add(name)
# De-Unknown GROUP fields
group_tins = [v for v in group_tins if v != "UNKNOWN"]
group_npis = [v for v in group_npis if v != "UNKNOWN"]
group_names = [v for v in group_names if v != "UNKNOWN"]
# Deduplicate while preserving order and remove any "UNKNOWN" entries and overlaps between group and other
group_tins = list(dict.fromkeys(group_tins))
other_tins = list(dict.fromkeys(other_tins))
other_tins = [v for v in other_tins if v not in group_tins and v != "UNKNOWN"]
group_npis = list(dict.fromkeys(group_npis))
other_npis = list(dict.fromkeys(other_npis))
other_npis = [v for v in other_npis if v not in group_npis and v != "UNKNOWN"]
group_names = list(dict.fromkeys(group_names))
other_names = list(dict.fromkeys(other_names))
other_names = [v for v in other_names if v not in group_names and v != "UNKNOWN"]
# Filter out records with NO_IDENTIFIERS_FOUND
prov_info_json_list = [
p
for p in prov_info_json_list
if "NO_IDENTIFIERS_FOUND" not in (p.get("NAME") or [])
]
# Deduplicate provider_list by NAME, keeping records with actual TIN/NPI values
prov_info_json_list = deduplicate_providers_by_name(prov_info_json_list)
# Reconstruct PROV_INFO_JSON and PROV_INFO_JSON_FORMATTED with IS_GROUP flags
one_to_one_results["PROV_INFO_JSON"] = json.dumps(prov_info_json_list)
one_to_one_results["PROV_INFO_JSON_FORMATTED"] = "\n".join(
[json.dumps(provider) for provider in prov_info_json_list]
)
# Merge group provider information into one_to_one_results
one_to_one_results["PROV_GROUP_TIN"] = group_tins if group_tins else []
one_to_one_results["PROV_GROUP_NPI"] = group_npis if group_npis else []
one_to_one_results["PROV_GROUP_NAME_FULL"] = group_names if group_names else []
# Merge other provider information into one_to_one_results
one_to_one_results["PROV_OTHER_TIN"] = other_tins if other_tins else []
one_to_one_results["PROV_OTHER_NPI"] = other_npis if other_npis else []
one_to_one_results["PROV_OTHER_NAME_FULL"] = other_names if other_names else []
return one_to_one_results
def reimbursement_tin_npi(
exhibit_dict: dict[str, str],
exhibit_page: str,
reimbursement_level_fields: FieldSet,
filename: str,
) -> tuple[dict, FieldSet]:
"""
Extracts Reimbursement-Level TIN and NPI values from the given exhibit chunk using an LLM.
Args:
exhibit_dict (dict[str, str]): Dictionary containing the exhibit text with page numbers as keys.
exhibit_page (str): The page number containing reimbursement details.
reimbursement_level_fields (FieldSet): The FieldSet object to store extracted field values.
filename (str): The name of the file being processed.
Returns:
tuple[dict, FieldSet]: A dictionary containing extracted TIN and NPI values, and an
updated FieldSet with additional fields if multiple values are found.
Fields Added: REIMB_PROV_NAME,
REIMB_PROV_TIN,
REIMB_PROV_NPI
"""
def get_list(field_name, filtered_providers):
# Get all values and filter out invalid ones
all_values = [
provider.get(field_name, "N/A") for provider in filtered_providers
]
valid_values = [
val
for val in all_values
if not string_utils.is_empty(val) and val != "UNKNOWN"
]
return list(set(valid_values)) # Remove duplicates
answer_dict = {}
# Extract all providers from the exhibit chunk
providers = get_provider_info(
text_dict=exhibit_dict, page_num=exhibit_page, filename=filename
)
# Clean, standardize, and deduplicate extracted providers
filtered_providers = deduplicate_providers(clean_provider_info(providers))
field_configs = [
("REIMB_PROV_TIN", "TIN"),
("REIMB_PROV_NPI", "NPI"),
("REIMB_PROV_NAME", "NAME"),
]
# Always add providers to reimbursement_level fields for proper term attribution
for field_name, provider_key in field_configs:
if len(filtered_providers) > 0:
field = Field.load_from_file(config.FIELD_JSON_PATH, field_name)
value_list = get_list(provider_key, filtered_providers)
if len(value_list) == 0:
answer_dict[field_name] = "N/A"
else:
field.update_valid_values(value_list)
reimbursement_level_fields.add_field(field)
else:
# If no providers are found, set default values
answer_dict[field_name] = "N/A"
return answer_dict, reimbursement_level_fields
def deduplicate_providers(
provider_info: list[dict],
) -> list[dict]: # TODO: add unit test
"""Deduplicates provider information based on TIN, NPI, and NAME fields.
This function checks for duplicates in the provider information list by comparing
the TIN, NPI, and NAME fields. If all three fields match, only one instance of the
provider is kept. If all three fields are unknown, the provider is skipped.
Args:
provider_info (list[dict]): A list of provider information dictionaries to be deduplicated.
Returns:
list[dict]: A list of deduplicated provider information dictionaries.
"""
seen = set()
valid_providers = []
# Deduplicate if all three fields match
for provider in provider_info:
tins = provider.get("TIN", [])
npis = provider.get("NPI", [])
names = provider.get("NAME", [])
# Normalize missing values
tins = tins if isinstance(tins, list) else []
npis = npis if isinstance(npis, list) else []
names = names if isinstance(names, list) else []
# Skip providers where all critical identification fields are unknown
if (
(not tins or tins == ["UNKNOWN"])
and (not npis or npis == ["UNKNOWN"])
and (not names or names == ["UNKNOWN"])
):
continue
key = (
tuple(tins),
tuple(npis),
tuple(names),
)
if key not in seen:
seen.add(key)
valid_providers.append(provider)
return valid_providers
def get_provider_info(
text_dict: dict, page_num: str, filename: str, payer_name: str
) -> list:
"""
Extracts provider information from a specified page in a document using a language model.
Args:
text_dict (dict): Dictionary of text content with page numbers as keys.
page_num (str): The page number to extract provider information from.
filename (str): The name of the file being processed, used for logging or tracking purposes.
first_page_provider_group_name (str, optional): Expected group name from page 1 for name verification.
Returns:
list: A list of extracted provider information dictionaries, each containing TIN, NPI,
NAME, and flags for presence on signature or intro pages.
Raises:
ValueError: If the response from the language model cannot be parsed as valid JSON.
"""
chunk = text_dict[page_num]
provider_fields = FieldSet(
file_path=config.FIELD_JSON_PATH, field_type="provider_info"
)
prompt, _parser = prompt_templates.TIN_NPI_TEMPLATE(
chunk, provider_fields.print_prompt_dict(), payer_name
)
llm_answer_raw = llm_utils.invoke_claude(
prompt,
"sonnet_latest",
filename,
max_tokens=8192,
cache=True,
instruction=prompt_templates.TIN_NPI_TEMPLATE_INSTRUCTION(),
) # Sometimes rosters can have 50+ provider
logging.debug(
f"Raw LLM response for provider info on page {page_num}: {llm_answer_raw}"
)
# Defensive programming for any json parsing errors
try:
providers = _parser(llm_answer_raw)
logging.debug(f"Parsed provider info on page {page_num}: {providers}")
# Ensure we have a list of providers (not a dict or other type)
if isinstance(providers, dict):
# Single provider returned as dict - wrap in list
providers = [providers]
elif not isinstance(providers, list):
# If not a list or dict, create a default list with error info
logging.warning(
f"Warning: Unexpected format in provider info response: {type(providers)}"
)
providers = [{"TIN": [], "NPI": [], "NAME": ["PARSING_ERROR"]}]
# VALIDATION LAYER - Cross-check with regex findings
page_text = text_dict[page_num]
# Get all TINs and NPIs found via regex on this page
regex_tins = [
tin
for tin, _ in get_all_matches_with_ocr(
page_text,
regex_patterns.TIN_PATTERN,
regex_patterns.TIN_OCR_PATTERN,
identifier_type="TIN",
)
]
regex_npis = [
npi
for npi, _ in get_all_matches_with_ocr(
page_text,
regex_patterns.NPI_PATTERN,
regex_patterns.NPI_OCR_PATTERN,
identifier_type="NPI",
)
]
# Collect all TINs/NPIs that LLM found
llm_found_tins = []
llm_found_npis = []
for provider in providers:
for tin in provider["TIN"] or []:
if tin and tin != "UNKNOWN":
llm_found_tins.append(tin.replace("-", "").replace(".", ""))
for npi in provider["NPI"] or []:
if npi and npi != "UNKNOWN":
llm_found_npis.append(npi.replace("-", "").replace(".", ""))
# Note: regex_tins and regex_npis are already normalized in get_all_matches_with_ocr
missed_tins = [tin for tin in regex_tins if tin not in llm_found_tins]
missed_npis = [npi for npi in regex_npis if npi not in llm_found_npis]
if missed_tins or missed_npis:
# WARN ONLY if there are missed identifiers
logging.warning(
f"LLM missed identifiers on page {page_num} of {filename}. Missed TINs: {missed_tins}, Missed NPIs: {missed_npis}"
)
return providers
except ValueError as e: # Handle JSON parsing error
logging.error(f"Error parsing JSON response from LLM: {str(e)}")
logging.error(f"Raw response: {llm_answer_raw}")
# Return a default value
return [{"TIN": [], "NPI": [], "NAME": ["PARSING_ERROR"]}]
def get_relevant_pages(all_tins, all_npis, text_dict, filename):
# If there are no identifiers, return early with default values
if not all_tins and not all_npis:
# if there are no TINs or NPIs, we need to extract signature pages
relevant_pages = list(
string_utils.extract_signature_page(text_dict, filename).keys()
)
else:
# Identify relevant pages containing either NPIs or TINs (with or without hyphen formatting, e.g., 68-0640053)
relevant_pages = [
page_num
for page_num, page_text in text_dict.items()
if any(
re.sub(r"[\s-]", "", tin) in re.sub(r"[\s-]", "", page_text)
for tin in all_tins
)
or any(
re.sub(r"[\s-]", "", npi) in re.sub(r"[\s-]", "", page_text)
for npi in all_npis
)
]
# Stopgap for when the TIN is written in the header or footer of every page (or nearly every page)
if (
len(relevant_pages) >= len(text_dict) - 1
and (len(all_tins) == 1 or len(all_tins) == 0)
and (len(all_npis) == 1 or len(all_npis) == 0)
):
relevant_pages = [relevant_pages[0]]
return relevant_pages
def get_prov_info_json(relevant_pages, text_dict, filename, payer_name):
if not relevant_pages:
prov_info_json_clean = [
{
"TIN": ["UNKNOWN"],
"NPI": ["UNKNOWN"],
"NAME": ["NO_IDENTIFIERS_FOUND"],
}
]
else:
# Process each page separately and combine results
prov_info_json = []
for page_num in relevant_pages:
page_provider_info = get_provider_info(
text_dict, page_num, filename, payer_name
)
prov_info_json.extend(page_provider_info)
# Clean and standardize results
prov_info_json_clean = clean_provider_info(prov_info_json)
# Deduplicate providers based on TIN and NPI
prov_info_json_clean = deduplicate_providers(prov_info_json_clean)
return prov_info_json_clean
def get_all_tins_and_npis(contract_text):
tin_matches = get_all_matches_with_ocr(
contract_text,
regex_patterns.TIN_PATTERN,
regex_patterns.TIN_OCR_PATTERN,
identifier_type="TIN",
)
npi_matches = get_all_matches_with_ocr(
contract_text,
regex_patterns.NPI_PATTERN,
regex_patterns.NPI_OCR_PATTERN,
identifier_type="NPI",
)
# Extract just the values from the (value, match_quality) tuples for downstream processing
all_tins = [tin for tin, quality in tin_matches]
all_npis = [npi for npi, quality in npi_matches]
logging.debug(f"Extracted TINs: {all_tins}")
logging.debug(f"Extracted NPIs: {all_npis}")
return all_tins, all_npis, tin_matches, npi_matches
def get_tin_extraction_quality(tin_matches, npi_matches, filename):
# Log OCR corrections for monitoring
exact_tins = [tin for tin, quality in tin_matches if quality == "EXACT"]
ocr_corrected_tins = [
tin for tin, quality in tin_matches if quality == "OCR_CORRECTED"
]
exact_npis = [npi for npi, quality in npi_matches if quality == "EXACT"]
ocr_corrected_npis = [
npi for npi, quality in npi_matches if quality == "OCR_CORRECTED"
]
logging.debug(
f"TIN extraction summary for {filename}: {len(exact_tins)} exact, {len(ocr_corrected_tins)} OCR-corrected"
)
logging.debug(
f"NPI extraction summary for {filename}: {len(exact_npis)} exact, {len(ocr_corrected_npis)} OCR-corrected"
)
return exact_tins, exact_npis, ocr_corrected_tins, ocr_corrected_npis
def run_provider_info_fields(
contract_text: str, text_dict: dict, filename: str, payer_name: str
):
"""
Extracts provider information from a contract text using regex-based identifier extraction,
processes the relevant text chunks, and cleans the results.
Args:
contract_text (str): The full text of the contract to process.
text_dict (dict): A dictionary containing text data, organized by pages
filename (str): The name of the file being processed, used for logging or reference.
payer_name (str): The name of the payer extracted from the full context answers.
This is not being used right now, but it might be useful for filtering providers later.
Returns:
list[dict]: A cleaned and standardized list containing provider information extracted
from the contract text.
"""
all_tins, all_npis, tin_matches, npi_matches = get_all_tins_and_npis(contract_text)
exact_tins, exact_npis, ocr_corrected_tins, ocr_corrected_npis = get_tin_extraction_quality(tin_matches, npi_matches, filename)
relevant_pages = get_relevant_pages(all_tins, all_npis, text_dict, filename)
prov_info_json_clean = get_prov_info_json(relevant_pages, text_dict, filename, payer_name)
# Create columns
one_to_one_results = {}
one_to_one_results["PROV_INFO_JSON"] = json.dumps(prov_info_json_clean)
one_to_one_results["PROV_INFO_JSON_FORMATTED"] = "\n".join(
[json.dumps(provider) for provider in prov_info_json_clean]
)
one_to_one_results["TIN_EXTRACTION_QUALITY"] = (
f"{len(exact_tins)} exact, {len(ocr_corrected_tins)} OCR-corrected"
)
one_to_one_results["NPI_EXTRACTION_QUALITY"] = (
f"{len(exact_npis)} exact, {len(ocr_corrected_npis)} OCR-corrected"
)
# Add FILENAME_TIN
filename_tin = get_all_matches(filename, regex_patterns.FILE_NAME_TIN_PATTERN)
one_to_one_results["FILENAME_TIN"] = filename_tin[0] if filename_tin else "N/A"
print("run_provider_info_fields: ", one_to_one_results)
return one_to_one_results
def deduplicate_providers_by_name(provider_list: list[dict]) -> list[dict]:
"""
Deduplicates a list of provider dictionaries by NAME, preferring records with actual TIN/NPI values.
Args:
provider_list (list[dict]): List of provider dictionaries with NAME, TIN, NPI keys.
Returns:
list[dict]: Deduplicated list with one entry per unique NAME, preferring non-UNKNOWN values.
"""
seen_names = set()
deduplicated_list = []
# Sort so providers with actual TIN/NPI values come first
sorted_providers = sorted(
provider_list,
key=lambda p: (
not p.get("TIN") or p.get("TIN") == ["UNKNOWN"],
not p.get("NPI") or p.get("NPI") == ["UNKNOWN"],
),
)
for provider in sorted_providers:
names = provider.get("NAME", [])
# Skip invalid NAME values
if not isinstance(names, list) or not names or names == ["UNKNOWN"]:
continue
# Check if any name was already seen
is_duplicate = False
for name in names:
if name in seen_names:
is_duplicate = True
break
if is_duplicate:
continue
# Mark all names as seen
for name in names:
seen_names.add(name)
deduplicated_list.append(provider)
return deduplicated_list