From e9264d908bf55c0f651fcbef914172455f052e2b Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Wed, 28 Feb 2024 23:39:36 -0800 Subject: [PATCH] Adds function to get state to tracker So that we can pull the last state given an ID from the UI. Updates examples and adds a new one that mirrors the LCEL example with the tool calling. This variant works. Where as the Hamilton DAG also doing tool calling somehow doesn't quite work as well. Still digging in there. Need a run diff tool now... or a way to compare runs... --- burr/core/application.py | 20 +- burr/tracking/client.py | 55 ++++- .../multi-agent-collaboration/application.py | 132 ++++------- .../multi-agent-collaboration/func_agent.py | 4 +- .../hamilton-multi-agent-v2.png | Bin 0 -> 41547 bytes .../hamilton_application.py | 222 ++++++++++++++++++ .../lcel_application.py | 31 ++- 7 files changed, 358 insertions(+), 106 deletions(-) create mode 100644 examples/multi-agent-collaboration/hamilton-multi-agent-v2.png create mode 100644 examples/multi-agent-collaboration/hamilton_application.py diff --git a/burr/core/application.py b/burr/core/application.py index ecf54ff71..f6812fd6c 100644 --- a/burr/core/application.py +++ b/burr/core/application.py @@ -44,6 +44,7 @@ class Transition: TerminationCondition = Literal["any_complete", "all_complete"] PRIOR_STEP = "__PRIOR_STEP" +SEQUENCE_ID = "__SEQUENCE_ID" def _run_function(function: Function, state: State, inputs: Dict[str, Any]) -> dict: @@ -259,7 +260,14 @@ def _step( else: result = _run_function(next_action, self._state, inputs) new_state = _run_reducer(next_action, self._state, result, next_action.name) - new_state = new_state.update(**{PRIOR_STEP: next_action.name}) + + new_state = new_state.update( + **{ + PRIOR_STEP: next_action.name, + # make it a string for future proofing + SEQUENCE_ID: str(int(self._state.get(SEQUENCE_ID, 0)) + 1), + } + ) self._set_state(new_state) except Exception as e: exc = e @@ -312,7 +320,13 @@ async def astep(self, inputs: Dict[str, Any] = None) -> Optional[Tuple[Action, d else: result = await _arun_function(next_action, self._state, inputs=inputs) new_state = _run_reducer(next_action, self._state, result, next_action.name) - new_state = new_state.update(**{PRIOR_STEP: next_action.name}) + new_state = new_state.update( + **{ + PRIOR_STEP: next_action.name, + # make it a string for future proofing + SEQUENCE_ID: str(int(self._state.get(SEQUENCE_ID, 0)) + 1), + } + ) except Exception as e: exc = e logger.exception(_format_error_message(next_action, self._state, inputs)) @@ -780,7 +794,7 @@ def with_tracker( ): """Adds a "tracker" to the application. The tracker specifies a project name (used for disambiguating groups of tracers), and plugs into the - Burr UI. Currently the only supported tracker is local, which takes in the params + Burr UI. Currently, the only supported tracker is local, which takes in the params `storage_dir` and `app_id`, which have automatic defaults. :param project: Project name diff --git a/burr/tracking/client.py b/burr/tracking/client.py index 3a34932b6..2b01f6963 100644 --- a/burr/tracking/client.py +++ b/burr/tracking/client.py @@ -39,11 +39,12 @@ class LocalTrackingClient(PostApplicationCreateHook, PreRunStepHook, PostRunStep GRAPH_FILENAME = "graph.json" LOG_FILENAME = "log.jsonl" + DEFAULT_STORAGE_DIR = "~/.burr" def __init__( self, project: str, - storage_dir: str = "~/.burr", + storage_dir: str = DEFAULT_STORAGE_DIR, app_id: Optional[str] = None, ): """Instantiates a local tracking client. This will create the following directories, if they don't exist: @@ -61,12 +62,62 @@ def __init__( """ if app_id is None: app_id = f"app_{str(uuid.uuid4())}" - storage_dir = os.path.join(os.path.expanduser(storage_dir), project) + storage_dir = self.get_storage_path(project, storage_dir) self.app_id = app_id self.storage_dir = storage_dir self._ensure_dir_structure() self.f = open(os.path.join(self.storage_dir, self.app_id, self.LOG_FILENAME), "a") + @staticmethod + def get_storage_path(project, storage_dir): + return os.path.join(os.path.expanduser(storage_dir), project) + + @classmethod + def get_state( + cls, + project: str, + app_id: str, + sequence_no: int = -1, + storage_dir: str = DEFAULT_STORAGE_DIR, + ) -> tuple[dict, str]: + """Initialize the state to debug from an exception. + + :param project: + :param app_id: + :param sequence_no: + :param storage_dir: + :return: + """ + if sequence_no is None: + sequence_no = -1 # get the last one + path = os.path.join(cls.get_storage_path(project, storage_dir), app_id, cls.LOG_FILENAME) + if not os.path.exists(path): + raise ValueError(f"No logs found for {project}/{app_id} under {storage_dir}") + with open(path, "r") as f: + json_lines = f.readlines() + json_lines = [json.loads(js_line) for js_line in json_lines] + json_lines = [js_line for js_line in json_lines if js_line["type"] == "end_entry"] + line = {} + if sequence_no < 0: + line = json_lines[sequence_no] + else: + found_line = False + for line in json_lines: + if line["sequence_no"] == sequence_no: + found_line = True + break + if not found_line: + raise ValueError(f"Sequence number {sequence_no} not found for {project}/{app_id}.") + state = line["state"] + to_delete = [] + for key in state.keys(): + if key.startswith("__"): + to_delete.append(key) + for key in to_delete: + del state[key] + entry_point = line["action"] + return state, entry_point + def _ensure_dir_structure(self): if not os.path.exists(self.storage_dir): logger.info(f"Creating storage directory: {self.storage_dir}") diff --git a/examples/multi-agent-collaboration/application.py b/examples/multi-agent-collaboration/application.py index d346ba515..076a1c9c0 100644 --- a/examples/multi-agent-collaboration/application.py +++ b/examples/multi-agent-collaboration/application.py @@ -1,6 +1,3 @@ -import json -import os.path - import func_agent from hamilton import driver from langchain_community.tools.tavily_search import TavilySearchResults @@ -10,32 +7,10 @@ from burr.core import Action, ApplicationBuilder, State from burr.core.action import action from burr.lifecycle import PostRunStepHook +from burr.tracking import client as burr_tclient -# @action(reads=["code"], writes=["code_result"]) -# def run_code(state: State) -> tuple[dict, State]: -# _code = state["code"] -# try: -# result = repl.run(_code) -# except BaseException as e: -# _code_result = {"status": "error", "result": f"Failed to execute. Error: {repr(e)}" -# return {"result": f"Failed to execute. Error: {repr(e)}"}, state.update(code_result=_code_result) -# _code_result = {"status": "success", "result": result} -# return {"status": "success", "result": result}, state.update(code_result=_code_result) -# -# @action(reads=["code"], writes=["code_result"]) -# def run_tavily(state: State) -> tuple[dict, State]: -# _code = state["code"] -# try: -# result = repl.run(_code) -# except BaseException as e: -# _code_result = {"status": "error", "result": f"Failed to execute. Error: {repr(e)}" -# return {"result": f"Failed to execute. Error: {repr(e)}"}, state.update(code_result=_code_result) -# _code_result = {"status": "success", "result": result} -# return {"status": "success", "result": result}, state.update(code_result=_code_result) - - +# Initialize some things needed for tools. tool_dag = driver.Builder().with_modules(func_agent).build() - repl = PythonREPL() @@ -126,26 +101,6 @@ def post_run_step(self, *, state: "State", action: "Action", **future_kwargs): print("state======\n", state) -def initialize_state_from_logs(tracker_name: str, app_id: str) -> tuple[dict, str]: - """Initialize the state to debug from an exception - - :param tracker_name: - :param app_id: - :return: - """ - # open ~/.burr/{tracker_name}/{app_id}/log.jsonl - # find the first entry with an exception -- and pull state from it. - with open(f"{os.path.expanduser('~/')}/.burr/{tracker_name}/{app_id}/log.jsonl", "r") as f: - lines = f.readlines() - for line in lines: - line = json.loads(line) - if "exception" in line: - state = line["state"] - entry_point = line["action"] - return state, entry_point - raise ValueError(f"No exception found in logs for {tracker_name}/{app_id}") - - def default_state_and_entry_point() -> tuple[dict, str]: return { "messages": [], @@ -157,9 +112,11 @@ def default_state_and_entry_point() -> tuple[dict, str]: def main(app_instance_id: str = None): - tracker_name = "hamilton-multi-agent" + project_name = "demo:hamilton-multi-agent" if app_instance_id: - state, entry_point = initialize_state_from_logs(tracker_name, app_instance_id) + state, entry_point = burr_tclient.LocalTrackingClient.get_state( + project_name, app_instance_id + ) else: state, entry_point = default_state_and_entry_point() @@ -189,54 +146,45 @@ def main(app_instance_id: str = None): ) .with_entrypoint(entry_point) .with_hooks(PrintStepHook()) - .with_tracker(tracker_name) + .with_tracker(project_name) .build() ) + app.visualize( + output_file_path="hamilton-multi-agent", include_conditions=True, view=True, format="png" + ) app.run(halt_after=["terminal"]) - # return app if __name__ == "__main__": - _app_id = "app_4d1618d2-79d1-4d89-8e3f-70c216c71e63" + # Add an app_id to restart from last sequence in that state + # e.g. fine the ID in the UI and then put it in here "app_4d1618d2-79d1-4d89-8e3f-70c216c71e63" + _app_id = None main(_app_id) - import sys - - sys.exit(0) - """TODO: - 1. need to figure out the messages state. - 2. the current design is each DAG run also calls the tool. - 3. so need to then update messages history appropriately - so that the "agents" can iterate until they are done. - - Note: https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/multi-agent-collaboration.ipynb - Is a little messy to figure out. So best to approach from first - principles what is actually going on. - - """ - repl = PythonREPL() - tavily_tool = TavilySearchResults(max_results=5) - result = tool_dag.execute( - ["executed_tool_calls"], - inputs={ - "tools": [tavily_tool], - "system_message": "You should provide accurate data for the chart generator to use.", - "user_query": "Fetch the UK's GDP over the past 5 years," - " then draw a line graph of it." - " Once you have written code for the graph, finish.", - }, - ) - import pprint - - pprint.pprint(result) - - result = tool_dag.execute( - ["executed_tool_calls"], - inputs={ - "tools": [python_repl], - "system_message": "Any charts you display will be visible by the user.", - "user_query": "Draw a simple line graph of y = x", - }, - ) - import pprint - pprint.pprint(result) + # some test code + # tavily_tool = TavilySearchResults(max_results=5) + # result = tool_dag.execute( + # ["executed_tool_calls"], + # inputs={ + # "tools": [tavily_tool], + # "system_message": "You should provide accurate data for the chart generator to use.", + # "user_query": "Fetch the UK's GDP over the past 5 years," + # " then draw a line graph of it." + # " Once you have written code for the graph, finish.", + # }, + # ) + # import pprint + # + # pprint.pprint(result) + # + # result = tool_dag.execute( + # ["executed_tool_calls"], + # inputs={ + # "tools": [python_repl], + # "system_message": "Any charts you display will be visible by the user.", + # "user_query": "Draw a simple line graph of y = x", + # }, + # ) + # import pprint + # + # pprint.pprint(result) diff --git a/examples/multi-agent-collaboration/func_agent.py b/examples/multi-agent-collaboration/func_agent.py index 547085743..cba1cb7af 100644 --- a/examples/multi-agent-collaboration/func_agent.py +++ b/examples/multi-agent-collaboration/func_agent.py @@ -91,9 +91,9 @@ def base_system_prompt(tool_names: list[str], system_message: str) -> str: "You are a helpful AI assistant, collaborating with other assistants." " Use the provided tools to progress towards answering the question." " If you are unable to fully answer, that's OK, another assistant with different tools " - " will help where you left off. Execute what you can to make progress." + " will help where you left off. Execute what you can to make progress.\n\n" " If you or any of the other assistants have the final answer or deliverable," - " prefix your response with FINAL ANSWER so the team knows to stop." + " prefix your response with FINAL ANSWER so the team knows to stop.\n\n" f" You have access to the following tools: {tool_names}.\n{system_message}" ) diff --git a/examples/multi-agent-collaboration/hamilton-multi-agent-v2.png b/examples/multi-agent-collaboration/hamilton-multi-agent-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..b4490c0441d198df5759c34e64f6056f2d7e1c56 GIT binary patch literal 41547 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 v-n`NhbgKS{ZmcSN=cySk!BPM?Z`SzxEup&Uke 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 literal 0 HcmV?d00001 diff --git a/examples/multi-agent-collaboration/hamilton_application.py b/examples/multi-agent-collaboration/hamilton_application.py new file mode 100644 index 000000000..da30413dd --- /dev/null +++ b/examples/multi-agent-collaboration/hamilton_application.py @@ -0,0 +1,222 @@ +import json + +import func_agent +from hamilton import driver +from langchain_community.tools.tavily_search import TavilySearchResults +from langchain_experimental.utilities import PythonREPL + +from burr import core +from burr.core import Action, ApplicationBuilder, State, action, default +from burr.lifecycle import PostRunStepHook +from burr.tracking import client as burr_tclient + +# Initialize some things needed for tools. +tool_dag = driver.Builder().with_modules(func_agent).build() +repl = PythonREPL() + + +def python_repl(code: str) -> dict: + """Use this to execute python code. If you want to see the output of a value, + you should print it out with `print(...)`. This is visible to the user. + + :param code: string. The python code to execute. + :return: the output + """ + try: + result = repl.run(code) + except BaseException as e: + return {"error": repr(e), "status": "error", "code": f"```python\n{code}\n```"} + return {"status": "success", "code": f"```python\n{code}\n```", "Stdout": result} + + +@action(reads=["query", "messages"], writes=["messages", "next_hop"]) +def chart_generator(state: State) -> tuple[dict, State]: + query = state["query"] + result = tool_dag.execute( + ["parsed_tool_calls", "llm_function_message"], + inputs={ + "tools": [python_repl], + "system_message": "Any charts you display will be visible by the user.", + "user_query": query, + "messages": state["messages"], + }, + ) + # _code = result["parsed_tool_calls"][0]["function_args"]["code"] + new_message = result["llm_function_message"] + parsed_tool_calls = result["parsed_tool_calls"] + state = state.update(parsed_tool_calls=parsed_tool_calls) + state = state.append(messages=new_message) + state = state.update(sender="chart_generator") + return result, state + + +tavily_tool = TavilySearchResults(max_results=5) + + +@action(reads=["query", "messages"], writes=["messages", "next_hop"]) +def researcher(state: State) -> tuple[dict, State]: + query = state["query"] + result = tool_dag.execute( + ["parsed_tool_calls", "llm_function_message"], + inputs={ + "tools": [tavily_tool], + "system_message": "You should provide accurate data for the chart generator to use.", + "user_query": query, + "messages": state["messages"], + }, + ) + new_message = result["llm_function_message"] + parsed_tool_calls = result["parsed_tool_calls"] + state = state.update(parsed_tool_calls=parsed_tool_calls) + state = state.append(messages=new_message) + state = state.update(sender="researcher") + return result, state + + +tools = [tavily_tool, python_repl] + + +@action(reads=["messages", "parsed_tool_calls"], writes=["messages", "parsed_tool_calls"]) +def tool_node(state: State) -> tuple[dict, State]: + """This runs tools in the graph + + It takes in an agent action and calls that tool and returns the result.""" + new_messages = [] + parsed_tool_calls = state["parsed_tool_calls"] + + for tool_call in parsed_tool_calls: + tool_name = tool_call["function_name"] + tool_args = tool_call["function_args"] + tool_found = False + for tool in tools: + name = getattr(tool, "name", None) + if name is None: + name = tool.__name__ + if name == tool_name: + tool_found = True + kwargs = json.loads(tool_args) + if hasattr(tool, "_run"): + result = tool._run(**kwargs) + else: + result = tool(**kwargs) + # str_result = str(result) + new_messages.append( + { + "tool_call_id": tool_call["id"], + "role": "tool", + "name": tool_name, + "content": result, + } + ) + if not tool_found: + raise ValueError(f"Tool {tool_name} not found.") + + for tool_result in new_messages: + state = state.append(messages=tool_result) + state = state.update(parsed_tool_calls=[]) + # We return a list, because this will get added to the existing list + return {"messages": new_messages}, state + + +@action(reads=[], writes=[]) +def terminal_step(state: State) -> tuple[dict, State]: + return {}, state + + +class PrintStepHook(PostRunStepHook): + def post_run_step(self, *, state: "State", action: "Action", **future_kwargs): + print("action=====\n", action) + print("state======\n", state) + + +def default_state_and_entry_point() -> tuple[dict, str]: + return { + "messages": [], + "query": "Fetch the UK's GDP over the past 5 years," + " then draw a line graph of it." + " Once you code it up, finish.", + "sender": "", + "parsed_tool_calls": [], + }, "researcher" + + +def main(app_instance_id: str = None): + project_name = "demo:hamilton-multi-agent-v1" + if app_instance_id: + state, entry_point = burr_tclient.LocalTrackingClient.get_state( + project_name, app_instance_id + ) + else: + state, entry_point = default_state_and_entry_point() + + app = ( + ApplicationBuilder() + .with_state(**state) + .with_actions( + researcher=researcher, + chart_generator=chart_generator, + tool_node=tool_node, + terminal=terminal_step, + ) + .with_transitions( + ("researcher", "tool_node", core.expr("len(parsed_tool_calls) > 0")), + ( + "researcher", + "terminal", + core.expr("'FINAL ANSWER' in messages[-1]['content']"), + ), + ("researcher", "chart_generator", default), + ("chart_generator", "tool_node", core.expr("len(parsed_tool_calls) > 0")), + ( + "chart_generator", + "terminal", + core.expr("'FINAL ANSWER' in messages[-1]['content']"), + ), + ("chart_generator", "researcher", default), + ("tool_node", "researcher", core.expr("sender == 'researcher'")), + ("tool_node", "chart_generator", core.expr("sender == 'chart_generator'")), + ) + .with_entrypoint(entry_point) + .with_hooks(PrintStepHook()) + .with_tracker(project_name) + .build() + ) + app.visualize( + output_file_path="hamilton-multi-agent-v2", include_conditions=True, view=True, format="png" + ) + app.run(halt_after=["terminal"]) + + +if __name__ == "__main__": + # Add an app_id to restart from last sequence in that state + # e.g. fine the ID in the UI and then put it in here "app_f0e4a918-b49c-4ee1-9d2b-30c15104c51c" + _app_id = None # "app_fb52ca3b-9198-4e5a-9ee0-9c07df1d7edd" + main(_app_id) + + # some test code + # tavily_tool = TavilySearchResults(max_results=5) + # result = tool_dag.execute( + # ["executed_tool_calls"], + # inputs={ + # "tools": [tavily_tool], + # "system_message": "You should provide accurate data for the chart generator to use.", + # "user_query": "Fetch the UK's GDP over the past 5 years," + # " then draw a line graph of it." + # " Once you have written code for the graph, finish.", + # }, + # ) + # import pprint + # + # pprint.pprint(result) + # + # result = tool_dag.execute( + # ["executed_tool_calls"], + # inputs={ + # "tools": [python_repl], + # "system_message": "Any charts you display will be visible by the user.", + # "user_query": "Draw a simple line graph of y = x", + # }, + # ) + # import pprint + # + # pprint.pprint(result) diff --git a/examples/multi-agent-collaboration/lcel_application.py b/examples/multi-agent-collaboration/lcel_application.py index 91b5d1b56..f70f60958 100644 --- a/examples/multi-agent-collaboration/lcel_application.py +++ b/examples/multi-agent-collaboration/lcel_application.py @@ -13,6 +13,7 @@ from burr import core from burr.core import Action, State, action, default, expr from burr.lifecycle import PostRunStepHook +from burr.tracking import client as burr_tclient tavily_tool = TavilySearchResults(max_results=5) @@ -143,10 +144,9 @@ def post_run_step(self, *, state: "State", action: "Action", **future_kwargs): print("state======\n", state) -def application(): - app = ( - core.ApplicationBuilder() - .with_state( +def default_state_and_entry_point() -> tuple[dict, str]: + return ( + dict( messages=[ HumanMessage( content="Fetch the UK's GDP over the past 5 years," @@ -155,7 +155,23 @@ def application(): ) ], sender=None, + ), + "researcher", + ) + + +def main(app_instance_id: str = None): + project_name = "demo:hamilton-multi-agent" + if app_instance_id: + initial_state, entry_point = burr_tclient.LocalTrackingClient.get_state( + project_name, app_instance_id ) + # TODO: rehydrate langchain objects from JSON + else: + initial_state, entry_point = default_state_and_entry_point() + app = ( + core.ApplicationBuilder() + .with_state(**initial_state) .with_actions( researcher=research_node, charter=chart_node, @@ -172,9 +188,9 @@ def application(): ("call_tool", "researcher", expr("sender == 'Researcher'")), ("call_tool", "charter", expr("sender == 'Chart Generator'")), ) - .with_entrypoint("researcher") + .with_entrypoint(entry_point) .with_hooks(PrintStepHook()) - .with_tracker("lcel-multi-agent") + .with_tracker("demo:lcel-multi-agent") .build() ) app.visualize( @@ -184,4 +200,5 @@ def application(): if __name__ == "__main__": - application() + main() + # main(SOME_APP_ID) # use this to restart from a previous state