Skip to content

[FEATURE] [Godot] Calling tools from main thread #513

@KNITLIS

Description

@KNITLIS

First of all thank you for your great tool! It made integrating llm into a game infinitely simpler than I anticipated before stumbling upon nobodywho

I propose this feature because of few issues what are very annoying to work around them and even then solutions leaves footguns in your project:

1) Triggering redraw from a tool call

Nobodywho allows to integrate llm model into your game and quite reasonable scenario is running npc dialog with it. And when llm controls npc you want it's tools be able to interact with the world.
Lets take "door keeper" scenario as an example:

There is a door keeper in your game and player can ask them to open the door. They can do it with a tool call:

func _door_open_tool() -> String:
    if door.is_openable():
        door.texture = opened_door_texture
        return "door opened"
    return "door can't be opened"

This will throw an exception because you can't queue_redraw() from background thread. So you have to call door.texture = opened_door_texture deferred if you want to return conditional string from tool. Moreover in normal usecase tool handle is in npc script and door logic is in door script, so you just do var result: String = door.open() from tool function and then you need to design door.open() with llm tool call in mind, separating anything what would queue redraw to be called defered.

2) Awaiting signal from tool call

Sometimes you want llm powered npc to prompt player to do or "give" something to them with tool call:

signal item_given(item: ItemBase)

func _give_something_tool() -> String:
    GameUi.show_item_prompt()
    var item = await item_given
    return "Player gave you %s" % item.item_name

In that case your best choice is to create this monstrosity:

var tool_return_sema = Semaphore.new()
var tool_return_value: Variant

func _tool_wait() -> Variant:
	tool_return_sema.wait()
	return tool_return_value

func tool_return(answer: Variant):
	tool_return_value = answer
	tool_return_sema.post()

func _give_something_tool() -> String:
    GameUi.show_item_prompt.call_deferred()
    var item: ItemBase = await _tool_wait()
    return "Player gave you %s" % item.item_name

But it breaks type safety unless you're creating workaround for every answer type you can get.


So I propose moving execution of a tool call callable to main thread. Perhaps workaround from second example can become solution here:

# didn't coded enough in rust to give example in that language
var tool_return_sema = Semaphore.new()
var tool_return_string: String

func execute_tool(callable: Callable) -> String:
    _tool_wrapper.call_deferred()
    tool_return_sema.wait()
    return tool_return_string

func _tool_wrapper(callable: Callable) -> void:
    tool_return_string = callable.call()
    tool_return_sema.post()

Since calling tool halt's llm anyways there should be no issue with waiting for semaphore and no breaking type safety either since tool call can only return string

If backwards compatibility is concern here, calling from main thread can be added as flag on tool addition - func add_tool(callable: Callable, description: String, call_from_main_thread: bool = false) -> void

As an alternative solution there can be added a wrapper what would allow calling main thread exclusive functions and returning from them:

func _random_tool() -> String:
    var result = wrap_main_exclusive(_redrawing_function)
    return result

func _redrawing_function() -> String:
    door.texture = opened_door_texture
    return "abc"

That would work too, but it is way less desirable

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions