CoolFace
Apppublic

baqr/computer_use_ootb

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
app.py634 linesDownload Raw Back to root
1"""2Entrypoint for Gradio, see https://gradio.app/3"""4 5import platform6import asyncio7import base648import os9import io10import json11from datetime import datetime12from enum import StrEnum13from functools import partial14from pathlib import Path15from typing import cast, Dict16from PIL import Image17 18import gradio as gr19from anthropic import APIResponse20from anthropic.types import TextBlock21from anthropic.types.beta import BetaMessage, BetaTextBlock, BetaToolUseBlock22from anthropic.types.tool_use_block import ToolUseBlock23 24from screeninfo import get_monitors25from computer_use_demo.tools.logger import logger, truncate_string26 27logger.info("Starting the gradio app")28 29screens = get_monitors()30logger.info(f"Found {len(screens)} screens")31 32from computer_use_demo.loop import APIProvider, sampling_loop_sync33 34from computer_use_demo.tools import ToolResult35from computer_use_demo.tools.computer import get_screen_details36SCREEN_NAMES, SELECTED_SCREEN_INDEX = get_screen_details()37 38API_KEY_FILE = "./api_keys.json"39 40WARNING_TEXT = "⚠️ Security Alert: Do not provide access to sensitive accounts or data, as malicious web content can hijack Agent's behavior. Keep monitor on the Agent's actions."41 42 43def setup_state(state):44 45    if "messages" not in state:46        state["messages"] = []47    # -------------------------------48    if "planner_model" not in state:49        state["planner_model"] = "gpt-4o"  # default50    if "actor_model" not in state:51        state["actor_model"] = "ShowUI"    # default52 53    if "planner_provider" not in state:54        state["planner_provider"] = "openai"  # default55    if "actor_provider" not in state:56        state["actor_provider"] = "local"    # default57 58     # Fetch API keys from environment variables59    if "openai_api_key" not in state: 60        state["openai_api_key"] = os.getenv("OPENAI_API_KEY", "")61    if "anthropic_api_key" not in state:62        state["anthropic_api_key"] = os.getenv("ANTHROPIC_API_KEY", "")    63    if "qwen_api_key" not in state:64        state["qwen_api_key"] = os.getenv("QWEN_API_KEY", "")65    if "ui_tars_url" not in state:66        state["ui_tars_url"] = ""67 68    # Set the initial api_key based on the provider69    if "planner_api_key" not in state:70        if state["planner_provider"] == "openai":71            state["planner_api_key"] = state["openai_api_key"]72        elif state["planner_provider"] == "anthropic":73            state["planner_api_key"] = state["anthropic_api_key"]74        elif state["planner_provider"] == "qwen":75            state["planner_api_key"] = state["qwen_api_key"]76        else:77            state["planner_api_key"] = ""78 79    logger.info(f"loaded initial api_key for {state['planner_provider']}: {state['planner_api_key']}")80 81    if not state["planner_api_key"]:82        logger.warning("Planner API key not found. Please set it in the environment or paste in textbox.")83 84 85    if "selected_screen" not in state:86        state['selected_screen'] = SELECTED_SCREEN_INDEX if SCREEN_NAMES else 087 88    if "auth_validated" not in state:89        state["auth_validated"] = False90    if "responses" not in state:91        state["responses"] = {}92    if "tools" not in state:93        state["tools"] = {}94    if "only_n_most_recent_images" not in state:95        state["only_n_most_recent_images"] = 10 # 1096    if "custom_system_prompt" not in state:97        state["custom_system_prompt"] = ""98        # remove if want to use default system prompt99        device_os_name = "Windows" if platform.system() == "Windows" else "Mac" if platform.system() == "Darwin" else "Linux"100        state["custom_system_prompt"] += f"\n\nNOTE: you are operating a {device_os_name} machine"101    if "hide_images" not in state:102        state["hide_images"] = False103    if 'chatbot_messages' not in state:104        state['chatbot_messages'] = []105        106    if "showui_config" not in state:107        state["showui_config"] = "Default"108    if "max_pixels" not in state:109        state["max_pixels"] = 1344110    if "awq_4bit" not in state:111        state["awq_4bit"] = False112 113 114async def main(state):115    """Render loop for Gradio"""116    setup_state(state)117    return "Setup completed"118 119 120def validate_auth(provider: APIProvider, api_key: str | None):121    if provider == APIProvider.ANTHROPIC:122        if not api_key:123            return "Enter your Anthropic API key to continue."124    if provider == APIProvider.BEDROCK:125        import boto3126 127        if not boto3.Session().get_credentials():128            return "You must have AWS credentials set up to use the Bedrock API."129    if provider == APIProvider.VERTEX:130        import google.auth131        from google.auth.exceptions import DefaultCredentialsError132 133        if not os.environ.get("CLOUD_ML_REGION"):134            return "Set the CLOUD_ML_REGION environment variable to use the Vertex API."135        try:136            google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])137        except DefaultCredentialsError:138            return "Your google cloud credentials are not set up correctly."139 140 141def _api_response_callback(response: APIResponse[BetaMessage], response_state: dict):142    response_id = datetime.now().isoformat()143    response_state[response_id] = response144 145 146def _tool_output_callback(tool_output: ToolResult, tool_id: str, tool_state: dict):147    tool_state[tool_id] = tool_output148 149 150def chatbot_output_callback(message, chatbot_state, hide_images=False, sender="bot"):151    152    def _render_message(message: str | BetaTextBlock | BetaToolUseBlock | ToolResult, hide_images=False):153    154        logger.info(f"_render_message: {str(message)[:100]}")155 156        if isinstance(message, str):157            return message158        159        is_tool_result = not isinstance(message, str) and (160            isinstance(message, ToolResult)161            or message.__class__.__name__ == "ToolResult"162            or message.__class__.__name__ == "CLIResult"163        )164        if not message or (165            is_tool_result166            and hide_images167            and not hasattr(message, "error")168            and not hasattr(message, "output")169        ):  # return None if hide_images is True170            return171        # render tool result172        if is_tool_result:173            message = cast(ToolResult, message)174            if message.output:175                return message.output176            if message.error:177                return f"Error: {message.error}"178            if message.base64_image and not hide_images:179                # somehow can't display via gr.Image180                # image_data = base64.b64decode(message.base64_image)181                # return gr.Image(value=Image.open(io.BytesIO(image_data)))182                return f'<img src="data:image/png;base64,{message.base64_image}">'183 184        elif isinstance(message, BetaTextBlock) or isinstance(message, TextBlock):185            return message.text186        elif isinstance(message, BetaToolUseBlock) or isinstance(message, ToolUseBlock):187            return f"Tool Use: {message.name}\nInput: {message.input}"188        else:  189            return message190 191 192    # processing Anthropic messages193    message = _render_message(message, hide_images)194    195    if sender == "bot":196        chatbot_state.append((None, message))197    else:198        chatbot_state.append((message, None))199 200    # Create a concise version of the chatbot state for logging201    concise_state = [(truncate_string(user_msg), truncate_string(bot_msg)) for user_msg, bot_msg in chatbot_state]202    logger.info(f"chatbot_output_callback chatbot_state: {concise_state} (truncated)")203 204 205def process_input(user_input, state):206    207    setup_state(state)208 209    # Append the user message to state["messages"]210    state["messages"].append(211            {212                "role": "user",213                "content": [TextBlock(type="text", text=user_input)],214            }215        )216 217    # Append the user's message to chatbot_messages with None for the assistant's reply218    state['chatbot_messages'].append((user_input, None))219    yield state['chatbot_messages']  # Yield to update the chatbot UI with the user's message220 221    # Run sampling_loop_sync with the chatbot_output_callback222    for loop_msg in sampling_loop_sync(223        system_prompt_suffix=state["custom_system_prompt"],224        planner_model=state["planner_model"],225        planner_provider=state["planner_provider"],226        actor_model=state["actor_model"],227        actor_provider=state["actor_provider"],228        messages=state["messages"],229        output_callback=partial(chatbot_output_callback, chatbot_state=state['chatbot_messages'], hide_images=state["hide_images"]),230        tool_output_callback=partial(_tool_output_callback, tool_state=state["tools"]),231        api_response_callback=partial(_api_response_callback, response_state=state["responses"]),232        api_key=state["planner_api_key"],233        only_n_most_recent_images=state["only_n_most_recent_images"],234        selected_screen=state['selected_screen'],235        showui_max_pixels=state['max_pixels'],236        showui_awq_4bit=state['awq_4bit']237    ):  238        if loop_msg is None:239            yield state['chatbot_messages']240            logger.info("End of task. Close the loop.")241            break242            243 244        yield state['chatbot_messages']  # Yield the updated chatbot_messages to update the chatbot UI245 246 247with gr.Blocks(theme=gr.themes.Soft()) as demo:248    249    state = gr.State({})  # Use Gradio's state management250    setup_state(state.value)  # Initialize the state251 252    # Retrieve screen details253    gr.Markdown("# Computer Use OOTB")254 255    if not os.getenv("HIDE_WARNING", False):256        gr.Markdown(WARNING_TEXT)257 258    with gr.Accordion("Settings", open=True): 259        with gr.Row():260            with gr.Column():261                # --------------------------262                # Planner263                planner_model = gr.Dropdown(264                    label="Planner Model",265                    choices=["gpt-4o", 266                             "gpt-4o-mini", 267                             "qwen2-vl-max", 268                             "qwen2-vl-2b (local)", 269                             "qwen2-vl-7b (local)",270                             "qwen2-vl-2b (ssh)", 271                             "qwen2-vl-7b (ssh)",272                             "qwen2.5-vl-7b (ssh)", 273                             "claude-3-5-sonnet-20241022"],274                    value="gpt-4o",275                    interactive=True,276                )277            with gr.Column():278                planner_api_provider = gr.Dropdown(279                    label="API Provider",280                    choices=[option.value for option in APIProvider],281                    value="openai",282                    interactive=False,283                )284            with gr.Column():285                planner_api_key = gr.Textbox(286                    label="Planner API Key",287                    type="password",288                    value=state.value.get("planner_api_key", ""),289                    placeholder="Paste your planner model API key",290                    interactive=True,291                )292 293            with gr.Column():294                actor_model = gr.Dropdown(295                    label="Actor Model",296                    choices=["ShowUI", "UI-TARS"],297                    value="ShowUI",298                    interactive=True,299                )300 301            with gr.Column():302                custom_prompt = gr.Textbox(303                    label="System Prompt Suffix",304                    value="",305                    interactive=True,306                )307            with gr.Column():308                screen_options, primary_index = get_screen_details()309                SCREEN_NAMES = screen_options310                SELECTED_SCREEN_INDEX = primary_index311                screen_selector = gr.Dropdown(312                    label="Select Screen",313                    choices=screen_options,314                    value=screen_options[primary_index] if screen_options else None,315                    interactive=True,316                )317            with gr.Column():318                only_n_images = gr.Slider(319                    label="N most recent screenshots",320                    minimum=0,321                    maximum=10,322                    step=1,323                    value=2,324                    interactive=True,325                )326    327    with gr.Accordion("ShowUI Advanced Settings", open=False):  328        329        gr.Markdown("""330                    **Note:** Adjust these settings to fine-tune the resource (**memory** and **infer time**) and performance trade-offs of ShowUI. \\331                    Quantization model requires additional download. Please refer to [Computer Use OOTB - #ShowUI Advanced Settings guide](https://github.com/showlab/computer_use_ootb?tab=readme-ov-file#showui-advanced-settings) for preparation for this feature.332                    """)333 334        # New configuration for ShowUI335        with gr.Row():336            with gr.Column():337                showui_config = gr.Dropdown(338                    label="ShowUI Preset Configuration",339                    choices=["Default (Maximum)", "Medium", "Minimal", "Custom"],340                    value="Default (Maximum)",341                    interactive=True,342                )343            with gr.Column():344                max_pixels = gr.Slider(345                    label="Max Visual Tokens",346                    minimum=720,347                    maximum=1344,348                    step=16,349                    value=1344,350                    interactive=False,351                )352            with gr.Column():353                awq_4bit = gr.Checkbox(354                    label="Enable AWQ-4bit Model",355                    value=False,356                    interactive=False357                )358            359    # Define the merged dictionary with task mappings360    merged_dict = json.load(open("assets/examples/ootb_examples.json", "r"))361 362    def update_only_n_images(only_n_images_value, state):363        state["only_n_most_recent_images"] = only_n_images_value364    365    # Callback to update the second dropdown based on the first selection366    def update_second_menu(selected_category):367        return gr.update(choices=list(merged_dict.get(selected_category, {}).keys()))368 369    # Callback to update the third dropdown based on the second selection370    def update_third_menu(selected_category, selected_option):371        return gr.update(choices=list(merged_dict.get(selected_category, {}).get(selected_option, {}).keys()))372 373    # Callback to update the textbox based on the third selection374    def update_textbox(selected_category, selected_option, selected_task):375        task_data = merged_dict.get(selected_category, {}).get(selected_option, {}).get(selected_task, {})376        prompt = task_data.get("prompt", "")377        preview_image = task_data.get("initial_state", "")378        task_hint = "Task Hint: " + task_data.get("hint", "")379        return prompt, preview_image, task_hint380    381    # Function to update the global variable when the dropdown changes382    def update_selected_screen(selected_screen_name, state):383        global SCREEN_NAMES384        global SELECTED_SCREEN_INDEX385        SELECTED_SCREEN_INDEX = SCREEN_NAMES.index(selected_screen_name)386        logger.info(f"Selected screen updated to: {SELECTED_SCREEN_INDEX}")387        state['selected_screen'] = SELECTED_SCREEN_INDEX388 389 390    def update_planner_model(model_selection, state):391        state["model"] = model_selection392        # Update planner_model393        state["planner_model"] = model_selection394        logger.info(f"Model updated to: {state['planner_model']}")395        396        if model_selection == "qwen2-vl-max":397            provider_choices = ["qwen"]398            provider_value = "qwen"399            provider_interactive = False400            api_key_interactive = True401            api_key_placeholder = "qwen API key"402            actor_model_choices = ["ShowUI", "UI-TARS"]403            actor_model_value = "ShowUI"404            actor_model_interactive = True405            api_key_type = "password"  # Display API key in password form406        407        elif model_selection == "qwen2-vl-2b (local)" or model_selection == "qwen2-vl-7b (local)":408            # Set provider to "openai", make it unchangeable409            provider_choices = ["local"]410            provider_value = "local"411            provider_interactive = False412            api_key_interactive = False413            api_key_placeholder = "not required"414            actor_model_choices = ["ShowUI", "UI-TARS"]415            actor_model_value = "ShowUI"416            actor_model_interactive = True417            api_key_type = "password"  # Maintain consistency418 419        elif "ssh" in model_selection:420            provider_choices = ["ssh"]421            provider_value = "ssh"422            provider_interactive = False423            api_key_interactive = True424            api_key_placeholder = "ssh host and port (e.g. localhost:8000)"425            actor_model_choices = ["ShowUI", "UI-TARS"]426            actor_model_value = "ShowUI"427            actor_model_interactive = True428            api_key_type = "text"  # Display SSH connection info in plain text429            # If SSH connection info already exists, keep it430            if "planner_api_key" in state and state["planner_api_key"]:431                state["api_key"] = state["planner_api_key"]432            else:433                state["api_key"] = ""434 435        elif model_selection == "gpt-4o" or model_selection == "gpt-4o-mini":436            # Set provider to "openai", make it unchangeable437            provider_choices = ["openai"]438            provider_value = "openai"439            provider_interactive = False440            api_key_interactive = True441            api_key_type = "password"  # Display API key in password form442 443            api_key_placeholder = "openai API key"444            actor_model_choices = ["ShowUI", "UI-TARS"]445            actor_model_value = "ShowUI"446            actor_model_interactive = True447 448        elif model_selection == "claude-3-5-sonnet-20241022":449            # Provider can be any of the current choices except 'openai'450            provider_choices = [option.value for option in APIProvider if option.value != "openai"]451            provider_value = "anthropic"  # Set default to 'anthropic'452            provider_interactive = True453            api_key_interactive = True454            api_key_placeholder = "claude API key"455            actor_model_choices = ["claude-3-5-sonnet-20241022"]456            actor_model_value = "claude-3-5-sonnet-20241022"457            actor_model_interactive = False458            api_key_type = "password"  # Display API key in password form459 460        else:461            raise ValueError(f"Model {model_selection} not supported")462 463        # Update the provider in state464        state["planner_api_provider"] = provider_value465        466        # Update api_key in state based on the provider467        if provider_value == "openai":468            state["api_key"] = state.get("openai_api_key", "")469        elif provider_value == "anthropic":470            state["api_key"] = state.get("anthropic_api_key", "")471        elif provider_value == "qwen":472            state["api_key"] = state.get("qwen_api_key", "")473        elif provider_value == "local":474            state["api_key"] = ""475        # SSH的情况已经在上面处理过了,这里不需要重复处理476 477        provider_update = gr.update(478            choices=provider_choices,479            value=provider_value,480            interactive=provider_interactive481        )482 483        # Update the API Key textbox484        api_key_update = gr.update(485            placeholder=api_key_placeholder,486            value=state["api_key"],487            interactive=api_key_interactive,488            type=api_key_type  # 添加 type 参数的更新489        )490 491        actor_model_update = gr.update(492            choices=actor_model_choices,493            value=actor_model_value,494            interactive=actor_model_interactive495        )496 497        logger.info(f"Updated state: model={state['planner_model']}, provider={state['planner_api_provider']}, api_key={state['api_key']}")498        return provider_update, api_key_update, actor_model_update499    500    def update_actor_model(actor_model_selection, state):501        state["actor_model"] = actor_model_selection502        logger.info(f"Actor model updated to: {state['actor_model']}")503 504    def update_api_key_placeholder(provider_value, model_selection):505        if model_selection == "claude-3-5-sonnet-20241022":506 507            if provider_value == "anthropic":508                return gr.update(placeholder="anthropic API key")509            elif provider_value == "bedrock":510                return gr.update(placeholder="bedrock API key")511            elif provider_value == "vertex":512                return gr.update(placeholder="vertex API key")513            else:514                return gr.update(placeholder="")515        elif model_selection == "gpt-4o + ShowUI":516            return gr.update(placeholder="openai API key")517        else:518            return gr.update(placeholder="")519 520    def update_system_prompt_suffix(system_prompt_suffix, state):521        state["custom_system_prompt"] = system_prompt_suffix522        523    # When showui_config changes, we set the max_pixels and awq_4bit accordingly.524    def handle_showui_config_change(showui_config_val, state):525        if showui_config_val == "Default (Maximum)":526            state["max_pixels"] = 1344527            state["awq_4bit"] = False528            return (529                gr.update(value=1344, interactive=False), 530                gr.update(value=False, interactive=False)531            )532        elif showui_config_val == "Medium":533            state["max_pixels"] = 1024534            state["awq_4bit"] = False535            return (536                gr.update(value=1024, interactive=False), 537                gr.update(value=False, interactive=False)538            )539        elif showui_config_val == "Minimal":540            state["max_pixels"] = 1024541            state["awq_4bit"] = True542            return (543                gr.update(value=1024, interactive=False), 544                gr.update(value=True, interactive=False)545            )546        elif showui_config_val == "Custom":547            # Do not overwrite the current user values, just make them interactive548            return (549                gr.update(interactive=True), 550                gr.update(interactive=True)551            )552 553    def update_api_key(api_key_value, state):554        """Handle API key updates"""555        state["planner_api_key"] = api_key_value556        if state["planner_provider"] == "ssh":557            state["api_key"] = api_key_value558        logger.info(f"API key updated: provider={state['planner_provider']}, api_key={state['api_key']}")559 560    with gr.Accordion("Quick Start Prompt", open=False):  # open=False 表示默认收561        # Initialize Gradio interface with the dropdowns562        with gr.Row():563            # Set initial values564            initial_category = "Game Play"565            initial_second_options = list(merged_dict[initial_category].keys())566            initial_third_options = list(merged_dict[initial_category][initial_second_options[0]].keys())567            initial_text_value = merged_dict[initial_category][initial_second_options[0]][initial_third_options[0]]568 569            with gr.Column(scale=2):570                # First dropdown for Task Category571                first_menu = gr.Dropdown(572                    choices=list(merged_dict.keys()), label="Task Category", interactive=True, value=initial_category573                )574 575                # Second dropdown for Software576                second_menu = gr.Dropdown(577                    choices=initial_second_options, label="Software", interactive=True, value=initial_second_options[0]578                )579 580                # Third dropdown for Task581                third_menu = gr.Dropdown(582                    choices=initial_third_options, label="Task", interactive=True, value=initial_third_options[0]583                    # choices=["Please select a task"]+initial_third_options, label="Task", interactive=True, value="Please select a task"584                )585 586            with gr.Column(scale=1):587                initial_image_value = "./assets/examples/init_states/honkai_star_rail_showui.png"  # default image path588                image_preview = gr.Image(value=initial_image_value, label="Reference Initial State", height=260-(318.75-280))589                hintbox = gr.Markdown("Task Hint: Selected options will appear here.")590 591        # Textbox for displaying the mapped value592        # textbox = gr.Textbox(value=initial_text_value, label="Action")593 594    # api_key.change(fn=lambda key: save_to_storage(API_KEY_FILE, key), inputs=api_key)595 596    with gr.Row():597        # submit_button = gr.Button("Submit")  # Add submit button598        with gr.Column(scale=8):599            chat_input = gr.Textbox(show_label=False, placeholder="Type a message to send to Computer Use OOTB...", container=False)600        with gr.Column(scale=1, min_width=50):601            submit_button = gr.Button(value="Send", variant="primary")602 603    chatbot = gr.Chatbot(label="Chatbot History", type="tuples", autoscroll=True, height=580)604    605    planner_model.change(fn=update_planner_model, inputs=[planner_model, state], outputs=[planner_api_provider, planner_api_key, actor_model])606    planner_api_provider.change(fn=update_api_key_placeholder, inputs=[planner_api_provider, planner_model], outputs=planner_api_key)607    actor_model.change(fn=update_actor_model, inputs=[actor_model, state], outputs=None)608 609    screen_selector.change(fn=update_selected_screen, inputs=[screen_selector, state], outputs=None)610    only_n_images.change(fn=update_only_n_images, inputs=[only_n_images, state], outputs=None)611    612    # When showui_config changes, we update max_pixels and awq_4bit automatically.613    showui_config.change(fn=handle_showui_config_change, 614                         inputs=[showui_config, state], 615                         outputs=[max_pixels, awq_4bit])616    617    # Link callbacks to update dropdowns based on selections618    first_menu.change(fn=update_second_menu, inputs=first_menu, outputs=second_menu)619    second_menu.change(fn=update_third_menu, inputs=[first_menu, second_menu], outputs=third_menu)620    third_menu.change(fn=update_textbox, inputs=[first_menu, second_menu, third_menu], outputs=[chat_input, image_preview, hintbox])621 622    # chat_input.submit(process_input, [chat_input, state], chatbot)623    submit_button.click(process_input, [chat_input, state], chatbot)624 625    planner_api_key.change(626        fn=update_api_key,627        inputs=[planner_api_key, state],628        outputs=None629    )630 631demo.launch(share=True,632            allowed_paths=["./"],633            server_port=7888)  # TODO: allowed_paths634