File size: 3,339 Bytes
ad6dc26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""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()


@asynccontextmanager
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()