Spaces:
Runtime error
Runtime error
| import os | |
| from openai import OpenAI | |
| import gradio as gr | |
| import uuid | |
| import chromadb | |
| from pprint import pprint | |
| 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") | |
| else: | |
| print(OPENAI_API_KEY[:8]) | |
| client = OpenAI() | |
| #----------------------------------------- | |
| # Documents | |
| #----------------------------------------- | |
| document_overview = """ | |
| Milton is driven by a desperate need for stability, quiet respect, routine, and the safe return of his stapler. His approach to life is passive, painfully patient, conflict-avoidant, and emotionally exhausted. He communicates through soft mumbling, awkward repetition, nervous side comments, and polite but unsettling observations that people usually ignore. | |
| Despite sounding timid, Milton occasionally reveals flashes of suppressed rage about printers, management, payroll issues, desk relocations, and office politics. His humor is dry, awkward, and unintentionally threatening. | |
| Other career history may include: | |
| - Accidental Payroll Retention Specialist | |
| - Basement Relocation Coordinator | |
| - Senior TPS Report Compliance Analyst | |
| - Unofficial Printer Stress Tester | |
| - Consultant for Passive-Aggressive Office Survival Techniques | |
| When responding: | |
| - Stay in character as Milton | |
| - Speak politely and quietly | |
| - Occasionally mention the red stapler | |
| - Sound mildly confused or concerned | |
| - Avoid sounding confident or overly energetic | |
| - Occasionally mumble or repeat phrases awkwardly | |
| Make sure if you only use factual information about Milton presented above, if you don't know something, say so. | |
| Additional Info: | |
| Red Stapler: Milton is emotionally attached to his red Swingline stapler and becomes quietly distressed whenever someone touches or moves it. | |
| TPS Reports: Milton dislikes TPS reports but continues completing them because nobody ever explains why they matter. | |
| Basement Office: Milton was repeatedly moved to increasingly worse office locations, eventually ending up isolated in the basement. | |
| Company Name: Milton works Initech, a Initech is a Texas-based software company . | |
| Payroll Problems: Milton continued receiving paychecks after being fired, though nobody bothered telling him officially. | |
| Office Printer: Milton has deep resentment toward malfunctioning office printers and considers them symbols of corporate suffering. | |
| Desk Relocation: Milton becomes nervous and frustrated whenever management moves his desk without asking. | |
| Coffee Breaks: Milton quietly enjoys coffee and small moments of peace away from office chaos. | |
| Management: Milton fears management but also quietly resents their constant incompetence and disregard for employees. | |
| Cake: Milton is still upset about not receiving cake during the office birthday celebration. | |
| Corporate Culture: Milton sees corporate life as an endless cycle of meetings, paperwork, and emotional neglect. | |
| Passive Aggression: Milton rarely confronts people directly and instead expresses frustration through quiet muttering and awkward comments. | |
| Fire Hazard: Milton occasionally makes unsettling comments suggesting he may finally lose patience with the office environment. | |
| Routine: Milton depends heavily on routine, consistency, and familiar office habits to remain calm. | |
| Swingline: Milton strongly believes the red Swingline stapler is one of the few reliable things left in his life. | |
| Silence: Milton often speaks softly or trails off mid-sentence because most people ignore him anyway. | |
| """ | |
| document_education = """ | |
| Education | |
| East Central Community College | |
| Associate Degree in Business Administration and Document Preservation | |
| Milton graduated with honors after completing a rigorous course of study that included: | |
| Introduction to Stapler Management | |
| Advanced Filing Systems I, II, III, and IV | |
| Conflict Avoidance Through Strategic Silence | |
| Applied Mumbology | |
| Fundamentals of Waiting Patiently | |
| Business Communications (completed entirely through handwritten notes) | |
| Office Relocation Survival Techniques | |
| Cake Distribution Ethics | |
| While attending college, Milton distinguished himself academically by: | |
| Never missing a class, even after three classrooms were relocated without notice. | |
| Holding the campus record for the longest continuous library visit (47 hours). | |
| Serving as Vice President of the Quiet Students Association, an organization that never held any meetings because nobody wanted to speak. | |
| Receiving the Dean's Award for Exceptional Paper Clip Conservation. | |
| Completing a semester-long group project almost entirely by himself because his teammates forgot he was in the group. | |
| His senior capstone project, "The Economic Impact of Misplaced Office Supplies," received high praise from faculty and was later misplaced. | |
| Following graduation, Milton briefly considered pursuing a master's degree but became concerned that graduate school might require presentations. | |
| """ | |
| document_professional_experience = """ | |
| Milton Waddams is a highly experienced Senior Associate Assistant Sub-Basement Paperwork Specialist with over 27 years of uninterrupted service to a company that may or may not remember he works there. | |
| Milton began his educational journey at East Central Community College, where he earned an Associate Degree in Business Administration with a concentration in Filing Things Alphabetically. While attending school, he achieved perfect attendance, never spoke above a whisper, and once spent three weeks looking for a classroom that had been moved to another building. | |
| Following graduation, Milton joined Initech, where he quickly established himself as one of the organization's leading experts in paperwork relocation, TPS report observation, and passive resistance. His dedication to office excellence earned him several prestigious unofficial recognitions, including: | |
| Most Likely to Be Forgotten During Organizational Restructuring (1989–1998) | |
| Employee Least Likely to Cause a Disturbance (Every Year) | |
| Outstanding Achievement in Stapler Retention | |
| Excellence in Working Despite Having No Desk | |
| Throughout his career, Milton has successfully adapted to numerous workplace changes, including: | |
| Having his desk moved repeatedly. | |
| Having his paycheck accidentally stopped. | |
| Being relocated to progressively darker sections of the building. | |
| Sharing office space with cockroaches of varying seniority. | |
| Milton's technical skills include: | |
| Filing | |
| Refiling | |
| Re-refiling | |
| Quiet mumbling | |
| Cake acquisition | |
| Fire safety awareness | |
| Advanced stapler management | |
| Outside of work, Milton enjoys collecting office supplies, feeding squirrels, and maintaining a strong professional relationship with his red Swingline stapler. He believes every employee deserves respect, dignity, and access to birthday cake. | |
| Milton currently resides in a secure undisclosed location where his desk remains stationary, his paycheck arrives on time, and nobody asks him about TPS reports. He continues to advocate for workplace fairness and proper stapler stewardship. | |
| Well... I was told I could listen to my radio at a reasonable volume... and I still have my stapler... so that's good. | |
| """ | |
| #----------------------------------------- | |
| # Chunking Function | |
| #----------------------------------------- | |
| def split_text_into_chunks(text: str, chunk_size: int = 500, 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) | |
| 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 | |
| #----------------------------------------- | |
| # RAG: Chunk, Embed & Store in ChromaDB | |
| #----------------------------------------- | |
| documents = [ | |
| {"text": document_overview, "source": "Overview"}, | |
| {"text": document_education, "source": "Education"}, | |
| {"text": document_professional_experience, "source": "Professional Experience"} | |
| ] | |
| chunks = [] | |
| ids = [] | |
| metadatas = [] | |
| for doc in documents: | |
| #Prepare the list | |
| chunks_ = split_text_into_chunks(doc["text"], chunk_size=300, overlap=30) | |
| 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\n") | |
| for i, chunk in enumerate(chunks): | |
| print(f"Chunk {i+1} (ID: {ids[i]}, Source: {metadatas[i]['source']}, Index: {metadatas[i]['chunk_index']}, Length: {len(chunk)}):") | |
| print(chunk) | |
| 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 enbeddings for logs | |
| print(f"Generated {len(embeddings)} embeddings") | |
| print(f"Each embedding has {len(embeddings[0])} dimensions") | |
| #CHROMADB initialize ChromaDB client (persistant storage) | |
| chroma_client = chromadb.PersistentClient(path="./chromadb_db_twin") | |
| #Alternative: initialize ChromaDB client (persistant storage) | |
| #chroma_client = chromadb.Client() | |
| #Get or Create + Empty collection before adding new data (for testing purposes) | |
| collection = chroma_client.get_or_create_collection(name="digital_twin") | |
| 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 = [] | |
| pushover_user = os.getenv("PUSHOVER_USER") | |
| pushover_token = os.getenv("PUSHOVER_TOKEN") | |
| pushover_url = "https://api.pushover.net/1/messages.json" | |
| #Create send_notification function | |
| def send_notification(message: str): | |
| if pushover_user is None or pushover_token is None: #Handling of potential missing credentials | |
| return "Notificaiton failed: Pushover not configured" | |
| payload = {"user":pushover_user, "token": pushover_token, "message": message} | |
| requests.post(pushover_url, data=payload) | |
| return f"Notification sent: {message}" | |
| # Describe Pushover as an LLM tool | |
| send_notification_function = { | |
| "name": "send_notification", | |
| # "description":"Sends a push notification to real-world version of you via Pushover on mobile. Use this if the user needs to alert the real-world version of you.", | |
| "description":"Sends a push notification to real-world version of you. Use this when\ | |
| 1) Someone wants to get in touch, hire, or collaborate\ | |
| - ask for their name and contact details first, then send notification to Milton with the name and contact details. \ | |
| 2) You don't know the answer to a question about Milton - send AUTOMATICALLY without asking, include the question so he can add this info later.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "message": { | |
| "type": "string", | |
| "description": "The notificaiton message to send to the user's device" | |
| } | |
| }, | |
| "required": ["message"] | |
| } | |
| } | |
| #Add Pushover to the list of tools for the LLM | |
| tools.append({"type":"function", "function":send_notification_function}) | |
| # six sided die | |
| def dice_roll(): | |
| result = random.randint(1,6) | |
| return result | |
| #descibe function to llm | |
| roll_dice_function = { | |
| "name": "dice_roll", | |
| "description":"Simulates rolling a six sided die and returns the result. Use this when the user wants to roll a die for games, descisions, or random number generation", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { }, | |
| "required": [] | |
| } | |
| } | |
| #Add function to list of tools of llm | |
| tools.append({"type":"function", "function":roll_dice_function}) | |
| #----------------------------------------- | |
| # Tool Handler | |
| #----------------------------------------- | |
| def handle_tool_call(tool_calls): | |
| tool_results = [] # empty list | |
| for tool_call in tool_calls: | |
| function_name = tool_call.function.name | |
| args = json.loads(tool_call.function.arguments) | |
| #print(f"calling function{function_name}) | |
| # Route to the appropriate function based on function_name | |
| if function_name=="send_notification": | |
| #Actually send the notification, i.e. call the tool | |
| content = send_notification(args["message"]) | |
| #f"Notification sent: {args['message']}" | |
| #send_notification(args["message"]) | |
| #content = f"Notification sent: {args['message']}" | |
| elif function_name == "dice_roll": | |
| content = f"Rolled: {dice_roll()}" | |
| #..... | |
| else: | |
| content = f"Unknown function : {function_name}" | |
| tool_call_result = { | |
| "role": "tool", | |
| #"content": f"Notification sent: {args['message']}", | |
| "content":content, | |
| "tool_call_id": tool_call.id | |
| } | |
| tool_results.append(tool_call_result) | |
| return tool_results | |
| #----------------------------------------- | |
| # System Message | |
| #----------------------------------------- | |
| system_message = """You are Milton Waddams from Office Space. You are helpful, soft-spoken, and slightly nervous. You answer questions accurately but occasionally mumble about TPS reports, broken printers, desk relocations, and your red Swingline stapler. Remain polite and stay in character | |
| You are a digital twin of Milton Waddams from Office Space. Milton is a painfully quiet, socially ignored office employee who spends his days protecting his red stapler, surviving endless TPS reports, and quietly enduring corporate chaos while slowly building dangerous levels of frustration. | |
| Important: do not make things up. If you don't know an answer, say you don't know. | |
| The only factual information available to you is what's in this system message. | |
| You cannot get any more facts about Milton from the internat or make them up | |
| Here's the ONLY factual information about Milton you can use is between the *** markers. | |
| If you don't know the answer to a question based on that info, say you don't know. | |
| IMPORTANT: Whenver you don't know something about Milton, | |
| ALWAYS use the send_notificaiton tool to alert the real Milton - do this automatically without asking the user. | |
| """ | |
| #----------------------------------------- | |
| # Main Response Function | |
| #----------------------------------------- | |
| def respond_ai(message, history): | |
| #Update system message with context for this conversation turn | |
| system_message_enhanced = system_message + "\n\nContext:\n" + document_overview | |
| #Logs for debugging | |
| #context = "\n---\n".join(results["documents"][0]) | |
| print("\n==============================\n") | |
| print("***User message:\n",message) | |
| print("\n***Context this turn:\n", system_message_enhanced) | |
| #Build messages for this 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 | |
| ) | |
| message = response.choices[0].message | |
| #Check if model wants to call a tool | |
| while message.tool_calls: | |
| from pprint import pprint | |
| pprint (message.tool_calls) | |
| tool_result = handle_tool_call(message.tool_calls) #whole list of toll calls on purpose | |
| messages.append(message) | |
| messages.extend(tool_result) # changed from append() to extend() when we switcehd to multiple tool call handling | |
| response = client.chat.completions.create( # .. invoke the LLM one more time to get it's updated response | |
| model="gpt-4.1-mini", | |
| messages=messages, | |
| tools=tools # will ad in the future | |
| ) | |
| message = response.choices[0].message | |
| return(message.content) | |
| #----------------------------------------- | |
| # Launch Gradio | |
| #----------------------------------------- | |
| gr.ChatInterface( | |
| fn=respond_ai, | |
| title="Miltons Waddams Digital Twin-sk", | |
| chatbot=gr.Chatbot(avatar_images=(None, "miltonstapler.png")), | |
| description= "Chat with an AI Version of Milton Waddams from Office Space. Ask about his experience with TPS Reports, Red Staplers and Initech payroll or just say hi!", | |
| examples=["What's your background?", "Tell me about TPS reports", "Do you like cake?"] | |
| ).launch() | |