Custom actions class attributes

My custom action class has an attribute that is set based on the tracker. However I noticed that when action-server is simultaneously running this custom action for different conversations, this attribute gets overridden, as if both actions are run with the same instance of this custom action class. Is my speculation correct?

Code snippet:

class ActionRunLLM(Action):

    def __init__(self) -> None:
        self.llm_state = None

    def name(self) -> Text:
        return "action_run_llm"

    def other_method(self):
        llm_state = self.llm_state
        
        # do something with llm_state

    async def run(
        self,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) -> List[Dict[Text, Any]]:
        
        self.llm_state = tracker.get_slot("llm_state")

        self.other_method()

        return []

Hello,

Yes, your speculation is correct.

In your code snippet, the ActionRunLLM class has an attribute self.llm_state. Since this attribute is defined within the class itself, it’s shared across all instances of the class.

When multiple conversations are handled concurrently by the action server, each conversation will essentially be using the same ActionRunLLM class instance. This means that when the run method is executed for different conversations, the self.llm_state attribute will be overwritten for each conversation, leading to unexpected behavior.

Here’s how you can fix this:

Make llm_state a local variable within the run method: Python

class ActionRunLLM(Action):

def name(self) -> Text:
    return "action_run_llm"

def other_method(self, llm_state):
    # do something with llm_state

async def run(
    self,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
    llm_state = tracker.get_slot("llm_state") 
    self.other_method(llm_state) 
    return []

By defining llm_state within the run method, it becomes a local variable specific to each conversation, preventing the issue of shared state.

Use a thread-local storage mechanism: If you need to maintain some state across method calls within the same thread (for a specific conversation), you can use a thread-local storage mechanism. This allows you to store data that is specific to the current thread of execution.

Python

from threading import local

class ActionRunLLM(Action):

_thread_local = local() 

def name(self) -> Text:
    return "action_run_llm"

def other_method(self):
    llm_state = self._thread_local.llm_state
    # do something with llm_state

async def run(
    self,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
) -> List[Dict[Text, Any]]:
    self._thread_local.llm_state = tracker.get_slot("llm_state") 
    self.other_method() 
    return [] 

This approach ensures that each thread (representing a conversation) has its own isolated llm_state.

Key takeaway:

When dealing with concurrent execution in an action server, be mindful of shared state within your action classes. Use techniques like local variables or thread-local storage to avoid unexpected behavior.

Best Regards