mirror of
https://github.com/hwchase17/langchain
synced 2024-11-16 06:13:16 +00:00
834 lines
53 KiB
Plaintext
834 lines
53 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "f1571abe-8e84-44d1-b222-e4121fdbb4be",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Advanced RAG Eval\n",
|
|
"\n",
|
|
"The cookbook walks through the process of running eval(s) on advanced RAG. \n",
|
|
"\n",
|
|
"This can be very useful to determine the best RAG approach for your application."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "0d8415ee-709c-407f-9ac2-f03a9d697aaf",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"! pip install -U langchain openai chromadb langchain-experimental # (newest versions required for multi-modal)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "191f8465-fd6b-4017-8f0e-d284971b45ae",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# lock to 0.10.19 due to a persistent bug in more recent versions\n",
|
|
"! pip install \"unstructured[all-docs]==0.10.19\" pillow pydantic lxml pillow matplotlib tiktoken open_clip_torch torch"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "45949db5-d9b6-44a9-85f8-96d83a288616",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Data Loading\n",
|
|
"\n",
|
|
"Let's look at an [example whitepaper](https://sgp.fas.org/crs/misc/IF10244.pdf) that provides a mixture of tables, text, and images about Wildfires in the US."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "961a42b9-c16b-472e-b994-3c3f73afbbcb",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Option 1: Load text"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "12f24fc0-c176-4201-982b-8a84b278ff1b",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Path\n",
|
|
"path = \"/Users/rlm/Desktop/cpi/\"\n",
|
|
"\n",
|
|
"# Load\n",
|
|
"from langchain_community.document_loaders import PyPDFLoader\n",
|
|
"\n",
|
|
"loader = PyPDFLoader(path + \"cpi.pdf\")\n",
|
|
"pdf_pages = loader.load()\n",
|
|
"\n",
|
|
"# Split\n",
|
|
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
|
"\n",
|
|
"text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)\n",
|
|
"all_splits_pypdf = text_splitter.split_documents(pdf_pages)\n",
|
|
"all_splits_pypdf_texts = [d.page_content for d in all_splits_pypdf]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "92fc1870-1836-4bc3-945a-78e2c16ad823",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Option 2: Load text, tables, images \n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"id": "7d863632-f894-4471-b4cc-a1d9aa834d29",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from unstructured.partition.pdf import partition_pdf\n",
|
|
"\n",
|
|
"# Extract images, tables, and chunk text\n",
|
|
"raw_pdf_elements = partition_pdf(\n",
|
|
" filename=path + \"cpi.pdf\",\n",
|
|
" extract_images_in_pdf=True,\n",
|
|
" infer_table_structure=True,\n",
|
|
" chunking_strategy=\"by_title\",\n",
|
|
" max_characters=4000,\n",
|
|
" new_after_n_chars=3800,\n",
|
|
" combine_text_under_n_chars=2000,\n",
|
|
" image_output_dir_path=path,\n",
|
|
")\n",
|
|
"\n",
|
|
"# Categorize by type\n",
|
|
"tables = []\n",
|
|
"texts = []\n",
|
|
"for element in raw_pdf_elements:\n",
|
|
" if \"unstructured.documents.elements.Table\" in str(type(element)):\n",
|
|
" tables.append(str(element))\n",
|
|
" elif \"unstructured.documents.elements.CompositeElement\" in str(type(element)):\n",
|
|
" texts.append(str(element))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "65f399c5-bd91-4ed4-89c6-c89d2e17466e",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Store\n",
|
|
"\n",
|
|
"### Option 1: Embed, store text chunks"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"id": "7d7ecdb2-0bb5-46b8-bcff-af8fc272e88e",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from langchain_community.vectorstores import Chroma\n",
|
|
"from langchain_openai import OpenAIEmbeddings\n",
|
|
"\n",
|
|
"baseline = Chroma.from_texts(\n",
|
|
" texts=all_splits_pypdf_texts,\n",
|
|
" collection_name=\"baseline\",\n",
|
|
" embedding=OpenAIEmbeddings(),\n",
|
|
")\n",
|
|
"retriever_baseline = baseline.as_retriever()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "6a0eaefe-5e4b-4853-94c7-5abd6f7fbeac",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Option 2: Multi-vector retriever\n",
|
|
"\n",
|
|
"#### Text Summary"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"id": "3d4b4b43-e96e-48ab-899d-c39d0430562e",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from langchain_core.output_parsers import StrOutputParser\n",
|
|
"from langchain_core.prompts import ChatPromptTemplate\n",
|
|
"from langchain_openai import ChatOpenAI\n",
|
|
"\n",
|
|
"# Prompt\n",
|
|
"prompt_text = \"\"\"You are an assistant tasked with summarizing tables and text for retrieval. \\\n",
|
|
"These summaries will be embedded and used to retrieve the raw text or table elements. \\\n",
|
|
"Give a concise summary of the table or text that is well optimized for retrieval. Table or text: {element} \"\"\"\n",
|
|
"prompt = ChatPromptTemplate.from_template(prompt_text)\n",
|
|
"\n",
|
|
"# Text summary chain\n",
|
|
"model = ChatOpenAI(temperature=0, model=\"gpt-4\")\n",
|
|
"summarize_chain = {\"element\": lambda x: x} | prompt | model | StrOutputParser()\n",
|
|
"\n",
|
|
"# Apply to text\n",
|
|
"text_summaries = summarize_chain.batch(texts, {\"max_concurrency\": 5})\n",
|
|
"\n",
|
|
"# Apply to tables\n",
|
|
"table_summaries = summarize_chain.batch(tables, {\"max_concurrency\": 5})"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "bdb5c903-5b4c-4ddb-8f9a-e20f5155dfb9",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Image Summary"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"id": "4570578c-531b-422c-bedd-cc519d9b7887",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Image summary chain\n",
|
|
"import base64\n",
|
|
"import io\n",
|
|
"import os\n",
|
|
"from io import BytesIO\n",
|
|
"\n",
|
|
"from langchain_core.messages import HumanMessage\n",
|
|
"from PIL import Image\n",
|
|
"\n",
|
|
"\n",
|
|
"def encode_image(image_path):\n",
|
|
" \"\"\"Getting the base64 string\"\"\"\n",
|
|
" with open(image_path, \"rb\") as image_file:\n",
|
|
" return base64.b64encode(image_file.read()).decode(\"utf-8\")\n",
|
|
"\n",
|
|
"\n",
|
|
"def image_summarize(img_base64, prompt):\n",
|
|
" \"\"\"Image summary\"\"\"\n",
|
|
" chat = ChatOpenAI(model=\"gpt-4-vision-preview\", max_tokens=1024)\n",
|
|
"\n",
|
|
" msg = chat.invoke(\n",
|
|
" [\n",
|
|
" HumanMessage(\n",
|
|
" content=[\n",
|
|
" {\"type\": \"text\", \"text\": prompt},\n",
|
|
" {\n",
|
|
" \"type\": \"image_url\",\n",
|
|
" \"image_url\": {\"url\": f\"data:image/jpeg;base64,{img_base64}\"},\n",
|
|
" },\n",
|
|
" ]\n",
|
|
" )\n",
|
|
" ]\n",
|
|
" )\n",
|
|
" return msg.content\n",
|
|
"\n",
|
|
"\n",
|
|
"# Store base64 encoded images\n",
|
|
"img_base64_list = []\n",
|
|
"\n",
|
|
"# Store image summaries\n",
|
|
"image_summaries = []\n",
|
|
"\n",
|
|
"# Prompt\n",
|
|
"prompt = \"\"\"You are an assistant tasked with summarizing images for retrieval. \\\n",
|
|
"These summaries will be embedded and used to retrieve the raw image. \\\n",
|
|
"Give a concise summary of the image that is well optimized for retrieval.\"\"\"\n",
|
|
"\n",
|
|
"# Apply to images\n",
|
|
"for img_file in sorted(os.listdir(path)):\n",
|
|
" if img_file.endswith(\".jpg\"):\n",
|
|
" img_path = os.path.join(path, img_file)\n",
|
|
" base64_image = encode_image(img_path)\n",
|
|
" img_base64_list.append(base64_image)\n",
|
|
" image_summaries.append(image_summarize(base64_image, prompt))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "87e03f07-4c82-4743-a3c6-d0597fb55107",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Option 2a: Multi-vector retriever w/ raw images\n",
|
|
"\n",
|
|
"* Return images to LLM for answer synthesis"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 11,
|
|
"id": "6bf8a07d-203f-4397-8b0b-a84ec4d0adab",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import uuid\n",
|
|
"from base64 import b64decode\n",
|
|
"\n",
|
|
"from langchain.retrievers.multi_vector import MultiVectorRetriever\n",
|
|
"from langchain.storage import InMemoryStore\n",
|
|
"from langchain_core.documents import Document\n",
|
|
"\n",
|
|
"\n",
|
|
"def create_multi_vector_retriever(\n",
|
|
" vectorstore, text_summaries, texts, table_summaries, tables, image_summaries, images\n",
|
|
"):\n",
|
|
" # Initialize the storage layer\n",
|
|
" store = InMemoryStore()\n",
|
|
" id_key = \"doc_id\"\n",
|
|
"\n",
|
|
" # Create the multi-vector retriever\n",
|
|
" retriever = MultiVectorRetriever(\n",
|
|
" vectorstore=vectorstore,\n",
|
|
" docstore=store,\n",
|
|
" id_key=id_key,\n",
|
|
" )\n",
|
|
"\n",
|
|
" # Helper function to add documents to the vectorstore and docstore\n",
|
|
" def add_documents(retriever, doc_summaries, doc_contents):\n",
|
|
" doc_ids = [str(uuid.uuid4()) for _ in doc_contents]\n",
|
|
" summary_docs = [\n",
|
|
" Document(page_content=s, metadata={id_key: doc_ids[i]})\n",
|
|
" for i, s in enumerate(doc_summaries)\n",
|
|
" ]\n",
|
|
" retriever.vectorstore.add_documents(summary_docs)\n",
|
|
" retriever.docstore.mset(list(zip(doc_ids, doc_contents)))\n",
|
|
"\n",
|
|
" # Add texts, tables, and images\n",
|
|
" # Check that text_summaries is not empty before adding\n",
|
|
" if text_summaries:\n",
|
|
" add_documents(retriever, text_summaries, texts)\n",
|
|
" # Check that table_summaries is not empty before adding\n",
|
|
" if table_summaries:\n",
|
|
" add_documents(retriever, table_summaries, tables)\n",
|
|
" # Check that image_summaries is not empty before adding\n",
|
|
" if image_summaries:\n",
|
|
" add_documents(retriever, image_summaries, images)\n",
|
|
"\n",
|
|
" return retriever\n",
|
|
"\n",
|
|
"\n",
|
|
"# The vectorstore to use to index the summaries\n",
|
|
"multi_vector_img = Chroma(\n",
|
|
" collection_name=\"multi_vector_img\", embedding_function=OpenAIEmbeddings()\n",
|
|
")\n",
|
|
"\n",
|
|
"# Create retriever\n",
|
|
"retriever_multi_vector_img = create_multi_vector_retriever(\n",
|
|
" multi_vector_img,\n",
|
|
" text_summaries,\n",
|
|
" texts,\n",
|
|
" table_summaries,\n",
|
|
" tables,\n",
|
|
" image_summaries,\n",
|
|
" img_base64_list,\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 32,
|
|
"id": "84d5b4ea-51b8-49cf-8ad1-db8f7a50e3cf",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Testing on retrieval\n",
|
|
"query = \"What percentage of CPI is dedicated to Housing, and how does it compare to the combined percentage of Medical Care, Apparel, and Other Goods and Services?\"\n",
|
|
"suffix_for_images = \" Include any pie charts, graphs, or tables.\"\n",
|
|
"docs = retriever_multi_vector_img.get_relevant_documents(query + suffix_for_images)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 19,
|
|
"id": "8db51ac6-ec0c-4c5d-a9a7-0316035e139d",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"<img src=\"data:image/jpeg;base64,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sUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWZq//LH/AIF/StOszV/+WP8AwL+lAF62/wCPWH/cH8qlqK2/49Yf9wfyqWgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKzNX/5Y/wDAv6Vp1mav/wAsf+Bf0oAvW3/HrD/uD+VS0UUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABWZq/8Ayx/4F/SiAAAAAooA/9k=\" />"
|
|
],
|
|
"text/plain": [
|
|
"<IPython.core.display.HTML object>"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
}
|
|
],
|
|
"source": [
|
|
"from IPython.display import HTML, display\n",
|
|
"\n",
|
|
"\n",
|
|
"def plt_img_base64(img_base64):\n",
|
|
" # Create an HTML img tag with the base64 string as the source\n",
|
|
" image_html = f'<img src=\"data:image/jpeg;base64,{img_base64}\" />'\n",
|
|
"\n",
|
|
" # Display the image by rendering the HTML\n",
|
|
" display(HTML(image_html))\n",
|
|
"\n",
|
|
"\n",
|
|
"plt_img_base64(docs[1])"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "48b268ec-db04-4107-9833-ea1615f6dbd1",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Option 2b: Multi-vector retriever w/ image summaries\n",
|
|
"\n",
|
|
"* Return text summary of images to LLM for answer synthesis"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 20,
|
|
"id": "ae57c804-0dd1-4806-b761-a913efc4f173",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# The vectorstore to use to index the summaries\n",
|
|
"multi_vector_text = Chroma(\n",
|
|
" collection_name=\"multi_vector_text\", embedding_function=OpenAIEmbeddings()\n",
|
|
")\n",
|
|
"\n",
|
|
"# Create retriever\n",
|
|
"retriever_multi_vector_img_summary = create_multi_vector_retriever(\n",
|
|
" multi_vector_text,\n",
|
|
" text_summaries,\n",
|
|
" texts,\n",
|
|
" table_summaries,\n",
|
|
" tables,\n",
|
|
" image_summaries,\n",
|
|
" image_summaries,\n",
|
|
")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "580a3d55-5025-472d-9c14-cec7a384379f",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Option 3: Multi-modal embeddings"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 22,
|
|
"id": "8dbed5dc-f7a3-4324-9436-1c3ebc24f9fd",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from langchain_experimental.open_clip import OpenCLIPEmbeddings\n",
|
|
"\n",
|
|
"# Create chroma w/ multi-modal embeddings\n",
|
|
"multimodal_embd = Chroma(\n",
|
|
" collection_name=\"multimodal_embd\", embedding_function=OpenCLIPEmbeddings()\n",
|
|
")\n",
|
|
"\n",
|
|
"# Get image URIs\n",
|
|
"image_uris = sorted(\n",
|
|
" [\n",
|
|
" os.path.join(path, image_name)\n",
|
|
" for image_name in os.listdir(path)\n",
|
|
" if image_name.endswith(\".jpg\")\n",
|
|
" ]\n",
|
|
")\n",
|
|
"\n",
|
|
"# Add images and documents\n",
|
|
"if image_uris:\n",
|
|
" multimodal_embd.add_images(uris=image_uris)\n",
|
|
"if texts:\n",
|
|
" multimodal_embd.add_texts(texts=texts)\n",
|
|
"if tables:\n",
|
|
" multimodal_embd.add_texts(texts=tables)\n",
|
|
"\n",
|
|
"# Make retriever\n",
|
|
"retriever_multimodal_embd = multimodal_embd.as_retriever()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "647abb6c-adf3-4d29-acd2-885c4925fa12",
|
|
"metadata": {},
|
|
"source": [
|
|
"## RAG\n",
|
|
"\n",
|
|
"### Text Pipeline"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 23,
|
|
"id": "73440ca0-4330-4c16-9d9d-6f27c249ae58",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from operator import itemgetter\n",
|
|
"\n",
|
|
"from langchain_core.runnables import RunnablePassthrough\n",
|
|
"\n",
|
|
"# Prompt\n",
|
|
"template = \"\"\"Answer the question based only on the following context, which can include text and tables:\n",
|
|
"{context}\n",
|
|
"Question: {question}\n",
|
|
"\"\"\"\n",
|
|
"rag_prompt_text = ChatPromptTemplate.from_template(template)\n",
|
|
"\n",
|
|
"\n",
|
|
"# Build\n",
|
|
"def text_rag_chain(retriever):\n",
|
|
" \"\"\"RAG chain\"\"\"\n",
|
|
"\n",
|
|
" # LLM\n",
|
|
" model = ChatOpenAI(temperature=0, model=\"gpt-4\")\n",
|
|
"\n",
|
|
" # RAG pipeline\n",
|
|
" chain = (\n",
|
|
" {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
|
|
" | rag_prompt_text\n",
|
|
" | model\n",
|
|
" | StrOutputParser()\n",
|
|
" )\n",
|
|
"\n",
|
|
" return chain"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "14b358ad-42fd-4c6d-b2c0-215dba135707",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Multi-modal Pipeline"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 24,
|
|
"id": "ae89ce84-283e-4634-8169-9ff16f152807",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import re\n",
|
|
"\n",
|
|
"from langchain_core.documents import Document\n",
|
|
"from langchain_core.runnables import RunnableLambda\n",
|
|
"\n",
|
|
"\n",
|
|
"def looks_like_base64(sb):\n",
|
|
" \"\"\"Check if the string looks like base64.\"\"\"\n",
|
|
" return re.match(\"^[A-Za-z0-9+/]+[=]{0,2}$\", sb) is not None\n",
|
|
"\n",
|
|
"\n",
|
|
"def is_image_data(b64data):\n",
|
|
" \"\"\"Check if the base64 data is an image by looking at the start of the data.\"\"\"\n",
|
|
" image_signatures = {\n",
|
|
" b\"\\xFF\\xD8\\xFF\": \"jpg\",\n",
|
|
" b\"\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A\": \"png\",\n",
|
|
" b\"\\x47\\x49\\x46\\x38\": \"gif\",\n",
|
|
" b\"\\x52\\x49\\x46\\x46\": \"webp\",\n",
|
|
" }\n",
|
|
" try:\n",
|
|
" header = base64.b64decode(b64data)[:8] # Decode and get the first 8 bytes\n",
|
|
" for sig, format in image_signatures.items():\n",
|
|
" if header.startswith(sig):\n",
|
|
" return True\n",
|
|
" return False\n",
|
|
" except Exception:\n",
|
|
" return False\n",
|
|
"\n",
|
|
"\n",
|
|
"def split_image_text_types(docs):\n",
|
|
" \"\"\"Split base64-encoded images and texts.\"\"\"\n",
|
|
" b64_images = []\n",
|
|
" texts = []\n",
|
|
" for doc in docs:\n",
|
|
" # Check if the document is of type Document and extract page_content if so\n",
|
|
" if isinstance(doc, Document):\n",
|
|
" doc = doc.page_content\n",
|
|
" if looks_like_base64(doc) and is_image_data(doc):\n",
|
|
" b64_images.append(doc)\n",
|
|
" else:\n",
|
|
" texts.append(doc)\n",
|
|
" return {\"images\": b64_images, \"texts\": texts}\n",
|
|
"\n",
|
|
"\n",
|
|
"def img_prompt_func(data_dict):\n",
|
|
" # Joining the context texts into a single string\n",
|
|
" formatted_texts = \"\\n\".join(data_dict[\"context\"][\"texts\"])\n",
|
|
" messages = []\n",
|
|
"\n",
|
|
" # Adding image(s) to the messages if present\n",
|
|
" if data_dict[\"context\"][\"images\"]:\n",
|
|
" image_message = {\n",
|
|
" \"type\": \"image_url\",\n",
|
|
" \"image_url\": {\n",
|
|
" \"url\": f\"data:image/jpeg;base64,{data_dict['context']['images'][0]}\"\n",
|
|
" },\n",
|
|
" }\n",
|
|
" messages.append(image_message)\n",
|
|
"\n",
|
|
" # Adding the text message for analysis\n",
|
|
" text_message = {\n",
|
|
" \"type\": \"text\",\n",
|
|
" \"text\": (\n",
|
|
" \"Answer the question based only on the provided context, which can include text, tables, and image(s). \"\n",
|
|
" \"If an image is provided, analyze it carefully to help answer the question.\\n\"\n",
|
|
" f\"User-provided question / keywords: {data_dict['question']}\\n\\n\"\n",
|
|
" \"Text and / or tables:\\n\"\n",
|
|
" f\"{formatted_texts}\"\n",
|
|
" ),\n",
|
|
" }\n",
|
|
" messages.append(text_message)\n",
|
|
" return [HumanMessage(content=messages)]\n",
|
|
"\n",
|
|
"\n",
|
|
"def multi_modal_rag_chain(retriever):\n",
|
|
" \"\"\"Multi-modal RAG chain\"\"\"\n",
|
|
"\n",
|
|
" # Multi-modal LLM\n",
|
|
" model = ChatOpenAI(temperature=0, model=\"gpt-4-vision-preview\", max_tokens=1024)\n",
|
|
"\n",
|
|
" # RAG pipeline\n",
|
|
" chain = (\n",
|
|
" {\n",
|
|
" \"context\": retriever | RunnableLambda(split_image_text_types),\n",
|
|
" \"question\": RunnablePassthrough(),\n",
|
|
" }\n",
|
|
" | RunnableLambda(img_prompt_func)\n",
|
|
" | model\n",
|
|
" | StrOutputParser()\n",
|
|
" )\n",
|
|
"\n",
|
|
" return chain"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "5e8b0e26-bb7e-420a-a7bd-8512b7eef92f",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Build RAG Pipelines"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 25,
|
|
"id": "4f1ec8a9-f0fe-4f08-928f-23504803897c",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# RAG chains\n",
|
|
"chain_baseline = text_rag_chain(retriever_baseline)\n",
|
|
"chain_mv_text = text_rag_chain(retriever_multi_vector_img_summary)\n",
|
|
"\n",
|
|
"# Multi-modal RAG chains\n",
|
|
"chain_multimodal_mv_img = multi_modal_rag_chain(retriever_multi_vector_img)\n",
|
|
"chain_multimodal_embd = multi_modal_rag_chain(retriever_multimodal_embd)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "448d943c-a1b1-4300-9197-891a03232ee4",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Eval set"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 34,
|
|
"id": "9aabf72f-26be-437f-9372-b06dc2509235",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/html": [
|
|
"<div>\n",
|
|
"<style scoped>\n",
|
|
" .dataframe tbody tr th:only-of-type {\n",
|
|
" vertical-align: middle;\n",
|
|
" }\n",
|
|
"\n",
|
|
" .dataframe tbody tr th {\n",
|
|
" vertical-align: top;\n",
|
|
" }\n",
|
|
"\n",
|
|
" .dataframe thead th {\n",
|
|
" text-align: right;\n",
|
|
" }\n",
|
|
"</style>\n",
|
|
"<table border=\"1\" class=\"dataframe\">\n",
|
|
" <thead>\n",
|
|
" <tr style=\"text-align: right;\">\n",
|
|
" <th></th>\n",
|
|
" <th>Question</th>\n",
|
|
" <th>Answer</th>\n",
|
|
" <th>Source</th>\n",
|
|
" </tr>\n",
|
|
" </thead>\n",
|
|
" <tbody>\n",
|
|
" <tr>\n",
|
|
" <th>0</th>\n",
|
|
" <td>What percentage of CPI is dedicated to Housing?</td>\n",
|
|
" <td>Housing occupies 42% of CPI.</td>\n",
|
|
" <td>Figure 1</td>\n",
|
|
" </tr>\n",
|
|
" <tr>\n",
|
|
" <th>1</th>\n",
|
|
" <td>Medical Care and Transportation account for wh...</td>\n",
|
|
" <td>Transportation accounts for 18% of CPI. Medica...</td>\n",
|
|
" <td>Figure 1</td>\n",
|
|
" </tr>\n",
|
|
" <tr>\n",
|
|
" <th>2</th>\n",
|
|
" <td>Based on the CPI Owners' Equivalent Rent and t...</td>\n",
|
|
" <td>The FHFA Purchase Only Price Index appears to ...</td>\n",
|
|
" <td>Figure 2</td>\n",
|
|
" </tr>\n",
|
|
" </tbody>\n",
|
|
"</table>\n",
|
|
"</div>"
|
|
],
|
|
"text/plain": [
|
|
" Question \\\n",
|
|
"0 What percentage of CPI is dedicated to Housing? \n",
|
|
"1 Medical Care and Transportation account for wh... \n",
|
|
"2 Based on the CPI Owners' Equivalent Rent and t... \n",
|
|
"\n",
|
|
" Answer Source \n",
|
|
"0 Housing occupies 42% of CPI. Figure 1 \n",
|
|
"1 Transportation accounts for 18% of CPI. Medica... Figure 1 \n",
|
|
"2 The FHFA Purchase Only Price Index appears to ... Figure 2 "
|
|
]
|
|
},
|
|
"execution_count": 34,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"# Read\n",
|
|
"import pandas as pd\n",
|
|
"\n",
|
|
"eval_set = pd.read_csv(path + \"cpi_eval.csv\")\n",
|
|
"eval_set.head(3)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 35,
|
|
"id": "7fdeb77a-e185-47d2-a93f-822f1fc810a2",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from langsmith import Client\n",
|
|
"\n",
|
|
"# Dataset\n",
|
|
"client = Client()\n",
|
|
"dataset_name = f\"CPI Eval {str(uuid.uuid4())}\"\n",
|
|
"dataset = client.create_dataset(dataset_name=dataset_name)\n",
|
|
"\n",
|
|
"# Populate dataset\n",
|
|
"for _, row in eval_set.iterrows():\n",
|
|
" # Get Q, A\n",
|
|
" q = row[\"Question\"]\n",
|
|
" a = row[\"Answer\"]\n",
|
|
" # Use the values in your function\n",
|
|
" client.create_example(\n",
|
|
" inputs={\"question\": q}, outputs={\"answer\": a}, dataset_id=dataset.id\n",
|
|
" )"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 36,
|
|
"id": "3c4faf4b-f29f-4a42-9cf2-bfbb5158ab59",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"View the evaluation results for project 'CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126-baseline' at:\n",
|
|
"https://smith.langchain.com/o/1fa8b1f4-fcb9-4072-9aa9-983e35ad61b8/projects/p/533846be-d907-4d9c-82db-ce2f1a18fdbf?eval=true\n",
|
|
"\n",
|
|
"View all tests for Dataset CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126 at:\n",
|
|
"https://smith.langchain.com/datasets/d1762232-5e01-40e7-9978-63002a4c95a3\n",
|
|
"[------------------------------------------------->] 4/4View the evaluation results for project 'CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126-mv_text' at:\n",
|
|
"https://smith.langchain.com/o/1fa8b1f4-fcb9-4072-9aa9-983e35ad61b8/projects/p/f5caeede-6f8e-46f7-b4f2-9f23daa31eda?eval=true\n",
|
|
"\n",
|
|
"View all tests for Dataset CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126 at:\n",
|
|
"https://smith.langchain.com/datasets/d1762232-5e01-40e7-9978-63002a4c95a3\n",
|
|
"[------------------------------------------------->] 4/4View the evaluation results for project 'CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126-mv_img' at:\n",
|
|
"https://smith.langchain.com/o/1fa8b1f4-fcb9-4072-9aa9-983e35ad61b8/projects/p/48cf1002-7ae2-451d-a9b1-5bd8088f6a69?eval=true\n",
|
|
"\n",
|
|
"View all tests for Dataset CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126 at:\n",
|
|
"https://smith.langchain.com/datasets/d1762232-5e01-40e7-9978-63002a4c95a3\n",
|
|
"[------------------------------------------------->] 4/4View the evaluation results for project 'CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126-mm_embd' at:\n",
|
|
"https://smith.langchain.com/o/1fa8b1f4-fcb9-4072-9aa9-983e35ad61b8/projects/p/aaa1c2e3-79b0-43e0-b5d5-8e3d00a51d50?eval=true\n",
|
|
"\n",
|
|
"View all tests for Dataset CPI Eval 9648e7fe-5ae2-469f-8701-33c63212d126 at:\n",
|
|
"https://smith.langchain.com/datasets/d1762232-5e01-40e7-9978-63002a4c95a3\n",
|
|
"[------------------------------------------------->] 4/4"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"from langchain.smith import RunEvalConfig\n",
|
|
"\n",
|
|
"eval_config = RunEvalConfig(\n",
|
|
" evaluators=[\"qa\"],\n",
|
|
")\n",
|
|
"\n",
|
|
"\n",
|
|
"def run_eval(chain, run_name, dataset_name):\n",
|
|
" _ = client.run_on_dataset(\n",
|
|
" dataset_name=dataset_name,\n",
|
|
" llm_or_chain_factory=lambda: (lambda x: x[\"question\"] + suffix_for_images)\n",
|
|
" | chain,\n",
|
|
" evaluation=eval_config,\n",
|
|
" project_name=run_name,\n",
|
|
" )\n",
|
|
"\n",
|
|
"\n",
|
|
"for chain, run in zip(\n",
|
|
" [chain_baseline, chain_mv_text, chain_multimodal_mv_img, chain_multimodal_embd],\n",
|
|
" [\"baseline\", \"mv_text\", \"mv_img\", \"mm_embd\"],\n",
|
|
"):\n",
|
|
" run_eval(chain, dataset_name + \"-\" + run, dataset_name)"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.9.16"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|