""" Agent Ken โ Data-Informed PM + AI Copilot Powered by Azure AI Foundry + Microsoft Fabric HuggingFace Space (Gradio) โ with file upload support """ import os import gradio as gr from azure.ai.projects import AIProjectClient from azure.identity import ClientSecretCredential from azure.ai.agents.models import ListSortOrder, FilePurpose, MessageAttachment, CodeInterpreterTool # Azure config PROJECT_ENDPOINT = os.environ["PROJECT_ENDPOINT"] AGENT_ID = os.environ["AGENT_ID"] AZURE_TENANT_ID = os.environ["AZURE_TENANT_ID"] AZURE_CLIENT_ID = os.environ["AZURE_CLIENT_ID"] AZURE_CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"] # Azure client credential = ClientSecretCredential( tenant_id=AZURE_TENANT_ID, client_id=AZURE_CLIENT_ID, client_secret=AZURE_CLIENT_SECRET, ) project = AIProjectClient(credential=credential, endpoint=PROJECT_ENDPOINT) _agent = None def get_agent(): global _agent if _agent is None: _agent = project.agents.get_agent(AGENT_ID) return _agent # Supported file types SUPPORTED_EXTENSIONS = { ".csv", ".xlsx", ".xls", ".json", ".txt", ".md", ".pdf", ".doc", ".docx", ".pptx", ".png", ".jpg", ".jpeg", ".gif", ".py", ".html", ".css", ".js", ".xml", ".zip", ".tar", } def is_supported_file(filename: str) -> bool: ext = os.path.splitext(filename)[1].lower() return ext in SUPPORTED_EXTENSIONS # Chat logic (generator for typing indicator) def respond(user_message: str, uploaded_files, history: list, thread_state: dict | None): # Normalize inputs user_message = (user_message or "").strip() if not uploaded_files: uploaded_files = [] # Nothing to send if not user_message and not uploaded_files: yield history, thread_state return # Create thread if needed if thread_state is None or "thread_id" not in thread_state: thread = project.agents.threads.create() thread_state = {"thread_id": thread.id} thread_id = thread_state["thread_id"] # Show typing indicator immediately display_msg = user_message if uploaded_files: file_names_display = [os.path.basename(str(f)) for f in uploaded_files] if user_message: display_msg = f"{user_message}\n\n๐ {', '.join(file_names_display)}" else: display_msg = f"๐ {', '.join(file_names_display)}" history.append({"role": "user", "content": display_msg}) history.append({"role": "assistant", "content": "๐ง Thinking..."}) yield history, thread_state try: agent = get_agent() # Handle file uploads attachments = [] file_names = [] for file_path in uploaded_files: path_str = str(file_path) filename = os.path.basename(path_str) if not os.path.exists(path_str): history[-1] = { "role": "assistant", "content": f"โ ๏ธ File `{filename}` could not be read. Please re-upload and try again." } yield history, thread_state return if not is_supported_file(filename): history[-1] = { "role": "assistant", "content": ( f"โ ๏ธ File `{filename}` is not supported.\n\n" f"**Supported formats:** CSV, Excel, PDF, Word, TXT, PNG, JPG, JSON, PowerPoint, Python, HTML, XML." ) } yield history, thread_state return try: uploaded = project.agents.files.upload_and_poll( file_path=path_str, purpose=FilePurpose.AGENTS, ) attachments.append( MessageAttachment( file_id=uploaded.id, tools=[CodeInterpreterTool().definitions[0]], ) ) file_names.append(filename) except Exception as upload_err: history[-1] = { "role": "assistant", "content": f"โ ๏ธ Failed to upload `{filename}`: {str(upload_err)}\n\nPlease try again." } yield history, thread_state return # Build message content if file_names and user_message: content = ( f"{user_message}\n\n" f"๐ Uploaded files: {', '.join(file_names)}\n\n" f"Instructions: Read and analyze the uploaded file(s) immediately using code interpreter. " f"Show the data structure, key findings, and actionable insights. " f"Do NOT ask the user to re-upload or confirm โ the file is already attached and accessible." ) elif file_names: content = ( f"๐ Uploaded files: {', '.join(file_names)}\n\n" f"Instructions: Read and analyze the uploaded file(s) immediately using code interpreter. " f"Show a summary of the data (columns, rows, data types), then provide key findings, " f"trends, and actionable insights. Start analyzing right away." ) else: content = user_message # Send message to Azure msg_kwargs = { "thread_id": thread_id, "role": "user", "content": content, } if attachments: msg_kwargs["attachments"] = attachments project.agents.messages.create(**msg_kwargs) # Run agent run = project.agents.runs.create_and_process( thread_id=thread_id, agent_id=agent.id, ) if run.status == "failed": assistant_reply = ( f"โ ๏ธ Agent run failed: {run.last_error}\n\n" "Please try again or start a new conversation." ) else: messages = project.agents.messages.list( thread_id=thread_id, order=ListSortOrder.DESCENDING ) assistant_reply = "๐ค No response received. Please try again." for msg in messages: if msg.role == "assistant" and msg.text_messages: assistant_reply = msg.text_messages[-1].text.value break except Exception as e: assistant_reply = ( f"โ Connection error: {str(e)}\n\n" "Please try again. If this persists, the Azure endpoint may be temporarily unavailable." ) # Replace typing indicator with actual response history[-1] = {"role": "assistant", "content": assistant_reply} yield history, thread_state def new_conversation(): return [], None # CSS CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); * { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; } .gradio-container { max-width: 880px !important; margin: 0 auto !important; } footer { display: none !important; } /* Header */ .header-main { text-align: center; padding: 28px 16px 6px 16px; } .header-main h1 { font-size: 1.7rem; font-weight: 700; color: #1A202C; margin: 0 0 4px 0; } .header-main .tagline { font-size: 0.88rem; color: #718096; margin: 0; } /* Architecture badge */ .arch-badge { display: flex; justify-content: center; gap: 8px; flex-wrap: wrap; margin: 14px auto 6px auto; max-width: 640px; } .arch-badge .chip { display: inline-flex; align-items: center; gap: 5px; padding: 5px 13px; border-radius: 8px; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.02em; } .chip-foundry { background: #EBF4FF; color: #2B6CB0; border: 1px solid #BEE3F8; } .chip-fabric { background: #F0FFF4; color: #276749; border: 1px solid #C6F6D5; } .chip-ml { background: #FAF5FF; color: #6B46C1; border: 1px solid #E9D8FD; } .chip-model { background: #FFFAF0; color: #C05621; border: 1px solid #FEEBC8; } /* Welcome card */ .welcome-card { background: linear-gradient(135deg, #EBF4FF 0%, #F7FAFC 40%, #F0FFF4 100%); border: 1px solid #D6E4F0; border-radius: 16px; padding: 24px 28px 20px 28px; margin: 10px 0 14px 0; text-align: center; } .welcome-card .wave { font-size: 2rem; display: block; margin-bottom: 6px; } .welcome-card h2 { font-size: 1.1rem; font-weight: 600; color: #2D3748; margin: 0 0 6px 0; } .welcome-card p { font-size: 0.84rem; color: #4A5568; margin: 0 0 14px 0; line-height: 1.55; } .welcome-tags { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; } .welcome-tags span { background: white; border: 1px solid #E2E8F0; border-radius: 20px; padding: 5px 13px; font-size: 0.74rem; color: #4A5568; font-weight: 500; } /* Data section */ .data-section { background: #F7FAFC; border: 1px solid #E2E8F0; border-radius: 12px; padding: 14px 20px; margin: 0 0 14px 0; } .data-section h3 { font-size: 0.82rem; font-weight: 600; color: #2D3748; margin: 0 0 8px 0; } .data-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; } .data-card { background: white; border: 1px solid #E2E8F0; border-radius: 10px; padding: 10px 12px; text-align: center; } .data-card .number { font-size: 1.15rem; font-weight: 700; color: #2B6CB0; display: block; } .data-card .label { font-size: 0.68rem; color: #718096; margin-top: 2px; display: block; } /* Upload hint */ .upload-hint { background: #FFFFF0; border: 1px solid #FEFCBF; border-radius: 10px; padding: 10px 16px; margin: 0 0 10px 0; font-size: 0.76rem; color: #744210; text-align: center; } /* Footer */ .footer-info { text-align: center; padding: 10px 0; font-size: 0.7rem; color: #A0AEC0; line-height: 1.6; } """ # Examples EXAMPLES = [ ["What's our D7 retention rate?"], ["Which acquisition channel has the highest LTV?"], ["How did our checkout experiment perform?"], ["Which user segments are at highest churn risk?"], ["Help me write a PRD for a referral program feature"], ["Score these features using RICE: push notifications, dark mode, onboarding revamp"], ["Design an A/B test for our new premium pricing"], ["Were there any anomalies in our metrics recently?"], ] # UI with gr.Blocks(title="Agent Ken โ Data-Informed PM Copilot") as demo: # Header gr.HTML("""
Your AI Companion for Product Management & Data
I help you learn product management, explore AI technology, and discover insights from my hands-on experience with Microsoft Fabric โ including data pipelines, ML models, and turning raw data into real product decisions.