First_agent_template / Gradio_UI.py
AIbyKaindu's picture
Update Gradio_UI.py
5272e18 verified
Raw
History Blame Contribute Delete
21.6 kB
#!/usr/bin/env python
# coding=utf-8
"""
Gradio_UI.py — BRIANNA's interface.
Replaces the default smolagents template UI with:
• BRIANNA branding & a dark neural-network aesthetic
• 🎙️ Voice in / 🔊 voice out (speak to her, hear her reply) — open-source,
no API keys (Whisper STT + gTTS TTS, see voice.py)
• File upload (PDF, DOCX, TXT, images, .py files)
• Image & audio rendering in chat
• Clear-chat button, example prompts, per-step token/timing footer
The UI degrades gracefully: if voice models or optional Gradio features are
unavailable, the text chat keeps working.
"""
import mimetypes
import os
import re
import shutil
import tempfile
from typing import Optional
from smolagents.agent_types import AgentAudio, AgentImage, AgentText, handle_agent_output_types
from smolagents.agents import ActionStep, MultiStepAgent
from smolagents.memory import MemoryStep
from smolagents.utils import _is_package_available
# ─────────────────────────────────────────────────────────────────────────────
# Open-source voice helpers (inlined) — BRIANNA's ears and mouth.
# • Listening : OpenAI Whisper via the transformers pipeline (local, no key).
# • Speaking : gTTS (free, no key). Both are lazy-loaded and fully graceful,
# so a missing model/package degrades to text instead of crashing the app.
# ─────────────────────────────────────────────────────────────────────────────
_STT_PIPELINE = None
_STT_MODEL_ID = os.environ.get("BRIANNA_STT_MODEL", "openai/whisper-base")
def _get_stt_pipeline():
"""Lazily build (and cache) the Whisper speech-to-text pipeline."""
global _STT_PIPELINE
if _STT_PIPELINE is None:
from transformers import pipeline # imported lazily on first use
_STT_PIPELINE = pipeline(
task="automatic-speech-recognition",
model=_STT_MODEL_ID,
device=-1, # CPU; set to 0 if a GPU Space is available
)
return _STT_PIPELINE
def _voice_transcribe(audio_path):
"""Speech → text: microphone filepath in, recognised text out."""
if not audio_path:
return ""
try:
pipe = _get_stt_pipeline()
# return_timestamps=True lets Whisper handle clips longer than 30s.
result = pipe(audio_path, return_timestamps=True)
text = result["text"] if isinstance(result, dict) else str(result)
return text.strip()
except Exception as e: # noqa: BLE001 — voice must never crash the app
return f"[Speech-to-text unavailable: {e}]"
def _clean_for_speech(text, max_chars=1200):
"""Strip markdown / code so the spoken version sounds natural."""
text = re.sub(r"```.*?```", " (code block omitted) ", text, flags=re.DOTALL)
text = re.sub(r"[#*`_>|]", " ", text)
text = re.sub(r"\s+", " ", text).strip()
return text[:max_chars]
def _voice_synthesize(text):
"""Text → spoken .mp3 path (autoplayed by Gradio), or None on failure."""
spoken = _clean_for_speech(text or "")
if not spoken:
return None
try:
from gtts import gTTS # imported lazily on first use
tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
tmp.close()
gTTS(text=spoken, lang="en").save(tmp.name)
return tmp.name
except Exception: # noqa: BLE001 — silently fall back to text-only
return None
# ─────────────────────────────────────────────────────────────────────────────
# Message extraction from agent steps
# ─────────────────────────────────────────────────────────────────────────────
def pull_messages_from_step(step_log: MemoryStep):
"""Extract gr.ChatMessage objects from agent steps with proper nesting."""
import gradio as gr
if isinstance(step_log, ActionStep):
step_number = f"Step {step_log.step_number}" if step_log.step_number is not None else ""
yield gr.ChatMessage(role="assistant", content=f"**{step_number}**")
# Thought / reasoning output
if getattr(step_log, "model_output", None) is not None:
model_output = step_log.model_output.strip()
model_output = re.sub(r"```\s*<end_code>", "```", model_output)
model_output = re.sub(r"<end_code>\s*```", "```", model_output)
model_output = re.sub(r"```\s*\n\s*<end_code>", "```", model_output)
model_output = model_output.strip()
yield gr.ChatMessage(role="assistant", content=model_output)
# Tool call block
if getattr(step_log, "tool_calls", None):
first_tool_call = step_log.tool_calls[0]
used_code = first_tool_call.name == "python_interpreter"
parent_id = f"call_{len(step_log.tool_calls)}"
args = first_tool_call.arguments
if isinstance(args, dict):
content = str(args.get("answer", str(args)))
else:
content = str(args).strip()
if used_code:
content = re.sub(r"```.*?\n", "", content)
content = re.sub(r"\s*<end_code>\s*", "", content)
content = content.strip()
if not content.startswith("```python"):
content = f"```python\n{content}\n```"
parent_message_tool = gr.ChatMessage(
role="assistant",
content=content,
metadata={
"title": f"🛠️ Used tool {first_tool_call.name}",
"id": parent_id,
"status": "pending",
},
)
yield parent_message_tool
# Execution logs nested under the tool call
if getattr(step_log, "observations", None) and step_log.observations.strip():
log_content = re.sub(r"^Execution logs:\s*", "", step_log.observations.strip())
yield gr.ChatMessage(
role="assistant",
content=f"{log_content}",
metadata={"title": "📝 Execution Logs", "parent_id": parent_id, "status": "done"},
)
# Errors nested under the tool call
if getattr(step_log, "error", None) is not None:
yield gr.ChatMessage(
role="assistant",
content=str(step_log.error),
metadata={"title": "💥 Error", "parent_id": parent_id, "status": "done"},
)
parent_message_tool.metadata["status"] = "done"
elif getattr(step_log, "error", None) is not None:
yield gr.ChatMessage(
role="assistant",
content=str(step_log.error),
metadata={"title": "💥 Error"},
)
# Step footer: token counts + duration
step_footnote = f"{step_number}"
if hasattr(step_log, "input_token_count") and hasattr(step_log, "output_token_count"):
try:
step_footnote += (
f" | In: {step_log.input_token_count:,} tokens"
f" | Out: {step_log.output_token_count:,} tokens"
)
except Exception: # noqa: BLE001
pass
if getattr(step_log, "duration", None):
step_footnote += f" | ⏱ {round(float(step_log.duration), 2)}s"
yield gr.ChatMessage(
role="assistant",
content=f'<span style="color:#6ee7b7;font-size:11px;font-family:monospace;">{step_footnote}</span>',
)
yield gr.ChatMessage(role="assistant", content="---")
# ─────────────────────────────────────────────────────────────────────────────
# Streaming runner
# ─────────────────────────────────────────────────────────────────────────────
def stream_to_gradio(
agent,
task: str,
reset_agent_memory: bool = False,
additional_args: Optional[dict] = None,
):
"""Run the agent and stream messages as gradio ChatMessages."""
if not _is_package_available("gradio"):
raise ModuleNotFoundError(
"Please install 'gradio': pip install 'smolagents[gradio]'"
)
import gradio as gr
for step_log in agent.run(
task, stream=True, reset=reset_agent_memory, additional_args=additional_args
):
if hasattr(agent.model, "last_input_token_count") and isinstance(step_log, ActionStep):
step_log.input_token_count = agent.model.last_input_token_count
step_log.output_token_count = agent.model.last_output_token_count
for message in pull_messages_from_step(step_log):
yield message
final_answer = handle_agent_output_types(step_log)
if isinstance(final_answer, AgentText):
yield gr.ChatMessage(
role="assistant",
content=f"**✅ Final Answer:**\n\n{final_answer.to_string()}",
)
elif isinstance(final_answer, AgentImage):
yield gr.ChatMessage(
role="assistant",
content={"path": final_answer.to_string(), "mime_type": "image/png"},
)
elif isinstance(final_answer, AgentAudio):
yield gr.ChatMessage(
role="assistant",
content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
)
else:
yield gr.ChatMessage(role="assistant", content=f"**✅ Final Answer:** {str(final_answer)}")
# ─────────────────────────────────────────────────────────────────────────────
# GradioUI class — BRIANNA's full interface
# ─────────────────────────────────────────────────────────────────────────────
class GradioUI:
"""BRIANNA's full-capability Gradio interface (chat + voice + files)."""
ALLOWED_MIME_TYPES = [
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"text/plain",
"text/x-python",
"text/markdown",
"image/png",
"image/jpeg",
"image/webp",
"image/gif",
]
def __init__(self, agent: MultiStepAgent, file_upload_folder: str = "uploads"):
if not _is_package_available("gradio"):
raise ModuleNotFoundError(
"Please install 'gradio': pip install 'smolagents[gradio]'"
)
self.agent = agent
self.file_upload_folder = file_upload_folder
os.makedirs(self.file_upload_folder, exist_ok=True)
# ── File handling ──────────────────────────────────────────────────────
def upload_file(self, file, file_uploads_log):
import gradio as gr
if file is None:
return gr.Textbox("No file uploaded", visible=True), file_uploads_log
try:
mime_type, _ = mimetypes.guess_type(file.name)
except Exception as e: # noqa: BLE001
return gr.Textbox(f"Error: {e}", visible=True), file_uploads_log
if mime_type not in self.ALLOWED_MIME_TYPES:
return (
gr.Textbox(
f"⚠️ File type '{mime_type}' not allowed. "
"Supported: PDF, DOCX, TXT, Python, Markdown, PNG/JPG/WEBP/GIF",
visible=True,
),
file_uploads_log,
)
original_name = os.path.basename(file.name)
sanitized_name = re.sub(r"[^\w\-.]", "_", original_name)
file_path = os.path.join(self.file_upload_folder, sanitized_name)
shutil.copy(file.name, file_path)
return (
gr.Textbox(f"✅ Uploaded: {sanitized_name}", visible=True),
file_uploads_log + [file_path],
)
def log_user_message(self, text_input, file_uploads_log):
"""Attach uploaded file paths to the user message and clear the textbox."""
full_message = text_input or ""
if file_uploads_log:
full_message += (
f"\n\nYou have been provided with these files (use them as needed): "
f"{file_uploads_log}"
)
return full_message, ""
# ── Voice in ───────────────────────────────────────────────────────────
def transcribe_voice(self, audio_path):
"""Microphone → text. Returns the recognised text for the input box."""
if not audio_path:
return ""
return _voice_transcribe(audio_path)
# ── Chat interaction (streams chat, then speaks the final answer) ───────
def interact_with_agent(self, prompt, messages):
import gradio as gr
if not prompt or not str(prompt).strip():
yield messages, None
return
messages.append(gr.ChatMessage(role="user", content=prompt))
yield messages, None
last_text = ""
for msg in stream_to_gradio(self.agent, task=prompt, reset_agent_memory=False):
messages.append(msg)
if isinstance(getattr(msg, "content", None), str) and msg.content.strip() not in ("", "---"):
last_text = msg.content
yield messages, None
# Speak BRIANNA's final answer aloud (autoplayed by the audio component).
spoken_path = _voice_synthesize(last_text)
yield messages, spoken_path
def clear_chat(self):
"""Reset conversation history."""
return [], [], None
# ── Launch ─────────────────────────────────────────────────────────────
def launch(self, **kwargs):
import gradio as gr
custom_css = """
:root {
--bg-deep:#020810; --bg-panel:#060d1a; --bg-card:#0a1628;
--accent:#00e5a0; --accent-dim:#00a06e; --accent2:#3b82f6;
--text:#e2eaf8; --text-muted:#6b7fa3; --border:#1a2d4a; --error:#f87171;
}
.gradio-container { background: var(--bg-deep) !important; color: var(--text) !important; }
.brianna-header { text-align:center; padding:24px 0 6px 0; }
.brianna-header h1 {
font-weight:800; font-size:2.6rem; letter-spacing:0.12em; margin:0;
background:linear-gradient(90deg,#00e5a0 0%,#3b82f6 60%,#a78bfa 100%);
-webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
}
.brianna-header p {
color:var(--text-muted); font-family:monospace; font-size:0.74rem;
letter-spacing:0.16em; margin:6px 0 0 0;
}
.capability-pills { display:flex; flex-wrap:wrap; gap:6px; justify-content:center; margin:10px 0 16px 0; }
.pill {
background:var(--bg-card); border:1px solid var(--border); border-radius:20px;
padding:3px 12px; font-family:monospace; font-size:0.7rem; color:var(--text-muted);
}
button.primary {
background:linear-gradient(135deg,var(--accent) 0%,var(--accent-dim) 100%) !important;
color:#020810 !important; font-weight:600 !important; border:none !important;
}
footer { display:none !important; }
"""
EXAMPLES = [
"Give me a PyTorch transformer template and explain each block",
"Search the web for the latest papers on diffusion models and summarise the top 3",
"Find the most downloaded text-classification model on HuggingFace",
"Generate an image of a glowing neural network diagram",
"What time is it right now in Tokyo and New York?",
"Create a new tool that converts text to a word-frequency dictionary",
]
with gr.Blocks(css=custom_css, title="BRIANNA — ML Code Agent", fill_height=True) as demo:
stored_messages = gr.State([])
file_uploads_log = gr.State([])
gr.HTML(
"""
<div class="brianna-header">
<h1>BRIANNA</h1>
<p>BRILLIANTLY RESPONSIVE INTELLIGENT ASSISTANT FOR NEURAL NETWORK APPLICATIONS</p>
</div>
<div class="capability-pills">
<span class="pill">🎙️ Voice</span>
<span class="pill">🔍 Web Search</span>
<span class="pill">🧠 PyTorch / ML</span>
<span class="pill">🤗 HuggingFace Hub</span>
<span class="pill">🎨 Image Gen</span>
<span class="pill">📁 Files</span>
<span class="pill">🔧 Self-Extension</span>
<span class="pill">🌐 Custom IP / API</span>
</div>
"""
)
chatbot = gr.Chatbot(
label="",
type="messages",
height=520,
show_copy_button=True,
avatar_images=(None, None),
)
# Spoken reply (autoplays BRIANNA's final answer).
voice_output = gr.Audio(
label="🔊 BRIANNA speaks",
autoplay=True,
interactive=False,
visible=True,
)
with gr.Row(equal_height=True):
text_input = gr.Textbox(
lines=1,
label="Message BRIANNA",
placeholder="Type, or use the mic below — ML, code, web research, or 'create a new tool that…'",
scale=5,
)
send_btn = gr.Button("Send ▶", variant="primary", scale=1, min_width=80)
clear_btn = gr.Button("🗑 Clear", variant="secondary", scale=1, min_width=80)
with gr.Accordion("🎙️ Talk to BRIANNA — record, and she replies out loud", open=True):
voice_input = gr.Audio(
sources=["microphone"],
type="filepath",
label="Hold to record, release to send",
)
with gr.Accordion("📎 Attach Files (PDF, DOCX, TXT, Python, Images)", open=False):
upload_file = gr.File(label="Upload file", file_count="single")
upload_status = gr.Textbox(
label="Upload Status", interactive=False, visible=False, max_lines=1
)
with gr.Accordion("💡 Example Prompts — click to use", open=False):
for ex in EXAMPLES:
ex_btn = gr.Button(f"› {ex}", size="sm")
ex_btn.click(fn=lambda msg=ex: msg, outputs=text_input)
# ── Wiring ───────────────────────────────────────────────────
upload_file.change(
self.upload_file,
inputs=[upload_file, file_uploads_log],
outputs=[upload_status, file_uploads_log],
)
# Text submit (button or Enter): log message → run agent → speak.
for trigger in [send_btn.click, text_input.submit]:
trigger(
self.log_user_message,
inputs=[text_input, file_uploads_log],
outputs=[stored_messages, text_input],
).then(
self.interact_with_agent,
inputs=[stored_messages, chatbot],
outputs=[chatbot, voice_output],
)
# Voice submit: transcribe → fill textbox → run agent → speak (hands-free).
voice_input.stop_recording(
self.transcribe_voice,
inputs=[voice_input],
outputs=[text_input],
).then(
self.log_user_message,
inputs=[text_input, file_uploads_log],
outputs=[stored_messages, text_input],
).then(
self.interact_with_agent,
inputs=[stored_messages, chatbot],
outputs=[chatbot, voice_output],
)
clear_btn.click(self.clear_chat, outputs=[chatbot, stored_messages, voice_output])
demo.launch(**kwargs)
__all__ = ["stream_to_gradio", "GradioUI"]