langchain-ai / langgraphjs

Build resilient language agents as graphs.
https://langchain-ai.github.io/langgraphjs/
MIT License
699 stars 111 forks source link

Add state updates and node changes to the `streamEvents` function #302

Open airhorns opened 3 months ago

airhorns commented 3 months ago

Right now, to stream the graph changes as they happen, there are two top level functions, .stream and .streamEvents. You only get told when the current node changes from the former, and only get LLM tokens from the latter. Can there be an option to .streamEvents to include the same events that .stream() would produce for node changes as well? That way, consumers can react to changes in the state or the current node while still streaming tokens back to the user.

I also considered trying to merge the streams of events from these two functions, but then realized that'd actually invoke the graph twice, so I don't think there's an easy way to do this in userland right now.

hinthornw commented 3 months ago

Hm right now the outputs of the nodes in a graph would be tracked within on_chain_end events. Is that what you are looking for?

for await (const event of events) {
  if (event?.event == 'on_chain_end')
  {
    console.log(event);

  }
}
// {
//   event: 'on_chain_end',
//   data: { output: { output: undefined }, input: { messages: [Array] } },
//   run_id: '5e426a6e-a563-4a7b-a9de-5c32e8523631',
//   name: '__start__',
//   tags: [ 'graph:step:5', 'langsmith:hidden' ],
//   metadata: { thread_id: '42' }
// }
// {
//   event: 'on_chain_end',
//   data: { output: { messages: [Array] }, input: { messages: [Array] } },
//   run_id: 'e0b80533-36bd-438d-bda5-5ec98b72db96',
//   name: 'agent',
//   tags: [ 'seq:step:1' ],
//   metadata: { thread_id: '42' }
// }
// {
//   event: 'on_chain_end',
//   data: { output: { output: undefined }, input: { messages: [Array] } },
//   run_id: 'c4a396ef-662e-4b9a-9581-6fe63baa3489',
//   name: 'ChannelWrite<messages,agent>',
//   tags: [ 'seq:step:2', 'langsmith:hidden' ],
//   metadata: { thread_id: '42' }
// }
airhorns commented 3 months ago

That's a start for sure! But, for building that ChatGPT style spinner while tools are running, then we'd need start events as well as end events. I guess you could track the most recently seen node outside the graph and then look at each new event to see if it's still within the same spot, but that seems kind of annoying and doesn't work great if there are parallel nodes executing.

And, I think that a node can technically run many chains right, potentially in parallel again within them? From a DX perspective, I was hoping I could just get the same state events as the other top level .stream function as well.

hinthornw commented 3 months ago

For tools, you'd start and stop the spinners based on on_tool_start and on_tool_end events. For nods, you'd look at on_chain_start and on_chain_end events. If they're in parallel, then they'd be emitted around the same time.

Each event has a unique ID so you can track when it's opened or closed. Spinner starts when start event occurs. Spinner stops when that ID is seen in an end event (or error event)

airhorns commented 3 months ago

Right -- I was hoping that I wouldn't have to do a bunch of deduplication in userland. I have a node for example that fires off a couple chains in parallel, and I just want to get an event for when that node is entered and when that node is exited. The chain events describe what's happening inside the node, but not the transitions into and out of the nodes themselves. My parallel chains fire off several chain start and end events that don't correspond to what I want to show the user, because the chains are kind of an implementation detail of how that node does it's thing. It also seems a little brittle to have some downstream consumer of the agent couple tightly to what a node does internally by looking at the chain events -- if someone else changes how the node works, then the event listener has to be adjusted too, whereas if it could just listen to node entry / exit events it wouldn't need to. I think you can make the same inference from the chain events too but again requires some annoying state tracking outside the event loop, especially if there's ever parallel node visiting.

Also I should say I am new to LangGraph, and thanks for your guidance! If this isn't at all the way you folks envisioned people using these primitives it could very well just be me being thick!

fer112233 commented 3 months ago

I am surprised this is not a thing. In my case, using streamEvents() and checking the on_chain_end returns an empty object on the "output". Using the regular stream() I do get the proper state updates.