233 lines
9.6 KiB
Python
233 lines
9.6 KiB
Python
|
|
""" USING-LLM-FOR-TABLE-READING Recipes - this example consists of 3 recipes that illustrate the building blocks
|
|
of using locally-deployed small specialized language models for table question-answering with complex financial
|
|
and business documents.
|
|
|
|
Note: this is a *** leading-edge *** set of recipes - it won't always work perfectly out of the box, and generally
|
|
will require some tinkering with the pre-processing and post-processing and strategies at each step to
|
|
improve accuracy.
|
|
|
|
LLMs used:
|
|
-- table reading:
|
|
-- dragon-qwen2-7b-gguf
|
|
-- dragon-yi-9b-gguf
|
|
|
|
-- semantic reranker: jina-reranker-turbo (example 3)
|
|
|
|
The recipes build on each other:
|
|
|
|
Example 1 - basic recipe for using a LLM to answer a question based on a table text passage
|
|
|
|
Example 2 - integrates parsing - parses a 10K document - extracts key tables - and then asks questions
|
|
directly against the parsed, extracted tables (assumes we know the right questions to ask
|
|
each table.
|
|
|
|
Example 3 - integrates semantic similarity - parses and extracts tables from 10K, applies a semantic reranker
|
|
to identify the table with the highest semantic similarity to our question, and then 'chooses'
|
|
that table, and then runs the inference with the question and the highest ranked table.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
from llmware.models import ModelCatalog
|
|
from llmware.parsers import Parser
|
|
from llmware.setup import Setup
|
|
from llmware.util import Utilities
|
|
|
|
|
|
def example1_getting_started (model_name="dragon-qwen-7b-gguf"):
|
|
|
|
""" Basic recipe for running an inference to read a table. """
|
|
|
|
# sample table text with \t separators between items
|
|
|
|
text = ("\t2022 12/31/22\t2021 12/31/21\t2020 12/31/20\t2019 12/31/19 NET SALES OR REVENUES"
|
|
"\t81,462\t53,823\t31,536\t24,578 Cost of Goods Sold (Excl Depreciation)\t57,066\t37,306\t22,584\t18,402 "
|
|
"Depreciation, Depletion And Amortization\t3,543\t2,911\t2,322\t2,107 Depreciation\t2,655\t2,146\t1,802\t1,298 "
|
|
"Amortization of Intangibles\t888\t51\t51\t44Amortization of Deferred Charges\t--\t714\t469\t765 GROSS "
|
|
"INCOME\t20,853\t13,606\t6,630\t4,069 Selling, General & Admin Expenses\t7,021\t7,110\t4,636\t3,989 "
|
|
"Research and Development Expense\t3,075\t2,593\t1,491\t1,343OPERATING INCOME\t13,832\t6,496\t1,994\t80 "
|
|
"Extraordinary Charge - Pretax\t(228)\t(101)\t0\t(196) Non-Operating Interest Income\t297\t56\t30\t44 "
|
|
"Other Income/Expenses - Net\t(15)\t263\t(122)\t92 Interest Expense On Debt\t167\t424\t796\t716 Interest "
|
|
"Capitalized\t0\t53\t48\t31 PRETAX INCOME\t13,719\t6,343\t1,154\t(665) Income "
|
|
"Taxes\t(1,132)\t(699)\t(292)\t(110) Current Domestic Income Tax\t62\t9\t4\t5 Current Foreign Income "
|
|
"Tax\t1,266\t839\t248\t86Deferred Domestic Income Tax\t27\t0\t0\t(4) Deferred Foreign Income "
|
|
"Tax\t(223)\t(149)\t40\t23 Minority Interest\t4\t120\t172\t95 NET INCOME BEFORE EXTRA ITEMS/PREFERRED "
|
|
"DIVIDENDS\t12,583\t5,524\t690\t(870) NET INCOME USED TO CALCULATE BASIC EARNINGS PER "
|
|
"SHARE\t12,583\t5,524\t690\t(870) Shares used in computing earnings per share - "
|
|
"Fully Diluted\t3,475\t3,387\t3,249\t2,661Earning per Common Share - "
|
|
"Basic\t4.02\t1.87\t0.25\t(0.33)Earning per Common Share - Fully Diluted\t3.62\t1.63\t0.21\t(0.33)() = "
|
|
"Negative Values In U.S. Dollars Values are displayed in Millions except for earnings per share "
|
|
"and where noted")
|
|
|
|
questions = ["What is the pretax income in 2022?",
|
|
"What is the pretax income in 2021?",
|
|
"What is the amount of depreciation in 2020?",
|
|
"What is the fully diluted earnings per share in 2022?",
|
|
"What is the amount of dividends in 2021 and 2022?",
|
|
"What is the SG&A expense in 2022?",
|
|
"What were revenues in 2019?"]
|
|
|
|
model = ModelCatalog().load_model(model_name, temperature=0.0, sample=False)
|
|
|
|
for question in questions:
|
|
print("question: ", question)
|
|
response = model.inference(question, add_context=text)
|
|
print("response: ", response)
|
|
|
|
return True
|
|
|
|
|
|
def example2_parse_tables_and_ask_questions(model_name="dragon-qwen-7b-gguf"):
|
|
|
|
""" Parse a 10K, extract key tables, and then ask specific questions to specific tables. """
|
|
|
|
sample_files = Setup().load_sample_files()
|
|
folder = "FinDocs"
|
|
fp = os.path.join(sample_files, folder)
|
|
fn = "Amazon-2021-Annual-Report.pdf"
|
|
|
|
# table_grid = True will provide a HTML representation of the table
|
|
# table_grid = False will provide a simpler /t and /n separators in representing the table
|
|
|
|
parser_output = Parser(table_grid=False).parse_one_pdf(fp,fn)
|
|
|
|
tables = []
|
|
for i, chunks in enumerate(parser_output):
|
|
if chunks["content_type"] == "table":
|
|
print("text chunks: ", i, chunks)
|
|
tables.append(chunks["table"])
|
|
|
|
questions_by_table = [
|
|
["What is the amount of owned square footage of office space in North America?",
|
|
"How many international stores?"
|
|
],
|
|
|
|
["What was the amount of cash at the end of 2020?",
|
|
"What was the amount of cash at the end of 2021?",
|
|
"What was net income in 2020?",
|
|
"What was stock compensation in 2021?",
|
|
"What was the amount of net increase in cash in 2020?"
|
|
],
|
|
|
|
["What were total net sales in 2021?"],
|
|
|
|
["What is the balance amount on January 1, 2019?"]
|
|
|
|
]
|
|
|
|
model = ModelCatalog().load_model(model_name, temperature=0.0, sample=False)
|
|
|
|
for t, table in enumerate(tables):
|
|
|
|
print("\nEvaluating table: ", t, table)
|
|
|
|
for q, question in enumerate(questions_by_table[t]):
|
|
|
|
print("\nQuestion: ", q, question)
|
|
response = model.inference(question, add_context=table)
|
|
print("answer: ", response)
|
|
|
|
return True
|
|
|
|
|
|
def example3_table_reading_e2e(model_name="dragon-qwen-7b-gguf"):
|
|
|
|
""" Will parse, extract tables, apply semantic reranking to find the best fit table, and then ask the
|
|
key question only to that table. """
|
|
|
|
""" Parse a 10K, extract key tables, and then ask specific questions to specific tables. """
|
|
|
|
question = "What was the amount of cash at the end of 2020?"
|
|
|
|
# Step 1 - parse and extract the tables from 10K
|
|
|
|
sample_files = Setup().load_sample_files()
|
|
folder = "FinDocs"
|
|
fp = os.path.join(sample_files, folder)
|
|
fn = "Amazon-2021-Annual-Report.pdf"
|
|
|
|
# table_grid = True will provide a HTML representation of the table
|
|
# table_grid = False will provide a simpler /t and /n separators in representing the table
|
|
|
|
parser_output = Parser(table_grid=False).parse_one_pdf(fp, fn)
|
|
|
|
tables = []
|
|
print(f"\nStep 1 - parsing output - {fn} - created {len(parser_output)} text chunks total.")
|
|
|
|
for i, chunks in enumerate(parser_output):
|
|
if chunks["content_type"] == "table":
|
|
print("tables found: ", i, chunks)
|
|
|
|
if len(chunks["table"]) > 100:
|
|
text_snippet = str(chunks["table"][0:100])
|
|
else:
|
|
text_snippet = chunks["table"]
|
|
|
|
# optional / clean up the text snippet for display on screen
|
|
text_snippet = re.sub(r"[\n\r\t]","", text_snippet)
|
|
|
|
tables.append({"text":chunks["table"],
|
|
"page_num": chunks["master_index"],
|
|
"file_source": chunks["file_source"],
|
|
"text_snippet": text_snippet})
|
|
|
|
# Step 2 - apply semantic ranking to compare the question with the extracted tables to find the
|
|
# table most likely to provide the answer
|
|
|
|
print(f"\nStep 2 - select the extracted table most likely to be able to answer the question.")
|
|
|
|
print("--option 1 - simple text search option (illustrative)")
|
|
exact_key = "CASH EQUIVALENTS"
|
|
results = Utilities().fast_search_dicts(exact_key,tables)
|
|
for i, res in enumerate(results):
|
|
print("text search results: ", i, exact_key,res)
|
|
|
|
# could use as a substitute below
|
|
top_result = results[0]
|
|
|
|
print(f"\n--option 2 - semantic similarity ranking (selected method)")
|
|
|
|
reranker_model = ModelCatalog().load_model("jina-reranker-turbo")
|
|
output = reranker_model.inference(question, tables)
|
|
|
|
for i, ranking in enumerate(output):
|
|
|
|
if i==0:
|
|
print("TOP TABLE - ", i, ranking["rerank_score"], ranking["text_snippet"])
|
|
else:
|
|
print(i, ranking["rerank_score"], ranking["text_snippet"])
|
|
|
|
top_table = output[0]
|
|
|
|
print("\nTOP TABLE SOURCE: ", top_table["text"])
|
|
|
|
# Step 3 - run the query against the table to get the answer
|
|
|
|
print(f"\nStep 3 - loading dragon model to answer the question using the 'top table' found")
|
|
|
|
model = ModelCatalog().load_model(model_name, temperature=0.0, sample=False)
|
|
|
|
print("question: ", question)
|
|
response = model.inference(question, add_context=top_table["text"])
|
|
print("response: ", response)
|
|
print("source: ", top_table["file_source"])
|
|
print("page num: ", top_table["page_num"])
|
|
|
|
return response
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# we would recommend either "dragon-qwen-7b-gguf" or "dragon-yi-9b-gguf"
|
|
|
|
# shows basic recipe for passing a table context and asking a question
|
|
example1_getting_started(model_name="dragon-qwen-7b-gguf")
|
|
|
|
# parse, extract table, ask questions to tables
|
|
example2_parse_tables_and_ask_questions(model_name="dragon-qwen-7b-gguf")
|
|
|
|
# *leading edge* - ask a question to a 100 page pdf 10k, find the right table, and get answer from it
|
|
example3_table_reading_e2e(model_name="dragon-qwen-7b-gguf")
|
|
|