-
Notifications
You must be signed in to change notification settings - Fork 75
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
Add condition
to halt_after
/ halt_before
#436
Comments
Tangentially, I'm curious how this would impact best practices when it comes to handling human input. For example, if I want to build a basic chat app, I would currently do as suggested in the cookbooks: @action(reads=[], writes=["prompt", "chat_history"])
def human_input(state: State) -> Tuple[dict, State]:
chat_item = {
"content": prompt,
"role": "user"
}
# return the prompt as the result
# put the prompt in state and update the chat_history
return (
{"prompt": prompt},
state.update(prompt=prompt).append(chat_history=chat_item)
)
def create_chat_app():
return (
ApplicationBuilder().with_actions(
human_input=human_input,
ai_response=ai_response
).with_transitions(
("human_input", "ai_response"),
("ai_response", "human_input")
).with_state(chat_history=[])
.with_entrypoint("human_input")
.build()
)
def chat():
app = create_chat_app()
print("Hi, how can I help you? \n")
while True:
user_input = input()
if user_input.lower() == "q":
break
final_action, result, final_state = app.run(
halt_after=["ai_response"],
inputs={"prompt" : user_input}
)
print("\n" + final_state['response'] + "\n") If it were possible to @action(reads=[], writes=["prompt", "chat_history", "halt"])
def human_input(state: State) -> Tuple[dict, State]:
prompt = input()
if prompt.lower() == "q":
return (
{"prompt": prompt},
state.update(halt=True)
)
chat_item = {
"content": prompt,
"role": "user"
}
# return the prompt as the result
# put the prompt in state and update the chat_history
return (
{"prompt": prompt},
state.update(prompt=prompt).append(chat_history=chat_item)
)
def create_chat_app():
return (
ApplicationBuilder().with_actions(
human_input=human_input,
ai_response=ai_response
).with_transitions(
("human_input", "ai_response"),
("ai_response", "human_input")
).with_state(chat_history=[])
.with_entrypoint("human_input")
.build()
)
def chat():
app = create_chat_app()
print("Hi, how can I help you? \n")
final_action, result, final_state = app.run(
halt_after=[("human_input", when(halt=True)],
)
print("\n" + "Goodbye" + "\n") Essentially, in that second scenario we'd only halt the app when the user actively asks for it (or quits the website, or switches app). Not sure of all the implications since I'm just starting out with Burr, but it makes sense intuitively for my use case at least. |
@gamarin2 yeah so
This is interesting. We'd want to inject this as an implict edge in the graph if you think about visualizing it. It would reduce the need for a "terminal and or input" action which is just a placeholder. This might come with edge cases with respect to restarting. E.g. we'd need to double check what happens if you resume an application after halting (I think it might just work, but then again it might not without augmenting state).
Can you expand on more how you would determine this? We've built the Burr data model around enabling someone to build the "left hand side" of the ChatGPT interface. E.g. you either click new, or you click into an old conversation. With burr |
Yes, that's the idea!
When the user opens chatgpt, they are presented with a chatbox for a new chat. They can click on that and start typing or they can select a previous chat to continue it. The issue is that when the user has many chats, they may not want to scroll or bother finding the one relevant to their current prompt, or they might forget they have started one already, and so they would just start typing immediately. Hence the need for routing. One way one could implement it is by making a quick call to a small model like It may be overkill for chatgpt, but for my use case it makes a lot of sense. |
Oh that's interesting. You'd want to switch the Otherwise to me splitting it into two stages doesn't sounds unreasonable to me.
|
I built several human-in-the-loop apps and faced similar challenges. I generally prefer a 3rd pattern that more explicitly define I/O of the app. The input is always a function param ( @action(reads=[], writes=["chat_history"])
def bot_turn(state: State, user_input: str) -> Tuple[dict, State]:
system_prompt = ...
# if there's no existing history, start a new history with the system prompt
chat_history = state.get(
"chat_history",
[{"role": system, "content": system_prompt}]
)
user_message = {"role": "user", "content": user_input}
response = client.chat.completions.create(
model=...,
messages=chat_history + [user_message]
)
# append the user message and LLm message to history
return state.append(chat_history=[user_message, response.choices[0].message])
def create_chat_app():
return (
ApplicationBuilder()
.with_actions(bot_turn)
.with_transitions(("bot_turn", "bot_turn"))
.with_entrypoint("bot_turn")
.build()
)
def chat_in_terminal():
app = create_chat_app()
print("Hi, how can I help you? \n")
while True:
user_input = input()
if user_input.lower() == "q":
break
final_action, result, final_state = app.run(
halt_before=["bot_turn"],
inputs={"user_input" : user_input}
)
print("\n" + final_state['response'] + "\n") Other notes:
Conditional haltingI like the proposed syntax for Alternative solutionCurrently, this is not possible through def run(self, ...):
gen = self.iterate(halt_before=halt_before, halt_after=halt_after, inputs=inputs)
while True:
try:
next(gen)
except StopIteration as e:
result = e.value
return result So it seems reasonable to create custom "run methods" manually def run_app(app: Application):
gen = app.iterate(halt_before=halt_before, halt_after=halt_after, inputs=inputs)
while True:
try:
action_name, result, state = next(gen)
if state["halt"] is True:
break # return something
except StopIteration as e:
result = e.value
return result
return Looking directly at the for action, result, state in app.iterate(halt_after=["human_input"]):
if state["halt"] is True:
break Virtual edges
I don't think there's a reason to modify the graph here. The condition is not on an edge given there's currently no implicit "terminal state action" AFAIK. It simply exhausts the generator by hitting |
Agreed with @skrawcz -- there's the solution of halting when a certain condition is met (which I think is, on its own, useful), but to me this is really a question of how you get the data for the prior apps. Something like:
So it's an indexing layer on top of Burr -- E.G. through a custom persister or some other tool. Then the proposed solution here would help -- E.G. deciding whether to halt, but you don't have to use burr for that (it's kind of a nice-to-have). What @zilto suggested could work as well -- there are quite a few different constructs that could make it possible ( Regarding the syntax -- I like it quite a bit. There's a nice internal construct we have that's hidden -- the prior action is actually stored in state, and no reason we couldn't store the net action. Then everything becomes a condition of state -- E.G. halt when |
One thing I sketched up and could explore further:
This eventually leads to the idea of a "memory" layer. It could be vector DB, full-text search, image comparison, custom rankers, etc. for retrieval |
Yep, something like this could be a nice solution -- curious whether it would be a separate layer, an implementation of the persister, or just a hook/stuff in the action... |
The main design question is: when and how do you call memory?
If I was building for a production use case, I would have two Burr
Implementation:
|
Yes, the idea is to always create an app ID. Then, depending on the result of the router, either continue or halt and ditch the app for the more relevant one.
I guess so. You do lose some of the benefits of graph vizualisation and tracing though (the first LLM call isn't part of your state machine in that scenario).
I mean it depends on the use case, but as far as my app is concerned, my (naive) solution would be to ask gpt-4o-mini to produce a summary of the chat at various logical halting points and store that in my User class (outside the state). Then I would just pass these summaries as inputs whenever I restart the app. If we're talking about indexing state for the same app at various points in the lifecycle and making that accessible, then that's a whole separate issue. |
Is your feature request related to a problem? Please describe.
Currently, it is not possible to halt the app after a given action iff a
condition
is met. Here is a motivating example of when it might be needed:Say you have a chatgpt-like app with various "chats", where each chat is a distinct burr app. When a user starts a new chat, you'd like to have a quick routing step that will detect, based on the first prompt, if the new chat is actually the continuation of a previous chat. If so, we want to halt the current app and restart the relevant app. If not, the app can continue its normal lifecycle.
Describe the solution you'd like
Instead of providing a list of actions to
halt_after
(orhalt_before
), provide a list of tuples(action, condition)
. The application will halt after a givenaction
if and only ifcondition
is met. If no condition is provided, the app will always halt afteraction
is processed (current behavior).Describe alternatives you've considered
Right now, the alternative is to have a dummy action to halt on and a conditional transition to it.
And then when calling
app.run
:This is less clean than the suggested API.
Another possible way to address this would be to enable halting the application from within an action, although it may be harder to implement.
The text was updated successfully, but these errors were encountered: