I need help using assitant in python (required_action infinite loop)

I have created a assistant using the openai ui and i have tested it in playground, and works perfect, its a assistant with a single function and a single input message that returns a json with some data transformed by chatgpt.
Im not clear where check in the documentation the steps to make it in python:

This are my python lines:

    thread = openai.beta.threads.create()
    message = openai.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content="Use custom instructions and return JSON using this text: " + custom_text
    )

    run = openai.beta.threads.runs.create(
            thread_id=thread.id,
            assistant_id="my assistant key created in the openai webside")
    while run.status != 'completed':
        print(run.status)
        run = openai.beta.threads.runs.retrieve(
            thread_id=thread.id,
            run_id=run.id
        )
        try:
            messages = openai.beta.threads.messages.list(
                thread_id=thread.id
            )
            print(messages)
        except:
            continue
    return messages

The thread run starts on queue, but the next state is required_action and never change, if i print the messsage i can see the custom_text added in the content.
requires_action
SyncCursorPage[ThreadMessage](data= info about the message content)
Y need execute that text using the assistant function, but i donā€™t know how.

1 Like

Run changes to required_action state when assistant calls a function. Here is snippet from OpenAI docs.

When using the Function calling tool, the Run will move to a required_action state once the model determines the names and arguments of the functions to be called. You must then run those functions and submit the outputs before the run proceeds. If the outputs are not provided before the expires_at timestamp passes (roughly 10 mins past creation), the run will move to an expired status.

So you need to process the function and call submit_outputs for run to proceed. It will remain at required_action state till you process it or the run expires(roughly 10 mins after the state has been changed to required_action)

Also run can fail, cancel or expire. You need to exit the loop when that happens.

Here is more info on the run lifecycle.

2 Likes

Hi. I have been struggling with similar issues. Can you help me by showing your complete code? I am very new to coding. I am trying to use the assistant api to loop through data with a single retrieval function and single input message at a time. To run every questions as input in an excel sequentially. Thank you!

Yes i finally have a code that is not the best but works

def myfuction():
    thread = openai.beta.threads.create()
    message = openai.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content="Explaining what do"
    )
    running=True
    run = openai.beta.threads.runs.create(
            thread_id=thread.id,
            assistant_id="My assistant ID")
    count=0

    tool_outputs = []
    while run.status != 'completed':
        run = openai.beta.threads.runs.retrieve(
                thread_id=thread.id,
                run_id=run.id
                )
        messages = openai.beta.threads.messages.list(
                thread_id=thread.id
                )
        #print(run)
        #print(messages)
        if run.status == "requires_action" and count==0:
            tool_call_id = None
            try:
                for message in run.messages:
#this part is so ugly and take me time to make it work, i didnt find it in the doc, its to execute the function
                    if "tool_call_id" in message.content:
                        tool_call_id = message.content.split("tool_call_id: ")[1].split()[0]
                        print(tool_call_id)
                        break

                if tool_call_id:
                    count=1
                    print(tool_call_id)
                    # Sent action
                    run = openai.beta.threads.runs.submit_tool_outputs(
                        thread_id=thread.id,
                        run_id=run.id,
                        tool_outputs=[
                            {
                                "tool_call_id": tool_call_id
                            }
                        ]
                    )

        if run.required_action and run.required_action.submit_tool_outputs and run.required_action.submit_tool_outputs.tool_calls:
            for tool_call in run.required_action.submit_tool_outputs.tool_calls:
                tool_call_id = tool_call.id
                arguments = json.loads(tool_call.function.arguments)
                response = arguments  # arguments is the response
                print(response)
                tool_outputs.append({
                    "tool_call_id": tool_call_id,
                    "outputs": {"response": response}
                })
                return response

        print(run.status)
        run = openai.beta.threads.runs.retrieve(
            thread_id=thread.id,
            run_id=run.id
        )
        if run.status =="succeeded":
#Actual code never enter here
            messages = openai.beta.threads.messages.list(
                thread_id=thread.id
            )
            return messages
        elif run.status == "cancelled" or run.status == "failed":
            raise "thread cancelled"
        elif run.status == "in_progress":
            #wait for next iteration
            import time
            time.sleep(5)
    return None

and i call response = myFunction to use it.

This is great! Thanks! I have been working on it with similar thoughts.

> if run.status =="succeeded":
> #Actual code never enter here

ā€œSucceededā€ is not a valid state. When it is successful you will have receive ā€œcompletedā€ state. One way to manage this is do while(true) and inside handle all the possible states for the ā€œrun.statusā€.

Using switch case will make it simpler to read.

Make sure to break for default case, your code will be stuck in infinite loop if you fail to handle any case or if API is updated to return any state not mentioned in documents.

1 Like

Hereā€™s my take it on it - that can handle all kinds of functions easily.

3 Likes

I did a litte refactor, could be helpfully for somebody:


def handle_requires_action(run, thread):
    #this is uggly for me but i didnt understand very well how get the function_call, and why is needed do it from the messages, but works for now and i dont want lost time and credits in more attempts
    tool_call_id = None
    try:
        for message in run.messages:
            if "tool_call_id" in message.content:
                tool_call_id = message.content.split("tool_call_id: ")[1].split()[0]
                print(tool_call_id)
                break

        if tool_call_id:
            print(tool_call_id)
            #this call the function of the assistent in the active threat with "action_required" and move their status to "in_progress"
            run = openai.beta.threads.runs.submit_tool_outputs(
                thread_id=thread.id,
                run_id=run.id,
                tool_outputs=[
                    {
                        "tool_call_id": tool_call_id
                    }
                ]
            )
    except Exception as e:
        print(e)

def handle_succeeded(run, thread):
#never called in my case
    messages = openai.beta.threads.messages.list(
        thread_id=thread.id
    )
    #print(run)
    #print(messages)
    return messages

def handle_cancelled_or_failed(run, thread):
    raise "thread cancelled"

def handle_in_progress(run, thread):
    time.sleep(5)

def get_json_from_article_new(article): 
    openai.api_key = openaikey
    thread = openai.beta.threads.create()
    openai.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content="describe action, not sure if this is really required"
    )

    run = openai.beta.threads.runs.create(
            thread_id=thread.id,
            assistant_id="Assistant id")
    tool_outputs = []


    status_handlers = {
        "requires_action": handle_requires_action,
        "succeeded": handle_succeeded,
        "cancelled": handle_cancelled_or_failed,
        "failed": handle_cancelled_or_failed,
        "expired": handle_cancelled_or_failed,
        "in_progress": handle_in_progress,
        "queued": handle_in_progress
    }

    while run.status != 'completed':
        run = openai.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id)
        print(run.status)

        if run.required_action and run.required_action.submit_tool_outputs and run.required_action.submit_tool_outputs.tool_calls:
            #Here i recovery the response that i need, and the function ends.
            #Enter when the "in_progress" finish and go back to the state required_action but in the messages comes the response.
            for tool_call in run.required_action.submit_tool_outputs.tool_calls:
                tool_call_id = tool_call.id
                arguments = json.loads(tool_call.function.arguments)
                response = arguments
                tool_outputs.append({
                    "tool_call_id": tool_call_id,
                    "outputs": {"response": response}
                })
                #return the result
                return response

        elif run.status in status_handlers:
            status_handlers[run.status](run, thread)
        else:
            print(f"Unhandled status: {run.status}")
            raise Exception("Unhandled status")
            break
    return None

@inachomsky thank you for starting this thread.

I use JS and struggled to get the tool call id as well, and I found a possible solution. As @jlvanhulst pointed oput in his blog, ā€œtool_calls is a list!ā€ I decided not to implemet his solution because 1. its in python and I use JS, 2. I am only calling one function anyway, so it is a bit overkill for me.

Anyway, tool_calls is a list, so I just need the ID of the first item, so for me in JS language as follows:

const retrieveRun = await openai.beta.threads.runs.retrieve(
    threadId,
    runId
  );

const toolCalls = retrieveRun.required_action.submit_tool_outputs.tool_calls;

const run = await openai.beta.threads.runs.submitToolOutputs(
        threadId,
        runId,
        {
          tool_outputs: [
            {
              tool_call_id: toolCalls[0].id,
              output: "true",
            },
          ],
        }
        );

Why the output is true, donā€™t ask me, it just worked inside the Asssistant Playground.

Let me know if this helped.

I worked on a new version as well - now fully async version running on Django/Celery

Why on earth do they make it so hard to use the assistance API? Seems to me the entire point is to use tools, so why the assistant does not handle passing arguments between the thread and the tools is beyond me. llama library does, but I have found the performance is severely degraded.

2 Likes

thanks for JS codeā€¦ Iā€™ve tried it but is says ā€œCannot read properties of undefined (reading ā€˜submit_tool_outputsā€™)ā€

Hereā€™s my node:


import OpenAI from 'openai';
export default async function retrieveOpenAIRun({
    thread_id,
    run_id,
    apiKey
}) {
    const openai = new OpenAI({
    apiKey
    });
  //const run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
    
  const toolCalls = retrieveOpenAIRun.required_action.submit_tool_outputs.tool_calls;
  
  const run = await openai.beta.threads.runs.submitToolOutputs(
        thread_id,
        run_id,
        {
          tool_outputs: [
            {
              tool_call_id: toolCalls[0].id,
              output: "true",
            },
          ],
        }
        );
  
  return run;
}