From d224c45c6c60dee34a7c570b4c1a26f7dde8290e Mon Sep 17 00:00:00 2001 From: "AARETE\\agupta" Date: Tue, 18 Jun 2024 12:11:20 -0500 Subject: [PATCH] Setting up multipage app --- streamlit-server/main.tf | 2 +- streamlit/multipage/Interface_0.py | 241 +++++++ streamlit/multipage/aaretelogo.png | Bin 0 -> 69902 bytes streamlit/multipage/constants.py | 228 +++++++ streamlit/multipage/pages/Interface_1.py | 277 +++++++++ streamlit/multipage/pages/Interface_2.py | 267 ++++++++ streamlit/multipage/pages/Interface_3.py | 759 +++++++++++++++++++++++ streamlit/multipage/security.py | 77 +++ streamlit/multipage/sf_conn.py | 140 +++++ streamlit/multipage/util.py | 35 ++ 10 files changed, 2025 insertions(+), 1 deletion(-) create mode 100644 streamlit/multipage/Interface_0.py create mode 100644 streamlit/multipage/aaretelogo.png create mode 100644 streamlit/multipage/constants.py create mode 100644 streamlit/multipage/pages/Interface_1.py create mode 100644 streamlit/multipage/pages/Interface_2.py create mode 100644 streamlit/multipage/pages/Interface_3.py create mode 100644 streamlit/multipage/security.py create mode 100644 streamlit/multipage/sf_conn.py create mode 100644 streamlit/multipage/util.py diff --git a/streamlit-server/main.tf b/streamlit-server/main.tf index 86ed245..ee5a6a9 100644 --- a/streamlit-server/main.tf +++ b/streamlit-server/main.tf @@ -194,7 +194,7 @@ resource "aws_instance" "streamlit_server" { Type=simple Restart=always WorkingDirectory=/home/ubuntu/doczy.ai/streamlit/multipage - ExecStart=/home/ubuntu/.local/bin/streamlit run /home/ubuntu/doczy.ai/streamlit/multipage/full_interface.py --server.port 8505 + ExecStart=/home/ubuntu/.local/bin/streamlit run /home/ubuntu/doczy.ai/streamlit/multipage/Interface_0.py --server.port 8505 [Install] WantedBy=multi-user.target diff --git a/streamlit/multipage/Interface_0.py b/streamlit/multipage/Interface_0.py new file mode 100644 index 0000000..26a4b57 --- /dev/null +++ b/streamlit/multipage/Interface_0.py @@ -0,0 +1,241 @@ +import json +import security +import streamlit as st +from streamlit_extras.add_vertical_space import add_vertical_space +import os +import streamlit as st +import pandas as pd +from io import StringIO +from datetime import datetime +import boto3 +import util +import requests +from sf_conn import get_secret, save_to_sf +from io import StringIO, BytesIO +import time +from sf_conn import get_client_names, insert_upload_logs +from constants import USER_LIST + +(REDIRECT_URI, create_batch_url, doczy_pipeline) = util.load_page_details(0) + +user_list = USER_LIST +if 'uploading' not in st.session_state: + st.session_state.uploading = False +if 'upload_key' not in st.session_state: + st.session_state.upload_key = 0 +if 'file_list' not in st.session_state: + st.session_state.file_list = [] +if 'show_batchID' not in st.session_state: + st.session_state.show_batchID = False +if 'landing_zone' not in st.session_state: + st.session_state.landing_zone = 'contracts-landing-zone' +if 'batch_id' not in st.session_state: + st.session_state.batch_id = 'failed_cases' +if 'client_bucket' not in st.session_state: + st.session_state.client_bucket = 'default_bucket' + +def insert_upload_logs(batch_id, client_name, file_name, upload_datetime, upload_user): + """ + Input: batch_id, client_name, file_name, upload_datetime, upload_user + Output: status of the insert query + """ + try: + return 'Log inserted successfully' + except Exception as e: + return e + + +st.set_page_config(layout = "wide") +# # # Sidebar contents +with st.sidebar: + st.title("Doczy.AI ™") + st.markdown( + """ + ## About + This app extracts data from contracts + + """ + ) + add_vertical_space(15) + # st.write("Doczy") + +# AARETE LOGO +x,y,z = st.columns([15,2,15]) +with y: + st.image('aaretelogo.png') + +hide_img_fs = ''' + +''' +st.markdown(hide_img_fs, unsafe_allow_html=True) + +_,c1= st.columns([5,1]) +try: + util.setup_page(REDIRECT_URI) +except Exception as e: + st.write(f"SSO Failed = {e}") + st.session_state['user_info'] = {'mail': 'maamseek@aarete.com', 'displayName': 'Mayank Aamseek'} +try: + c1.write(f"User: **{st.session_state.user_info['displayName']}**") + user_mail = st.session_state.user_info['mail'] +except KeyError as e: + st.write("Session Expired.") + auth_url = security.get_auth_url(REDIRECT_URI) + st.markdown(f"Sign In", unsafe_allow_html=True) + st.stop() + +s3_client = boto3.client('s3', + region_name="us-east-2", +) + +client_list = ['doczy-ai-client-1', 'Delaware First Health, Inc.', 'Community Health Choice, Inc','CareSource Network Partners LLC', + 'HealthNet of Cali', 'Oklahoma Complete Health, Inc', 'HealthFirst', 'Molina Healthcare of TX', 'AvMed', 'Arizona Care1st', + 'WellCare New Jersey'] + +# This is the list of client fetched from Snowflake +# TODO: Need to update the streamlit code to use the client names from this list +# And use the s3 paths to save the objects for the respective client +client_list, s3_paths = get_client_names() +client_s3_paths = dict(zip(client_list, s3_paths)) + +client_row = st.columns([0.1, 0.8]) +with client_row[0]: + st.write("**Client Name**") +with client_row[1]: + client = st.selectbox('Client Name',(client_list), label_visibility = "collapsed", index = None) + +client_bucket = client_s3_paths.get(client) + +# to be deleted when client buckets are created +client_bucket = 'doczy-dev-infra-textract' + +file_row = st.columns([0.1, 0.8]) +with file_row[0]: + st.write("**Upload Files**") +with file_row[1]: + file_list = st.file_uploader("Upload", type=['docx','tiff','pdf'], accept_multiple_files=True, label_visibility = "collapsed", help="Only PDF, TIFF and DOCX file formats are supported.", disabled=st.session_state.uploading, key = st.session_state.upload_key) + +add_vertical_space(2) +df = pd.DataFrame(columns=['Contract Name']) +df['Contract Name'] = file_list +file_names = [] +buttons = st.columns([0.4, 0.4, 0.2]) + +def set_uploading_state(): + if not client == None and not len(file_list) == 0: + st.session_state.file_list = file_list + st.session_state.upload_key += 1 + st.session_state.uploading = True + +with buttons[1]: + if st.button("Create Batch", on_click=set_uploading_state): + file_list = st.session_state.file_list + if client == None: + st.error("No Client Name Selected.") + elif len(file_list) == 0: + st.error("No Files Selected.") + else: + with st.spinner('Running...'): + myobj = { "client-bucket-name": client_bucket } + response = requests.post(create_batch_url, json = myobj) + if response.status_code >= 200 and response.status_code < 300: + try: + batch_id = json.loads(json.loads(response.text)['body'])['batch_id'] + landing_zone = json.loads(json.loads(response.text)['body'])['landing_zone'] + except: + # st.write(myobj) + # st.write(response.text) + st.write("Internal Error. Reach out to Doczy.AI Team.") + batch_id = 'failed_cases' + landing_zone = 'contracts-landing-zone' + else: + st.error("Failed") + for uploaded_file in file_list: + stringio = BytesIO(uploaded_file.getvalue()) + stringio.seek(0) + s3_client.put_object(Bucket=client_bucket, Body=stringio.getvalue(), Key= + landing_zone+batch_id+'/'+str(uploaded_file.name)) + # s3_client.put_object_tagging(Bucket=client_bucket, Key= + # landing_zone+batch_id+'/'+str(uploaded_file.name), Tagging = {'TagSet': [ { 'Key': 'BatchId', 'Value': batch_id }]}) + + # TODO: Test this insert function with snowflake + upload_log = insert_upload_logs(batch_id, client, str(uploaded_file.name), datetime.now().strftime("%Y-%m-%d %H:%M:%S"), user_mail) + st.write(upload_log) + + file_names.append(str(uploaded_file.name)) + st.session_state.uploading = False + st.session_state.show_batchID = True + st.session_state.client_bucket = client_bucket + st.session_state.batch_id = batch_id + st.session_state.landing_zone = landing_zone + st.rerun() + if st.session_state.show_batchID: + batch_id = st.session_state.batch_id + client_bucket = st.session_state.client_bucket + landing_zone = st.session_state.landing_zone + st.write("Batch ID Created - ") + st.code(f"{batch_id}") + st.write(f"Files uploaded to s3://{client_bucket}/{landing_zone}{batch_id}") + st.session_state.show_batchID = False + st.session_state.file_list = [] + st.session_state.batch_id = 'failed_cases' + st.session_state.client_bucket = 'default_bucket' + st.session_state.landing_zone = 'contracts-landing-zone' + + + + # @st.cache_data + # def convert_df(df): + # return df.to_csv(index=False).encode('utf-8') + + # csv = convert_df(df) + + # df = df.reset_index() # make sure indexes pair with number of rows + + # contract_list = [] + # for index, row in df.iterrows(): + # group_list = [] + # if row['Unique Key']: + # group_list.append('Unique Key') + # if row['Pricing Before Carveouts']: + # group_list.append('Pricing Before Carveouts') + # if row['Contract Related']: + # group_list.append('Contract Related') + # if row['Provider']: + # group_list.append('Provider') + # if row['Timeline']: + # group_list.append('Timeline') + # if row['Carveout Indicator']: + # group_list.append('Carveout Indicator') + # if row['Carveout Methodology']: + # group_list.append('Carveout Methodology') + # entry_dict = { + # "contract_name": row['Contract Name'], + # "groups": group_list, + # "contract_source_path": "batches/batch_1/"+client+"/"+row['Contract Name'] + # } + # contract_list.append(entry_dict) + # contract_list = list(df['Contract Name']) + + # myobj = { + # "s3_bucket": 'doczy-dev-infra-textract', + # "batch_id": "1", + # "client_name": client, + # "username": user_mail, + # "contract_list": contract_list + # } + + # buttons = st.columns([0.8, 0.2]) + # with buttons[0]: + # st.download_button("Download Table", csv, "file.csv", "text/csv", key='download-csv') + # with buttons[1]: + # if st.button("Upload to DB"): + # response = requests.post(doczy_pipeline, json = myobj) + # if response.status_code >= 200 and response.status_code < 300: + # st.write("Success") + # else: + # st.write("Failed") + # # st.write(response.text) diff --git a/streamlit/multipage/aaretelogo.png b/streamlit/multipage/aaretelogo.png new file mode 100644 index 0000000000000000000000000000000000000000..255e50ff3078bdbab3af7c172288cad683b08951 GIT binary patch literal 69902 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 +button[title="View fullscreen"]{ + visibility: hidden;} + +''' +st.markdown(hide_img_fs, unsafe_allow_html=True) + +_,c1= st.columns([5,1]) +try: + util.setup_page(REDIRECT_URI) +except: + st.write("SSO Failed") + st.session_state['user_info'] = {'mail': 'maamseek@aarete.com', 'displayName': 'Mayank Aamseek'} +try: + c1.write(f"User: **{st.session_state.user_info['displayName']}**") + user_mail = st.session_state.user_info['mail'] +except KeyError as e: + # Do we add a link to get to the login page here? + st.write("Session Expired.") + # st.write("Please sign-in to use this app.") + auth_url = security.get_auth_url(REDIRECT_URI) + st.markdown(f"Sign In", unsafe_allow_html=True) + st.stop() + + + +s3_client = boto3.client('s3', + region_name="us-east-2", +) + +# # to be replaced with snowflake data +# client_list = ['doczy-ai-client-1', 'Delaware First Health, Inc.', 'Community Health Choice, Inc','CareSource Network Partners LLC', +# 'HealthNet of Cali', 'Oklahoma Complete Health, Inc', 'HealthFirst', 'Molina Healthcare of TX', 'AvMed', 'Arizona Care1st', +# 'WellCare New Jersey'] + +client_list, s3_paths = get_client_names() +client_s3_paths = dict(zip(client_list, s3_paths)) + +client_row = st.columns([0.1, 0.8]) +with client_row[0]: + st.write("**Client Name**") +with client_row[1]: + client = st.selectbox('Client Name',(client_list), label_visibility = "collapsed", index = None) # MODIFIED for Ticket DOC-344 + +client_bucket = client_s3_paths.get(client) + +# to be deleted when buckets for different clients are ready; below line is added only for testing the corresponding DAG +client_bucket = 'doczy-dev-infra-textract' + +batch_objects = s3_client.list_objects_v2(Bucket=client_bucket + , Prefix="contracts-landing-zone/", Delimiter='/') + +batch_list = [] +for prefix in batch_objects['CommonPrefixes']: + batch_list.append(prefix['Prefix'][:-1].split('/')[-1]) + +# Hardcoded batch_list for testing purposes +# batch_list = ['batch_020524103737', 'batch_090524131433', 'batch_090524131607', 'batch_100524123000', 'batch_130524064322', +# 'batch_160524071331', 'batch_200524213550', 'batch_250424112237', 'batch_280524120530', 'batch_280524121721', 'batch_280524144222', +# 'batch_290524123926', 'batch_290524164044', 'batch_310524102029', 'batch_310524124050', 'batch_310524162346', 'batch_310524162631'] + +if 'sorted_list' not in st.session_state: + st.session_state.sorted_list = batch_list + +def sort_list(ex_list, sort_by, order): + if sort_by == 'Alphabetical': + ex_list = sorted(ex_list, reverse=(order == 'Descending')) + elif sort_by == 'Create Date': + ex_list = ex_list if order == 'Ascending' else list(reversed(ex_list)) + + return ex_list + +col1, col2, col3, col4 = st.columns([0.5, 0.5, 0.5, 0.5]) + + +with col1: + sort_by = st.radio("**Sort Batch_IDs**", ('Alphabetical', 'Create Date')) + +with col2: + order = st.radio('', ('Ascending','Descending')) + +with col3: + add_vertical_space(2) + if st.button('Apply'): + st.session_state.sorted_list = sort_list(batch_list, sort_by, order) + +path_row = st.columns([0.1, 0.8]) +with path_row[0]: + st.write("**Batch ID**") +with path_row[1]: + batch_id = st.selectbox('**Batch ID**', st.session_state.sorted_list, label_visibility = "collapsed", index = None) # MODIFIED for Ticket DOC-344 + +if not batch_id: + batch_id = "None" + +checks = st.columns([0.1, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12]) +with checks[0]: + st.write("**Group No.**") + +with checks[1]: + a = st.checkbox('Unique Key', key = str(1), args="Unique") +with checks[2]: + b = st.checkbox('Pricing Before Carveouts', key = str(2)) +with checks[3]: + c = st.checkbox('Contract Related', key = str(3)) +with checks[4]: + d = st.checkbox('Provider', key = str(4)) +with checks[5]: + e = st.checkbox('Timeline', key = str(5)) +with checks[6]: + f = st.checkbox('Carveout Indicator', key = str(6)) +with checks[7]: + g = st.checkbox('Carveout Methodology', key = str(7)) + +add_vertical_space(1) + +df = pd.DataFrame(columns=['Contract Name', 'Unique Key','Pricing Before Carveouts' + , 'Contract Related', 'Provider', 'Timeline', 'Carveout Indicator', 'Carveout Methodology']) +file_list = [] +file_objects = s3_client.list_objects_v2(Bucket=client_bucket + , Prefix="contracts_landing_zone/"+batch_id+"/", Delimiter='/') + +# Hardcoded file_list for testing purposes +# file_list = ['Boilerplate_TX Amendment Mission Health Network effective_040114 MU.pdf', 'Custom_TX - MP AMENDMENT - MISSION HEALTH NETWORK - MU.pdf', +# 'Delaware First Health_First State Homecare Agency_212260_7 MU.pdf', 'Molina Healthcare of Texas, Inc. Amendment 4 - HIX ACA__EFF 01012016_MU.pdf'] + +if st.button("Read the contracts from Path"): + for obj in file_objects.get('Contents',[]): + if not obj['Key'].endswith('/'): + file_list.append(obj['Key'].split('/')[-1]) + + df['Contract Name'] = file_list + # df['Request ID'] = range(len(file_list)) + # df['Contract ID'] = file_list + df['Unique Key'] = a + df['Pricing Before Carveouts'] = b + df['Contract Related'] = c + df['Provider'] = d + df['Timeline'] = e + df['Carveout Indicator'] = f + df['Carveout Methodology'] = g + dir_path = os.path.dirname(os.path.realpath(__file__)) + print(f'DEBUGGING: PWD= {dir_path}') + df.to_csv('temp1.csv', index=False) + +add_vertical_space(1) +df2 = pd.read_csv('temp1.csv') +edited_df = st.data_editor(df2) + +edited_df['REQUEST_USER'] = user_mail +edited_df['LATEST_FLAG BOOLEAN'] = True +edited_df['PIPELINE_KICKOFF_DATETIME'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") +edited_df['REQUEST_DATETIME'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + +@st.cache_data +def convert_df(df): + return df.to_csv(index=False).encode('utf-8') + +csv = convert_df(edited_df) +# edited_df = edited_df.reset_index() # make sure indexes pair with number of rows + +# additional_info = pd.DataFrame(columns=['REQUEST_ID','T_DRIVE_PATH','CLIENT_NAME' +# , 'GROUP_NAME', 'REQUEST_USERNAME', 'REQUEST_DATETIME']) +additional_info = pd.DataFrame(columns=['CLIENT_NAME', 'BATCH_ID', 'REQUEST_USERNAME', 'REQUEST_DATETIME']) +additional_info.loc[0] = [client, batch_id, st.session_state.user_info['mail'], datetime.now().strftime("%Y-%m-%d %H:%M:%S")] +st.write(additional_info) + +st.session_state.contract_count = 0 +contract_list = [] +for index, row in edited_df.iterrows(): + allow_run_for_contract = False + group_list = [] + if row['Unique Key']: + group_list.append('A') + allow_run_for_contract = True + if row['Pricing Before Carveouts']: + group_list.append('B') + allow_run_for_contract = True + if row['Contract Related']: + group_list.append('C') + allow_run_for_contract = True + if row['Provider']: + group_list.append('D') + allow_run_for_contract = True + if row['Timeline']: + group_list.append('E') + allow_run_for_contract = True + if row['Carveout Indicator']: + group_list.append('F') + allow_run_for_contract = True + if row['Carveout Methodology']: + group_list.append('G') + allow_run_for_contract = True + if allow_run_for_contract: st.session_state.contract_count += 1 + entry_dict = { + "contract_name": row['Contract Name'], + "groups": group_list, + "contract_source_path": "contracts_landing_zone/"+batch_id+"/"+row['Contract Name'] + } + contract_list.append(entry_dict) + +myobj = { + "s3_bucket": client_bucket, + "batch_id": batch_id, + "client_name": client, + "username": user_mail, + "contract_list": contract_list +} + +buttons = st.columns([0.8, 0.2]) +with buttons[0]: + st.download_button("Download Table", csv, "file.csv", "text/csv", key='download-csv') +with buttons[1]: + if st.button("Run Doczy.AI Pipeline"): + if not st.session_state.contract_count == len(edited_df): + st.error("Select at least one Group No. for every Contract") + else: + with st.spinner('Running...'): + # csv_buf = StringIO() + # additional_info.to_csv(csv_buf, header=True, index=False) + # csv_buf.seek(0) + # s3_client.put_object(Bucket='doczy-dev-infra-raw-data-ingestion', Body=csv_buf.getvalue(), Key='training_interface/request_submission.csv') + # csv_buf = StringIO() + # edited_df.to_csv(csv_buf, header=True, index=False) + # csv_buf.seek(0) + # s3_client.put_object(Bucket='doczy-dev-infra-raw-data-ingestion', Body=csv_buf.getvalue(), Key='training_interface/contract_config.csv') + # try: + # save_to_sf('load_request_and_contract_submissions', request_submission_file_name = "request_submission.csv", contract_config_file_name = "contract_config.csv") + # except Exception as e: + # st.write(e) + response = requests.post(doczy_pipeline, json = myobj) + if response.status_code >= 200 and response.status_code < 300: + st.write("Success") + # st.write(myobj) + else: + st.write("Failed") + # st.write(response.text) + diff --git a/streamlit/multipage/pages/Interface_2.py b/streamlit/multipage/pages/Interface_2.py new file mode 100644 index 0000000..9006927 --- /dev/null +++ b/streamlit/multipage/pages/Interface_2.py @@ -0,0 +1,267 @@ +import json +import security +import boto3 +from langchain.prompts import PromptTemplate +from langchain.embeddings.bedrock import BedrockEmbeddings +from langchain.llms.bedrock import Bedrock +from langchain_community.vectorstores import Chroma +from constants import CHROMA_SETTINGS, EMBEDDING_MODEL_NAME, PERSIST_DIRECTORY, MODEL_ID, MODEL_BASENAME, SOURCE_DIRECTORY, USER_LIST +from langchain.chains import RetrievalQA + +import streamlit as st +from streamlit_extras.add_vertical_space import add_vertical_space +from streamlit_pdf_viewer import pdf_viewer # needs to be installed on the server +import os +import pandas as pd +import numpy as np +import util +import anthropic +from pydantic import BaseModel +from typing import List +import re +import base64 +from sf_conn import get_snowflake_conn +from sf_conn import get_client_names, get_secret, save_to_sf +import io + +(REDIRECT_URI, create_batch_url, doczy_pipeline) = util.load_page_details(2) +user_list = USER_LIST + +st.set_page_config(layout = "wide") +# Sidebar contents +# with st.sidebar: +# st.title("Doczy.AI ™") +# st.markdown( +# """ +# ## About +# This app extracts data from contracts + +# """ +# ) +# add_vertical_space(15) +# # st.write("Doczy") + +# AARETE LOGO +x,y,z = st.columns([15,2,15]) +with y: + st.image('aaretelogo.png') + +hide_img_fs = ''' + +''' +st.markdown(hide_img_fs, unsafe_allow_html=True) + +_,c1= st.columns([5,1]) +try: + util.setup_page(REDIRECT_URI) +except: + st.write("SSO Failed") + st.session_state['user_info'] = {'mail': 'maamseek@aarete.com', 'displayName': 'Mayank Aamseek'} +try: + c1.write(f"User: **{st.session_state.user_info['displayName']}**") + user_mail = st.session_state.user_info['mail'] +except KeyError as e: + st.write("Session Expired.") + #st.write("Please sign-in to use this app.") + auth_url = security.get_auth_url(REDIRECT_URI) + st.markdown(f"Sign In", unsafe_allow_html=True) + st.stop() + +# remove below try except statement if comparison with actual vales is not required +try: + conn = get_snowflake_conn('STG') + cur = conn.cursor() + query = 'select * from "TRAINING_DATA_RAW"' + cur.execute(query) + field_values = pd.DataFrame.from_records(iter(cur), columns=[x[0] for x in cur.description]) + field_values['Document_Name'] = field_values['DOCUMENT_NAME'] +except: + # field_values = pd.read_csv('contract_field_values.csv', encoding='unicode_escape', skipinitialspace=True) + # field_values = field_values.loc[:, ~field_values.columns.str.contains('Unnamed:')] + st.write("Conn failed, unable to fetch data from training data table in Snowflake") + +try: + query = 'select * from "PROMPT_CONFIG"' + cur.execute(query) + fields = pd.DataFrame.from_records(iter(cur), columns=[x[0] for x in cur.description]) + + fields.rename(columns={'FIELD_DESC': 'Field Name'}, inplace = True) + fields.rename(columns={'PROMPT': 'Interrogation Question?'}, inplace = True) + fields.rename(columns={'GROUP_ID': 'PRIORITY'}, inplace = True) + fields.rename(columns={'FIELD_NAME': 'SF_DB_COL_NAME'}, inplace = True) + fields.rename(columns={'FM_MODEL_ID': 'llm_selected'}, inplace = True) +except Exception as e: + st.write("Unable to fetch data from Snowflake: ",e) + # fields = pd.read_csv('contract_fields.csv', encoding='unicode_escape', skipinitialspace=True) + # fields = fields[~fields['SF_COL_NAME'].str.endswith('_PG', na=None)] + +# change the code below if contract list is fetched from snowflake +s3_client = boto3.client('s3', + region_name="us-east-2" +) + +client_list, s3_paths = get_client_names() +client_s3_paths = dict(zip(client_list, s3_paths)) + +client_row = st.columns([0.2, 0.7, 0.1]) +with client_row[0]: + st.write("**Client Name**") +with client_row[1]: + client = st.selectbox('Client Name',(client_list), label_visibility = "collapsed", index= None) + +# client_bucket = client_s3_paths.get(client) +client_bucket = 'doczy-dev-infra-textract' + +batch_objects = s3_client.list_objects_v2(Bucket=client_bucket + , Prefix="contracts-landing-zone/", Delimiter='/') + +batch_list = [] +for prefix in batch_objects['CommonPrefixes']: + batch_list.append(prefix['Prefix'][:-1].split('/')[-1]) + +path_row = st.columns([0.1, 0.8]) +with path_row[0]: + st.write("**Batch ID**") +with path_row[1]: + batch_id = st.selectbox('**Batch ID**', batch_list, label_visibility = "collapsed", index = None) + +if batch_id: + + objects = s3_client.list_objects_v2(Bucket=client_bucket, Prefix="contract-text-file/"+batch_id+"/") + file_list = [] + for obj in objects['Contents']: + if not obj['Key'].endswith('/'): + file_list.append(obj['Key']) + + contract_list = sorted(file_list) + + file_row = st.columns([0.2, 0.7, 0.1]) + with file_row[0]: + st.write("**Contract Name**") + with file_row[1]: + file_name = st.selectbox('Select a file', ['All'] + contract_list, label_visibility = "collapsed", index= None) + + field_row = st.columns([0.2, 0.7, 0.1]) + with field_row[0]: + st.write("**Field Group**") + with field_row[1]: + field_group = st.selectbox('Field Group',('Unique Key', 'Contract Related', 'Pricing Before Carveouts - I' + , 'Pricing Before Carveouts - II', 'Carveout Indicator, Code Type and Code #s - I' + , 'Carveout Indicator, Code Type and Code #s - II', 'Carveout Indicator, Code Type and Code #s - III' + , 'Optimize Carving Indic.', 'Carveout Method - I', 'Carveout Method - II', 'Provider' + , 'Timeline'), label_visibility = "collapsed", index = None) + + if field_group == 'Unique Key': + fields = fields[fields['PRIORITY'] == 'A'] + elif field_group == 'Contract Related': + fields = fields[fields['PRIORITY'] == 'C'] + elif field_group == 'Pricing Before Carveouts - I': + fields = fields[fields['PRIORITY'] == 'B'] + fields = np.array_split(fields, 2)[0] + elif field_group == 'Pricing Before Carveouts - II': + fields = fields[fields['PRIORITY'] == 'B'] + fields = np.array_split(fields, 2)[1] + elif field_group == 'Carveout Indicator, Code Type and Code #s - I': + fields = fields[fields['PRIORITY'] == 'F'] + fields = np.array_split(fields, 3)[0] + elif field_group == 'Carveout Indicator, Code Type and Code #s - II': + fields = fields[fields['PRIORITY'] == 'F'] + fields = np.array_split(fields, 3)[1] + elif field_group == 'Carveout Indicator, Code Type and Code #s - III': + fields = fields[fields['PRIORITY'] == 'F'] + fields = np.array_split(fields, 3)[2] + elif field_group == 'Carveout Methodology - I': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[0] + elif field_group == 'Carveout Methodology - II': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[1] + elif field_group == 'Carveout Method - III': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[2] + elif field_group == 'Carveout Method - IV': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[3] + elif field_group == 'Provider': + fields = fields[fields['PRIORITY'] == 'D'] + elif field_group == 'Timeline': + fields = fields[fields['PRIORITY'] == 'E'] + + if st.button("Show Results"): + query = 'select * from "DOCZY_PIPELINE_RAW_OUTPUT"' + cur.execute(query) + df2 = pd.DataFrame.from_records(iter(cur), columns=[x[0] for x in cur.description]) + + # get this dataframe from snowflake table + # df2 = pd.DataFrame(columns=['Contract Name','Field Name', 'SF_DB_COL_NAME', 'Snippet','Page Number' + # , 'Field Extracted Value', 'Actual Value','Imputed Value']) + df2.to_csv('temp2.csv', index=False) + + if st.button("Show PDF"): + if file_name == None or file_name == "All": + st.error("Choose one specific file.") + else: + with st.sidebar: + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + s3_obj = s3_client.get_object(Bucket = bucket, Key = 'textract-receiver-processed-pdfs/batch_2/2000-01-01 UCSD Medical Center PPA 14007097.PDF') + data=s3_obj['Body'].read() + pdf_viewer(data, width=1500) + + # if st.button("Show PDF"): + # if file_name == None or file_name == "All": + # st.error("Choose one specific file.") + # else: + # with st.sidebar: + # with open(file_name, "rb") as f: + # base64_pdf = base64.b64encode(f.read()).decode('utf-8') + # # Embedding PDF in HTML + # pdf_display = F'' + # # Displaying File + # st.markdown( + # """ + # + # """, + # unsafe_allow_html=True, + # ) + # st.markdown(pdf_display, unsafe_allow_html=True) + + df2 = pd.read_csv('temp2.csv') + df2['Imputed Value'] = '' + edited_df = st.data_editor(df2) + + @st.cache_data + def convert_df(df): + return df.to_csv(index=False).encode('utf-8') + + csv = convert_df(edited_df) + + buttons = st.columns(3) + with buttons[0]: + # st.button("Save All Imputations") + st.download_button("Download Table", csv, "file.csv", "text/csv", key='download-csv') + with buttons[1]: + # st.download_button("Download Table", csv, "file.csv", "text/csv", key='download-csv') + st.write("") + with buttons[2]: + if st.button("Kickoff Database Integration"): + st.write("Stored in DB") + + + + diff --git a/streamlit/multipage/pages/Interface_3.py b/streamlit/multipage/pages/Interface_3.py new file mode 100644 index 0000000..2da0b40 --- /dev/null +++ b/streamlit/multipage/pages/Interface_3.py @@ -0,0 +1,759 @@ +import json +import boto3 +from langchain.prompts import PromptTemplate +from langchain.embeddings.bedrock import BedrockEmbeddings +from langchain.llms.bedrock import Bedrock +from langchain_community.vectorstores import Chroma +from constants import CHROMA_SETTINGS, EMBEDDING_MODEL_NAME, PERSIST_DIRECTORY, MODEL_ID, MODEL_BASENAME, SOURCE_DIRECTORY, USER_LIST +from langchain.chains import RetrievalQA + +import streamlit as st +from streamlit_extras.add_vertical_space import add_vertical_space + +import pandas as pd +import numpy as np +from datetime import datetime +import random +import os +import dateutil +import util +import anthropic +import re +import snowflake.connector +from sf_conn import get_secret, save_to_sf +from io import StringIO + + +(REDIRECT_URI, create_batch_url, doczy_pipeline) = util.load_page_details(3) +user_list = USER_LIST + +st.set_page_config(layout = "wide") +# Sidebar contents +with st.sidebar: + st.title("Doczy.AI ™") + st.markdown( + """ + ## About + This app extracts data from contracts + + """ + ) + add_vertical_space(15) + # st.write("Doczy") + +_,c1= st.columns([5,1]) +# util.setup_page(REDIRECT_URI) +try: + util.setup_page(REDIRECT_URI) +except: + st.write("SSO Failed") + st.session_state['user_info'] = {'mail': 'maamseek@aarete.com', 'displayName': 'Mayank Aamseek'} +try: + c1.write(f"User: **{st.session_state.user_info['displayName']}**") + user_mail = st.session_state.user_info['mail'] +except KeyError as e: + st.write("Session Expired.") + st. stop() + +try: + sf_secrets = json.loads(get_secret()) + conn = snowflake.connector.connect( + user=sf_secrets.get('user'), + password=sf_secrets.get('password'), + account="aarete-doczyai", + role = "DEVADMIN", + warehouse="DEV_XS", + database="DOCZY_DEV", + schema="STG" + ) + cur = conn.cursor() + query = 'select * from "TRAINING_DATA_RAW"' + cur.execute(query) + field_values = pd.DataFrame.from_records(iter(cur), columns=[x[0] for x in cur.description]) + # st.write(field_values) + field_values['Document_Name'] = field_values['DOCUMENT_NAME'] + # field_values['Contract ID'] = field_values['CONTRACT_TITLE'] + # error('table values are incorrect') +except: + field_values = pd.read_csv('contract_field_values.csv', encoding='utf-8-sig', skipinitialspace=True) + # field_values.rename(columns={'(internal) Document Name': 'Document_Name'}, inplace = True) + ## field_values.rename(columns={'(Internal) Carveout ID': 'Contract ID'}, inplace = True) + field_values['Document_Name'] = field_values['DOCUMENT_NAME'] + field_values = field_values.loc[:, ~field_values.columns.str.contains('Unnamed:')] + st.write("Local copy of TRAINING_DATA_RAW table loaded") + +try: + # error('table is not updated') + query = 'select * from "BUSINESS_CONFIG"' + cur.execute(query) + fields = pd.DataFrame.from_records(iter(cur), columns=[x[0] for x in cur.description]) + fields.rename(columns={'FIELD_NAME': 'Field Name'}, inplace = True) + fields.rename(columns={'QUESTION': 'Interrogation Question?'}, inplace = True) + fields.rename(columns={'SF_COL_NAME': 'SF_DB_COL_NAME'}, inplace = True) + fields = fields[~fields['SF_DB_COL_NAME'].str.endswith('_PG', na=None)] + fields['Field Name'] = fields['SF_DB_COL_NAME'] + # fields.rename(columns={'FM_MODEL_ID': 'llm_selected'}, inplace = True) + # error('table is empty') +except: + fields = pd.read_csv('contract_fields.csv', encoding='utf-8-sig', skipinitialspace=True) + # fields = fields.drop_duplicates(subset='Field Name', keep="first").sort_values('Field Name') + # fields['Field Name'] = fields['SF_DB_COL_NAME'] + # fields = fields[~fields['Field Name'].isnull()] + fields.rename(columns={'FIELD_NAME': 'Field Name'}, inplace = True) + fields.rename(columns={'QUESTION': 'Interrogation Question?'}, inplace = True) + fields.rename(columns={'SF_COL_NAME': 'SF_DB_COL_NAME'}, inplace = True) + fields = fields[~fields['SF_DB_COL_NAME'].str.endswith('_PG', na=None)] + fields['Field Name'] = fields['SF_DB_COL_NAME'] + st.write("Local copy of BUSINESS_CONFIG table loaded") + +try: + query = 'select * from "TRAINING_ATTEMPT_LOGS"' + cur.execute(query) + history = pd.DataFrame.from_records(iter(cur), columns=[x[0] for x in cur.description]) + history.rename(columns={'FIELD_NAME': 'Field Name'}, inplace = True) + history.rename(columns={'CONTRACTS_TESTED': '# Contracts Tested'}, inplace = True) + history.rename(columns={'USERNAME': 'Username'}, inplace = True) + history.rename(columns={'DATE_TIME': 'Date/Time'}, inplace = True) + history.rename(columns={'ACCURACY': 'Accuracy'}, inplace = True) + history.rename(columns={'ATTEMPT_NUM': 'Attempt #'}, inplace = True) + history = history[['Field Name', '# Contracts Tested', 'Username', 'Date/Time', 'Accuracy', 'Attempt #']] +except: + history = pd.read_csv('history.csv') + st.write("Local copy of TRAINING_ATTEMPT_LOGS table loaded") + + + +field_row = st.columns([0.15, 0.45, 0.4]) +with field_row[0]: + st.write("**Field Group**") +with field_row[1]: + field_group = st.selectbox('Field Group',('Unique and Contract Related', 'Pricing Before Carveouts - All' + , 'Pricing Before Carveouts - I', 'Pricing Before Carveouts - II', 'Carveout Indicator, Code Type and Code #s - I' + , 'Carveout Indicator, Code Type and Code #s - II', 'Carveout Indicator, Code Type and Code #s - III' + , 'Optimize Carving Indic.', 'Carveout Method - I', 'Carveout Method - II', 'Provider' + , 'Timeline'), label_visibility = "collapsed") + + +# priorty column will be relaced by group_id in snowflake db +if field_group == 'Unique and Contract Related': + fields = fields[fields['PRIORITY'].isin(['A','C'])] +elif field_group == 'Pricing Before Carveouts - All': + fields = fields[fields['PRIORITY'] == 'B'] +elif field_group == 'Pricing Before Carveouts - I': + fields = fields[fields['PRIORITY'] == 'B'] + fields = np.array_split(fields, 2)[0] +elif field_group == 'Pricing Before Carveouts - II': + fields = fields[fields['PRIORITY'] == 'B'] + fields = np.array_split(fields, 2)[1] +elif field_group == 'Carveout Indicator, Code Type and Code #s - I': + fields = fields[fields['PRIORITY'] == 'F'] + fields = np.array_split(fields, 3)[0] +elif field_group == 'Carveout Indicator, Code Type and Code #s - II': + fields = fields[fields['PRIORITY'] == 'F'] + fields = np.array_split(fields, 3)[1] +elif field_group == 'Carveout Indicator, Code Type and Code #s - III': + fields = fields[fields['PRIORITY'] == 'F'] + fields = np.array_split(fields, 3)[2] +elif field_group == 'Carveout Methodology - I': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[0] +elif field_group == 'Carveout Methodology - II': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[1] +elif field_group == 'Carveout Method - III': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[2] +elif field_group == 'Carveout Method - IV': + fields = fields[fields['PRIORITY'] == 'G'] + fields = np.array_split(fields, 4)[3] +elif field_group == 'Provider': + fields = fields[fields['PRIORITY'] == 'D'] +elif field_group == 'Timeline': + fields = fields[fields['PRIORITY'] == 'E'] + +fields['Interrogation Question?'] = fields['Interrogation Question?'].fillna(' ') +field_prompt_mapping = dict(zip(fields['Field Name'], fields['Interrogation Question?'])) + +mode_row = st.columns([0.15, 0.45, 0.4]) +with mode_row[0]: + st.write("**Mode**") +with mode_row[1]: + mode = st.selectbox('Mode',('Single field - Non Empty values', 'Multiple fields', 'One-to-many fields'), index=0, label_visibility = "collapsed") + +field_row = st.columns([0.15, 0.45, 0.4]) +with field_row[0]: + st.write("**Field Name**") +with field_row[1]: + if mode == 'Single field - Non Empty values': + field = st.selectbox('Field Name',sorted(set(field_prompt_mapping.keys())), index=0, label_visibility = "collapsed") + else: + field = st.multiselect('Field Name',sorted(set(field_prompt_mapping.keys())), sorted( + set(field_prompt_mapping.keys())), label_visibility = "collapsed") + field_prompt_mapping = {key: field_prompt_mapping[key] for key in field} + +contract_count_row = st.columns([0.15, 0.45, 0.4]) +with contract_count_row[0]: + st.write("**# of Contracts**") +with contract_count_row[1]: + contract_count = st.selectbox('Contract count',('1', '10', '20', '30', '50', '100', '200','Part-1', 'Part-2', 'All'), index=1, label_visibility = "collapsed") + +s3_client = boto3.client('s3', + region_name="us-east-2" +) +bucket = 'doczy-dev-infra-textract' +objects = s3_client.list_objects_v2(Bucket=bucket, Prefix="training-data/") +file_list = [] +for obj in objects['Contents']: + if not obj['Key'].endswith('/'): + file_list.append(obj['Key']) + # print(os.path.join('RAW_DOCUMENTS', obj['Key'].rsplit('/',1)[1])) + # s3_client.download_file('doczy-dev-infra-textract', obj['Key'], os.path.join('RAW_DOCUMENTS', obj['Key'].rsplit('/',1)[1])) + +contract_list = sorted(file_list) +# contract_list = sorted(os.listdir(SOURCE_DIRECTORY)) + +if mode == 'Single field - Non Empty values': + # column_name = fields.loc[fields['Field Name'] == field, 'SF_DB_COL_NAME'].iloc[0] + # field_values = field_values[~field_values[column_name].isnull()] + field_values = field_values[~field_values[field].isnull()] + +# df = pd.DataFrame({'col':contract_list}) +# st.write(df) +# st.write(len(contract_list)) +# contract_list = [contract for contract in contract_list if contract.rsplit('/',1)[1].replace(' MU','').replace('_MU','').replace('.txt','') in list(field_values['Document_Name'])] +contract_list = [str(contract)[:-4]+'.pdf' for contract in contract_list] +contract_list = [contract for contract in contract_list if contract.rsplit('/',1)[1] in list(field_values['Document_Name'])] +# st.write(len(contract_list)) + +if contract_count == 'All': + contract_count = len(contract_list) +if contract_count == 'Part-1': + contract_list = np.array_split(contract_list, 2)[0] + contract_count = len(contract_list) +if contract_count == 'Part-2': + contract_list = np.array_split(contract_list, 2)[1] + contract_count = len(contract_list) + +seed_row = st.columns([0.15, 0.45, 0.4]) +with seed_row[0]: + if contract_count in ['10', '20', '30', '50', '100', '200']: + st.write("**Seed Value**") + elif contract_count == '1': + st.write("**Contract Name**") +with seed_row[1]: + if contract_count in ['10', '20', '30', '50', '100', '200']: + seed_value = st.text_input("**Seed Value**", value = 20, label_visibility = "collapsed") + random.seed(seed_value) + # contract_list = sorted(random.choices(os.listdir(SOURCE_DIRECTORY), k=int(contract_count))) + contract_list = sorted(random.choices(contract_list, k=int(contract_count))) + elif contract_count == '1': + contract_name = st.selectbox('Contract Name', (contract_list), label_visibility = "collapsed") + contract_list = [contract_name] + + +llm_row = st.columns([0.15, 0.45, 0.4]) +with llm_row[0]: + st.write("**Langauge Model**") +with llm_row[1]: + llm_selected = st.selectbox('Langauge Model',('Claude 2', 'Claude 3 - Haiku', 'Claude 3 - Sonnet', 'Claude Instant' + , 'Llama 2 Chat 70B', 'Titan Text Express'), index=3, label_visibility = "collapsed") + +st.write("**Prompt**") +if mode != 'Single field - Non Empty values': + sequence_input = json.dumps(field_prompt_mapping) +else: + sequence_input = field_prompt_mapping.get(field) +prompt_row = st.columns([0.8, 0.2]) +with prompt_row[1]: + if st.button("Clear Prompt"): + sequence_input = '' + if st.button("Back to default"): + prompt = sequence_input + # st.button("Save Prompt") +with prompt_row[0]: + prompt = st.text_area("**Prompt**", sequence_input, height = 100, label_visibility = "collapsed") + + +# column_name = fields.loc[fields['Field Name'] == field, 'SF_DB_COL_NAME'].iloc[0] +# column_list = ['Document_Name', column_name] +# # column_list = ['Document_Name', 'Contract ID', column_name] +# if column_name+'_PG' in list(field_values.columns): +# column_list.append(column_name+'_PG') +# # field_values = field_values[column_list] +# # field_values.rename(columns={'Document_Name': 'Contract Name', column_name: 'Actual Value Stored' +# # , column_name+'_PG': 'Original Page Number'}, inplace=True) +field_values.rename(columns={'Document_Name': 'Contract Name'}, inplace=True) +if mode != 'One-to-many fields': + field_values = field_values.drop_duplicates(subset='Contract Name', keep="first").sort_values('Contract Name') +field_values = field_values[field_values['Contract Name'].isin([contract.rsplit('/',1)[1] for contract in contract_list])] + +# Setup bedrock +bedrock_runtime = boto3.client( + service_name="bedrock-runtime", + region_name="us-east-1", +) + +# question = prompt +if mode != 'Single field - Non Empty values': + prompt_dict = json.loads(prompt) + prompt_dict_pg = prompt_dict | {str(k)+'_PG': "On which page can I find answer to the question - "+str( + v) for k, v in prompt_dict.items()} + question = json.dumps(dict(sorted(prompt_dict_pg.items()))) +else: + question = json.dumps({field: prompt, str(field)+'_PG': "On which page can I find answer to the question - "+str(prompt)}) + +# st.write(question) +question_with_schema = question +attempt = 0 + +def run_llm(attempt, bucket, contract_list, llm_selected, field, field_values, history): + + # df = pd.DataFrame(columns=['Contract Name','Contract ID','Actual Value Stored','New Extracted value','Confidence Level','Snippet', + # 'Original Page Number', 'New Page Number', 'Revised Prompt', 'Result']) + df = pd.DataFrame(columns=['Contract Name' ,'New Extracted value','Confidence Level','Snippet','New Page Number' + , 'Revised Prompt', 'Result']) + + field_list = [] + answer_list = [] + snippet_list = [] + page_no_list = [] + contract_list_f = [] + attempt = attempt + 1 + + for contract in contract_list: + # with open(os.path.join(SOURCE_DIRECTORY, contract[:-4]+'.txt'), 'r') as infile: + # context = infile.read() + data = s3_client.get_object(Bucket=bucket, Key=str(contract)[:-4]+'.txt') + contents = data['Body'].read() + context = contents.decode("utf-8") + # st.write(question_with_schema) + + # Add "You must answer in correct JSON format." + # Add Answer in JSON format: {{ + if llm_selected == "Titan Text Express": + context = context[:16000] + prompt_data = f"""Answer the question based only on the information provided between ## and give step by step guide. You must answer in correct JSON format. + # + {context} + # + + Question: {question} + Answer: Answer in JSON format: {{""" + parameters = { + "maxTokenCount":2048, + "stopSequences":[], + "temperature":0, + "topP":0.9 + } + + body = json.dumps({"inputText": prompt_data, "textGenerationConfig": parameters}) + model_id = "amazon.titan-text-express-v1" # change this to use a different version from the model provider + + elif llm_selected == 'Llama 2 Chat 70B': + context = context[:6000] + prompt_data = f"""Answer the question based only on the information provided between ## and give step by step guide. You must answer in correct JSON format. + ## + {context} + ## + + Question: {question} + Answer: Answer in JSON format: {{""" + payload={ + "prompt":"[INST]"+ prompt_data +"[/INST]", + "max_gen_len":2048, + "temperature":0.0, + "top_p":0.9 + } + body=json.dumps(payload) + model_id="meta.llama2-70b-chat-v1" + + elif llm_selected in ['Claude Instant', 'Claude 2', 'Claude 3 - Haiku', 'Claude 3 - Sonnet']: + if llm_selected == 'Claude Instant': + context = context[:175000] + + prompt_data = f""" + + Human: Use the following pieces of context to provide a concise answer to the questions at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. You must answer in correct JSON format. + + {context} + + Question: {question_with_schema} + + Assistant: Answer in JSON format: {{""" + + if llm_selected == "Claude 2": + model_id = "anthropic.claude-v2:1" + body = json.dumps( + {"prompt": anthropic.HUMAN_PROMPT + prompt_data + anthropic.AI_PROMPT, + "max_tokens_to_sample": 4096, + "temperature":0.0, + "top_p":1, + "top_k":250, + "stop_sequences":[anthropic.HUMAN_PROMPT] + }) + elif llm_selected in ["Claude 3 - Haiku", "Claude 3 - Sonnet"]: + if llm_selected == "Claude 3 - Haiku": + model_id = 'anthropic.claude-3-haiku-20240307-v1:0' + else: + model_id = 'anthropic.claude-3-sonnet-20240229-v1:0' + body = json.dumps({ + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 4096, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text":anthropic.HUMAN_PROMPT + prompt_data + anthropic.AI_PROMPT + } + ] + } + ], + "temperature": 0.0 + } + ) + else: + model_id = "anthropic.claude-instant-v1" + body = json.dumps( + {"prompt": anthropic.HUMAN_PROMPT + prompt_data + anthropic.AI_PROMPT, + "max_tokens_to_sample": 2048, + "temperature":0.0, + "top_p":1, + "top_k":250, + "stop_sequences":[anthropic.HUMAN_PROMPT] + }) + + try: + response = bedrock_runtime.invoke_model( + body=body, + modelId=model_id, + accept="application/json", + contentType="application/json" + ) + response_body = json.loads(response.get("body").read()) + + if llm_selected == "Titan Text Express": + response_text = response_body.get("results")[0].get("outputText") + elif llm_selected == 'Llama 2 Chat 70B': + response_text = response_body['generation'] + elif llm_selected in ['Claude Instant', 'Claude 2']: + response_text = response_body['completion'] + elif llm_selected in ['Claude 3 - Haiku', 'Claude 3 - Sonnet']: + response_text = response_body['content'][0]['text'] + + except: + response_text = "failed" + + raw_response_text = response_text + response_text = response_text.strip() + + try: + if response_text.split("{",1)[1].strip()[0] == '"': + response_text = "{" + response_text.split("{",1)[1] + else: + response_text = "{" + response_text + except: + response_text = "{" + response_text + + if len(response_text.split("}",1)) > 1: + if response_text.rsplit("}",1)[0].strip()[-1] == '"': + response_text = response_text.rsplit("}",1)[0] + "}" + else: + response_text = response_text.rstrip(",") + response_text = response_text + "}" + + try: + response_dict = json.loads(response_text) + except: + if mode != 'Single field - Non Empty values': + response_dict = {"Test field": "Failed to extract"} + else: + response_dict = {field:response_text.strip("{").strip("}")} + + # if mode == 'Multiple fields': + # field_l = list(response_dict.keys()) + # answer_l = list(response_dict.values()) + # else: + # field_l = [field] + # # answer = response_dict.get(field, " ") + # try: + # if isinstance(response_dict, dict): + # answer_l = list(response_dict.values())[:1] + # else: + # answer_l = list(response_dict)[:1] + # except: + # answer_l = [response_dict] + if mode != 'Single field - Non Empty values': + field_l = list(response_dict.keys()) + answer_l = list(response_dict.values()) + field_dict = {k: v for k, v in response_dict.items() if not k.endswith('_PG')} + page_dict = {k: v for k, v in response_dict.items() if k.endswith('_PG')} + page_dict = {k[:-3]: v for k, v in response_dict.items()} + field_l = list(field_dict.keys()) + answer_l = list(field_dict.values()) + page_no_l = [page_dict.get(x, "") for x in field_l] + else: + field_l = [field] + answer_l = [list(response_dict.values())[0]] + try: + page_no_l = [list(response_dict.values())[1]] + except: + page_no_l = [''] + + field_list.extend(field_l) + answer_list.extend(answer_l) + contract_list_f.extend([contract]*len(field_l)) + # location = context.find(answer) if isinstance(answer, str) and answer != "" else -1 + # snippet = ' '.join(context[:location].split()[-25:]) + ' ' + ' '.join(context[location:].split()[:30]) if location != -1 else ' ' + # page_no = " " if location == -1 else context[:location].rsplit("Start of Page No. = ", 1)[1] if len(context[:location].rsplit( + # "Start of Page No. = ", 1)) > 1 else context[:location].rsplit("Start of Page No. = ", 1)[0] + # page_no = re.search(r'\d+', page_no).group() if page_no != " " and re.search(r'\d+', page_no) is not None else "" + + try: + location_l = [context.find(a, context.find("Start of Page No. = "+str(p))) if isinstance( + a, str) and a != "" else -1 for a, p in zip(answer_l, page_no_l)] + except: + location_l = [context.find(answer) if isinstance(answer, str) and answer != "" else -1 for answer in answer_l] + snippet_l = [' '.join(context[:location].split('.')[-4:]) + ' ' + ' '.join(context[location:].split('. ')[:5] + ) if location != -1 else ' ' for location in location_l] + # page_no_l = [" " if location == -1 else context[:location].rsplit("Start of Page No. = ", 1)[1] if len(context[:location].rsplit( + # "Start of Page No. = ", 1)) > 1 else context[:location].rsplit("Start of Page No. = ", 1)[0] for location in location_l] + # # st.write(location_list) + # page_no_l = [re.search(r'\d+', page).group() if page != " " and re.search(r'\d+', page) is not None else "" for page in page_no_l] + snippet_list.extend(snippet_l) + page_no_list.extend(page_no_l) + + df['Field Name'] = field_list + df['Raw value'] = answer_list + # post-processing + # if 'Date' in field: + # date_list = [] + # for answer in answer_list: + # try: + # extracted_date = dateutil.parser.parse(str(answer).replace('"',''), fuzzy=True).date() + # except: + # extracted_date = " " + # date_list.append(extracted_date) + # answer_list = date_list + try: + answer_list = [str(answer).strip("\n").strip().strip("[").strip("]").strip("{").strip("}").strip('"').rstrip('"').strip( + ' ') if answer is not None else None for answer in answer_list] + if llm_selected in ['Llama 2 Chat 13B', 'Llama 2 Chat 70B']: + answer_list = [answer.rstrip(".") for answer in answer_list] + answer_list = [answer if "I don't know" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "N/A" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "does not contain" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "None" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Not specified in the contract" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Not applicable" not in str(answer) else " " for answer in answer_list] + elif llm_selected in ['Claude 2','Claude 3 - Haiku', 'Claude 3 - Sonnet', 'Claude Instant']: + answer_list = [answer if "do not have" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "do not see" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "does not specify" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Does not specify" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "does not explicitly" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "N/A" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "don't know" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "do not see" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Not specified" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Don't know" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "don't see" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "don't have" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Does not apply" not in str(answer) else " " for answer in answer_list] + answer_list = [answer if "Nothing found" not in str(answer) else " " for answer in answer_list] + answer_list = [answer.rstrip(".") for answer in answer_list] + else: + answer_list = [answer.rstrip(".") for answer in answer_list] + # answer_list = [str(x).rsplit(':',1)[0] if len(str(x).rsplit(':',1)) < 2 else str(x).rsplit(':',1)[1] for x in answer_list] + except Exception as e: + st.write("post processing error") + + # to be deleted later + # contract_list_f = [contract.rsplit('/',1)[1].replace(' MU','').replace('_MU','').replace('.txt','') for contract in contract_list_f] + contract_list_f = [contract.rsplit('/',1)[1] for contract in contract_list_f] + + df['Contract Name'] = contract_list_f + df['New Extracted value'] = answer_list + df['Confidence Level'] = ' ' + df['Snippet'] = snippet_list + df['New Page Number'] = page_no_list + df['New Page Number'] = df['New Page Number'].apply(lambda x: re.search(r'\d+', x).group( + ) if isinstance(x, str) and re.search(r'\d+', x) is not None else " ") + df['Revised Prompt'] = [prompt] * len(contract_list_f) + + df = pd.merge(df, fields[['Field Name', 'SF_DB_COL_NAME']], how ='left', on ='Field Name') + + field_values_2 = pd.DataFrame(columns=['Contract Name', 'LOB_Product_Network_Metal_Area_Program_Type_Speciality', 'SF_DB_COL_NAME' + , 'Actual Value Stored', 'Original Page Number']) + for file_name in contract_list: + # document_name = [x for x in list(field_values['Contract Name']) if not pd.isna(x) and file_name.rsplit('/',1)[1].replace(' MU','').replace( + # '_MU','').replace('.txt','') in x][0] + document_name = [x for x in list(field_values['Contract Name']) if not pd.isna(x) and file_name.rsplit('/',1)[1] in x][0] + + temp_df = field_values[field_values['Contract Name'] == document_name].fillna('NA') + unique_identifier = str(temp_df.at[temp_df.index[0],'CONTRACT_LOB']) + '__' + str( + temp_df.at[temp_df.index[0],'CONTRACT_PRODUCT']) + '__' + str( + temp_df.at[temp_df.index[0],'CONTRACT_NETWORK']) + '__' + str( + temp_df.at[temp_df.index[0],'CONTRACT_MARKETPLACE_METAL_LEVEL']) + '__' + str( + temp_df.at[temp_df.index[0],'CONTRACT_SERVICE_AREA']) + '__' + str( + temp_df.at[temp_df.index[0],'CONTRACT_PROGRAM']) + '__' + str( + temp_df.at[temp_df.index[0],'PROV_TYPE']) + '__' + str(temp_df.at[temp_df.index[0],'PROV_SPECIALTY']) + field_values_1 = field_values[field_values['Contract Name'] == document_name].head(1).transpose().reset_index() + field_values_1.columns = ['SF_DB_COL_NAME', 'Actual Value Stored'] + field_values_p1 = field_values_1[~field_values_1['SF_DB_COL_NAME'].str.endswith('_PG')] + field_values_p2 = field_values_1[field_values_1['SF_DB_COL_NAME'].str.endswith('_PG')] + field_values_p2.columns = ['SF_DB_COL_NAME', 'Original Page Number'] + field_values_p2["SF_DB_COL_NAME"] = field_values_p2["SF_DB_COL_NAME"].str.replace("_PG", "") + + field_values_1 = pd.merge(field_values_p1, field_values_p2, how ='left', on =['SF_DB_COL_NAME']) + field_values_1['Contract Name'] = document_name + field_values_1['LOB_Product_Network_Metal_Area_Program_Type_Speciality'] = unique_identifier + field_values_2 = pd.concat([field_values_2, field_values_1], ignore_index = True) + + if mode == 'One-to-many fields': + for i in range(1, field_values[field_values['Contract Name'] == document_name].shape[0]): + unique_identifier = str(temp_df.at[temp_df.index[i],'CONTRACT_LOB']) + '__' + str( + temp_df.at[temp_df.index[i],'CONTRACT_PRODUCT']) + '__' + str( + temp_df.at[temp_df.index[i],'CONTRACT_NETWORK']) + '__' + str( + temp_df.at[temp_df.index[i],'CONTRACT_MARKETPLACE_METAL_LEVEL']) + '__' + str( + temp_df.at[temp_df.index[i],'CONTRACT_SERVICE_AREA']) + '__' + str( + temp_df.at[temp_df.index[i],'CONTRACT_PROGRAM']) + '__' + str( + temp_df.at[temp_df.index[i],'PROV_TYPE']) + '__' + str(temp_df.at[temp_df.index[i],'PROV_SPECIALTY']) + field_values_1 = field_values[field_values['Contract Name'] == document_name].iloc[[i]].transpose().reset_index() + field_values_1.columns = ['SF_DB_COL_NAME', 'Actual Value Stored'] + field_values_p1 = field_values_1[~field_values_1['SF_DB_COL_NAME'].str.endswith('_PG')] + field_values_p2 = field_values_1[field_values_1['SF_DB_COL_NAME'].str.endswith('_PG')] + field_values_p2.columns = ['SF_DB_COL_NAME', 'Original Page Number'] + field_values_p2["SF_DB_COL_NAME"] = field_values_p2["SF_DB_COL_NAME"].str.replace("_PG", "") + + field_values_1 = pd.merge(field_values_p1, field_values_p2, how ='left', on =['SF_DB_COL_NAME']) + field_values_1['Contract Name'] = document_name + field_values_1['LOB_Product_Network_Metal_Area_Program_Type_Speciality'] = unique_identifier + field_values_2 = pd.concat([field_values_2, field_values_1], ignore_index = True) + + + # st.write(df) + # st.write(field_values_2) + df = pd.merge(df, field_values_2, how ='right', on =['Contract Name', 'SF_DB_COL_NAME']) + if mode != 'Single field - Non Empty values': + df = df[df['SF_DB_COL_NAME'].isin(list(field_prompt_mapping.keys()))] + else: + df = df[df['SF_DB_COL_NAME'].isin([field])] + if mode != 'One-to-many fields': + df = df.drop_duplicates(subset=['SF_DB_COL_NAME', 'Contract Name'], keep="first") + df['Original Page Number'] = df['Original Page Number'].apply(lambda x: re.search(r'\d+', x).group( + ) if isinstance(x, str) and re.search(r'\d+', x) is not None else " ") + df['Raw value 2'] = df['New Extracted value'] + df_date = df[df['SF_DB_COL_NAME'].str.contains('_DT', na=False)] + df_others = df[~df['SF_DB_COL_NAME'].str.contains('_DT', na=False)] + + df_date['Actual Value Stored'] = pd.to_datetime(df_date['Actual Value Stored'],errors='coerce').dt.strftime('%Y-%m-%d').fillna(" ") + df_date['New Extracted value'] = pd.to_datetime(df_date['New Extracted value'],errors='coerce').dt.strftime('%Y-%m-%d').fillna(" ") + df = pd.concat([df_date, df_others], ignore_index = True) + df.sort_values(['SF_DB_COL_NAME', 'Contract Name'], inplace=True) + + df.fillna(" ", inplace=True) + df['Raw value 3'] = df['New Extracted value'] + df['Actual Value Stored'] = df['Actual Value Stored'].apply(lambda x: x.strip() if isinstance(x, str) else '') + df['New Extracted value'] = df['New Extracted value'].apply(lambda x: x.strip() if isinstance(x, str) else '') + actual_value_list = list(df['Actual Value Stored']) + actual_value_list = [answer if str(answer) != "12 months" else "1 year" for answer in actual_value_list] + actual_value_list = [answer if str(answer) != "Fifth" else "5" for answer in actual_value_list] + actual_value_list = [answer if str(answer) != "Seventh" else "7" for answer in actual_value_list] + + answer_list = list(df['New Extracted value']) + answer_list = [answer if str(answer) != "one-year" else "1 year" for answer in answer_list] + answer_list = [answer if str(answer) != "one year" else "1 year" for answer in answer_list] + answer_list = [answer if str(answer) != "one" else "1 year" for answer in answer_list] + answer_list = [answer if str(answer) != "one (1) year" else "1 year" for answer in answer_list] + answer_list = [answer if str(answer) != "twelve" else "1 year" for answer in answer_list] + answer_list = [answer if str(answer) != "XI" else "11" for answer in answer_list] + answer_list = [answer if str(answer) != "Third" else "3" for answer in answer_list] + answer_list = [answer if str(answer) != "Six" else "6" for answer in answer_list] + + actual_value_list = [s.replace('-', '').replace(' ', '').replace('[', '').replace(']', '').lower() for s in actual_value_list] + answer_list = [s.replace('-', '').replace(' ', '').replace('[', '').replace(']', '').lower() for s in answer_list] + result_list = [(i in j) or (j in i) if isinstance(i, str) and isinstance( + j, str) and ((i != '') == (j != '')) else False for i, j in zip(actual_value_list, answer_list)] + df['Result'] = [str(x) for x in result_list] + + # df = df[~df['Contract ID'].isnull()] + # df['Contract ID'] = contract_list_f + df['Contract ID'] = range(len(actual_value_list)) + + df = df[['Contract Name','Contract ID', 'LOB_Product_Network_Metal_Area_Program_Type_Speciality', 'SF_DB_COL_NAME', 'Actual Value Stored', 'Raw value', 'Raw value 2', 'Raw value 3' + , 'New Extracted value','Confidence Level','Snippet','Original Page Number', 'New Page Number', 'Revised Prompt', 'Result']] + + try: + accuracy = round(sum(bool(x) for x in result_list) * 100 / len(list(df['Result'])), 2) + except: + accuracy = 'NA' + + if mode != 'Single field - Non Empty values': + field = field_group + history.loc[len(history.index)] = [field, str(contract_count), st.session_state.user_info['mail'], datetime.now().strftime("%Y-%m-%d %H:%M:%S"), accuracy, attempt] + # df.to_csv("RESULTS\\"+field.replace("?","").replace("/","_")+'-'+llm_selected+'.csv', index=False) + + return df, history, attempt, raw_response_text + +raw_response_text = '' +df = pd.DataFrame(columns=['Contract Name','Contract ID', 'Actual Value Stored','New Extracted value','Confidence Level' + ,'Snippet','New Page Number', 'Revised Prompt', 'Result']) + +if st.button("Test Configuration"): + df, history, attempt, raw_response_text = run_llm(attempt, bucket, contract_list, llm_selected, field, field_values, history) + df_1 = df[['Contract Name','Contract ID', 'Actual Value Stored','New Extracted value','Confidence Level' + ,'Snippet','Original Page Number', 'New Page Number', 'Revised Prompt', 'Result']] + df.to_csv('results.csv', index=False) + history.to_csv('history.csv', index=False) + # s3_client.upload_file('results.csv', bucket, key) + csv_buf = StringIO() + df_1.to_csv(csv_buf, header=True, index=False) + csv_buf.seek(0) + s3_client.put_object(Bucket='doczy-dev-infra-raw-data-ingestion', Body=csv_buf.getvalue(), Key='training_interface/results.csv') + csv_buf = StringIO() + history.tail(1).to_csv(csv_buf, header=True, index=False) + csv_buf.seek(0) + s3_client.put_object(Bucket='doczy-dev-infra-raw-data-ingestion', Body=csv_buf.getvalue(), Key='training_interface/history.csv') + df = df[['Contract Name','Contract ID', 'LOB_Product_Network_Metal_Area_Program_Type_Speciality','SF_DB_COL_NAME', 'Actual Value Stored','New Extracted value','Confidence Level' + ,'Snippet','Original Page Number', 'New Page Number', 'Revised Prompt', 'Result']] + +try: + df = pd.read_csv('results.csv') + df['Result'] = df['Result'].astype('str') +except: + df = pd.DataFrame(columns=['Contract Name' ,'New Extracted value','Confidence Level','Snippet','New Page Number' + , 'Revised Prompt', 'Result']) +history = pd.read_csv('history.csv') +st.dataframe(df) +st.dataframe(history) + +# @st.cache_data +# def convert_df(df): +# return df.to_csv(index=False).encode('utf-8') + +# csv = convert_df(edited_df) + +# buttons = st.columns(3) +# with buttons[0]: +# st.button("Save All Imputations") +# with buttons[1]: +# st.download_button("Download Table", csv, "file.csv", "text/csv", key='download-csv') +# with buttons[2]: +# st.button("Kickoff Database Integration") + +add_vertical_space(20) +st.write(field) +# if mode != 'Multiple fields': +# st.write(fields.loc[fields['SF_DB_COL_NAME'] == field, 'Field Name'].iloc[0]) + +st.write(len(contract_list)) +st.write(raw_response_text) + +try: + save_to_sf('load_training_results', training_results_file_name = "results.csv", attempt_logs_file_name = "history.csv") +except: + st.write("running locally") + + diff --git a/streamlit/multipage/security.py b/streamlit/multipage/security.py new file mode 100644 index 0000000..b3bcee3 --- /dev/null +++ b/streamlit/multipage/security.py @@ -0,0 +1,77 @@ +import streamlit as st +import msal +import requests +import boto3 +from botocore.exceptions import ClientError +import json + +# Replace with your own values +CLIENT_ID = 'effafe90-7ed7-43a3-ab03-19a0be2f1758' +# CLIENT_SECRET = 'bjQ8Q~lpR2uBcGI34VDu16t73doz8Crj0YY_~dgD' +# TENANT_ID = '' + +AUTHORITY = 'https://login.microsoftonline.com/organizations/' +SCOPE = ['User.Read'] +REDIRECT_URI = 'https://172.29.20.102:8501' + +# Initialize boto3 client to interact with AWS Secrets Manager + + + + +def get_secret(): + + secret_name = "doczy-sso-azure-app-key" + region_name = "us-east-2" + + # Create a Secrets Manager client + # session = boto3.session.Session() + # client = session.client( + # service_name='secretsmanager', + # region_name=region_name + # ) + client = boto3.client('secretsmanager', region_name=region_name) + try: + get_secret_value_response = client.get_secret_value( + SecretId=secret_name + ) + except ClientError as e: + raise e + + secret = get_secret_value_response['SecretString'] + secret = json.loads(secret)['CLIENT_SECRET'] + + return secret + + +CLIENT_SECRET = get_secret() + +app = msal.ConfidentialClientApplication(CLIENT_ID, authority=AUTHORITY, client_credential=CLIENT_SECRET) + + +def get_auth_url(REDIRECT_URI): + auth_url = app.get_authorization_request_url(SCOPE, redirect_uri=REDIRECT_URI) + return auth_url + +@st.cache_data +def get_token_from_code(auth_code, REDIRECT_URI): + app = msal.ConfidentialClientApplication(CLIENT_ID, authority=AUTHORITY, client_credential=CLIENT_SECRET) + result = app.acquire_token_by_authorization_code(auth_code, scopes=SCOPE, redirect_uri=REDIRECT_URI) + return result['access_token'] + + +def get_user_info(access_token): + headers = {'Authorization': f'Bearer {access_token}'} + response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers) + return response.json() + + +def handle_redirect(REDIRECT_URI): + if not st.session_state.get('access_token'): + code = st.query_params.get('code') + if code: + access_token = get_token_from_code(code, REDIRECT_URI) + st.session_state['access_token'] = access_token + st.session_state + + \ No newline at end of file diff --git a/streamlit/multipage/sf_conn.py b/streamlit/multipage/sf_conn.py new file mode 100644 index 0000000..199f1a5 --- /dev/null +++ b/streamlit/multipage/sf_conn.py @@ -0,0 +1,140 @@ +# Use this code snippet in your app. +# If you need more information about configurations +# or implementing the sample code, visit the AWS docs: +# https://aws.amazon.com/developer/language/python/ + +import boto3 +from botocore.exceptions import ClientError +import http.client +import base64 +import ast +import snowflake.connector +import json + + +def get_secret(): + + secret_name = "doczy_dev_db_creds" + region_name = "us-east-2" + + # Create a Secrets Manager client + session = boto3.session.Session() + client = session.client( + service_name='secretsmanager', + region_name=region_name + ) + + try: + get_secret_value_response = client.get_secret_value( + SecretId=secret_name + ) + except ClientError as e: + # For a list of exceptions thrown, see + # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html + raise e + + secret = get_secret_value_response['SecretString'] + return secret + +# get_secret() + +# TODO: This function needs to be changed to accept Kwargs +# The function name should be more generic and cofnigurable +def save_to_sf(dag_name, **kwargs): + + mwaa_env_name = 'doczy-dev-infra-mwaa' + dag_name = dag_name + mwaa_cli_command = 'dags trigger' + + # Create the client with the specified profile + session = boto3.Session() + client = session.client('mwaa', region_name='us-east-2') + + # get web token + mwaa_cli_token = client.create_cli_token( + Name=mwaa_env_name + ) + + conn = http.client.HTTPSConnection(mwaa_cli_token['WebServerHostname']) + + # This section passes the payload to the MWAA CLI + # The file parameters should be added dynamically in streamlit, once the file names are passed while triggering the dag, the data will be ingested + # training_results_file = "training_results_sample.csv" + # attempt_logs_file = "attempt_logs_sample.csv" + # conf = "{\"" + "training_results_file_name" + "\":\"" + {training_results_file} + "\", \"" + "attempt_logs_file_name" + "\":\"" + {attempt_logs_file} + "\"}".format(training_results_file=training_results_file, attempt_logs_file=attempt_logs_file) + + conf = json.dumps(kwargs) + + payload = mwaa_cli_command + " " + dag_name + " --conf '{}'".format(conf) + headers = { + 'Authorization': 'Bearer ' + mwaa_cli_token['CliToken'], + 'Content-Type': 'text/plain' + } + conn.request("POST", "/aws_mwaa/cli", payload, headers) + res = conn.getresponse() + data = res.read() + dict_str = data.decode("UTF-8") + mydata = ast.literal_eval(dict_str) + return payload + + +# save_to_sf("2024-03-13T17-36_prompt_results.csv", "2024-03-14T23-19_history.csv") + + +# Create snowflake connection with the secrets fetched from secrets manager for the schema passed as an argument +def get_snowflake_conn(schema): + secret = get_secret() + secret_dict = eval(secret) + conn = snowflake.connector.connect( + user=secret_dict['user'], + password=secret_dict['password'], + account=secret_dict['account_alias'], + warehouse=secret_dict['warehouse'], + database=secret_dict['database'], + schema=schema + ) + return conn + +def get_client_names(): + """ + Input: None + Output: client_names, s3_paths lists + """ + try: + # Get conn from snowflake_conn function for STG schema + conn = get_snowflake_conn('STG') + cursor = conn.cursor() + + # Query to get client names and their s3_paths + query = "SELECT DISTINCT client_name, bucket_name FROM STG.CLIENT_LOGS" + cursor.execute(query) + # Create 2 lists from the query results + client_names = [] + s3_paths = [] + + for row in cursor: + client_names.append(row[0]) + s3_paths.append(row[1]) # Extracting the bucket name from the s3 path + cursor.close() + conn.close() + return client_names, s3_paths + except Exception as e: + print(f"Error while fetching client names from snowflake: {e}") + return None, None + + +def insert_upload_logs(batch_id, client_name, file_name, upload_datetime, upload_user): + """ + Input: batch_id, client_name, file_name, upload_datetime, upload_user + Output: status of the insert query + """ + try: + conn = get_snowflake_conn('STG') + cursor = conn.cursor() + query = f"INSERT INTO STG.CONTRACT_UPLOAD_LOGS (BATCH_ID, CLIENT_NAME, FILE_NAME, UPLOAD_DATETIME, UPLOAD_USER) VALUES ('{batch_id}', '{client_name}', '{file_name}', '{upload_datetime}', '{upload_user}')" + cursor.execute(query) + cursor.close() + conn.close() + return 'Log inserted successfully' + except Exception as e: + return e diff --git a/streamlit/multipage/util.py b/streamlit/multipage/util.py new file mode 100644 index 0000000..78c2782 --- /dev/null +++ b/streamlit/multipage/util.py @@ -0,0 +1,35 @@ + +import streamlit as st +import security +import os +import constants + +def setup_page(REDIRECT_URI): + # st.set_page_config( + # page_title=page_title, + # page_icon="👋", + # ) + + if st.query_params.get('code'): + security.handle_redirect(REDIRECT_URI) + + access_token = st.session_state.get('access_token') + + if access_token: + user_info = security.get_user_info(access_token) + st.session_state['user_info'] = user_info + return True + else: + st.write("Please sign-in to use this app.") + auth_url = security.get_auth_url(REDIRECT_URI) + st.markdown(f"Sign In", unsafe_allow_html=True) + st.stop() + +def load_page_details(interface): + env_var = os.environ.get('ENVIRONMENT', 'DEV') + if env_var == 'UAT': + return (constants.DOCZY_REDIRECT_URL_UAT + str(interface), constants.DOCZY_CREATE_BATCH_URL_UAT, constants.DOCZY_PIPELINE_URL_UAT) + elif env_var == 'DEV': + return (constants.DOCZY_REDIRECT_URL_DEV + str(interface), constants.DOCZY_CREATE_BATCH_URL_DEV, constants.DOCZY_PIPELINE_URL_DEV) + else: + return (constants.DOCZY_REDIRECT_URL_DEV + str(interface), constants.DOCZY_CREATE_BATCH_URL_DEV, constants.DOCZY_PIPELINE_URL_DEV)