diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/build/build.md b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/build.md index c0db7b1b..9ce016f6 100644 --- a/dev-ai-app-dev-finance/dev-guide-ai-finance/build/build.md +++ b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/build.md @@ -97,7 +97,410 @@ This lab assumes you have: 4. Run the code block to connect to the database. - ![Connect to Database](./images/connect-to-db.png " ") + ![Connect to Database](./images/lab4task1.png " ") + +5. Create the tables and load the data. + +* We begin by dropping any old objects. +* Then we create all tables. +* We build a JSON duality view called `clients_dv`. +* We load the ONNX model we will later use for embedding. + + Run and review the code in a new cell: + + ```python + +DROP_OBJECTS_SQL = [ +"DROP VIEW IF EXISTS CLIENT_DV", +"DROP PROPERTY GRAPH IF EXISTS LOANS_GRAPH_VW", +"DROP TABLE IF EXISTS CLIENTS_TO_LOAN_RECOMMENDATIONS CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS CLIENTS_TO_LOAN CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS CLIENT_DEBT CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS LOAN_CHUNK CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS LOAN_APPLICATIONS CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS MOCK_LOAN_DATA CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS CLIENTS CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS FUNDING_PROVIDER CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS FUNDING_PROVIDER_TERMS CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS LENDER_TERMS CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS AFFORDABLE_HOUSING_ZONE CASCADE CONSTRAINTS PURGE", +"DROP TABLE IF EXISTS FLOODZONE CASCADE CONSTRAINTS PURGE", +] + +# ------------------------------------------------------------- +# CREATE TABLE STATEMENTS +# ------------------------------------------------------------- + +CREATE_CLIENTS = """ +CREATE TABLE IF NOT EXISTS CLIENTS ( +CUSTOMER_ID VARCHAR2(4000) PRIMARY KEY, +FIRST_NAME VARCHAR2(4000), +LAST_NAME VARCHAR2(4000), +CITY VARCHAR2(4000), +STATE VARCHAR2(4000), +ZIP_CODE NUMBER, +AGE NUMBER, +INCOME NUMBER, +VETERAN VARCHAR2(4000) +) +""" + +CREATE_LOAN_APPLICATIONS = """ +CREATE TABLE IF NOT EXISTS LOAN_APPLICATIONS ( +CUSTOMER_ID VARCHAR2(4000), +APPLICATION_ID NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1000 INCREMENT BY 1000) PRIMARY KEY, +REQUESTED_LOAN_AMOUNT NUMBER, +CREDIT_SCORE NUMBER, +ZIPCODE NUMBER, +LOAN_PURPOSE VARCHAR2(4000), +LOAN_STATUS VARCHAR2(4000), +STUDENT_STATUS VARCHAR2(4000), +EDUCATION_LEVEL VARCHAR2(4000), +FINAL_DECISION VARCHAR2(32767), +RECOMMENDATIONS VARCHAR2(32767), +TOTAL_DEBT NUMBER, +CREDIT_RANK NUMBER DEFAULT NULL, +CONSTRAINT fk_loan_app_clients FOREIGN KEY (CUSTOMER_ID) REFERENCES CLIENTS(CUSTOMER_ID) +) +""" + +CREATE_MOCK_LOAN_DATA = """ +CREATE TABLE IF NOT EXISTS MOCK_LOAN_DATA ( +LOAN_ID NUMBER PRIMARY KEY, +LOAN_PROVIDER_NAME VARCHAR2(4000), +LOAN_TYPE VARCHAR2(4000), +INTEREST_RATE NUMBER, +ORIGINATION_FEE NUMBER, +TIME_TO_CLOSE NUMBER, +CREDIT_SCORE NUMBER, +DEBT_TO_INCOME_RATIO NUMBER, +INCOME NUMBER, +DOWN_PAYMENT_PERCENT NUMBER, +IS_FIRST_TIME_HOME_BUYER VARCHAR2(10) NOT NULL, +OFFER_BEGIN_DATE DATE DEFAULT SYSDATE, +OFFER_END_DATE DATE DEFAULT SYSDATE + 30 +) +""" + +CREATE_CLIENT_DEBT = """ +CREATE TABLE IF NOT EXISTS CLIENT_DEBT ( +ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, +CUSTOMER_ID VARCHAR2(100), +APPLICATION_ID NUMBER, +DEBT_TYPE VARCHAR2(100), +DEBT_AMOUNT NUMBER +) +""" + +CREATE_CLIENTS_TO_LOAN = """ +CREATE TABLE IF NOT EXISTS CLIENTS_TO_LOAN ( +ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, +CUSTOMER_ID VARCHAR2(4000), +LOAN_ID NUMBER, +LOAN_APPLICATION_ID NUMBER, +CONSTRAINT fk_ctl_clients FOREIGN KEY (CUSTOMER_ID) REFERENCES CLIENTS(CUSTOMER_ID), +CONSTRAINT fk_ctl_loans FOREIGN KEY (LOAN_ID) REFERENCES MOCK_LOAN_DATA(LOAN_ID), +CONSTRAINT fk_ctl_loan_applications FOREIGN KEY (LOAN_APPLICATION_ID) REFERENCES LOAN_APPLICATIONS(APPLICATION_ID) +) +""" + +CREATE_CLIENTS_TO_LOAN_RECS = """ +CREATE TABLE IF NOT EXISTS CLIENTS_TO_LOAN_RECOMMENDATIONS ( +ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, +CUSTOMER_ID VARCHAR2(4000), +LOAN_ID NUMBER, +LOAN_APPLICATION_ID NUMBER, +ACTION_NEEDED VARCHAR2(4000), +CONSTRAINT fk_cltr_clients FOREIGN KEY (CUSTOMER_ID) REFERENCES CLIENTS(CUSTOMER_ID), +CONSTRAINT fk_cltr_loans FOREIGN KEY (LOAN_ID) REFERENCES MOCK_LOAN_DATA(LOAN_ID), +CONSTRAINT fk_cltr_loan_applications FOREIGN KEY (LOAN_APPLICATION_ID) REFERENCES LOAN_APPLICATIONS(APPLICATION_ID) +) +""" + +CREATE_LOAN_CHUNK = """ +CREATE TABLE IF NOT EXISTS LOAN_CHUNK ( +ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, +CUSTOMER_ID VARCHAR2(100), +CHUNK_ID NUMBER, +CHUNK_TEXT CLOB, +CHUNK_VECTOR VECTOR(384,*,DENSE) +) +""" + +CREATE_FUNDING_PROVIDER_TERMS = """ +CREATE TABLE IF NOT EXISTS FUNDING_PROVIDER_TERMS ( +LOAN_PROVIDER_ID NUMBER, +TERMS_ID NUMBER PRIMARY KEY, +INTEREST_RATE NUMBER, +LOAN_DESCRIPTION VARCHAR2(1000), +TIME_TO_CLOSE NUMBER, +LOAN_COSTS NUMBER, +OFFER_BEGIN_DATE DATE, +OFFER_END_DATE DATE +) +""" + +CREATE_LENDER_TERMS = """ +CREATE TABLE IF NOT EXISTS LENDER_TERMS ( +LENDER_ID NUMBER, +TERMS_ID NUMBER PRIMARY KEY, +LOAN_DESCRIPTION VARCHAR2(1000), +INTEREST_RATE_MARKUP NUMBER, +ORIGINATION_FEE NUMBER, +LENDER_TIME_TO_CLOSE NUMBER, +CREDIT_SCORE NUMBER, +DEBT_TO_INCOME_RATIO NUMBER, +INCOME NUMBER, +DOWN_PAYMENT_PERCENT NUMBER, +OFFER_BEGIN_DATE DATE, +OFFER_END_DATE DATE, +CONSTRAINT fk_lt_mock_loan_data FOREIGN KEY (LENDER_ID) REFERENCES MOCK_LOAN_DATA(LOAN_ID) +) +""" + +CREATE_AFFORDABLE_HOUSING_ZONE = """ +CREATE TABLE IF NOT EXISTS AFFORDABLE_HOUSING_ZONE ( +LAND_TRACT_ID NUMBER PRIMARY KEY, +ZIPCODE NUMBER +) +""" + +CREATE_FLOODZONE = """ +CREATE TABLE IF NOT EXISTS FLOODZONE ( +GEOMETRY MDSYS.SDO_GEOMETRY, +FID NUMBER, +DESCR VARCHAR2(32767) +) +""" + +# ------------------------------------------------------------- +# JSON DUALITY VIEW +# ------------------------------------------------------------- + +CREATE_CLIENTS_DV = """ +CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW clients_dv AS +SELECT JSON { +'_id': c.customer_id, +'firstName': c.first_name, +'lastName': c.last_name, +'city': c.city, +'state': c.state, +'zipCode': c.zip_code, +'age': c.age, +'income': c.income, +'veteran': c.veteran, +'clientDebt': [ +SELECT JSON{ + 'id': cd.id, + 'debtType': cd.debt_type, + 'debtAmount': cd.debt_amount +} +FROM client_debt cd WITH INSERT UPDATE DELETE +WHERE cd.customer_id = c.customer_id +], +'loanApplications': [ +SELECT JSON { + 'applicationId': la.application_id, + 'requestedLoanAmount': la.requested_loan_amount, + 'creditScore': la.credit_score, + 'zipcode': la.zipcode, + 'loanPurpose': la.loan_purpose, + 'loanStatus': la.loan_status, + 'studentStatus': la.student_status, + 'educationLevel': la.education_level, + 'finalDecision': la.final_decision, + 'recommendations': la.recommendations, + 'totalDebt' : la.total_debt, + 'creditRank': la.credit_rank +} +FROM loan_applications la WITH INSERT UPDATE DELETE +WHERE la.customer_id = c.customer_id +] +} +FROM clients c WITH INSERT UPDATE DELETE +""" + +# ------------------------------------------------------------- +# ONNX MODEL LOAD +# ------------------------------------------------------------- + +ONNX_MODEL_SQL = """ +DECLARE +model_count INT; +BEGIN +SELECT COUNT(*) INTO model_count +FROM user_mining_models +WHERE model_name = 'DEMO_MODEL'; +IF (model_count < 1) THEN +EXECUTE IMMEDIATE 'CREATE OR REPLACE DIRECTORY DEMO_DIR AS ''demodir'''; +DBMS_CLOUD.GET_OBJECT( + object_uri => 'https://objectstorage.us-ashburn-1.oraclecloud.com/p/mFBJq8UCjdar89xJpTQVOy_tONdxyrQI-B8UT0OS-nllmg8xyVAIIOTbGyIVJ1iJ/n/c4u04/b/llm/o/all_MiniLM_L12_v2.onnx', + directory_name => 'DEMO_DIR', + file_name => 'all_minilm_l12_v2.onnx' +); +DBMS_DATA_MINING.DROP_MODEL(model_name => 'DEMO_MODEL', force => TRUE); +DBMS_VECTOR.LOAD_ONNX_MODEL('DEMO_DIR', 'all_minilm_l12_v2.onnx', 'DEMO_MODEL'); +END IF; +END; +""" + +# ------------------------------------------------------------- +# SAMPLE DATA ARRAYS +# ------------------------------------------------------------- + +AFFORDABLE_HOUSING_ZONE_DATA = [ + (1, 48202) +] + +CLIENTS_DATA = [ + ('CUST_1000','James','Smith','New York City','NY',10033,52,130000,'Yes'), + ('CUST_2000','James','Woods','East Gina','AR',58967,27,5000,'Yes'), + ('CUST_3000','Evan','Burton','Detroit','MI',48202,24,65000,'No'), + ('CUST_4000','Alex','Anderson','Austin','TX',78744,26,25000,'No'), +] + +LOAN_APPLICATIONS_DATA = [ + ('CUST_1000',1000,200000,780,10033,'Mortgage','Pending Review','No','PhD',None,None,None,0), + ('CUST_2000',1001,100000,520,58967,'Mortgage','Pending Review','No','High School',None,None,None,0), + ('CUST_3000',1002,150000,675,48202,'Mortgage','Pending Review','No','Masters',None,None,None,0), +] + +MOCK_LOAN_DATA = [ + (1,'provider1','Bridge loan',4.75,1.06,38,649,27.15,150377,12.43,'NO'), + (2,'provider2','Second mortgage',3.31,1.92,25,776,28.81,154763,3.76,'NO'), +] + +CLIENT_DEBT_DATA = [ + ('CUST_1000',1000,'First Time Home Owner',90000), + ('CUST_2000',1001,'Home',8843), + ('CUST_2000',1001,'Credit Card',31560), +] + +CLIENTS_TO_LOAN_RECS_DATA = [ + ('CUST_1000', 1, 'Review Documents'), + ('CUST_2000', 2, 'Verify Credit'), +] + +# ------------------------------------------------------------- +# HELPERS +# ------------------------------------------------------------- + +def safe_execute(sql: str, params=None): + try: + cursor.execute(sql, params or {}) + except Exception as e: + print(f"(ignore) {e}") + +def run_schema_setup(): + # Drops + for stmt in DROP_OBJECTS_SQL: + safe_execute(stmt) + print("✅ Dropped any existing objects") + + # Creates + cursor.execute(CREATE_CLIENTS) + cursor.execute(CREATE_LOAN_APPLICATIONS) + cursor.execute(CREATE_MOCK_LOAN_DATA) + cursor.execute(CREATE_CLIENT_DEBT) + cursor.execute(CREATE_CLIENTS_TO_LOAN) + cursor.execute(CREATE_CLIENTS_TO_LOAN_RECS) + cursor.execute(CREATE_LOAN_CHUNK) + cursor.execute(CREATE_FUNDING_PROVIDER_TERMS) + cursor.execute(CREATE_LENDER_TERMS) + cursor.execute(CREATE_AFFORDABLE_HOUSING_ZONE) + cursor.execute(CREATE_FLOODZONE) + connection.commit() + print("✅ Tables created") + + # Duality View + cursor.execute(CREATE_CLIENTS_DV) + connection.commit() + print("✅ JSON Duality View clients_dv created") + + # ONNX model + cursor.execute(ONNX_MODEL_SQL) + connection.commit() + print("✅ DEMO_MODEL loaded (or already present)") + +def insert_seed_data(): + # Truncate to avoid duplicates + cursor.execute("TRUNCATE TABLE AFFORDABLE_HOUSING_ZONE") + cursor.execute("TRUNCATE TABLE CLIENTS_TO_LOAN_RECOMMENDATIONS") + cursor.execute("TRUNCATE TABLE CLIENT_DEBT") + cursor.execute("TRUNCATE TABLE LOAN_APPLICATIONS") + cursor.execute("TRUNCATE TABLE MOCK_LOAN_DATA") + cursor.execute("TRUNCATE TABLE CLIENTS") + connection.commit() + + # Affordable housing + cursor.executemany(""" + INSERT INTO AFFORDABLE_HOUSING_ZONE (LAND_TRACT_ID, ZIPCODE) VALUES (:1, :2) + """, AFFORDABLE_HOUSING_ZONE_DATA) + + # Clients + cursor.executemany(""" + INSERT INTO CLIENTS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, CITY, STATE, ZIP_CODE, AGE, INCOME, VETERAN) + VALUES (:1,:2,:3,:4,:5,:6,:7,:8,:9) + """, CLIENTS_DATA) + + # Loan applications + cursor.executemany(""" + INSERT INTO LOAN_APPLICATIONS ( + CUSTOMER_ID, APPLICATION_ID, REQUESTED_LOAN_AMOUNT, CREDIT_SCORE, ZIPCODE, + LOAN_PURPOSE, LOAN_STATUS, STUDENT_STATUS, EDUCATION_LEVEL, + FINAL_DECISION, RECOMMENDATIONS, CREDIT_RANK, TOTAL_DEBT + ) VALUES (:1,:2,:3,:4,:5,:6,:7,:8,:9,:10,:11,:12,:13) + """, LOAN_APPLICATIONS_DATA) + + # Mock loan data + cursor.executemany(""" + INSERT INTO MOCK_LOAN_DATA ( + LOAN_ID, LOAN_PROVIDER_NAME, LOAN_TYPE, INTEREST_RATE, ORIGINATION_FEE, + TIME_TO_CLOSE, CREDIT_SCORE, DEBT_TO_INCOME_RATIO, INCOME, DOWN_PAYMENT_PERCENT, IS_FIRST_TIME_HOME_BUYER + ) VALUES (:1,:2,:3,:4,:5,:6,:7,:8,:9,:10,:11) + """, MOCK_LOAN_DATA) + + # Client debt + cursor.executemany(""" + INSERT INTO CLIENT_DEBT (CUSTOMER_ID, APPLICATION_ID, DEBT_TYPE, DEBT_AMOUNT) + VALUES (:1,:2,:3,:4) + """, CLIENT_DEBT_DATA) + + # Client->Loan recs + cursor.executemany(""" + INSERT INTO CLIENTS_TO_LOAN_RECOMMENDATIONS (CUSTOMER_ID, LOAN_ID, ACTION_NEEDED) + VALUES (:1,:2,:3) + """, CLIENTS_TO_LOAN_RECS_DATA) + + # Derive total_debt per application + cursor.execute(""" + UPDATE LOAN_APPLICATIONS la + SET total_debt = ( + SELECT NVL(SUM(cd.debt_amount),0) + FROM CLIENT_DEBT cd + WHERE cd.customer_id = la.customer_id + AND cd.application_id = la.application_id + ) + """) + + connection.commit() + print("✅ Seed data inserted & totals updated") + +# ------------------------------------------------------------- +# RUN EVERYTHING +# ------------------------------------------------------------- +run_schema_setup() +insert_seed_data() +print("schema initialized.") + + + ``` + +6. Validate that all assets have been created successfully: + + ![Connect to Database](./images/tas2.png " ") + + ![Connect to Database](./images/task2result.png " ") ## Task 3: Create a Function to retrieve data from the database. @@ -114,37 +517,44 @@ You will query customer data from the `clients_dv` JSON duality view, which comb ```python def fetch_customer_data(customer_id): - cursor.execute("SELECT data FROM clients_dv WHERE JSON_VALUE(data, '$._id') = :customer_id", {'customer_id': customer_id}) - result = cursor.fetchone() - return json.loads(result[0]) if result and isinstance(result[0], str) else result[0] if result else None + cursor.execute( + "SELECT data FROM clients_dv WHERE JSON_VALUE(data, '$._id') = :customer_id", + {'customer_id': customer_id} + ) + result = cursor.fetchone() + return json.loads(result[0]) if result and isinstance(result[0], str) else result[0] if result else None selected_customer_id = "CUST_1000" customer_json = fetch_customer_data(selected_customer_id) if customer_json: - loan_app = customer_json.get("loanApplications", [{}])[0] - print(f"Customer: {customer_json['firstName']} {customer_json['lastName']}") - print(f"Loan Status: {loan_app['loanStatus']}") - - desired_fields = [ - ("Customer ID", selected_customer_id), - ("Application ID", loan_app.get("applicationId", "")), - ("First Name", customer_json.get("firstName", "")), - ("Last Name", customer_json.get("lastName", "")), - ("City", customer_json.get("city", "")), - ("State", customer_json.get("state", "")), - ("Zip code", customer_json.get("zipCode", "")), - ("Age", customer_json.get("age", 0)), - ("Income", customer_json.get("income", 0)), - ("Credit score", loan_app.get("creditScore", 600)), - ("Requested loan amount", loan_app.get("requestedLoanAmount", 0)), - ("Total Debt", loan_app.get("totalDebt", 0)), - ("Loan status", loan_app.get("loanStatus", "Pending Review")) - ] - df_customer_details = pd.DataFrame({field_name: [field_value] for field_name, field_value in desired_fields}) - display(df_customer_details) + loan_app = customer_json.get("loanApplications", [{}])[0] + print(f"Customer: {customer_json['firstName']} {customer_json['lastName']}") + print(f"Loan Status: {loan_app['loanStatus']}") + + desired_fields = [ + ("Customer ID", selected_customer_id), + ("Application ID", loan_app.get("applicationId", "")), + ("First Name", customer_json.get("firstName", "")), + ("Last Name", customer_json.get("lastName", "")), + ("City", customer_json.get("city", "")), + ("State", customer_json.get("state", "")), + ("Zip code", customer_json.get("zipCode", "")), + ("Age", customer_json.get("age", 0)), + ("Income", customer_json.get("income", 0)), + ("Credit score", loan_app.get("creditScore", 600)), + ("Requested loan amount", loan_app.get("requestedLoanAmount", 0)), + ("Total Debt", loan_app.get("totalDebt", 0)), + ("Loan status", loan_app.get("loanStatus", "Pending Review")) + ] + + df_customer_details = pd.DataFrame( + {field_name: [field_value] for field_name, field_value in desired_fields} + ) + display(df_customer_details) + else: - print("No data found for customer ID:", selected_customer_id) + print("No data found for customer ID:", selected_customer_id) ``` @@ -177,29 +587,29 @@ df_mock_loans = pd.DataFrame(cursor.fetchall(), columns=["LOAN_ID", "LOAN_PROVID # Generate Recommendations def generate_recommendations(customer_id, customer_json, df_mock_loans): - loan_app = customer_json.get("loanApplications", [{}])[0] - available_loans_text = "\n".join([f"{loan['LOAN_ID']}: {loan['LOAN_TYPE']} | {loan['INTEREST_RATE']}% interest | Credit Score: {loan['CREDIT_SCORE']} | DTI: {loan['DEBT_TO_INCOME_RATIO']}" for loan in df_mock_loans.to_dict(orient='records')]) - customer_profile_text = "\n".join([f"- {key.replace('_', ' ').title()}: {value}" for key, value in {**customer_json, **loan_app}.items() if key not in ["embedding_vector", "ai_response_vector", "chunk_vector"]]) - - prompt = f"""[INST] <>You are a Loan Approver AI. Use only the provided context to evaluate the applicant’s profile and recommend loans. Format results as plain text with numbered sections (1. Comprehensive Evaluation, 2. Top 3 Loan Recommendations, 3. Recommendations Explanations, 4. Final Suggestion). Use newlines between sections.> [/INST] - [INST]Available Loan Options:\n{available_loans_text}\nApplicant's Full Profile:\n{customer_profile_text}\nTasks:\n1. Comprehensive Evaluation\n2. Top 3 Loan Recommendations\n3. Recommendations Explanations\n4. Final Suggestion""" - - print("Generating AI response...") - print(" ") - - genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient(config=oci.config.from_file(os.getenv("OCI_CONFIG_PATH", "~/.oci/config")), service_endpoint=os.getenv("ENDPOINT")) - chat_detail = oci.generative_ai_inference.models.ChatDetails( - compartment_id=os.getenv("COMPARTMENT_OCID"), - chat_request=oci.generative_ai_inference.models.GenericChatRequest(messages=[oci.generative_ai_inference.models.UserMessage(content=[oci.generative_ai_inference.models.TextContent(text=prompt)])], temperature=0.0, top_p=1.00), - serving_mode=oci.generative_ai_inference.models.OnDemandServingMode(model_id="meta.llama-3.2-90b-vision-instruct") - ) - chat_response = genai_client.chat(chat_detail) - recommendations = chat_response.data.chat_response.choices[0].message.content[0].text - - return recommendations - -recommendations = generate_recommendations(selected_customer_id, customer_json, df_mock_loans) -print(recommendations) + loan_app = customer_json.get("loanApplications", [{}])[0] + available_loans_text = "\n".join([f"{loan['LOAN_ID']}: {loan['LOAN_TYPE']} | {loan['INTEREST_RATE']}% interest | Credit Score: {loan['CREDIT_SCORE']} | DTI: {loan['DEBT_TO_INCOME_RATIO']}" for loan in df_mock_loans.to_dict(orient='records')]) + customer_profile_text = "\n".join([f"- {key.replace('_', ' ').title()}: {value}" for key, value in {**customer_json, **loan_app}.items() if key not in ["embedding_vector", "ai_response_vector", "chunk_vector"]]) + + prompt = f"""[INST] <>You are a Loan Approver AI. Use only the provided context to evaluate the applicant’s profile and recommend loans. Format results as plain text with numbered sections (1. Comprehensive Evaluation, 2. Top 3 Loan Recommendations, 3. Recommendations Explanations, 4. Final Suggestion). Use newlines between sections.> [/INST] + [INST]Available Loan Options:\n{available_loans_text}\nApplicant's Full Profile:\n{customer_profile_text}\nTasks:\n1. Comprehensive Evaluation\n2. Top 3 Loan Recommendations\n3. Recommendations Explanations\n4. Final Suggestion""" + + print("Generating AI response...") + print(" ") + + genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient(config=oci.config.from_file(os.getenv("OCI_CONFIG_PATH", "~/.oci/config")), service_endpoint=os.getenv("ENDPOINT")) + chat_detail = oci.generative_ai_inference.models.ChatDetails( + compartment_id=os.getenv("COMPARTMENT_OCID"), + chat_request=oci.generative_ai_inference.models.GenericChatRequest(messages=[oci.generative_ai_inference.models.UserMessage(content=[oci.generative_ai_inference.models.TextContent(text=prompt)])], temperature=0.0, top_p=1.00), + serving_mode=oci.generative_ai_inference.models.OnDemandServingMode(model_id="meta.llama-3.2-90b-vision-instruct") + ) + chat_response = genai_client.chat(chat_detail) + recommendations = chat_response.data.chat_response.choices[0].message.content[0].text + + return recommendations + + recommendations = generate_recommendations(selected_customer_id, customer_json, df_mock_loans) + print(recommendations) ``` @@ -211,7 +621,7 @@ print(recommendations) >*Note:* Your result may be different due to non-deterministic character of generative AI. - ![ai recommendation](./images/return-task4recommendations.png " ") + ![ai recommendation](./images/task4recommendations.png " ") ## Task 5: Chunk & Store the Recommendations @@ -235,26 +645,26 @@ chunk_sizes = [50] # e.g., [50, 200, 500] # Insert chunks using VECTOR_CHUNKS. Make CHUNK_ID unique by (size + chunk_offset). for size in chunk_sizes: - insert_sql = f""" - INSERT INTO LOAN_CHUNK (CUSTOMER_ID, CHUNK_ID, CHUNK_TEXT) - SELECT :cust_id, - :chunk_size + vc.chunk_offset, - vc.chunk_text - FROM (SELECT :rec_text AS txt FROM dual) s, - VECTOR_CHUNKS( - dbms_vector_chain.utl_to_text(s.txt) - BY words - MAX {size} - OVERLAP 0 - SPLIT BY sentence - LANGUAGE american - NORMALIZE all - ) vc - """ - cursor.execute( - insert_sql, - {'cust_id': selected_customer_id, 'chunk_size': size, 'rec_text': recommendations} - ) + insert_sql = f""" + INSERT INTO LOAN_CHUNK (CUSTOMER_ID, CHUNK_ID, CHUNK_TEXT) + SELECT :cust_id, + :chunk_size + vc.chunk_offset, + vc.chunk_text + FROM (SELECT :rec_text AS txt FROM dual) s, + VECTOR_CHUNKS( + dbms_vector_chain.utl_to_text(s.txt) + BY words + MAX {size} + OVERLAP 0 + SPLIT BY sentence + LANGUAGE american + NORMALIZE all + ) vc + """ + cursor.execute( + insert_sql, + {'cust_id': selected_customer_id, 'chunk_size': size, 'rec_text': recommendations} + ) # Fetch chunks for preview cursor.execute(""" @@ -270,16 +680,16 @@ def _lob_to_str(v): return v.read() if isinstance(v, oracledb.LOB) else v items = [] for cid, ctext in rows: - txt = _lob_to_str(ctext) or "" - items.append({ - "CHUNK_ID": cid, - "Chars": len(txt), - "Words": len(txt.split()), - "Preview": (txt[:160] + "…") if len(txt) > 160 else txt - }) - -df_chunks = pd.DataFrame(items).sort_values("CHUNK_ID") -connection.commit() + txt = _lob_to_str(ctext) or "" + items.append({ + "CHUNK_ID": cid, + "Chars": len(txt), + "Words": len(txt.split()), + "Preview": (txt[:160] + "…") if len(txt) > 160 else txt + }) + + df_chunks = pd.DataFrame(items).sort_values("CHUNK_ID") + connection.commit() print(f"✅ Task 5 complete: recommendation chunked for customer {selected_customer_id} (sizes: {chunk_sizes}).") display(df_chunks) @@ -339,67 +749,67 @@ This step: - **Performs AI Vector Search**: Retrieve the relevant recommendation text from `LOAN_CHUNKS` table. Then find the most relevant recommendations using similarity search. - **Use RAG**: Combines the customer profile, policy rules using the retrieved recommendation context. -1. Review +1. Copy the code block below to implement RAG: ```python - question = "What 4th loan would James qualify for?" +question = "What 4th loan would James qualify for?" def vectorize_question(q): - cursor.execute(""" - SELECT dbms_vector_chain.utl_to_embedding( - :q, - JSON('{"provider":"database","model":"DEMO_MODEL","dimensions":384}') - ) FROM DUAL - """, {'q': q}) - return cursor.fetchone()[0] + cursor.execute(""" + SELECT dbms_vector_chain.utl_to_embedding( + :q, + JSON('{"provider":"database","model":"DEMO_MODEL","dimensions":384}') + ) FROM DUAL + """, {'q': q}) + return cursor.fetchone()[0] print("Processing your question using AI Vector Search across chunked recommendations...") try: - q_vec = vectorize_question(question) - - # Retrieve top recommendation chunks (across all sizes) for this customer - cursor.execute(""" - SELECT CHUNK_ID, CHUNK_TEXT - FROM LOAN_CHUNK - WHERE CUSTOMER_ID = :cust_id - AND CHUNK_VECTOR IS NOT NULL - ORDER BY VECTOR_DISTANCE(CHUNK_VECTOR, :qv, COSINE) - FETCH FIRST 4 ROWS ONLY - """, {'cust_id': selected_customer_id, 'qv': q_vec}) - retrieved = [ - (r[0], r[1].read() if isinstance(r[1], oracledb.LOB) else r[1]) - for r in cursor.fetchall() - ] - - if not retrieved: - # Fallback to full text as one chunk - retrieved = [(0, recommendations)] - - # Prepare clean context for the LLM - cleaned = [re.sub(r'[^\w\s\d.,\-\'"]', ' ', t).strip() for _, t in retrieved] - docs_as_one_string = "\n=========\n".join(cleaned) + "\n=========\n" - - # Rebuild available loans + customer profile (same structures used earlier) - available_loans_text = "\n".join( - [f"{loan['LOAN_ID']}: {loan['LOAN_TYPE']} | {loan['INTEREST_RATE']}% interest | " - f"Credit Score: {loan['CREDIT_SCORE']} | DTI: {loan['DEBT_TO_INCOME_RATIO']} | " - f"Origination Fee: ${loan['ORIGINATION_FEE']} | Time to Close: {loan['TIME_TO_CLOSE']} days" - for loan in df_mock_loans.to_dict(orient='records')] - ) - loan_app = customer_json.get("loanApplications", [{}])[0] - customer_profile_text = "\n".join( - [f"- {k.replace('_',' ').title()}: {v}" - for k, v in {**customer_json, **loan_app}.items() - if k not in ["embedding_vector","ai_response_vector","chunk_vector"]] - ) - - rag_prompt = f"""\ + q_vec = vectorize_question(question) + + # Retrieve top recommendation chunks (across all sizes) for this customer + cursor.execute(""" + SELECT CHUNK_ID, CHUNK_TEXT + FROM LOAN_CHUNK + WHERE CUSTOMER_ID = :cust_id + AND CHUNK_VECTOR IS NOT NULL + ORDER BY VECTOR_DISTANCE(CHUNK_VECTOR, :qv, COSINE) + FETCH FIRST 4 ROWS ONLY + """, {'cust_id': selected_customer_id, 'qv': q_vec}) + retrieved = [ + (r[0], r[1].read() if isinstance(r[1], oracledb.LOB) else r[1]) + for r in cursor.fetchall() + ] + + if not retrieved: + # Fallback to full text as one chunk + retrieved = [(0, recommendations)] + + # Prepare clean context for the LLM + cleaned = [re.sub(r'[^\w\s\d.,\-\'"]', ' ', t).strip() for _, t in retrieved] + docs_as_one_string = "\n=========\n".join(cleaned) + "\n=========\n" + + # Rebuild available loans + customer profile + available_loans_text = "\n".join( + [f"{loan['LOAN_ID']}: {loan['LOAN_TYPE']} | {loan['INTEREST_RATE']}% interest | " + f"Credit Score: {loan['CREDIT_SCORE']} | DTI: {loan['DEBT_TO_INCOME_RATIO']} | " + f"Origination Fee: ${loan['ORIGINATION_FEE']} | Time to Close: {loan['TIME_TO_CLOSE']} days" + for loan in df_mock_loans.to_dict(orient='records')] + ) + loan_app = customer_json.get("loanApplications", [{}])[0] + customer_profile_text = "\n".join( + [f"- {k.replace('_',' ').title()}: {v}" + for k, v in {**customer_json, **loan_app}.items() + if k not in ["embedding_vector","ai_response_vector","chunk_vector"]] + ) + + rag_prompt = f"""\ [INST] <> You are AI Loan Guru. Use only the provided context to answer. Do not mention sources outside of the provided context. - Do NOT provide warnings, disclaimers, or exceed the specified response length. - Keep under 300 words. Be specific and actionable. Have the ability to respond in Spanish, French, Italian, German, and Portuguese if asked. +Do NOT provide warnings, disclaimers, or exceed the specified response length. +Keep under 300 words. Be specific and actionable. Have the ability to respond in Spanish, French, Italian, German, and Portuguese if asked. <> [/INST] [INST] Question: "{question}" @@ -418,40 +828,40 @@ Tasks: 2) Briefly justify based on profile + loan options. [/INST]""" - print("Generating AI response...") - - genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient( - config=oci.config.from_file(os.getenv("OCI_CONFIG_PATH","~/.oci/config")), - service_endpoint=os.getenv("ENDPOINT") - ) - chat_detail = oci.generative_ai_inference.models.ChatDetails( - compartment_id=os.getenv("COMPARTMENT_OCID"), - chat_request=oci.generative_ai_inference.models.GenericChatRequest( - messages=[oci.generative_ai_inference.models.UserMessage( - content=[oci.generative_ai_inference.models.TextContent(text=rag_prompt)] - )], - temperature=0.0, - top_p=0.90 - ), - serving_mode=oci.generative_ai_inference.models.OnDemandServingMode( - model_id="meta.llama-3.2-90b-vision-instruct" + print("Generating AI response...") + + genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient( + config=oci.config.from_file(os.getenv("OCI_CONFIG_PATH","~/.oci/config")), + service_endpoint=os.getenv("ENDPOINT") + ) + chat_detail = oci.generative_ai_inference.models.ChatDetails( + compartment_id=os.getenv("COMPARTMENT_OCID"), + chat_request=oci.generative_ai_inference.models.GenericChatRequest( + messages=[oci.generative_ai_inference.models.UserMessage( + content=[oci.generative_ai_inference.models.TextContent(text=rag_prompt)] + )], + temperature=0.0, + top_p=0.90 + ), + serving_mode=oci.generative_ai_inference.models.OnDemandServingMode( + model_id="meta.llama-3.2-90b-vision-instruct" + ) ) - ) - chat_response = genai_client.chat(chat_detail) - ai_response = chat_response.data.chat_response.choices[0].message.content[0].text - ai_response = re.sub(r'[^\w\s\d.,\-\'"]', ' ', ai_response) + chat_response = genai_client.chat(chat_detail) + ai_response = chat_response.data.chat_response.choices[0].message.content[0].text + ai_response = re.sub(r'[^\w\s\d.,\-\'"]', ' ', ai_response) - print("\n🤖 AI Loan Guru Response:") - print(ai_response) + print("\n🤖 AI Loan Guru Response:") + print(ai_response) - # Print which chunks were retrieved (for transparency/debug) - print("\n📑 Retrieved Chunks Used in Response:") - for cid, text in retrieved: - preview = text[:140].replace("\n", " ") + ("..." if len(text) > 140 else "") - print(f"[Chunk {cid}] : {preview}") + # Print which chunks were retrieved (for transparency/debug) + print("\n📑 Retrieved Chunks Used in Response:") + for cid, text in retrieved: + preview = text[:140].replace("\n", " ") + ("..." if len(text) > 140 else "") + print(f"[Chunk {cid}] : {preview}") except Exception as e: - print(f"RAG flow error: {e}") + print(f"RAG flow error: {e}") ``` @@ -488,5 +898,5 @@ You may now proceed to the next lab. ## Acknowledgements * **Authors** - Francis Regalado -* **Contributors** - Kevin Lazarz +* **Contributors** - Kevin Lazarz, Linda Foinding * **Last Updated By/Date** - Linda Foinding, September 2025 \ No newline at end of file diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/lab4task1.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/lab4task1.png new file mode 100644 index 00000000..853011d9 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/lab4task1.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/tas2.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/tas2.png new file mode 100644 index 00000000..cb46679e Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/tas2.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/task2result.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/task2result.png new file mode 100644 index 00000000..5cdcd48d Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/build/images/task2result.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab101.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab101.png new file mode 100644 index 00000000..4438d355 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab101.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab102.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab102.png new file mode 100644 index 00000000..21af547e Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab102.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab103.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab103.png new file mode 100644 index 00000000..8a53a515 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab103.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab104.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab104.png new file mode 100644 index 00000000..ad842b21 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab104.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab105.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab105.png new file mode 100644 index 00000000..a7960c75 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab105.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab106.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab106.png new file mode 100644 index 00000000..67e7e05e Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab106.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab131.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab131.png new file mode 100644 index 00000000..cdb0d82c Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab131.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab132.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab132.png new file mode 100644 index 00000000..4053f280 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab132.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab133.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab133.png new file mode 100644 index 00000000..aa06e8fb Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab133.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab134.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab134.png new file mode 100644 index 00000000..e98ed2eb Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab134.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab135.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab135.png new file mode 100644 index 00000000..6636ecc7 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab135.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab136.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab136.png new file mode 100644 index 00000000..662c202a Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab136.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab137.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab137.png new file mode 100644 index 00000000..100b4787 Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lab137.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lowincome.png b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lowincome.png new file mode 100644 index 00000000..194a265e Binary files /dev/null and b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/images/lowincome.png differ diff --git a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/user-story.md b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/user-story.md index e3734510..d3a19bf9 100644 --- a/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/user-story.md +++ b/dev-ai-app-dev-finance/dev-guide-ai-finance/user-story/user-story.md @@ -26,13 +26,13 @@ This lab assumes you have: ![Click the Start Demo Link](./images/start-demo.png =50%x*) -2. Enter in a username and click **Login**. +2. Welcome to Seer Holdings! Select **Finance** as Industry and **Approval Officer** as role. Enter in a username and click **Login**. - ![Login](./images/login.png =50%x*) + ![Login](./images/lab101.png =50%x*) 3. Welcome to the SeerEquities Loan Management application! Congratulations, you are now connected to the demo environment. You can now execute the different tasks for this Lab. - ![Homepage](./images/app-home.png =50%x*) + ![Homepage](./images/lab102.png =50%x*) ## Task 2: Demo - Customer with strong credit score @@ -40,11 +40,11 @@ In this first example, you will use the application to approve a customer with s 1. On the Dashboard page, from the pending review list, select the Customer ID for **James Smith**. - ![Select James Smith](./images/james-smith.png =50%x*) + ![Select James Smith](./images/lab103.png =50%x*) 2. Opening James Smith’s profile reveals his loan application details—name, location, requested amount, debt, and credit score. - ![James Smith AI generated recommendations](./images/james-smith-ai.png =50%x*) + ![James Smith AI generated recommendations](./images/lab104.png =50%x*) 3. At the bottom of James Smith’s profile, you will find the **AI Loan Guru**—a chatbot built on Oracle Database 23ai and Vector search. When prompted, the system uses **RAG** to generate a response. It converts the question and loan data into embeddings, performs a similarity search, and then uses the **GenAI service** to turn the enriched context into a clear, natural language answer. If the customer calls with a question, you can quickly enter it into the AI Loan Guru to generate a relevant response. @@ -57,13 +57,13 @@ In this first example, you will use the application to approve a customer with s ``` - ![James Smith chatbot](./images/james-smith-chatbot.png =50%x*) + ![James Smith chatbot](./images/lab105.png =50%x*) >💡 In Oracle Database 23ai, **AI Vector Search** allows you to combine your business data with the Large Language Model (LLM) to reduce hallucinations and get accurate answers from your data. 4. Select the **Navigate To Decisions** button. - ![James Smith Decision](./images/james-smith-decision.png =50%x*) + ![James Smith Decision](./images/lab106.png =50%x*) After navigating to the decisions page, the AI evaluation runs in the background. It analyzes James’s profile and matches it against available loan options in the database. A custom AI prompt ensures the system uses only internal data—never the internet. In this case, the AI returns three loan options, each with a clear explanation. All options are displayed alongside the AI’s final recommendation: approval. @@ -120,7 +120,7 @@ In this example, you will navigate the application to review a customer and deny 1. On the Dashboard page, from the pending review list, select the Customer ID for **James Woods**. - ![Select James Woods](./images/james-woods.png =50%x*) + ![Select James Woods](./images/lab131.png =50%x*) 2. Opening James Woods’s profile displays his loan application details. Within a few seconds, the AI automatically generates recommendations. In this case, the system evaluates a less favorable profile and identifies key risk factors. @@ -132,11 +132,11 @@ In this example, you will navigate the application to review a customer and deny Despite the risk factors, the AI evaluates the profile and suggests next steps. In this case, it recommends a denial—but also provides clear, actionable guidance to help the customer improve their chances of approval in the future. - ![James Woods AI generated recommendations](./images/james-woods-ai.png =50%x*) + ![James Woods AI generated recommendations](./images/lab132.png =50%x*) 3. Select the **Navigate to Decisions** button. - ![James Woods Decision](./images/james-woods-decision.png =50%x*) + ![James Woods Decision](./images/lab133.png =50%x*) >⁉️ **What is the reason that the AI decided to deny this applicant?** ⁉️ @@ -238,6 +238,6 @@ By combining these advanced tools, the application enables faster, smarter decis * [Oracle Database 23ai Documentation](https://docs.oracle.com/en/database/oracle/oracle-database/23/) ## Acknowledgements -* **Authors** - Kamryn Vinson, Linda Foinding, Francis Regalado +* **Authors** - Linda Foinding, Francis Regalado * **Contributors** - Kevin Lazarz, Eddie Ambler, Ramona Magadan, Mark Nelson, Andy Tael, Anders Swanson, Rahul Tasker -* **Last Updated By/Date** - Linda Foinding, June 2025 \ No newline at end of file +* **Last Updated By/Date** - Linda Foinding, September 2025 \ No newline at end of file