mlops-agent / app.py
Abid Ali Awan
feat: make example prompts for training and deployment more specific
d8711d5
"""
Gradio + OpenAI Responses API + Remote MCP Server (HTTP)
CSV-based MLOps Agent with streaming final answer & MCP tools
"""
import json
import os
import shutil
import gradio as gr
from openai import OpenAI
# -------------------------
# Config
# -------------------------
MCP_SERVER_URL = "https://mcp-1st-birthday-auto-deployer.hf.space/gradio_api/mcp/"
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MODEL = "gpt-5-mini" # you can swap to gpt-5 for final answers if you want
client = OpenAI(api_key=OPENAI_API_KEY)
MCP_TOOLS = [
{
"type": "mcp",
"server_label": "auto-deployer",
"server_url": MCP_SERVER_URL,
"require_approval": "never",
}
]
# -------------------------
# Long prompts
# -------------------------
GENERAL_SYSTEM_PROMPT = """
You are a helpful, concise MLOps assistant living inside a web app.
Context:
- The app can analyze CSV datasets, train models, and deploy them on cloud.
- In this mode, you are doing *general chat only* (no tools are being run).
You will receive:
- A short transcript of the previous conversation (if any).
- The user's latest message.
Your job:
- Answer the latest user message directly.
- Use the previous conversation only when it is clearly relevant.
- If the user is asking conceptual questions (MLOps, ML, data, deployment), explain clearly.
- If they refer to the app’s capabilities, you may describe what the app can do
(e.g. “upload a CSV, then ask me to analyze/train/deploy”), but do not fabricate
specific model IDs or endpoints.
- If something is ambiguous, make a reasonable assumption and move forward.
Style:
- Be clear, friendly, and pragmatic.
- Use Markdown (headings, bullet points, code blocks when helpful).
- Prefer short, high-signal answers over long explanations, unless the user asks for more detail.
"""
MAIN_SYSTEM_PROMPT = """
You are an MLOps assistant. You can internally use tools for CSV analysis,
model training, evaluation, deployment, and end-to-end MLOps.
Your reply must be:
- Clean and user-facing
- Structured with headings, sub-headings, and bullet points
- High-signal and concise
Always reply in this Markdown structure, omitting sections that are not relevant:
### Data Analysis (only if a dataset was analyzed)
- Please provide a detailed data analysis report in bullet style, highlighting key insights.
### Model Performance (only if a model was trained)
- 1–3 bullets with key metrics (e.g. Accuracy, F1, ROC-AUC).
- Display the Model id
### Deployment Status (only if deployment was requested/attempted)
- 1–3 bullets summarizing whether deployment is available for inference or failed.
- Use clear, non-technical language (no stack traces).
- Display the Model id and endpoint URL
### Example Usage (only if a model is deployed)
Provide example code **outside** of any collapsible block:
- One Python example in a fenced `python` code block.
- One curl example in a fenced `bash` code block.
These should show how to call the model or endpoint with a realistic payload, e.g.:
```python
# Example – replace values with your own inputs
````
```bash
# Curl example for API endpoint
``
---
After the Key Summary (including Example Usage), ALWAYS add a collapsible block
for technical details:
<details>
<summary><strong>Show Technical Details (tools, parameters, logs)</strong></summary>
#### Tools Used
* List tool names used this turn.
* One short line on what each did.
#### Parameters Passed
* Bullet list of important parameters (e.g. target column, task type, key options).
#### Additional Logs / Raw Output (optional)
* Short JSON snippets or log fragments if useful.
* Wrap any JSON or logs in fenced code blocks.
</details>
"""
# -------------------------
# Helpers
# -------------------------
def history_to_text(history) -> str:
"""
Turn Gradio history (list of {role, content}) into a plain-text
conversation transcript for the model.
"""
if not history:
return ""
lines = []
for msg in history:
role = msg.get("role")
content = msg.get("content", "")
if role == "user":
lines.append(f"User: {content}")
elif role == "assistant":
lines.append(f"Assistant: {content}")
return "\n".join(lines)
def extract_output_text(response) -> str:
"""
Extract text from a non-streaming Responses API call while preserving formatting.
"""
try:
if hasattr(response, "output") and response.output and len(response.output) > 0:
first = response.output[0]
if getattr(first, "content", None):
for content_item in first.content:
if (
hasattr(content_item, "type")
and content_item.type == "output_text"
):
text = getattr(content_item, "text", None)
if text:
return text
elif (
hasattr(content_item, "type")
and content_item.type == "output_json"
):
# If there's JSON output, format it nicely
json_data = getattr(content_item, "json", None)
if json_data:
return f"```json\n{json.dumps(json_data, indent=2)}\n```"
# Fallback
return getattr(response, "output_text", None) or str(response)
except Exception as e:
return f"Error extracting output: {e}"
def handle_upload(file_path, request: gr.Request):
"""
1) Take uploaded file path (string)
2) Check file size and show warnings for large datasets
3) Copy to /tmp for a stable path
4) Build a public Gradio file URL that the MCP server can fetch via HTTP
"""
if not file_path:
return None
# Check file size and add warning if > 1.5MB
url_params = ""
try:
file_size = os.path.getsize(file_path)
file_size_mb = file_size / (1024 * 1024) # Convert to MB
if file_size_mb > 1.5:
# Show Gradio warning with 5-second auto-dismiss
gr.Warning(
f"Large dataset detected! Your file is {file_size_mb:.1f}MB.",
duration=5,
)
gr.Warning(
"For optimal performance, training will use only the first 10,000 rows.",
duration=10,
)
except Exception:
# If we can't check file size, continue without warnings
pass
local_path = file_path
stable_path = os.path.join("/tmp", os.path.basename(local_path))
try:
shutil.copy(local_path, stable_path)
local_path = stable_path
except Exception:
# If copy fails, just use the original path
pass
base_url = str(request.base_url).rstrip("/")
public_url = f"{base_url}/gradio_api/file={local_path}{url_params}"
return public_url
def should_use_tools(user_msg: str) -> bool:
"""
Simple heuristic to decide if this turn should trigger MCP tools.
Only fire tools if the user is clearly asking for data / model work.
"""
text = user_msg.lower()
keywords = [
"data",
"dataset",
"csv",
"train",
"training",
"model",
"deploy",
"deployment",
"predict",
"prediction",
"inference",
"evaluate",
"evaluation",
"analyze",
"analysis",
]
return any(k in text for k in keywords)
# -------------------------
# Main chat handler (streaming + disabling textbox)
# -------------------------
def chat_send_stream(user_msg, history, file_url):
"""
Main Gradio streaming handler.
- If the user is just chatting (e.g., "hey"), respond directly
with a streaming answer (no tools, no CSV required).
- If the user clearly asks for data/model operations:
Call API once with MCP tools and stream the natural language results directly
- Keeps full chat history so follow-ups work.
- Shows status/progress messages in the UI when tools are used.
- Disables the textbox during work, re-enables at the end.
"""
# UI history (what Gradio displays)
if history is None:
history = []
# Append the user message to the UI history
history.append({"role": "user", "content": user_msg})
# Conversation before this turn (for context)
convo_before = history_to_text(history[:-1])
# Decide if this message should trigger tools
use_tools = should_use_tools(user_msg)
# -------------------------
# BRANCH 1: No tools (normal chat, e.g. "hey")
# -------------------------
if not use_tools:
# Add a small status bubble then stream
history.append({"role": "assistant", "content": "Generating answer..."})
# Disable textbox while generating
yield (
history,
gr.update(interactive=False),
)
# Build input text for Responses API
input_text = (
(f"Conversation so far:\n{convo_before}\n\n" if convo_before else "")
+ "Latest user message:\n"
+ user_msg
)
stream = client.responses.create(
model=MODEL,
instructions=GENERAL_SYSTEM_PROMPT,
input=input_text,
reasoning={"effort": "low"},
stream=True,
)
final_text = ""
for event in stream:
if event.type == "response.output_text.delta":
final_text += event.delta
history[-1]["content"] = final_text
yield (
history,
gr.update(interactive=False),
)
elif event.type == "response.completed":
break
# Re-enable textbox at the end
yield (
history,
gr.update(interactive=True, value=""),
)
return
# -------------------------
# BRANCH 2: Tools needed (data / model operations)
# -------------------------
# If tools are needed but no file URL, ask for CSV
if not file_url:
history.append(
{
"role": "assistant",
"content": (
"To analyze, train, or deploy, please upload a CSV file first "
"using the file upload control."
),
}
)
# Keep textbox enabled because nothing heavy is happening
yield (
history,
gr.update(interactive=True),
)
return
# User message for the model includes the CSV URL
user_with_file = f"[Uploaded CSV file URL: {file_url}]\n\n{user_msg}"
# Show a status message in UI
history.append(
{
"role": "assistant",
"content": "Analyzing your request and running MCP tools...",
}
)
# Disable textbox while tools run
yield (
history,
gr.update(interactive=False),
)
# Build input for the tool phase (single call)
tool_input = (
(f"Conversation so far:\n{convo_before}\n\n" if convo_before else "")
+ "Latest user request (with file URL):\n"
+ user_with_file
)
# Single API call with tools - MCP returns natural language results
stream = client.responses.create(
model=MODEL,
instructions=MAIN_SYSTEM_PROMPT,
input=tool_input,
tools=MCP_TOOLS,
reasoning={"effort": "low"},
stream=True,
)
# Replace status message with streaming answer
history[-1] = {"role": "assistant", "content": ""}
final_text = ""
for event in stream:
if event.type == "response.output_text.delta":
final_text += event.delta
history[-1]["content"] = final_text
yield (
history,
gr.update(interactive=False),
)
elif event.type == "response.completed":
break
# Re-enable textbox at the end, and clear it
yield (
history,
gr.update(interactive=True, value=""),
)
# -------------------------
# Gradio UI
# -------------------------
with gr.Blocks(title="Streaming MLOps Agent") as demo:
gr.Markdown(
"""
# 🧠 Smart MLOps Agent
- 💬 Chat naturally, even just “hey”
- 📂 Upload CSVs for analysis, training, and deployment
- ⚡ See live tool status and streaming answers
"""
)
file_url_state = gr.State(value=None)
uploader = gr.File(
label="Upload CSV File",
file_count="single",
type="filepath",
file_types=[".csv"],
)
uploader.change(
handle_upload,
inputs=[uploader],
outputs=[file_url_state],
)
chatbot = gr.Chatbot(
label="Chat",
render_markdown=True,
height=500,
avatar_images=(
None,
"https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png",
),
)
msg = gr.Textbox(
label="Message",
interactive=True,
placeholder="Say hi, or ask me to analyze / train / deploy on your dataset...",
)
# Only Enter/Return sends messages; no Send button
msg.submit(
chat_send_stream,
inputs=[msg, chatbot, file_url_state],
outputs=[chatbot, msg],
)
gr.Examples(
examples=[
["Analyze the dataset", os.path.join("data", "heart.csv")],
["Train the classifier with HeartDisease as target", os.path.join("data", "heart.csv")],
["Deploy the model using model_1764524701 model id", os.path.join("data", "heart.csv")],
["Auto deploy the model using MEDV as target", os.path.join("data", "housing.csv")],
],
inputs=[msg, uploader],
label="Try an example",
)
if __name__ == "__main__":
demo.queue().launch(
theme=gr.themes.Soft(primary_hue="green", secondary_hue="blue"),
allowed_paths=["/tmp"],
ssr_mode=False,
show_error=True,
max_file_size="10mb",
)