#!/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*", "```", model_output) model_output = re.sub(r"\s*```", "```", model_output) model_output = re.sub(r"```\s*\n\s*", "```", 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*\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'{step_footnote}', ) 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( """

BRIANNA

BRILLIANTLY RESPONSIVE INTELLIGENT ASSISTANT FOR NEURAL NETWORK APPLICATIONS

πŸŽ™οΈ Voice πŸ” Web Search 🧠 PyTorch / ML πŸ€— HuggingFace Hub 🎨 Image Gen πŸ“ Files πŸ”§ Self-Extension 🌐 Custom IP / API
""" ) 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"]