Gradio Agents & MCP 黑客马拉松

获奖者
Gradio logo
  1. 使用 Blocks 构建
  2. Blocks 中的状态

管理状态

使用 gr.Blocks() 构建 Gradio 应用程序时,您可能希望在用户之间共享某些值(例如,页面访问者的计数),或者在特定交互中为单个用户持久化值(例如,聊天历史记录)。这被称为**状态**,在 Gradio 应用程序中管理状态有三种通用方法:

  • 全局状态:在 Gradio 应用程序运行时,在所有用户之间持久化和共享值。
  • 会话状态:在每个用户使用 Gradio 应用程序的单个会话期间持久化值。如果他们刷新页面,会话状态将被重置。
  • 浏览器状态:在浏览器的 localStorage 中为 Gradio 应用程序的每个用户持久化值,即使页面刷新或关闭后数据仍能持久存在。

全局状态

Gradio 应用程序中的全局状态非常简单:任何在函数外部创建的变量都在所有用户之间全局共享。

这使得全局状态的管理非常简单,并且不需要外部服务。例如,在此应用程序中,visitor_count 变量在所有用户之间共享。

import gradio as gr

# Shared between all users
visitor_count = 0

def increment_counter():
    global visitor_count
    visitor_count += 1
    return visitor_count

with gr.Blocks() as demo:    
    number = gr.Textbox(label="Total Visitors", value="Counting...")
    demo.load(increment_counter, inputs=None, outputs=number)

demo.launch()

这意味着任何时候您不希望在用户之间共享值时,您都应该在函数*内部*声明它。但是,如果您需要在函数调用之间共享值,例如聊天历史记录,该怎么办?在这种情况下,您应该使用以下方法之一来管理状态。

会话状态

Gradio 支持会话状态,数据在单个页面会话的多次提交中保持不变。重申一下,会话数据*不*在模型的不同用户之间共享,并且如果用户刷新页面以重新加载 Gradio 应用程序,数据也*不*会持久存在。要将会话数据存储到状态中,您需要做三件事:

  1. 创建一个 gr.State() 对象。如果此有状态对象有默认值,请将其传递给构造函数。请注意,gr.State 对象必须是 可深度复制的,否则您将需要使用下面描述的不同方法。
  2. 在事件监听器中,根据需要将 State 对象作为输入和输出。
  3. 在事件监听函数中,将变量添加到输入参数和返回值中。

让我们看一个简单的例子。下面有一个简单的结账应用程序,您可以在其中将商品添加到购物车中。您还可以查看购物车的大小。

import gradio as gr

with gr.Blocks() as demo:
    cart = gr.State([])
    items_to_add = gr.CheckboxGroup(["Cereal", "Milk", "Orange Juice", "Water"])

    def add_items(new_items, previous_cart):
        cart = previous_cart + new_items
        return cart

    gr.Button("Add Items").click(add_items, [items_to_add, cart], cart)

    cart_size = gr.Number(label="Cart Size")
    cart.change(lambda cart: len(cart), cart, cart_size)

demo.launch()

请注意我们如何使用状态来实现这一点:

  1. 我们将购物车商品存储在 gr.State() 对象中,此处将其初始化为空列表。
  2. 当将商品添加到购物车时,事件监听器将购物车作为输入和输出——它返回包含所有商品在内的更新后的购物车。
  3. 我们可以将一个 .change 监听器附加到购物车,该监听器也使用状态变量作为输入。

您可以将 gr.State 视为一个不可见的 Gradio 组件,它可以存储任何类型的值。在这里,cart 在前端不可见,但用于计算。

状态变量的 .change 监听器在任何事件监听器更改状态变量的值后触发。如果状态变量包含序列(如 listsetdict),则当其中任何元素发生更改时会触发更改。如果它包含对象或原始类型,则当值的**哈希**发生更改时会触发更改。因此,如果您定义一个自定义类并创建一个作为该类实例的 gr.State 变量,请确保该类包含一个合理的 __hash__ 实现。

当用户刷新页面时,会话状态变量的值将被清除。用户关闭选项卡后,该值会在应用程序后端存储 60 分钟(这可以通过 gr.Blocks 中的 delete_cache 参数进行配置)。

文档 中了解更多关于 State 的信息。

那些无法深度复制的对象怎么办?

如前所述,存储在 gr.State 中的值必须是 可深度复制的。如果您正在处理一个无法深度复制的复杂对象,您可以采取不同的方法,手动读取用户的 session_hash 并存储一个全局的 dictionary,其中包含每个用户的对象实例。您可以这样做:

import gradio as gr

class NonDeepCopyable:
    def __init__(self):
        from threading import Lock
        self.counter = 0
        self.lock = Lock()  # Lock objects cannot be deepcopied
    
    def increment(self):
        with self.lock:
            self.counter += 1
            return self.counter

# Global dictionary to store user-specific instances
instances = {}

def initialize_instance(request: gr.Request):
    instances[request.session_hash] = NonDeepCopyable()
    return "Session initialized!"

def cleanup_instance(request: gr.Request):
    if request.session_hash in instances:
        del instances[request.session_hash]

def increment_counter(request: gr.Request):
    if request.session_hash in instances:
        instance = instances[request.session_hash]
        return instance.increment()
    return "Error: Session not initialized"

with gr.Blocks() as demo:
    output = gr.Textbox(label="Status")
    counter = gr.Number(label="Counter Value")
    increment_btn = gr.Button("Increment Counter")
    increment_btn.click(increment_counter, inputs=None, outputs=counter)
    
    # Initialize instance when page loads
    demo.load(initialize_instance, inputs=None, outputs=output)    
    # Clean up instance when page is closed/refreshed
    demo.unload(cleanup_instance)    

demo.launch()

浏览器状态

Gradio 还支持浏览器状态,数据即使在页面刷新或关闭后也能持久保存在浏览器的 localStorage 中。这对于存储用户偏好、设置、API 密钥或其他应该跨会话持久存在的数据非常有用。要使用本地状态:

  1. 创建一个 gr.BrowserState 对象。您可以选择提供一个初始默认值和一个键,以便在浏览器的 localStorage 中标识数据。
  2. 像常规的 gr.State 组件一样,在事件监听器中将其用作输入和输出。

这是一个简单的示例,它在会话之间保存用户的用户名和密码:

import random
import string
import gradio as gr
import time
with gr.Blocks() as demo:
    gr.Markdown("Your Username and Password will get saved in the browser's local storage. "
                "If you refresh the page, the values will be retained.")
    username = gr.Textbox(label="Username")
    password = gr.Textbox(label="Password", type="password")
    btn = gr.Button("Generate Randomly")
    local_storage = gr.BrowserState(["", ""])
    saved_message = gr.Markdown("✅ Saved to local storage", visible=False)

    @btn.click(outputs=[username, password])
    def generate_randomly():
        u = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        p = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        return u, p

    @demo.load(inputs=[local_storage], outputs=[username, password])
    def load_from_local_storage(saved_values):
        print("loading from local storage", saved_values)
        return saved_values[0], saved_values[1]

    @gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage])
    def save_to_local_storage(username, password):
        return [username, password]

    @gr.on(local_storage.change, outputs=[saved_message])
    def show_saved_message():
        timestamp = time.strftime("%I:%M:%S %p")
        return gr.Markdown(
            f"✅ Saved to local storage at {timestamp}",
            visible=True
        )

demo.launch()

注意:如果 Gradio 应用程序重新启动,存储在 gr.BrowserState 中的值不会持久化。为了使其持久化,您可以在 gr.BrowserState 组件中硬编码 storage_keysecret 的特定值,并在相同的服务器名称和服务器端口上重新启动 Gradio 应用程序。但是,这仅应在您运行受信任的 Gradio 应用程序时进行,因为原则上,这可能允许一个 Gradio 应用程序访问由不同 Gradio 应用程序创建的 localStorage 数据。