NCEE-Build-Lab/watsonx.ai_Vector_Embedding_Visualizer_MNB
0
1# /// script2# [tool.marimo.display]3# custom_css = ["./custom_header_font.css"]4# ///5 6import marimo7 8__generated_with = "0.13.0"9app = marimo.App(width="full")10 11with app.setup:12 # Initialization code that runs before all other cells13 import marimo as mo14 from typing import Dict, Optional, List, Union, Any15 from ibm_watsonx_ai import APIClient, Credentials16 from pathlib import Path17 import pandas as pd18 import mimetypes19 import requests20 import zipfile21 import tempfile22 import certifi23 import base6424 import polars25 import nltk26 import time27 import json28 import ast 29 import os30 import io31 import re32 33 def get_iam_token(api_key):34 return requests.post(35 'https://iam.cloud.ibm.com/identity/token',36 headers={'Content-Type': 'application/x-www-form-urlencoded'},37 data={'grant_type': 'urn:ibm:params:oauth:grant-type:apikey', 'apikey': api_key},38 verify=certifi.where()39 ).json()['access_token']40 41 def setup_task_credentials(client):42 # Get existing task credentials43 existing_credentials = client.task_credentials.get_details()44 45 # Delete existing credentials if any46 if "resources" in existing_credentials and existing_credentials["resources"]:47 for cred in existing_credentials["resources"]:48 cred_id = client.task_credentials.get_id(cred)49 client.task_credentials.delete(cred_id)50 51 # Store new credentials52 return client.task_credentials.store()53 54 def get_cred_value(key, creds_var_name="baked_in_creds", default=""): ### Helper for working with preset credentials55 """56 Helper function to safely get a value from a credentials dictionary.57 58 Args:59 key: The key to look up in the credentials dictionary.60 creds_var_name: The variable name of the credentials dictionary.61 default: The default value to return if the key is not found.62 63 Returns:64 The value from the credentials dictionary if it exists and contains the key,65 otherwise returns the default value.66 """67 # Check if the credentials variable exists in globals68 if creds_var_name in globals():69 creds_dict = globals()[creds_var_name]70 if isinstance(creds_dict, dict) and key in creds_dict:71 # Return empty string if the value is None72 value = creds_dict[key]73 return "" if value is None else value74 return default75 76@app.cell77def client_variables(client_instantiation_form):78 client_setup = client_instantiation_form.value or None79 80 ### Extract Credential Variables:81 if client_setup:82 wx_url = client_setup["wx_region"] if client_setup["wx_region"] else "EU"83 wx_api_key = client_setup["wx_api_key"].strip() if client_setup["wx_api_key"] else None84 os.environ["WATSONX_APIKEY"] = wx_api_key or ""85 86 project_id = client_setup["project_id"].strip() if client_setup["project_id"] else None87 space_id = client_setup["space_id"].strip() if client_setup["space_id"] else None88 else:89 os.environ["WATSONX_APIKEY"] = ""90 project_id = space_id = wx_api_key = wx_url = None91 return client_setup, project_id, space_id, wx_api_key, wx_url92 93 94# @app.cell95# def _(client_setup, wx_api_key):96# if client_setup:97# token = get_iam_token(wx_api_key)98# else:99# token = None100# return101 102@app.cell103def _():104 from baked_in_credentials.creds import credentials105 from base_variables import wx_regions, wx_platform_url106 from helper_functions.helper_functions import wrap_with_spaces, get_key_by_value, markdown_spacing107 return (108 credentials, 109 wx_regions, 110 wx_platform_url, 111 wrap_with_spaces, 112 get_key_by_value,113 markdown_spacing,114 )115 116@app.cell117def client_instantiation(118 APIClient,119 Credentials,120 client_setup,121 project_id,122 space_id,123 wx_api_key,124 wx_url,125):126 ### Instantiate the watsonx.ai client127 if client_setup:128 try:129 wx_credentials = Credentials(url=wx_url, api_key=wx_api_key)130 project_client = (131 APIClient(credentials=wx_credentials, project_id=project_id)132 if project_id133 else None134 )135 deployment_client = (136 APIClient(credentials=wx_credentials, space_id=space_id)137 if space_id138 else None139 )140 instantiation_success = True141 instantiation_error = None142 except Exception as e:143 instantiation_success = False144 instantiation_error = str(e)145 wx_credentials = project_client = deployment_client = None146 else:147 wx_credentials = project_client = deployment_client = None148 instantiation_success = None149 instantiation_error = None150 151 return (152 deployment_client,153 instantiation_error,154 instantiation_success,155 project_client,156 )157 158 159@app.cell160def _():161 mo.md(162 r"""163 #watsonx.ai Embedding Visualizer - Marimo Notebook164 165 #### This marimo notebook can be used to develop a more intuitive understanding of how vector embeddings work by creating a 3D visualization of vector embeddings based on chunked PDF document pages. 166 167 #### It can also serve as a useful tool for identifying gaps in model choice, chunking strategy or contents used in building collections by showing how far you are from what you want.168 <br>169 170 /// admonition171 Created by ***Milan Mrdenovic*** [milan.mrdenovic@ibm.com] for IBM Ecosystem Client Engineering, NCEE - ***version 5.3** - 20.04.2025*172 ///173 174 175 >Licensed under apache 2.0, users hold full accountability for any use or modification of the code.176 ><br>This asset is part of a set meant to support IBMers, IBM Partners, Clients in developing understanding of how to better utilize various watsonx features and generative AI as a subject matter.177 178 <br>179 """180 )181 return182 183 184@app.cell185def _():186 mo.md("""###Part 1 - Client Setup, File Preparation and Chunking""")187 return188 189 190@app.cell191def accordion_client_setup(client_selector, client_stack):192 ui_accordion_part_1_1 = mo.accordion(193 {194 "Instantiate Client": mo.vstack([client_stack, client_selector], align="center"),195 }196 )197 198 ui_accordion_part_1_1199 return200 201 202@app.cell203def accordion_file_upload(select_stack):204 ui_accordion_part_1_2 = mo.accordion(205 {206 "Select Model & Upload Files": select_stack207 }208 )209 210 ui_accordion_part_1_2211 return212 213 214@app.cell215def loaded_texts(216 create_temp_files_from_uploads,217 file_loader,218 pdf_reader,219 run_upload_button,220 set_text_state,221):222 if file_loader.value is not None and run_upload_button.value:223 filepaths = create_temp_files_from_uploads(file_loader.value)224 loaded_texts = load_pdf_data_with_progress(pdf_reader, filepaths, file_loader.value, show_progress=True)225 226 set_text_state(loaded_texts)227 else:228 filepaths = None229 loaded_texts = None230 return231 232 233@app.cell234def accordion_chunker_setup(chunker_setup):235 ui_accordion_part_1_3 = mo.accordion(236 {237 "Chunker Setup": chunker_setup238 }239 )240 241 ui_accordion_part_1_3242 return243 244 245@app.cell246def chunk_documents_to_nodes(247 get_text_state,248 sentence_splitter,249 sentence_splitter_config,250 set_chunk_state,251):252 if sentence_splitter_config.value and sentence_splitter and get_text_state() is not None:253 chunked_texts = chunk_documents(get_text_state(), sentence_splitter, show_progress=True)254 set_chunk_state(chunked_texts)255 else:256 chunked_texts = None257 return (chunked_texts,)258 259 260@app.cell261def _():262 mo.md(r"""###Part 2 - Query Setup and Visualization""")263 return264 265 266@app.cell267def accordion_chunk_range(chart_range_selection):268 ui_accordion_part_2_1 = mo.accordion(269 {270 "Chunk Range Selection": chart_range_selection271 }272 )273 ui_accordion_part_2_1274 return275 276 277@app.cell278def chunk_embedding(279 chunks_to_process,280 embedding,281 sentence_splitter_config,282 set_embedding_state,283):284 if sentence_splitter_config.value is not None and chunks_to_process is not None:285 with mo.status.spinner(title="Embedding Documents...", remove_on_exit=True) as _spinner:286 output_embeddings = embedding.embed_documents(chunks_to_process)287 _spinner.update("Almost Done")288 time.sleep(1.5)289 set_embedding_state(output_embeddings)290 _spinner.update("Documents Embedded")291 else:292 output_embeddings = None293 return294 295 296@app.cell297def preview_chunks(chunks_dict):298 if chunks_dict is not None:299 stats = create_stats(chunks_dict, 300 bordered=True, 301 object_names=['text','text'], 302 group_by_row=True, 303 items_per_row=5, 304 gap=1,305 label="Chunk")306 ui_chunk_viewer = mo.accordion(307 {308 "View Chunks": stats,309 }310 )311 else:312 ui_chunk_viewer = None313 314 ui_chunk_viewer315 return316 317 318@app.cell319def accordion_query_view(chart_visualization, query_stack):320 ui_accordion_part_2_2 = mo.accordion(321 {322 "Query": mo.vstack([query_stack, mo.hstack([chart_visualization])], align="center", gap=3)323 }324 )325 ui_accordion_part_2_2326 return327 328 329@app.cell330def chunker_setup(sentence_splitter_config):331 chunker_setup = mo.hstack([sentence_splitter_config], justify="space-around", align="center", widths=[0.55])332 return (chunker_setup,)333 334 335@app.cell336def file_and_model_select(337 file_loader,338 get_embedding_model_list,339 run_upload_button,340):341 select_stack = mo.hstack([get_embedding_model_list(), mo.vstack([mo.md("Drag & Drop or Double Click to select PDFs, then press **Load Files**"),file_loader, run_upload_button], align="center")], justify="space-around", align="center", widths=[0.3,0.3])342 return (select_stack,)343 344 345@app.cell346def client_instantiation_form():347 baked_in_creds = credentials348 # Create a form with multiple elements349 client_instantiation_form = (350 mo.md('''351 ###**watsonx.ai credentials:**352 353 {wx_region}354 355 {wx_api_key}356 357 {project_id}358 359 {space_id}360 361 > You can add either a project_id, space_id or both, **only one is required**. 362 > If you provide both you can switch the active one in the dropdown.363 ''')364 .batch(365 wx_region = mo.ui.dropdown(366 wx_regions, 367 label="Select your watsonx.ai region:", 368 value=get_cred_value('region', creds_var_name='baked_in_creds'),369 searchable=True370 ),371 wx_api_key = mo.ui.text(372 placeholder="Add your IBM Cloud api-key...", 373 label="IBM Cloud Api-key:", 374 kind="password", 375 value=get_cred_value('api_key', creds_var_name='baked_in_creds')376 ),377 project_id = mo.ui.text(378 placeholder="Add your watsonx.ai project_id...", 379 label="Project_ID:", 380 kind="text", 381 value=get_cred_value('project_id', creds_var_name='baked_in_creds')382 ),383 space_id = mo.ui.text(384 placeholder="Add your watsonx.ai space_id...", 385 label="Space_ID:", 386 kind="text",387 value=get_cred_value('space_id', creds_var_name='baked_in_creds')388 )389 ,)390 .form(show_clear_button=True, bordered=False)391 )392 return (client_instantiation_form,)393 394 395@app.cell396def instantiation_status(397 client_callout_kind,398 client_instantiation_form,399 client_status,400):401 client_callout = mo.callout(client_status, kind=client_callout_kind)402 client_stack = mo.hstack([client_instantiation_form, client_callout], align="center", justify="space-around", gap=10)403 return (client_stack,)404 405@app.cell406def _(407 client,408 client_key,409 client_options,410 client_selector,411 client_setup,412 get_key_by_value,413 instantiation_error,414 instantiation_success,415 mo,416 wrap_with_spaces,417):418 active_client_name = get_key_by_value(client_options, client_key)419 420 if client_setup:421 if instantiation_success:422 client_status = mo.md(423 f"### โ
Client Instantiation Successful โ
\n\n"424 f"{client_selector}\n\n"425 f"**Active Client:**{wrap_with_spaces(active_client_name, prefix_spaces=5)}"426 )427 client_callout_kind = "success"428 else:429 client_status = mo.md(430 f"### โ Client Instantiation Failed\n**Error:** {instantiation_error}\n\nCheck your region selection and credentials"431 )432 client_callout_kind = "danger"433 else:434 client_status = mo.md(435 f"### Client Instantiation Status will turn Green When Ready\n\n"436 f"{client_selector}\n\n"437 f"**Active Client:**{wrap_with_spaces(active_client_name, prefix_spaces=5)}"438 )439 client_callout_kind = "neutral"440 441 return active_client_name, client_callout_kind, client_status442 443@app.cell444def client_selector(deployment_client, project_client):445 if deployment_client is not None:446 client_options = {"Deployment Client":deployment_client}447 448 elif project_client is not None:449 client_options = {"Project Client":project_client}450 451 elif project_client is not None and deployment_client is not None:452 client_options = {"Project Client":project_client,"Deployment Client":deployment_client}453 454 else:455 client_options = {"No Client": "Instantiate a Client"}456 457 default_client = next(iter(client_options))458 client_selector = mo.ui.dropdown(client_options, value=default_client, label="**Select your active client:**")459 return client_options, client_selector460 461 462@app.cell463def active_client(client_selector):464 client_key = client_selector.value465 if client_key == "Instantiate a Client":466 client = None467 else:468 client = client_key469 return client, client_key470 471@app.cell472def emb_model_selection(client, set_embedding_model_list):473 if client is not None:474 model_specs = client.foundation_models.get_embeddings_model_specs()475 # model_specs = client.foundation_models.get_model_specs()476 resources = model_specs["resources"]477 # Define embedding models reference data478 embedding_models = {479 "ibm/granite-embedding-107m-multilingual": {"max_tokens": 512, "embedding_dimensions": 384},480 "ibm/granite-embedding-278m-multilingual": {"max_tokens": 512, "embedding_dimensions": 768},481 "ibm/slate-125m-english-rtrvr-v2": {"max_tokens": 512, "embedding_dimensions": 768},482 "ibm/slate-125m-english-rtrvr": {"max_tokens": 512, "embedding_dimensions": 768},483 "ibm/slate-30m-english-rtrvr-v2": {"max_tokens": 512, "embedding_dimensions": 384},484 "ibm/slate-30m-english-rtrvr": {"max_tokens": 512, "embedding_dimensions": 384},485 "sentence-transformers/all-minilm-l6-v2": {"max_tokens": 128, "embedding_dimensions": 384},486 "sentence-transformers/all-minilm-l12-v2": {"max_tokens": 128, "embedding_dimensions": 384},487 "intfloat/multilingual-e5-large": {"max_tokens": 512, "embedding_dimensions": 1024}488 }489 490 # Get model IDs from resources491 model_id_list = []492 for resource in resources:493 model_id_list.append(resource["model_id"])494 495 # Create enhanced model data for the table496 embedding_model_data = []497 for model_id in model_id_list:498 model_entry = {"model_id": model_id}499 500 # Add properties if model exists in our reference, otherwise use 0501 if model_id in embedding_models:502 model_entry["max_tokens"] = embedding_models[model_id]["max_tokens"]503 model_entry["embedding_dimensions"] = embedding_models[model_id]["embedding_dimensions"]504 else:505 model_entry["max_tokens"] = 0506 model_entry["embedding_dimensions"] = 0507 508 embedding_model_data.append(model_entry)509 510 embedding_model_selection = mo.ui.table(511 embedding_model_data,512 selection="single", # Only allow selecting one row513 label="Select an embedding model to use.",514 page_size=30,515 initial_selection=[1]516 )517 set_embedding_model_list(embedding_model_selection)518 else:519 default_model_data = [{520 "model_id": "ibm/granite-embedding-107m-multilingual",521 "max_tokens": 512,522 "embedding_dimensions": 384523 }]524 525 set_embedding_model_list(create_emb_model_selection_table(default_model_data, initial_selection=0, selection_type="single", label="Select a model to use."))526 return527 528 529@app.function530def create_emb_model_selection_table(model_data, initial_selection=0, selection_type="single", label="Select a model to use."):531 embedding_model_selection = mo.ui.table(532 model_data,533 selection=selection_type, # Only allow selecting one row534 label=label,535 page_size=30,536 initial_selection=[initial_selection]537 )538 return embedding_model_selection539 540 541@app.cell542def embedding_model():543 get_embedding_model_list, set_embedding_model_list = mo.state(None)544 return get_embedding_model_list, set_embedding_model_list545 546 547@app.cell548def emb_model_parameters(emb_model_max_tk):549 from ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames as EmbedParams550 if embedding_model is not None:551 embed_params = {552 EmbedParams.TRUNCATE_INPUT_TOKENS: emb_model_max_tk,553 EmbedParams.RETURN_OPTIONS: {554 'input_text': True555 }556 }557 else:558 embed_params = {559 EmbedParams.TRUNCATE_INPUT_TOKENS: 128,560 EmbedParams.RETURN_OPTIONS: {561 'input_text': True562 }563 } 564 return embed_params565 566 567@app.cell568def emb_model_state(get_embedding_model_list):569 embedding_model = get_embedding_model_list()570 return (embedding_model,)571 572 573@app.cell574def emb_model_setup(embedding_model):575 if embedding_model is not None:576 emb_model = embedding_model.value[0]['model_id']577 emb_model_max_tk = embedding_model.value[0]['max_tokens']578 emb_model_emb_dim = embedding_model.value[0]['embedding_dimensions']579 else:580 emb_model = None581 emb_model_max_tk = None582 emb_model_emb_dim = None583 return emb_model, emb_model_emb_dim, emb_model_max_tk584 585 586@app.cell587def emb_model_instantiation(client, emb_model, embed_params):588 from ibm_watsonx_ai.foundation_models import Embeddings589 if client is not None:590 embedding = Embeddings(591 model_id=emb_model,592 api_client=client,593 params=embed_params,594 batch_size=1000,595 concurrency_limit=10596 )597 else:598 embedding = None599 return (embedding,)600 601 602@app.cell603def _():604 get_embedding_state, set_embedding_state = mo.state(None)605 return get_embedding_state, set_embedding_state606 607 608@app.cell609def _():610 get_query_state, set_query_state = mo.state(None)611 return get_query_state, set_query_state612 613 614@app.cell615def file_loader_input():616 file_loader = mo.ui.file(617 kind="area",618 filetypes=[".pdf"],619 label=" Load .pdf files ",620 multiple=True621 )622 return (file_loader,)623 624 625@app.cell626def file_loader_run(file_loader):627 if file_loader.value:628 run_upload_button = mo.ui.run_button(label="Load Files")629 else:630 run_upload_button = mo.ui.run_button(disabled=True, label="Load Files")631 return (run_upload_button,)632 633 634@app.cell635def helper_function_tempfiles():636 def create_temp_files_from_uploads(upload_results) -> List[str]:637 """638 Creates temporary files from a tuple of FileUploadResults objects and returns their paths.639 Args:640 upload_results: Object containing a value attribute that is a tuple of FileUploadResults641 Returns:642 List of temporary file paths643 """644 temp_file_paths = []645 646 # Get the number of items in the tuple647 num_items = len(upload_results)648 649 # Process each item by index650 for i in range(num_items):651 result = upload_results[i] # Get item by index652 653 # Create a temporary file with the original filename654 temp_dir = tempfile.gettempdir()655 file_name = result.name656 temp_path = os.path.join(temp_dir, file_name)657 # Write the contents to the temp file658 with open(temp_path, 'wb') as temp_file:659 temp_file.write(result.contents)660 # Add the path to our list661 temp_file_paths.append(temp_path)662 663 return temp_file_paths664 665 def cleanup_temp_files(temp_file_paths: List[str]) -> None:666 """Delete temporary files after use."""667 for path in temp_file_paths:668 if os.path.exists(path):669 os.unlink(path)670 return (create_temp_files_from_uploads,)671 672 673@app.function674def load_pdf_data_with_progress(pdf_reader, filepaths, file_loader_value, show_progress=True):675 """676 Loads PDF data for each file path and organizes results by original filename.677 Args:678 pdf_reader: The PyMuPDFReader instance679 filepaths: List of temporary file paths680 file_loader_value: The original upload results value containing file information681 show_progress: Whether to show a progress bar during loading (default: False)682 Returns:683 Dictionary mapping original filenames to their loaded text content684 """685 results = {}686 687 # Process files with or without progress bar688 if show_progress:689 import marimo as mo690 # Use progress bar with the length of filepaths as total691 with mo.status.progress_bar(692 total=len(filepaths),693 title="Loading PDFs",694 subtitle="Processing documents...",695 completion_title="PDF Loading Complete",696 completion_subtitle=f"{len(filepaths)} documents processed",697 remove_on_exit=True698 ) as bar:699 # Process each file path700 for i, file_path in enumerate(filepaths):701 702 original_file_name = file_loader_value[i].name703 bar.update(subtitle=f"Processing {original_file_name}...")704 loaded_text = pdf_reader.load_data(file_path=file_path, metadata=True)705 706 # Store the result with the original filename as the key707 results[original_file_name] = loaded_text708 # Update progress bar709 bar.update(increment=1)710 else:711 # Original logic without progress bar712 for i, file_path in enumerate(filepaths):713 original_file_name = file_loader_value[i].name714 loaded_text = pdf_reader.load_data(file_path=file_path, metadata=True)715 results[original_file_name] = loaded_text716 717 return results718 719 720@app.cell721def file_readers():722 from llama_index.readers.file import PyMuPDFReader723 from llama_index.readers.file import FlatReader724 from llama_index.core.node_parser import SentenceSplitter725 726 ### File Readers727 pdf_reader = PyMuPDFReader()728 # flat_file_reader = FlatReader()729 return SentenceSplitter, pdf_reader730 731 732@app.cell733def sentence_splitter_setup():734 ### Chunker Setup735 sentence_splitter_config = (736 mo.md('''737 ###**Chunking Setup:**738 739 > Unless you want to do some advanced sentence splitting, it's best to stick to adjusting only the chunk size and overlap. Changing the other settings might result in unexpected results.740 741 Separator value is set to **" "** by default, while the paragraph separator is **"\\n\\n\\n"**.742 743 {chunk_size} 744 745 {chunk_overlap}746 747 {separator} {paragraph_separator}748 749 {secondary_chunking_regex} {include_metadata}750 751 ''')752 .batch(753 chunk_size = mo.ui.slider(start=100, stop=5000, step=1, label="**Chunk Size:**", value=275, show_value=True, full_width=True),754 chunk_overlap = mo.ui.slider(start=0, stop=1000, step=1, label="**Chunk Overlap** *(Must always be smaller than Chunk Size)* **:**", value=0, show_value=True, full_width=True),755 separator = mo.ui.text(placeholder="Define a separator", label="**Separator:**", kind="text", value=" "),756 paragraph_separator = mo.ui.text(placeholder="Define a paragraph separator", 757 label="**Paragraph Separator:**", kind="text", 758 value="\n\n\n"),759 secondary_chunking_regex = mo.ui.text(placeholder="Define a secondary chunking regex", 760 label="**Chunking Regex:**", kind="text", 761 value="[^,.;?!]+[,.;?!]?"),762 include_metadata= mo.ui.checkbox(value=True, label="**Include Metadata**")763 )764 .form(show_clear_button=True, bordered=False, submit_button_label="Chunk Documents")765 )766 return (sentence_splitter_config,)767 768 769@app.cell770def sentence_splitter_instantiation(771 SentenceSplitter,772 sentence_splitter_config,773):774 ### Chunker/Sentence Splitter 775 def simple_whitespace_tokenizer(text):776 return text.split()777 778 if sentence_splitter_config.value is not None:779 sentence_splitter_config_values = sentence_splitter_config.value780 validated_chunk_overlap = min(sentence_splitter_config_values.get("chunk_overlap"), 781 int(sentence_splitter_config_values.get("chunk_size") * 0.3))782 783 sentence_splitter = SentenceSplitter(784 chunk_size=sentence_splitter_config_values.get("chunk_size"),785 chunk_overlap=validated_chunk_overlap,786 separator=sentence_splitter_config_values.get("separator"),787 paragraph_separator=sentence_splitter_config_values.get("paragraph_separator"),788 secondary_chunking_regex=sentence_splitter_config_values.get("secondary_chunking_regex"),789 include_metadata=sentence_splitter_config_values.get("include_metadata"),790 tokenizer=simple_whitespace_tokenizer791 )792 793 else:794 sentence_splitter = SentenceSplitter(795 chunk_size=2048,796 chunk_overlap=204,797 separator=" ",798 paragraph_separator="\n\n\n",799 secondary_chunking_regex="[^,.;?!]+[,.;?!]?",800 include_metadata=True,801 tokenizer=simple_whitespace_tokenizer802 )803 return (sentence_splitter,)804 805 806@app.cell807def text_state():808 get_text_state, set_text_state = mo.state(None)809 return get_text_state, set_text_state810 811 812@app.cell813def chunk_state():814 get_chunk_state, set_chunk_state = mo.state(None)815 return get_chunk_state, set_chunk_state816 817 818@app.function819def chunk_documents(loaded_texts, sentence_splitter, show_progress=True):820 """821 Process each document in the loaded_texts dictionary using the sentence_splitter,822 with an optional marimo progress bar tracking progress at document level.823 824 Args:825 loaded_texts (dict): Dictionary containing lists of Document objects826 sentence_splitter: The sentence splitter object with get_nodes_from_documents method827 show_progress (bool): Whether to show a progress bar during processing828 829 Returns:830 dict: Dictionary with the same structure but containing chunked texts831 """832 chunked_texts_dict = {}833 834 # Get the total number of documents across all keys835 total_docs = sum(len(docs) for docs in loaded_texts.values())836 processed_docs = 0837 838 # Process with or without progress bar839 if show_progress:840 import marimo as mo841 # Use progress bar with the total number of documents as total842 with mo.status.progress_bar(843 total=total_docs,844 title="Processing Documents",845 subtitle="Chunking documents...",846 completion_title="Processing Complete",847 completion_subtitle=f"{total_docs} documents processed",848 remove_on_exit=True849 ) as bar:850 # Process each key-value pair in the loaded_texts dictionary851 for key, documents in loaded_texts.items():852 # Update progress bar subtitle to show current key853 doc_count = len(documents)854 bar.update(subtitle=f"Chunking {key}... ({doc_count} documents)")855 856 # Apply the sentence splitter to each list of documents857 chunked_texts = sentence_splitter.get_nodes_from_documents(858 documents, 859 show_progress=False # Disable internal progress to avoid nested bars860 )861 862 # Store the result with the same key863 chunked_texts_dict[key] = chunked_texts864 time.sleep(0.15)865 866 # Update progress bar with the number of documents in this batch867 bar.update(increment=doc_count)868 processed_docs += doc_count869 else:870 # Process without progress bar871 for key, documents in loaded_texts.items():872 chunked_texts = sentence_splitter.get_nodes_from_documents(873 documents, 874 show_progress=True # Use the internal progress bar if no marimo bar875 )876 chunked_texts_dict[key] = chunked_texts877 878 return chunked_texts_dict879 880 881@app.cell882def chunked_nodes(chunked_texts, get_chunk_state, sentence_splitter):883 if chunked_texts is not None and sentence_splitter:884 chunked_documents = get_chunk_state()885 else:886 chunked_documents = None887 return (chunked_documents,)888 889 890@app.cell891def prep_cumulative_df(chunked_documents, llamaindex_convert_docs_multi):892 if chunked_documents is not None:893 dict_from_nodes = llamaindex_convert_docs_multi(chunked_documents)894 nodes_from_dict = llamaindex_convert_docs_multi(dict_from_nodes)895 else:896 dict_from_nodes = None897 nodes_from_dict = None898 return (dict_from_nodes,)899 900 901@app.cell902def chunks_to_process(903 dict_from_nodes,904 document_range_stack,905 get_data_in_range_triplequote,906):907 if dict_from_nodes is not None and document_range_stack is not None:908 909 chunk_dict_df = create_cumulative_dataframe(dict_from_nodes)910 911 if document_range_stack.value is not None:912 chunk_start_idx = document_range_stack.value[0]913 chunk_end_idx = document_range_stack.value[1]914 else:915 chunk_start_idx = 0916 chunk_end_idx = len(chunk_dict_df)917 918 chunk_range_index = [chunk_start_idx, chunk_end_idx]919 chunks_dict = get_data_in_range_triplequote(chunk_dict_df,920 index_range=chunk_range_index, 921 columns_to_include=["text"])922 923 chunks_to_process = chunks_dict['text'] if 'text' in chunks_dict else []924 else:925 chunk_objects = None926 chunks_dict = None927 chunks_to_process = None928 return chunks_dict, chunks_to_process929 930 931@app.cell932def helper_function_doc_formatting():933 def llamaindex_convert_docs_multi(items):934 """935 Automatically convert between document objects and dictionaries.936 937 This function handles:938 - Converting dictionaries to document objects939 - Converting document objects to dictionaries940 - Processing lists or individual items941 - Supporting dictionary structures where values are lists of documents942 943 Args:944 items: A document object, dictionary, or list of either.945 Can also be a dictionary mapping filenames to lists of documents.946 947 Returns:948 Converted item(s) maintaining the original structure949 """950 # Handle empty or None input951 if not items:952 return []953 954 # Handle dictionary mapping filenames to document lists (from load_pdf_data)955 if isinstance(items, dict) and all(isinstance(v, list) for v in items.values()):956 result = {}957 for filename, doc_list in items.items():958 result[filename] = llamaindex_convert_docs(doc_list)959 return result960 961 # Handle single items (not in a list)962 if not isinstance(items, list):963 # Single dictionary to document964 if isinstance(items, dict):965 # Determine document class966 doc_class = None967 if 'doc_type' in items:968 import importlib969 module_path, class_name = items['doc_type'].rsplit('.', 1)970 module = importlib.import_module(module_path)971 doc_class = getattr(module, class_name)972 if not doc_class:973 from llama_index.core.schema import Document974 doc_class = Document975 return doc_class.from_dict(items)976 # Single document to dictionary977 elif hasattr(items, 'to_dict'):978 return items.to_dict()979 # Return as is if can't convert980 return items981 982 # Handle list input983 result = []984 985 # Handle empty list986 if len(items) == 0:987 return result988 989 # Determine the type of conversion based on the first non-None item990 first_item = next((item for item in items if item is not None), None)991 992 # If we found no non-None items, return empty list993 if first_item is None:994 return result995 996 # Convert dictionaries to documents997 if isinstance(first_item, dict):998 # Get the right document class from the items themselves999 doc_class = None1000 # Try to get doc class from metadata if available1001 if 'doc_type' in first_item:1002 import importlib1003 module_path, class_name = first_item['doc_type'].rsplit('.', 1)1004 module = importlib.import_module(module_path)1005 doc_class = getattr(module, class_name)1006 if not doc_class:1007 # Fallback to default Document class from llama_index1008 from llama_index.core.schema import Document1009 doc_class = Document1010 1011 # Convert each dictionary to document1012 for item in items:1013 if isinstance(item, dict):1014 result.append(doc_class.from_dict(item))1015 elif item is None:1016 result.append(None)1017 elif isinstance(item, list):1018 result.append(llamaindex_convert_docs(item))1019 else:1020 result.append(item)1021 1022 # Convert documents to dictionaries1023 else:1024 for item in items:1025 if hasattr(item, 'to_dict'):1026 result.append(item.to_dict())1027 elif item is None:1028 result.append(None)1029 elif isinstance(item, list):1030 result.append(llamaindex_convert_docs(item))1031 else:1032 result.append(item)1033 1034 return result1035 1036 def llamaindex_convert_docs(items):1037 """1038 Automatically convert between document objects and dictionaries.1039 1040 Args:1041 items: A list of document objects or dictionaries1042 1043 Returns:1044 List of converted items (dictionaries or document objects)1045 """1046 result = []1047 1048 # Handle empty or None input1049 if not items:1050 return result1051 1052 # Determine the type of conversion based on the first item1053 if isinstance(items[0], dict):1054 # Get the right document class from the items themselves1055 # Look for a 'doc_type' or '__class__' field in the dictionary1056 doc_class = None1057 1058 # Try to get doc class from metadata if available1059 if 'doc_type' in items[0]:1060 import importlib1061 module_path, class_name = items[0]['doc_type'].rsplit('.', 1)1062 module = importlib.import_module(module_path)1063 doc_class = getattr(module, class_name)1064 1065 if not doc_class:1066 # Fallback to default Document class from llama_index1067 from llama_index.core.schema import Document1068 doc_class = Document1069 1070 # Convert dictionaries to documents1071 for item in items:1072 if isinstance(item, dict):1073 result.append(doc_class.from_dict(item))1074 else:1075 # Convert documents to dictionaries1076 for item in items:1077 if hasattr(item, 'to_dict'):1078 result.append(item.to_dict())1079 1080 return result1081 return (llamaindex_convert_docs_multi,)1082 1083 1084@app.cell1085def helper_function_create_df():1086 def create_document_dataframes(dict_from_docs):1087 """1088 Creates a pandas DataFrame for each file in the dictionary.1089 1090 Args:1091 dict_from_docs: Dictionary mapping filenames to lists of documents1092 1093 Returns:1094 List of pandas DataFrames, each representing all documents from a single file1095 """1096 dataframes = []1097 1098 for filename, docs in dict_from_docs.items():1099 # Create a list to hold all document records for this file1100 file_records = []1101 1102 for i, doc in enumerate(docs):1103 # Convert the document to a format compatible with DataFrame1104 if hasattr(doc, 'to_dict'):1105 doc_data = doc.to_dict()1106 elif isinstance(doc, dict):1107 doc_data = doc1108 else:1109 doc_data = {'content': str(doc)}1110 1111 # Add document index information1112 doc_data['doc_index'] = i1113 1114 # Add to the list of records for this file1115 file_records.append(doc_data)1116 1117 # Create a single DataFrame for all documents in this file1118 if file_records:1119 df = pd.DataFrame(file_records)1120 df['filename'] = filename # Add filename as a column1121 dataframes.append(df)1122 1123 return dataframes1124 1125 def create_dataframe_previews(dataframe_list, page_size=5):1126 """1127 Creates a list of mo.ui.dataframe components, one for each DataFrame in the input list.1128 1129 Args:1130 dataframe_list: List of pandas DataFrames (output from create_document_dataframes)1131 page_size: Number of rows to show per page for each component1132 1133 Returns:1134 List of mo.ui.dataframe components1135 """1136 # Create a list of mo.ui.dataframe components1137 preview_components = []1138 1139 for df in dataframe_list:1140 # Create a mo.ui.dataframe component for this DataFrame1141 preview = mo.ui.dataframe(df, page_size=page_size)1142 preview_components.append(preview)1143 1144 return preview_components1145 return1146 1147 1148@app.cell1149def helper_function_chart_preparation():1150 import altair as alt1151 import numpy as np1152 import plotly.express as px1153 from sklearn.manifold import TSNE1154 1155 def prepare_embedding_data(embeddings, texts, model_id=None, embedding_dimensions=None):1156 """1157 Prepare embedding data for visualization1158 1159 Args:1160 embeddings: List of embeddings arrays1161 texts: List of text strings1162 model_id: Embedding model ID (optional)1163 embedding_dimensions: Embedding dimensions (optional)1164 1165 Returns:1166 DataFrame with processed data and metadata1167 """1168 # Flatten embeddings (in case they're nested)1169 flattened_embeddings = []1170 for emb in embeddings:1171 if isinstance(emb, list) and len(emb) > 0 and isinstance(emb[0], list):1172 flattened_embeddings.append(emb[0]) # Take first element if nested1173 else:1174 flattened_embeddings.append(emb)1175 1176 # Convert to numpy array1177 embedding_array = np.array(flattened_embeddings)1178 1179 # Apply dimensionality reduction (t-SNE)1180 tsne = TSNE(n_components=2, random_state=42, perplexity=min(30, len(embedding_array)-1))1181 reduced_embeddings = tsne.fit_transform(embedding_array)1182 1183 # Create truncated texts for display1184 truncated_texts = [text[:50] + "..." if len(text) > 50 else text for text in texts]1185 1186 # Create dataframe for visualization1187 df = pd.DataFrame({1188 "x": reduced_embeddings[:, 0],1189 "y": reduced_embeddings[:, 1],1190 "text": truncated_texts,1191 "full_text": texts,1192 "index": range(len(texts))1193 })1194 1195 # Add metadata1196 metadata = {1197 "model_id": model_id,1198 "embedding_dimensions": embedding_dimensions1199 }1200 