""" File: app.py :author: Didier Guillevic :email: didier.guillevic@gmail.com :date: 2026-03-08 :license: Apache License 2.0 """ """ Agentic Chat Interface ====================== A Gradio chat UI backed by a smolagents CodeAgent connected to a locally-hosted Ministral-3B-Instruct model served via llama.cpp's OpenAI-compatible endpoint. Usage ----- 1. Start your llama-server, e.g.: llama-server -hf unsloth/Ministral-3-8B-Instruct-2512-GGUF --jinja \ --host 127.0.0.1 --port 8080 --api-key "Keep learning" \ --ctx-size 32768 --parallel 1 2. Install dependencies: pip install "smolagents[all]" 3. Run: python app.py The agent is initialised with all built-in smolagents tools: - DuckDuckGoSearchTool – web search - VisitWebpageTool – fetch & read a URL - PythonInterpreterTool – execute arbitrary Python - FinalAnswerTool – emit the final answer Every intermediate step (code written, tool calls, observations) is captured and shown inside a collapsible Accordion panel above the assistant reply. Example questions ----------------- • "What are the latest news about agentic systems?" • "What is the 112th number in the Fibonacci sequence?" """ import textwrap from datetime import date import os import gradio as gr from smolagents import CodeAgent, OpenAIServerModel, LiteLLMModel from smolagents import ( DuckDuckGoSearchTool, PythonInterpreterTool, VisitWebpageTool, ) from smolagents import ( FinalAnswerStep, ActionStep, ) # --------------------------------------------------------------------------- # Configuration – adjust to match your llama-server setup # --------------------------------------------------------------------------- MODEL_ID = "Ministral-3-8B-Instruct-2512" LLAMA_SERVER_BASE_URL = "http://127.0.0.1:8080/v1" API_KEY = "Keep learning" MAX_STEPS = 10 # --------------------------------------------------------------------------- # Model & Agent # --------------------------------------------------------------------------- #model = OpenAIServerModel( # model_id=MODEL_ID, # api_base=LLAMA_SERVER_BASE_URL, # api_key=API_KEY, # temperature=0.5 #) MODEL_GEMINI_ID = "gemini/gemini-2.5-flash-lite" API_KEY_GEMINI = os.environ["GEMINI_API_KEY"] model = LiteLLMModel( model_id=MODEL_GEMINI_ID, api_key=API_KEY_GEMINI, temperature=0.5 ) tools = [DuckDuckGoSearchTool(), VisitWebpageTool(), PythonInterpreterTool()] agent = CodeAgent(tools=tools, model=model, max_steps=MAX_STEPS, verbosity_level=2) # --------------------------------------------------------------------------- # Helper – format a single agent step for display # --------------------------------------------------------------------------- def format_step(step): if isinstance(step, ActionStep): parts = [f"🧠 **Step {step.step_number}**"] if getattr(step, "model_output", None): # Extract the Thought portion (text before the code block) model_output = str(step.model_output).strip() thought_part = model_output.split("```")[0].strip() if thought_part: parts.append(f"💭 **Thought:**\n{thought_part}") if getattr(step, "code", None): parts.append(f"💻 **Code:**\n```python\n{step.code.strip()}\n```") if getattr(step, "observations", None): #obs = textwrap.shorten(str(step.observations).strip(), width=500, placeholder="... (truncated)") obs = textwrap.fill(str(step.observations).strip(), width=70) parts.append(f"🔭 **Observation:**\n> {obs}") if getattr(step, "error", None): parts.append(f"❌ **Error:** {step.error}") return "\n\n".join(parts) if isinstance(step, FinalAnswerStep): return f"✅ **Final Answer**\n{step.output}" return str(step) # --------------------------------------------------------------------------- # Core generator – streams steps live to the Gradio UI # --------------------------------------------------------------------------- def run_agent(user_message, history): if history is None: history = [] today = date.today().strftime("%A, %B %d, %Y") augmented_message = f"[Today's date is {today}]\n\n{user_message}" # Add user message + a placeholder bubble for the first step history = history + [ {"role": "user", "content": user_message}, {"role": "assistant", "content": "⏳ Agent starting..."}, ] # Switch button to "Cancel" (red) while processing yield history, "", gr.update(value="⏹ Cancel", variant="stop", interactive=True) final_answer = None step_count = 0 steps = [] # accumulate rendered steps for the final collapse first_step_index = len(history) - 1 # index of the first assistant bubble we control try: for event in agent.run(augmented_message, stream=True, reset=True): if isinstance(event, FinalAnswerStep): final_answer = event.output continue step_count += 1 step_md = format_step(event) steps.append(step_md) # Fill in the current placeholder bubble with the live step content history[-1]["content"] = step_md yield history, "", gr.update(value="⏹ Cancel", variant="stop", interactive=True) # Append a fresh placeholder bubble ready for the next step history = history + [{"role": "assistant", "content": f"⏳ Working on step {step_count + 1}..."}] yield history, "", gr.update(value="⏹ Cancel", variant="stop", interactive=True) except Exception as e: history[-1]["content"] = f"❌ Agent error: {e}" yield history, "", gr.update(value="Send ➤", variant="primary", interactive=True) return if final_answer is None: final_answer = "_No final answer produced._" # Collapse all streamed step bubbles into a single
block, # then append the final answer — replacing everything from the first # assistant bubble onward with just two messages. steps_md = "\n\n---\n\n".join(steps) details_block = ( f"
\n🔎 Agent reasoning ({step_count} steps)\n\n" f"{steps_md}\n\n
" ) history = history[:first_step_index] + [ {"role": "assistant", "content": details_block}, {"role": "assistant", "content": f"✅ **Final Answer**\n\n{final_answer}"}, ] # Restore button to "Send" (green/primary) when done yield history, "", gr.update(value="Send ➤", variant="primary", interactive=True) # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- EXAMPLE_QUESTIONS = [ [( "What are the latest news about agentic systems?" " Please include some references with url links if possible." ),], ["What is the 112th number in the Fibonacci sequence?",], ["Compute the first 20 prime numbers.",], ] with gr.Blocks(title="🤖 (Smolagents) Agentic Chat") as demo: gr.Markdown( f""" # (Smolagents) Agentic Chat Ask anything. The agent can **search the web**, **read web pages**, as well as write and **run Python code** to answer your question. """ ) chatbot = gr.Chatbot( label="Conversation", height=300, ) msg_box = gr.Textbox( placeholder="Ask a multi-step question (e.g., Search for news then summarize)...", show_label=False, autofocus=True, ) with gr.Row(): # Single button that toggles between Send and Cancel states submit_btn = gr.Button("Send ➤", variant="primary") clear_btn = gr.Button("🗑 Clear conversation", variant="secondary") with gr.Accordion("Examples", open=False): gr.Examples( examples=EXAMPLE_QUESTIONS, inputs=msg_box, label="Example questions", ) # ----- Event wiring ----- # The submit event: runs the agent and updates the button state via the 3rd output. # Passing `submit_btn` into `cancels` means clicking it mid-run will stop the generator. submit_event = submit_btn.click( run_agent, inputs=[msg_box, chatbot], outputs=[chatbot, msg_box, submit_btn], ) textbox_event = msg_box.submit( run_agent, inputs=[msg_box, chatbot], outputs=[chatbot, msg_box, submit_btn], ) # When the button shows "Cancel" and is clicked again, cancel both running events. # Gradio detects the cancellation automatically because `cancels` references the # same event handles; the button will revert to Send on the next page interaction. # We achieve the toggle by simply cancelling the in-flight generator — the button # label/colour is reset at the end of run_agent (or on error), so after cancellation # the user just needs to click again (it will already say "⏹ Cancel" until the # generator is fully stopped, then revert on the next yield/return that never comes). # A cleaner UX: wire a dedicated cancel click that restores button state. cancel_btn_click = submit_btn.click( fn=lambda: gr.update(value="Send ➤", variant="primary", interactive=True), inputs=None, outputs=submit_btn, cancels=[submit_event, textbox_event], ) clear_btn.click(lambda: ([], gr.update(value="Send ➤", variant="primary", interactive=True)), outputs=[chatbot, submit_btn]) # --------------------------------------------------------------------------- if __name__ == "__main__": demo.launch( footer_links=["settings"], )