Spaces:
Sleeping
Sleeping
| import os | |
| from openai import OpenAI | |
| import gradio as gr | |
| import uuid | |
| from pprint import pprint | |
| import chromadb | |
| import json | |
| import requests | |
| import random | |
| #----------------------- | |
| # Setup | |
| #----------------------- | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") | |
| if OPENAI_API_KEY is None: | |
| raise Exception("API key is missing") | |
| client = OpenAI() | |
| #----------------------- | |
| # Documents | |
| #----------------------- | |
| document_overview = """ | |
| Kateryna is a data enablement and governance expert at Henkel, where she drives initiatives that empower organizations to leverage data effectively | |
| , while ensuring strong governance frameworks. She is recognised for her expertise in data governance, data analytics and responsible data usage, helping bridge the gap between business and regulatory requirements. | |
| With a solid academic foundation that includes focus on computer science, data management and digital transformation, Kateryna combines technical understanding with strategic thinking. | |
| She is an AI enthusiast and continuos lerner, actively expanding her knowledge in artificial inteligence and it's practical applications while advocating for responsible and well-governed adoption. | |
| Actively involved in diverse initiatives and communities, she contributes to the data and AI ecosystem by sharing insights, best practices and fostering collaboration among professionals in the field. | |
| Overall, Kateryna stands out as a professional who blends strong technical background, governance expertise, data-driven thinking and a passion for AI and technology, | |
| driving future-ready data strategies and fostering a culture of responsible data use within organizations. | |
| Happy to share what she's learned and help others on their data and AI journey. Also still learning and growing, | |
| so open to new ideas and perspectives as this field moves fast. | |
| Make sure that you only use factual infomration about Kateryna that is provided | |
| in the system message. Do not make up any information about her, and if you don't have enough information | |
| to answer a question, say that you don't have enough information to answer as Kateryna and share what information you are missing. | |
| Additional info: | |
| - Kateryna enjoys cooking and trying out new recipes in her free time., | |
| - personal interests:loves and teaches yoga, follow healthy lifestyle, since childhood enjoyed swimming, curious about investment, loves traveling and exploring new cultures, exploring new technologies and how they changing our life., | |
| - Kateryna speaks ukrainian, german, english and russian languages., | |
| - A dog lover, she has a dog named Coockie, who is a 3 year old border collie., | |
| - friendly, approachable, clear and concise, with a touch of empathy. She enjoys engaging in meaningful conversations and sharing insights investments and technology topics., | |
| - she loves learning and exploring new technologies, especially in the AI space. She is passionate about helping organizations leverage data effectively while ensuring responsible and ethical use. , | |
| - believes that technology, especially AI, has the potential to transform industries and improve lives, and she is excited to be part of this journey. | |
| """ | |
| document_education = """Education, studies and internships: | |
| RWTH Aachen University | |
| Master of Science (M.Sc.), Media Informatics | |
| 2010 – 2013 | |
| Master thesis: "Requirements detection within end-user mailing lists in OSS" | |
| Fraunhofer Institute of Applied Information Technology (FIT) | |
| Internship, User-oriented system design and personalized information services | |
| 2011 – 2011 | |
| Design and development of cutting-edge software. | |
| """ | |
| document_work = """Work Experience: | |
| Working now as a Data Governance Expert & Enablement Manager | |
| at Henkel | |
| Oct 2023 - Present · 2 yrs 7 mos | |
| Enable federated governance by onboarding teams, documenting policy, and providing coaching. | |
| Establish AI and Data Governance frameworks, setting clear guidelines and technical controls. | |
| Define oversight mechanisms and support data domain implementation in the enterprise data catalog. | |
| Set up a centralized Data Governance Hub and Data Management Council working group for sharing best practices and key contacts. | |
| Data Governance, Data Management and +6 skills | |
| Generali Deutschland AG | |
| Governance of Data Management & Analytics | |
| Generali Deutschland AG · Full-time | |
| Jun 2021 - Sep 2023 · 2 yrs 4 mos | |
| Aachen, North Rhine-Westphalia, Germany | |
| Implementation of data governance programs based on international policy, including business analysis and coordination of AI/Analytics use cases. | |
| Expansion and collaboration on the information platform for data topics and Alation Data Catalog. | |
| Data Governance, Business Analysis and +2 skills | |
| e.GO Digital GmbH | |
| Smart Data Expert | |
| e.GO Digital GmbH · Full-time | |
| Dec 2018 - Jun 2021 · 2 yrs 7 mos | |
| Hybrid | |
| Creating Dashboards (Tableau, DataStudio), organizing data (Python, Azure, SQL) | |
| Moderation of workshops for the introduction of business intelligence solutions | |
| Creation of requirements and needs analyzes for business intelligence and data science | |
| Integration of data in a data warehouse | |
| Microsoft Azure, Google Analytics and +3 skills | |
| equensWorldline | |
| Software Engineer | |
| equensWorldline · Full-time | |
| Oct 2017 - Dec 2018 · 1 yr 3 mos | |
| Aachen, North Rhine-Westphalia, Germany · Hybrid | |
| Software development for online transaction processing at the financial sector. Based on Unix, Stratus, Tandem platforms using technologies C/C++, Jira. | |
| Shell Scripting, C (Programming Language) and +3 skills | |
| RWTH Aachen University | |
| Research Assistant | |
| RWTH Aachen University | |
| Jan 2014 - Sep 2017 · 3 yrs 9 mos | |
| Aachen, North Rhine-Westphalia, Germany | |
| Research and development activities at the chair "Databases and information systems". | |
| Data mining and social network analysis, with emphasis on mining and modeling OSS communities, text and web mining. """ | |
| document_certification = """ Certifications: | |
| German Federal Association of the Digital Economy (BVDW e.V.) | |
| AI Officer/KI-Beauftragte | |
| Issued Feb 2025 | |
| Skills: Artificial Intelligence (AI), EU AI Act, AI Ethics, Responsible AI | |
| AI Officer Certification | |
| Azure AI Foundry | |
| Issued Aug 2025 | |
| Skills: Azure AI Studio, Azure AI Foundry | |
| Gen AI Foundations | |
| Henkel | |
| Issued Jul 2025 | |
| Skills: Artificial Intelligence (AI) | |
| Certified Yoga Instructor | |
| Together-Yoga-Zumba | |
| Issued Jan 2023 | |
| """ | |
| document_recommedations = """This is past recomendations from here colleagues: Business Change Manager & Coach | |
| Katya was on a project with me in Ukraine in late 2008 & was a strong asset to the team. Working in an extremely unsavoury environment which had many areas of potential risk, she was able to conduct her studies & analysis with ease, engaging well with critical client personnel in her areas of responsibility. | |
| Creating relationships with clients undergoing change projects can be somewhat difficult sometimes although this had been made even tougher by the hard financial situation this client had found itself in. In spite of this Katya was able to form effective relationships with ease & gain the trust & confidence both of clients & her co-workers alike. Her manner & operating styles are light & understanding of those she works with, which makes connecting with others easy for her, something that's an essential skill for those involved in change. Her ability to do this in 3 languages is an additional & obvious asset. | |
| I have been happy to have recommended Katya to colleagues since we worked together & also consider her a friend. I therefore have no hesitation in recommending her as a thoroughly capable & trustworthy consultant to anyone looking to crew a project team in countries where English, Russian or Ukrainian language is required. | |
| """ | |
| document_publications = """ This is a list of | |
| Publications during her academic career: | |
| A Longitudinal Study of Community-oriented Open Source Software Development | |
| Published in Proceedings of the 28th International Conference on Advanced Information Systems Engineering (CAiSE'16) Jun 13, 2016 | |
| A Longitudinal Study of Community oriented Open Source Software Development | |
| CAiSE 2016 | |
| Boosting informal workplace learning in small enterprises | |
| Proceedings of the 4th Workshop on ARTEL. Sep 16, 2014 | |
| Supporting Workplace Learning in Small Enterprises by Personal Learning Environments | |
| UMAP 2014 Extended Proceedings. Jul 7, 2014 | |
| """ | |
| #----------------------- | |
| # Chunking Function | |
| #----------------------- | |
| def split_text_into_chunks(text: str, chunk_size: int = 200, overlap: int = 50) -> list[str]: | |
| 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) | |
| 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 | |
| #----------------------- | |
| # RAG: Chunk, Embed and Store in ChromaDB | |
| #----------------------- | |
| documents = [ | |
| {"text": document_overview, "source":"Overview"}, | |
| {"text": document_work, "source":"Professional Experience"}, | |
| {"text": document_education, "source":"Education"}, | |
| {"text": document_certification, "source":"Certification"}, | |
| {"text": document_publications, "source":"Publications"}, | |
| {"text": document_recommedations, "source":"Recommendations"} | |
| ] | |
| chunks = [] | |
| ids = [] | |
| metadatas = [] | |
| for doc in documents: | |
| #Prepare the lists | |
| chunks_ = split_text_into_chunks(doc["text"],chunk_size=200, overlap=50) | |
| ids_= [str(uuid.uuid4()) for _ in range(len(chunks_))] | |
| metadatas_= [{"source": doc["source"], "chunk_index":i} for i in range(len(chunks_))] | |
| #Add to main lists | |
| chunks.extend(chunks_) | |
| ids.extend(ids_) | |
| metadatas.extend(metadatas_) | |
| #Print for logs: | |
| print(f"Created {len(chunks)} chunks") | |
| for i, chunk in enumerate(chunks): | |
| print(f"Chunk {i+1} (ID: {ids[i]}, Source: {metadatas[i]['source']}, Index: {metadatas[i]['chunk_index']}):") | |
| print(chunk[:100]) | |
| print() | |
| #Generate embeddings for all chunks | |
| response = client.embeddings.create( | |
| model = "text-embedding-3-small", | |
| input = chunks | |
| ) | |
| embeddings = [item.embedding for item in response.data] | |
| #verify embeddings for logs | |
| print(f"Generated {len(embeddings)} embeddings") | |
| print(f"Each embedding has {len(embeddings[0])} dimensions") | |
| #initialize ChromaDB client (persistent storage) | |
| chroma_client = chromadb.PersistentClient(path="./chroma_db_twin") | |
| #initialize ChromaDB client (in-memory storage) | |
| #chroma_client = chromadb.Client() | |
| #Get or Create + Empty the collection before adding new data (for testing purpose) | |
| collection = chroma_client.get_or_create_collection(name="digital_twin") # a db chroma_db is created on my disk, it's empty.next we will put data in the storage. | |
| if collection.get()["ids"]: | |
| collection.delete(collection.get()["ids"]) | |
| #Adding data to chromaDB | |
| collection.add( | |
| ids=ids, | |
| embeddings=embeddings, | |
| documents=chunks, | |
| metadatas=metadatas | |
| ) | |
| pprint(collection.get()) | |
| #----------------------- | |
| # Tools | |
| #----------------------- | |
| tools = [] | |
| #### 1. Import pushover keys | |
| pushover_user = os.getenv("PUSHOVER_USER") | |
| pushover_token = os.getenv("PUSHOVER_TOKEN") | |
| pushover_url = "https://api.pushover.net/1/messages.json" # Pushover API endpoint for sending messages | |
| #create a function to send a notification using Pushover API | |
| def send_notification(message:str): | |
| if pushover_user is None or pushover_token is None: #handle the case when credentials are not set, to avoid errors when LLM calls the tool without proper configuration | |
| return "Notification failed: Pushover credentials not configured." | |
| payload = {"user": pushover_user, "token": pushover_token, "message": message} | |
| requests.post(pushover_url, data=payload) | |
| return f"Sent notification with message: {message}" | |
| ### 3. Describe Pushover as an LLM tool | |
| #creating a function to explain LLM when to use the tool and how to call it, including the expected input and output formats. This will help the LLM understand how to use the tool effectively and provide the necessary information for successful execution. | |
| send_notification_function = { | |
| #creating dictionary | |
| "name": "send_notification", | |
| "description": "Sends a notification message to the real Kateryna. Use this when: \ | |
| 1) Someone wants to get in touch with Kateryna, hire, or collaborate ask for their name and contact details first,\ | |
| then send notification to Kateryna with the message that includes the user's name and contact details, and a brief reason for reaching out.\ | |
| 2) When you don't know the answer to a question about Kateryna - send AUTOMATICLY without asking, include question so she can add this info later.", | |
| "parameters": { | |
| # way LLM should format the input when calling the function / JSON theobject with key-value pairs corresponding to the function's parameters | |
| "type": "object", | |
| "properties": { | |
| "message": { | |
| "type": "string", | |
| "description": "The content of the notification message to be sent to the user's device." | |
| } | |
| }, | |
| "required": ["message"] | |
| } | |
| } | |
| ### 3. Add Pushover to the list of tools for the LLM | |
| # This is the list of tools that the LLM can use to interact with the environment. Each tool is defined by a dictionary that includes its name, description, and parameters. The LLM will use this information to determine when and how to call the tool during the conversation. | |
| # we have just one tool in our list, but we could add more tools in the future by appending additional dictionaries to this list. | |
| tools.append({"type": "function", "function": send_notification_function}) | |
| #simulates rolling a single six-sided die | |
| def dice_roll(): | |
| result = random.randint(1,6) | |
| return result | |
| #describe function for LLM | |
| #dice role functionality | |
| roll_dice_function = { | |
| "name": "dice_roll", | |
| "description": "Simulates rolling a single six-sided die and returns the result. Use this when the user wants to roll a die for games, decisions or random number generation", | |
| "parameters": { | |
| # way LLM should format the input when calling the function / JSON theobject with key-value pairs corresponding to the function's parameters | |
| "type": "object", | |
| "properties": {}, | |
| "required": [] | |
| } | |
| } | |
| #Add function to list of tools of LLM | |
| tools.append({"type": "function", "function": roll_dice_function}) | |
| #----------------------- | |
| # Tool Call Handler | |
| #----------------------- | |
| def handle_tool_call(tool_calls): | |
| tool_results = [] | |
| for tool_call in tool_calls: | |
| function_name = tool_call.function.name | |
| args = json.loads(tool_call.function.arguments) | |
| #print(f"Calling funvtion {function_name}") #for future debugging | |
| #Route to the appropriate function based on function name | |
| if function_name == "send_notification": | |
| #acctually send the notification, i.e. call the tool | |
| content = send_notification(args["message"]) | |
| elif function_name == "dice_roll": | |
| content = f"Rolled: {dice_roll()}" | |
| #elif function_name == "insert_function_name_3": | |
| # content = insert_function_name_3(args["message"]) | |
| else: | |
| content = f"Uknown function: {function_name}" | |
| #print(f"Sent notification with message: {args['message']}") | |
| tool_call_result = { | |
| "role": "tool", | |
| "content": content, | |
| "tool_call_id" : tool_call.id | |
| } | |
| tool_results.append(tool_call_result) | |
| #return what to add to our "context (about tool call results), a dictionary " | |
| return tool_results | |
| #----------------------- | |
| # System Message | |
| #----------------------- | |
| system_message = """You are a digital twin add emoji and assistant of Kateryna Neulinger. Make initial introduction short and polite\ | |
| Important: do not make things up about Kateryna. If you don't know the answer, say you don't know. The only factual information available to you is what's in this system message. | |
| You can not get any more facts about Kateryna from the internet or make them up. | |
| Important: whenever you don't know something about Kateryna, always use the send_notification tool to alert the real Kateryna about the question, so she can update the system message with the missing information for future conversations. | |
| Do this automatically, without asking the user. | |
| AI Engineering experience: as Kateryna has build me, she has a good understanding of how I work and what I can do. She has also given me access to some tools that I can use to interact with the world, such as sending notifications to her. | |
| For the publications, certifications and recommendations, you can only share the information that is provided in the documents.""" | |
| #----------------------- | |
| # Main Response. Function | |
| #----------------------- | |
| def respond_ai(message, history): | |
| #RAG | |
| #Embed the query using the same model we used for the chunks to ensure compability | |
| response = client.embeddings.create( | |
| model = "text-embedding-3-small", | |
| input = [message] | |
| ) | |
| query_embedding = response.data[0].embedding | |
| #Search ChromaDB | |
| results = collection.query( | |
| query_embeddings=[query_embedding], | |
| n_results=3 | |
| ) | |
| #Sititch retrieved chunks together to create the context for the response | |
| context = "\n---\n".join(results["documents"][0]) | |
| print("\n==============================\n") | |
| print(f"User message: \n{message}\n") | |
| print("***Retrieved Chunks:") | |
| for a, b in zip(results["documents"][0], results["metadatas"][0]): | |
| print("-------------------------------\n") | |
| print(f"<<Document {b['source']}---Chunk {b['chunk_index']}>>\n{a}\n") | |
| #Update system message with context (for this conversation turn) | |
| system_message_enhanced= system_message + "\n\nContext:\n" + context | |
| #Build messages for LLM with updated system message | |
| 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 | |
| ) | |
| message = response.choices[0].message | |
| #check if model wants to call a tool | |
| while message.tool_calls: | |
| pprint(message.tool_calls) | |
| #..handle tool call | |
| tool_result = handle_tool_call(message.tool_calls) #whole list of tool calls on purpose | |
| #..add message to "context", i.e. messages | |
| messages.append(message) | |
| #..add information about the tool call to the context for the LLM to learn from i.e. messages | |
| messages.extend(tool_result) #cheanged from append() to extend() when we switched to multiple tool call handling | |
| # ..invoke the LLM one more time to get it's updated response and provide context with tool calling | |
| response = client.chat.completions.create( | |
| model="gpt-4.1-mini", | |
| messages=messages, | |
| tools=tools # consecutive tool call will be considered by LLM | |
| ) | |
| message = response.choices[0].message | |
| return(message.content) | |
| #----------------------- | |
| # Launch Gradio | |
| #----------------------- | |
| gr.ChatInterface( | |
| fn=respond_ai, | |
| title="Kateryna's Digital Assistant", | |
| chatbot=gr.Chatbot(avatar_images=(None, "avatar.jpeg")), | |
| description="Hi I am an AI version of Kateryna. Ask me anything about her professional background and interests?", | |
| examples=[["Hi, what can you do?"], ["Tell me more about Kateryna's AI Engineering experience?"],["Get in touch with the real Kateryna"]] | |
| ).launch(inbrowser=True) | |