kenagent / app.py
kendrickfff's picture
Update app.py
c76a795 verified
"""
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("""
<div class="header-main">
<h1>🤖 Agent Ken</h1>
<p class="tagline">Your AI Companion for Product Management & Data</p>
</div>
""")
# Architecture chips
gr.HTML("""
<div class="arch-badge">
<span class="chip chip-foundry">☁️ Azure AI Foundry</span>
<span class="chip chip-fabric">🏭 Microsoft Fabric</span>
<span class="chip chip-ml">🧠 3 ML Models</span>
<span class="chip chip-model">⚡ GPT-5.1</span>
</div>
""")
# Welcome card
gr.HTML("""
<div class="welcome-card">
<span class="wave">👋</span>
<h2>Hi, I'm Agent Ken — your AI Companion for Product Management & Data</h2>
<p>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.</p>
<div class="welcome-tags">
<span>📊 Live Product Data</span>
<span>🔮 Churn Prediction</span>
<span>💰 LTV Analysis</span>
<span>⚠️ Anomaly Detection</span>
<span>🧪 A/B Test Results</span>
<span>📝 PRDs & Strategy</span>
<span>🤖 AI & Tech</span>
</div>
</div>
""")
# Data overview cards
gr.HTML("""
<div class="data-section">
<h3>📊 Connected Product Data (Fabric → Foundry)</h3>
<div class="data-grid">
<div class="data-card">
<span class="number">5,000</span>
<span class="label">Users Tracked</span>
</div>
<div class="data-card">
<span class="number">204K</span>
<span class="label">Events Analyzed</span>
</div>
<div class="data-card">
<span class="number">90 Days</span>
<span class="label">Data Window</span>
</div>
<div class="data-card">
<span class="number">25.6%</span>
<span class="label">D7 Retention</span>
</div>
<div class="data-card">
<span class="number">$19.87</span>
<span class="label">Avg LTV</span>
</div>
<div class="data-card">
<span class="number">5</span>
<span class="label">A/B Tests Run</span>
</div>
</div>
</div>
""")
# Upload hint
gr.HTML("""
<div class="upload-hint">
📎 <strong>You can also upload your files!</strong> (Supported: CSV, Excel, PDF, Word, Images, TXT, JSON.)
I'll analyze them and give you insights — and if you want, I can compare your data against our Fabric benchmark.
</div>
""")
# Chat
chatbot = gr.Chatbot(label="Agent Ken", height=440)
thread_state = gr.State(value=None)
# Input row
with gr.Row():
msg_input = gr.Textbox(
placeholder="Ask about metrics, upload a file, or any PM/tech question...",
label="", show_label=False, scale=5, lines=1, max_lines=3, container=False,
)
send_btn = gr.Button("Send 🚀", variant="primary", scale=1, min_width=100)
with gr.Row():
clear_btn = gr.Button("🗑️ New Conversation", variant="secondary", size="sm", scale=1)
# File upload
with gr.Accordion("📎 Upload a file (CSV, Excel, PDF, Word, Images, JSON)", open=False):
file_input = gr.File(
label="",
file_count="multiple",
file_types=[
".csv", ".xlsx", ".xls", ".json",
".txt", ".md", ".pdf",
".doc", ".docx", ".pptx",
".png", ".jpg", ".jpeg", ".gif",
".py", ".html", ".xml",
],
)
with gr.Accordion("💡 Try an example", open=False):
gr.Examples(examples=EXAMPLES, inputs=msg_input, label="")
# Footer
gr.HTML("""
<div class="footer-info">
Built by <strong>Kendrick Filbert</strong><br>
Azure AI Foundry (GPT-5.1) · Microsoft Fabric (OneLake + MLflow) · 3 ML Models (Churn · LTV · Anomaly)<br>
Supports: CSV · Excel · PDF · Word · Images · JSON · TXT
</div>
""")
# Events
send_btn.click(
fn=respond,
inputs=[msg_input, file_input, chatbot, thread_state],
outputs=[chatbot, thread_state],
).then(lambda: ("", None), outputs=[msg_input, file_input])
msg_input.submit(
fn=respond,
inputs=[msg_input, file_input, chatbot, thread_state],
outputs=[chatbot, thread_state],
).then(lambda: ("", None), outputs=[msg_input, file_input])
clear_btn.click(fn=new_conversation, outputs=[chatbot, thread_state])
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
css=CSS,
theme=gr.themes.Soft(),
ssr_mode=False,
)