Spaces:
Sleeping
Sleeping
| """Gradio chat UI for the Meridian support chatbot. | |
| Run with: uv run python -m app.ui | |
| """ | |
| import asyncio | |
| import uuid | |
| from contextlib import asynccontextmanager | |
| import gradio as gr | |
| from agents import SQLiteSession | |
| from .agent import build_agent | |
| from .chat import chat_turn | |
| from .config import Config | |
| from .mcp_client import build_mcp_server | |
| CONFIG = Config() | |
| async def lifespan(): | |
| """Connect MCP once, share across all chat turns; clean up on exit.""" | |
| server = build_mcp_server(CONFIG.mcp_server_url) | |
| await server.connect() | |
| try: | |
| yield server | |
| finally: | |
| await server.cleanup() | |
| def build_ui() -> gr.Blocks: | |
| with gr.Blocks(title="Meridian Support") as demo: | |
| gr.Markdown("# Meridian Electronics — Support Chat") | |
| cost_badge = gr.Markdown("**Conversation cost:** $0.00000") | |
| chatbot = gr.Chatbot(height=480) | |
| msg = gr.Textbox(placeholder="Ask about products, orders, or your account...", show_label=False) | |
| clear = gr.Button("Reset conversation") | |
| # Per-browser-session state. | |
| agent_state = gr.State() | |
| session_state = gr.State() | |
| running_cost = gr.State(0.0) | |
| async def initialize(): | |
| # Lazy: a single shared MCP server, attached to a fresh agent + session per browser. | |
| # For the prototype we accept that the MCP server reconnects per Gradio process restart. | |
| server = build_mcp_server(CONFIG.mcp_server_url) | |
| await server.connect() | |
| agent = build_agent(CONFIG.openai_model, server) | |
| session = SQLiteSession(f"meridian-{uuid.uuid4().hex[:8]}") | |
| return agent, session, 0.0, "**Conversation cost:** $0.00000" | |
| async def respond(user_message, history, agent, session, cost_so_far): | |
| history = (history or []) + [ | |
| {"role": "user", "content": user_message}, | |
| {"role": "assistant", "content": ""}, | |
| ] | |
| cost_label = f"**Conversation cost:** ${cost_so_far:.5f}" | |
| yield history, cost_so_far, cost_label, "" | |
| text = "" | |
| async for partial, cost in chat_turn(user_message, agent, session): | |
| text = partial | |
| history[-1]["content"] = text | |
| if cost is None: | |
| yield history, cost_so_far, cost_label, "" | |
| else: | |
| cost_so_far = cost_so_far + cost.usd | |
| cost_label = ( | |
| f"**Conversation cost:** ${cost_so_far:.5f} " | |
| f"· last turn: {cost.format()}" | |
| ) | |
| yield history, cost_so_far, cost_label, "" | |
| async def reset(): | |
| session = SQLiteSession(f"meridian-{uuid.uuid4().hex[:8]}") | |
| return [], session, 0.0, "**Conversation cost:** $0.00000" | |
| demo.load(initialize, outputs=[agent_state, session_state, running_cost, cost_badge]) | |
| msg.submit( | |
| respond, | |
| inputs=[msg, chatbot, agent_state, session_state, running_cost], | |
| outputs=[chatbot, running_cost, cost_badge, msg], | |
| ) | |
| clear.click(reset, outputs=[chatbot, session_state, running_cost, cost_badge]) | |
| return demo | |
| demo = build_ui() | |
| if __name__ == "__main__": | |
| demo.queue().launch() | |