Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: langchain stream not closing #201

Merged
merged 5 commits into from
Jun 29, 2023
Merged

Conversation

aranlucas
Copy link
Contributor

@aranlucas aranlucas commented Jun 22, 2023

fixes: #97

The problem

  1. Streaming handlers should be used in Request callbacks - https://js.langchain.com/docs/production/callbacks/#when-do-you-want-to-use-each-of-these
  2. You can have multiple chains. You need to know when all the chains before closing the stream. Currently, the stream is being closed after the first chain End

Solution

  1. Update docs to reflect request callbaks
  2. Add internal state that keeps track of all of the runs happening. If there are no runs, then the stream is safe to close.

@changeset-bot
Copy link

changeset-bot bot commented Jun 22, 2023

🦋 Changeset detected

Latest commit: 0a26a5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Member

@MaxLeiter MaxLeiter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Feel free to open an issue here for tracking the langchain discussion

@MaxLeiter MaxLeiter self-requested a review June 22, 2023 18:51
@MaxLeiter
Copy link
Member

Actually, I just remembered why @jaredpalmer made the change. Here's the initial issue: #63

It seems like we may want to let the consumer choose which callback to end on, as it's model dependent

@aranlucas
Copy link
Contributor Author

Yeah I figured that was the reasoning, thanks for confirming.

This change will probably break SequentialChain use case.

It seems like your suggestion might be the best one. Or langchainjs can expose have something like handleEnd where it doesn't depend on a model.

@aranlucas
Copy link
Contributor Author

aranlucas commented Jun 22, 2023

Last one here before I create an issue: https://js.langchain.com/docs/production/callbacks/#multiple-handlers

Before

  const { stream, handlers } = LangChainStream()
 
  const llm = new ChatOpenAI({
    streaming: true,
    callbackManager: CallbackManager.fromHandlers(handlers)
  })
 
  llm
    .call(
      (messages as Message[]).map(m =>
        m.role == 'user'
          ? new HumanChatMessage(m.content)
          : new AIChatMessage(m.content)
      )
    )
    .catch(console.error)
 
  return new StreamingTextResponse(stream)

After

  // Different handlers for llm, agents, chains
  const { stream, llmHandlers, chainHandlers } = LangChainStream()
 
  // Do not pass handlers here
  const llm = new ChatOpenAI({
    streaming: true,
  })
 
  // Pass handlers here instead
  llm
    .call(
      (messages as Message[]).map((m) =>
        m.role == "user"
          ? new HumanChatMessage(m.content)
          : new AIChatMessage(m.content)
      ),
      [llmHandlers]
    )
    .catch(console.error);
 
  return new StreamingTextResponse(stream)

@aranlucas
Copy link
Contributor Author

Created #205 for further discussions

This PR will probably break the linked usecase - can be closed or left until we find something.

@MaxLeiter MaxLeiter marked this pull request as draft June 22, 2023 22:23
@jaredpalmer
Copy link
Collaborator

jaredpalmer commented Jun 22, 2023

Big fan of const { stream, llmHandlers, chainHandlers } = LangChainStream() lets do that

@aranlucas
Copy link
Contributor Author

aranlucas commented Jun 23, 2023

@jaredpalmer that actually won't work after doing some research #205

As an exaggerated example, a chain can spawn a chain that spawns a chain, meaning that on the first chainEnd, the stream will close. You need to be able to count how many things are running otherwise only the first chain to finish will write to the stream, locking out the other chain results.

I'm leaning towards #205 (comment) as the solution now. It should cover all use-cases, and keep the API the same. (Still need to update docs though to use on "Request callbacks" rather than "Constructor callbacks")

@aranlucas aranlucas marked this pull request as ready for review June 23, 2023 00:29
@aranlucas
Copy link
Contributor Author

aranlucas commented Jun 23, 2023

Updated with my latest findings 👀

It may be oversimplified compared to what they have done with tracing but I think it covers all the use-cases.

@aranlucas
Copy link
Contributor Author

Take #205 (comment) as an example. Let's say we wanted to stream both the responses.

in first chain

Tragedy at Sunset on the Beach is a story of love, loss, and redemption. It follows the story of two young lovers, Jack and Jill, who meet on a beach at sunset. They quickly fall in love and plan to spend the rest of their lives together. \n\nHowever, tragedy strikes when Jack is killed in a car accident. Jill is left devastated and unable to cope with the loss of her beloved. She spirals into a deep depression and begins to lose hope. \n\nJust when all seems lost, Jill discovers that Jack had left her a letter before his death. In the letter, he tells her that he will always love her and that she should never give up hope. With this newfound strength, Jill is able to find the courage to move on with her life and find happiness again. \n\nTragedy at Sunset on the Beach is a story of love, loss, and redemption. It is a story of hope and courage in the face of tragedy. It is a story of finding strength in the darkest of times and of never giving up.",     

handleChainEnd gets called, stream is closed. Second chain can't write the second response

Tragedy at Sunset on the Beach is a powerful and moving story of love, loss, and redemption. The play follows the story of two young lovers, Jack and Jill, whose plans for a future together are tragically cut short when Jack is killed in a car accident. \n\nThe play is beautifully written and the performances are outstanding. The actors bring a depth of emotion to their characters that is both heartbreaking and inspiring. The story is full of unexpected twists and turns, and the audience is taken on an emotional rollercoaster as Jill struggles to come to terms with her loss. \n\nThe play is ultimately a story of hope and resilience, and it is sure to leave audiences with a newfound appreciation for life and love. Tragedy at Sunset on the Beach is a must-see for anyone looking for an emotionally charged and thought-provoking experience."

There's probably many ways to fix this, but the way I suggested in this PR seems to work for most use cases I've tried.

const handleError = async (e: Error, runId: string) => {
runs.delete(runId)
await writer.ready
await writer.abort(e)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to be careful with this one, in some chains that eg run multiple things in parallel there could be other events arriving after an error event, it doesn't look like you're handling those

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should it be handled? If there's an error, can it recover gracefully from it?

Right now this is definitely giving up on first error, but I'll be honest I haven't encountered a scenario where the error was recovered and it could continue the chain.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it will overall be cancelled when the first error occurs. The issue is if it's a chain with multiple LLM calls in parallel you may still receive new tokens from call #2 after call #1 has failed. In which case currently what would happen is it would probably throw an exception in handleLLMNewToken as you try to write to an already aborted writer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I noticed that in #205 (comment).

Error in handler Handler, handleChainEnd: TypeError: The stream (in closed state) is not in the writable state and cannot be closed      

I wonder what would be the best behavior, if to abort the call (ignoring the second one) or allow call 2 to keep writing to stream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the exception of writing to a stream in closed state seems to be handled somewhere that doesn't seem to cause catastrophic errors.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes any exceptions inside a callback handler are caught by langchain to avoid an error in a callback handler interrupting a run

Copy link
Contributor Author

@aranlucas aranlucas Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I see your point.

How does this suggestion sound.

Keep an error state handler

let errors = []

Whenever we encounter an error on any handle*Error push it errors

Then handleEnd

  const handleEnd = async (runId: string) => {
    runs.delete(runId)

    if (runs.size === 0) {
      if (error.length !=0) {
          // for loop over errors or join errors?
          await writer.ready
          await writer.abort(errors)
          return;
      }
      await writer.ready
      await writer.close()
    }
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or I can just re-throw the error similar to https://github.com/vercel-labs/ai/blob/main/packages/core/streams/ai-stream.ts#L152-L154

I think an issue I had yesterday working with this is that even though the stream had ended, the background processing had not stopped. This led me to have to restart dev server to clear out running processes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of rethrowing the error

@jaredpalmer
Copy link
Collaborator

Are we good to merge @nfcampos @MaxLeiter ?

@nfcampos
Copy link

looks good to me

@@ -34,6 +34,7 @@ export async function POST(req: Request) {
? new HumanChatMessage(m.content)
: new AIChatMessage(m.content)
),
[],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nfcampos I added this [] at all call sites in order to satisfy typescript. If these aren't necessary then, this may be an upstream issue with LC, likely missing an overload.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should be undefined

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stylistic suggestion: y'all should have only a two arguments to .call, passing undefined is very odd.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agreed, I've improved this here langchain-ai/langchainjs#1868 once that is merged you'll be able to do instead llm.call(messages, { callbacks: handlers })

@jaredpalmer jaredpalmer merged commit 8bf637a into vercel:main Jun 29, 2023
@aranlucas
Copy link
Contributor Author

aranlucas commented Jun 29, 2023

Thank you 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Stream never closes with LangChainStream using postman
4 participants