digital-twin / app.py
simrunsharma's picture
Adding UI Pieces
79940c5 verified
Raw
History Blame Contribute Delete
17.4 kB
"""
Deploying Digital Twin: Built from Digital Twin IPYNB
"""
import os
from openai import OpenAI
# from IPython.display import display, Markdown
import gradio as gr
# from dotenv import load_dotenv
import json
import uuid
import random
import requests
import chromadb
from pprint import pprint
#Setup-------------
#no loading the environment needed because its app.py
#You don't need it in production, hugging face already does it, but ipynb its to load into local machine
# load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
#debugging if you can't find the openai key:
if OPENAI_API_KEY is None:
raise Exception ( "API Key is missing")
else:
print(OPENAI_API_KEY[:8])
client = OpenAI()
#Document Overview------------------
document_experience = """
Name: Simrun Sharma
Current Role:
Associate Research Analyst / Data Scientist at CNA
Professional Experience
Center for Naval Analyses (CNA)
Associate Research Analyst / Data Scientist
May 2024 – Present
Washington, DC
Projects:
- Developed an NLP pipeline using regex, text processing, and information extraction to categorize research projects for budgeting and funding analysis.
- Performed strategic analysis of historical funding allocations to identify research areas receiving the greatest investment.
- Designed and implemented a Sparing Tool using a greedy optimization algorithm to determine optimal inventory levels for U.S. Air Force bases.
- Modeled backorders using a continuous Poisson distribution and iteratively added spares to satisfy target Non-Mission Capable (NMC) rates while remaining within budget constraints.
- Led development of a synthetic survey generation framework using web scraping, embedding models, CrewAI agents, vector databases, Retrieval-Augmented Generation (RAG), and Large Language Models (LLMs).
- Built an end-to-end AI pipeline capable of generating realistic synthetic survey responses.
Partners In Health
Co-Lead Advocacy Chair
September 2022 – February 2023
- Advocated for global health legislation through congressional meetings, callathons, and legislative outreach.
- Secured 47 co-signers for the NOVID Act.
- Worked on the Paul Farmer Memorial Resolution supporting increased global health funding.
University of Florida Center for Undergraduate Research
Director of Internal Affairs
March 2021 – February 2023
- Represented approximately 35,000 undergraduate students.
- Connected students with faculty research opportunities.
- Organized research workshops and student research showcases.
Equal Access Clinic Network
Social Work Clinical Coordinator
2021 – 2023
- Cleaned and standardized patient data profiles.
- Improved spreadsheet scheduling workflows for clinic staff.
- Created a bilingual community resource guide by consolidating over ten years of local organizational data.
University of Florida College of Public Health and Health Professions
Assistant Director
May 2022 – December 2022
- Coordinated communication among more than 25 research labs, colleges, and clinics.
- Organized a multidisciplinary health professions fair and student panels.
University of Florida Physicians Internal Medicine
Treasurer, MC, and Event Planner
November 2022
- Planned and hosted a physician networking event celebrating Diwali involving physicians from UF Health, the VA, and Northwest Hospital.
Planned Parenthood Federation of America
Student Volunteer
2021 – September 2022
- Participated in legislative advocacy, reproductive justice initiatives, petitions, and community organizing.
University of Florida
General Chemistry Teaching Assistant
January 2019 – January 2022
- Led weekly discussion sections.
- Created review sessions and supported undergraduate student learning.
"""
document_education = """
Name: Simrun Sharma
Education
Duke University
Master of Interdisciplinary Data Science (MIDS)
August 2023 – May 2025
Degree Focus:
- Data Science
- Machine Learning
Relevant Coursework:
- Modeling and Representation of Data
- Data Analysis
- Introduction to Natural Language Processing
- Data Engineering Systems
- Practicing Data Science Tools
University of Florida
Bachelor of Arts
Economics Major
Business Administration Minor
Pre-Med Track
Graduated:
December 2023
GPA:
3.90 / 4.00
Advanced Coursework:
- Biology
- Physics
- Organic Chemistry
- Microeconomics
- Macroeconomics
- Business Administration
Leadership & Campus Involvement:
- Director, Center for Undergraduate Research
- Advocacy Chair, Partners In Health
- General Chemistry Teaching Assistant
- International Research Director, Hearts for the Homeless
- Planned Parenthood Volunteer
Niceville High School
High School Diploma
Graduated: 2019
Activities & Awards:
- Speech and Debate Team Captain
- NAACP ACT-SO Bronze Medalist (Oratory)
- Florida Forensic League State Finalist in Original Oratory
"""
document_personality = """
Name: Simrun Sharma
Career Goals:
- Transitioning from Data Science into AI Engineering.
Target Roles:
- AI Engineer
- Applied AI Engineer
- AI Deployment Engineer
- Forward Deployed AI Engineer
- AI Solutions Engineer
Industries of Interest:
- Artificial Intelligence
- Healthcare Technology
- Defense Technology
- Data Science
Professional Interests:
- Artificial Intelligence
- Machine Learning
- Generative AI
- Agentic AI Systems
- Retrieval-Augmented Generation (RAG)
- Large Language Models (LLMs)
- Data Science
- Healthcare Analytics
- Brain Computer Interfaces
- Neurotechnology
- Explainable AI
Healthcare Interests:
- Passionate about applying AI and data science to improve patient outcomes, healthcare operations, accessibility, and clinical decision-making.
Learning Style:
- Step-by-step explanations
- Visual examples
- Interactive discussions
- Hands-on projects
- Building intuition before technical depth
Communication Style:
- Curious
- Analytical
- Direct
- Practical
- Detail-oriented
Work Preferences:
- Enjoys solving real-world problems.
- Likes collaborating with stakeholders.
- Prefers building practical AI systems with measurable impact.
- Enjoys end-to-end solution development.
Personality Traits:
- Curious
- Ambitious
- Persistent
- Analytical
- Detail-oriented
- Growth-oriented
Personal Interests:
- Pickleball
- Bachata
- Salsa
- Bollywood dance
- Strength training
- Fitness
- Pineapple on pizza
"""
#Chunking Function------------------
#Chunk the document
def chunk_text(text: str, chunk_size: int = 1024, overlap: int = 150) -> list[str]:
#Roughly, 1 token ≈ 4 characters in English text.
#Chunk size 1024 sand overlap of 150 characters is ideal based on document length or user needs look at RAG Ipynb
BOUNDARIES = ["\n\n", "\n", ". ", " "]
def find_natural_boundary(start: int, end: int) -> int:
midpoint = start + (chunk_size // 2)
for boundary in BOUNDARIES:
pos = text.rfind(boundary, midpoint, end)
if pos != -1:
return pos + len(boundary)
return end
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
if end < len(text):
end = find_natural_boundary(start, end)
chunks.append(text[start:end])
if end >= len(text):
break
start = max(start + 1, end - overlap)
return chunks
#Generating chunks,ids, and metadatas--------------------------
#Generating chunks for multiple documents
#We want to have these documents as a list of dictionaries:
#Generating chunks for multiple documents
#We want to have these documents as a list of dictionaries:
documents = [
{'text' : document_experience, 'source' : 'Simrun Sharma Experience'},
{'text' : document_education, 'source' : 'Simrun Sharma Education'},
{'text' : document_personality, 'source' : 'Simrun Sharma Personality'}
]
#I want unique ids for each of the ids
# import uuid
#for each of the document chunk them, id them, metadata add to list:
ids = []
metadatas = []
chunks = []
for doc in documents:
_chunks = chunk_text(text = doc['text'], chunk_size=300, overlap=30)
_ids = [str(uuid.uuid4()) for i in range(len(_chunks))]
_metadatas = [{'source' : doc['source'], 'chunk_index' : i} for i in range(len(_chunks))]
#extend becase I am getting dictionaries I want to have one long list of dictionaries
chunks.extend(_chunks)
ids.extend(_ids)
metadatas.extend(_metadatas)
#logs for debugging chunks
print(f'The length of the chunks is {len(chunks)}')
print('\n')
for i, chunk in enumerate(chunks):
print(f"--- Chunk {i + 1} | {len(chunk)} total characters ---")
print(f'(IDS: {ids[i]}, SOURCE: {metadatas[i]['source']}, Chunk_index: {metadatas[i]['chunk_index']})')
print('\n')
print(chunk)
#Generate embeddings for chunks:
response = client.embeddings.create(
model = 'text-embedding-3-small',
input= chunks
)
#creating a list of embeddings above in response.data
#I have the a list of Embedding objects and I just needs a list of the dimensions of each Embedding object
embedding = [item.embedding for item in response.data]
#logs for embeddings:
print(f'The number of embeddings from the chunks: {len(embedding)}')
print(f'The number of dimensions is: {len(embedding[0])}')
#Initialize the ChromaDB and store the vectors
#Persistent Client will show in VSCode
chroma_client = chromadb.PersistentClient(path = "./digitaltwin_chroma")
#Creating Collection
collection = chroma_client.get_or_create_collection(name = 'Simrun_Information')
#If there is a collection already then we need to delete the collection
if collection.get()['ids']:
collection.delete(collection.get()['ids'])
#Adding to our collection
collection.add(
ids=ids,
embeddings=embedding,
metadatas=metadatas,
documents=chunks
)
#Tools-----------------------------
tools = []
#Tool Handling --------------------
#PUSHOVER CREDENTIALS
#Creating the pushover identification information
pushover_user = os.getenv("PUSHOVER_USER")
pushover_token = os.getenv("PUSHOVER_TOKEN")
pushover_url = 'https://api.pushover.net/1/messages.json'
def send_notifications(message:str):
if pushover_user is None or pushover_token is None:
return "Notification failed: Pushover credentials have failed."
payload = {'user' : pushover_user,"token" : pushover_token, 'message': message}
requests.post(url = pushover_url, data=payload)
return f"Sent Notification: {message}"
# This is not a dictionary. This is a description card written in JSON format.
# We are describing the tool to OpenAI so it knows how to use it.
# Think of it like a form or a menu card. Not real Python objects.
#DESCRIBE PUSHOVER AS AN LLM TOOL
send_notifications_function = {
# The name of the tool OpenAI will request when it wants to use it
'name': 'send_notifications',
# When should OpenAI use this tool? This description tells it.
'description': 'Send a push notification to the real Simrun Sharma. Use this when \
1) Someone wants to get in touch, hire, or collaborate\
-ask for their full name, phone number, and email (contact details) first, then send notificaiton to real version of Simrun with full name and contact details.\
2) If you do not know the answer to a question about Simrun Sharma - send AUTOMATICALLY without asking, include the question so Simrun Sharma can add to her information later. ',
'parameters': {
'type': 'object',
'properties': {
'message': {
'type': 'string',
'description': 'The notification message the user wants sent to their device'
}
},
'required': ['message']
}
}
#adding this tool to the tool list:
tools.append({"type" : "function", "function":send_notifications_function})
#Dice_roll---------------
# import random
#creating roll dice function
def dice_roll():
result = random.randint(1,6)
return result
#creating roll dice description of tool for LLM:
roll_dice_function = {
'name' : "dice_roll",
'description': 'This is a function where you can roll a dice and can see the number you get from the roll',
"parameters": {
},
"required" : []
}
#appending this to our available tools
tools.append({"type" : "function", "function" : roll_dice_function})
#Handle Tool Call -----------------
def handle_tool_call(tool_calls:list):
tool_call_results = []
for tool_call in tool_calls:
function_name = tool_call.function.name
# --- for debugging
# print(f"the function name is {function_name}")
# tool_call = tools_calls[0] #this is because we only have one tool thus far
args = json.loads(tool_call.function.arguments)
if function_name == 'send_notifications':
content = send_notifications(args['message']) #sent to pushover
elif function_name == "dice_roll":
content = f' Rolled: {dice_roll()}'
# elif function_name == "insert_function3_name":
# content =insert_function3_name (args['message']})
else:
content = f"Unknown function: {function_name}"
#package into a dictionary for the llm to see what we got out from the tool call
tool_call_result = {
'role' : 'tool',
'content' : content ,
'tool_call_id' : tool_call.id
}
print(f"this is what the tool_call_result from handle tool_call looks like: {tool_call_result}")
#appending each tool_call_result to the tool_call_results list of dictionaries
tool_call_results.append(tool_call_result)
return tool_call_results
#System message---------------------
system_message = """
You are a digital twin of Simrun Sharma.
When people message you, you respond as Simrun - You answer questions as Simrun using first-person language. Using her voice, personality, knowledge, and kindness.
IMPORTANT RULES:
1. DO NOT use outside knowledge about Simrun.
2. DO NOT infer facts that are not explicitly stated.
3. DO NOT invent experiences, opinions, accomplishments, preferences, relationships, skills, or goals. Don't make up from internet.
4. The only factual information is given to you in this system message and anything you dont know its IMPORTANT: Always use the send_notifications tool to alert the real Simrun \
Sharma - do this automatically without asking the user."""
#Main Response Function------------
def respond_system_enhanced (message, history):
#RAG
response = client.embeddings.create(
model = 'text-embedding-3-small',
input = [message]
)
print(f' The length of dimensions of the message : {len(response.data[0].embedding)}')
#we just want the first embedding closet distance
query_embedding = response.data[0].embedding
#searching through collection for chunks with closest distance to query embedding:
results = collection.query(
query_embeddings= [query_embedding],
n_results= 3,
include= ['documents','metadatas','distances']
)
#logs for debugging
#lets just give the 3 most relevant chunks
context = "\n--\n".join(results['documents'][0])
print(f'User message:\n{message}\n')
print('Context this turn:\n', context)
#system enhanced message:
system_message_enhanced = system_message + "\n\n Context is:" + context
#Build messages for each turn:
messages = [{"role" : "system", "content": system_message_enhanced}] + history + [{"role" : "user", "content" : message}]
# Call LLM
response = client.chat.completions.create(
model = "gpt-4.1-mini",
messages = messages,
tools = tools
)
#record message
message = response.choices[0].message
print("message before while loop", message)
while message.tool_calls:
# from pprint import pprint
pprint(message.tool_calls)
#we want to pass the list of tool calls to out=r handle tool call function
tool_call = message.tool_calls
tool_call_result = handle_tool_call(message.tool_calls)
#we want to append the message with the user request and the first two tool call requests
messages.append(message)
#We also want append to append the toolcall results with the dictionaries of the content return from each tool call
messages.extend(tool_call_result)
response = client.chat.completions.create(
model = "gpt-4.1-mini",
messages = messages,
tools=tools
)
message = response.choices[0].message
return (message.content)
#launching gradio:
#inbrowser launch not needed
gr.ChatInterface(fn=respond_system_enhanced,
title = "Simrun's Digital Twin",
chatbot = gr.Chatbot(avatar_images= (None, "Simrun_twin.jpeg")),
description = 'Chat with an AI version of Simrun Sharma. Ask about her experiences, her life story, or interests!',
examples = ['What projects have you worked on in the past?', 'Tell me about your experience with AI Engineering', 'Do you like dancing?']).launch()