From 335844d07773dda6778ac2ad5e427316abab38eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Oct 2025 15:14:03 +0000 Subject: [PATCH] Refactor: Update imports from llama_index.core.workflow to workflows Co-authored-by: adrian --- docs/examples/agent/Chatbot_SEC.ipynb | 10 +- .../examples/agent/agent_workflow_basic.ipynb | 1017 +++++++------- .../examples/agent/agent_workflow_multi.ipynb | 34 +- docs/examples/agent/agents_as_tools.ipynb | 102 +- docs/examples/agent/anthropic_agent.ipynb | 11 +- docs/examples/agent/code_act_agent.ipynb | 14 +- docs/examples/agent/custom_multi_agent.ipynb | 257 +--- .../agent/from_scratch_code_act_agent.ipynb | 156 +-- docs/examples/agent/gemini_agent.ipynb | 12 +- .../agent/memory/chat_memory_buffer.ipynb | 14 +- .../agent/memory/summary_memory_buffer.ipynb | 14 +- docs/examples/agent/mistral_agent.ipynb | 11 +- ...nt_workflow_with_weaviate_queryagent.ipynb | 31 +- ...research_assistant_for_blog_creation.ipynb | 201 +-- .../nvidia_sub_question_query_engine.ipynb | 24 +- .../openai_agent_context_retrieval.ipynb | 19 +- .../agent/openai_agent_query_cookbook.ipynb | 30 +- .../agent/openai_agent_retrieval.ipynb | 14 +- .../openai_agent_with_query_engine.ipynb | 10 +- docs/examples/agent/react_agent.ipynb | 12 +- .../agent/react_agent_with_query_engine.ipynb | 15 +- docs/examples/agent/return_direct_agent.ipynb | 29 +- docs/examples/cookbooks/airtrain.ipynb | 17 +- ...nowledge_graph_with_neo4j_llamacloud.ipynb | 103 +- .../cookbooks/ollama_gpt_oss_cookbook.ipynb | 9 +- .../cookbooks/toolhouse_llamaindex.ipynb | 15 +- .../evaluation/step_back_argilla.ipynb | 26 +- docs/examples/memory/custom_memory.ipynb | 100 +- docs/examples/tools/mcp.ipynb | 35 +- docs/examples/tools/mcp_toolbox.ipynb | 61 +- .../workflow/JSONalyze_query_engine.ipynb | 151 +-- .../workflow/advanced_text_to_sql.ipynb | 108 +- .../workflow/checkpointing_workflows.ipynb | 1024 +++++++------- .../workflow/citation_query_engine.ipynb | 130 +- .../workflow/corrective_rag_pack.ipynb | 230 +--- .../workflow/function_calling_agent.ipynb | 975 +++++++------- .../human_in_the_loop_story_crafting.ipynb | 11 +- docs/examples/workflow/long_rag_pack.ipynb | 137 +- .../workflow/multi_step_query_engine.ipynb | 165 +-- .../workflow/multi_strategy_workflow.ipynb | 23 +- .../workflow/parallel_execution.ipynb | 1052 ++++++++------- .../examples/workflow/planning_workflow.ipynb | 161 +-- docs/examples/workflow/rag.ipynb | 96 +- docs/examples/workflow/react_agent.ipynb | 1181 ++++++++--------- docs/examples/workflow/reflection.ipynb | 105 +- .../workflow/router_query_engine.ipynb | 182 +-- .../workflow/self_discover_workflow.ipynb | 113 +- .../workflow/sub_question_query_engine.ipynb | 22 +- .../workflow/workflows_cookbook.ipynb | 1119 ++++++++-------- .../getting_started/starter_example.mdx | 2 +- .../getting_started/starter_example_local.mdx | 2 +- .../module_guides/deploying/agents/memory.mdx | 4 +- .../understanding/agent/human_in_the_loop.md | 7 +- .../understanding/agent/multi_agent.md | 12 +- .../framework/understanding/agent/state.md | 7 +- .../understanding/workflows/basic_flow.md | 17 +- .../understanding/workflows/observability.md | 19 +- .../understanding/workflows/resources.md | 11 +- .../understanding/workflows/state.md | 19 +- .../understanding/workflows/stream.mdx | 10 +- .../understanding/workflows/subclass.md | 10 +- 61 files changed, 3274 insertions(+), 6234 deletions(-) diff --git a/docs/examples/agent/Chatbot_SEC.ipynb b/docs/examples/agent/Chatbot_SEC.ipynb index 40a5fd76a2..d0c3a2d3a9 100644 --- a/docs/examples/agent/Chatbot_SEC.ipynb +++ b/docs/examples/agent/Chatbot_SEC.ipynb @@ -388,13 +388,7 @@ } ], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "# Setup the context for this specific interaction\n", - "ctx = Context(agent)\n", - "\n", - "response = await agent.run(\"hi, i am bob\", ctx=ctx)\n", - "print(str(response))" + "from workflows import Context\n\n# Setup the context for this specific interaction\nctx = Context(agent)\n\nresponse = await agent.run(\"hi, i am bob\", ctx=ctx)\nprint(str(response))" ] }, { @@ -571,4 +565,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/agent_workflow_basic.ipynb b/docs/examples/agent/agent_workflow_basic.ipynb index 86a388bad1..3d3018397e 100644 --- a/docs/examples/agent/agent_workflow_basic.ipynb +++ b/docs/examples/agent/agent_workflow_basic.ipynb @@ -1,546 +1,493 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# FunctionAgent / AgentWorkflow Basic Introduction\n", - "\n", - "The `AgentWorkflow` is an orchestrator for running a system of one or more agents. In this example, we'll create a simple workflow with a single `FunctionAgent`, and use that to cover the basic functionality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install llama-index" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "In this example, we will use `OpenAI` as our LLM. For all LLMs, check out the [examples documentation](https://docs.llamaindex.ai/en/stable/examples/llm/openai/) or [LlamaHub](https://llamahub.ai/?tab=llms) for a list of all supported LLMs and how to install/use them." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.llms.openai import OpenAI\n", - "\n", - "llm = OpenAI(model=\"gpt-4o-mini\", api_key=\"sk-...\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To make our agent more useful, we can give it tools/actions to use. In this case, we'll use Tavily to implement a tool that can search the web for information. You can get a free API key from [Tavily](https://tavily.com/)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install tavily-python" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When creating a tool, its very important to:\n", - "- give the tool a proper name and docstring/description. The LLM uses this to understand what the tool does.\n", - "- annotate the types. This helps the LLM understand the expected input and output types.\n", - "- use async when possible, since this will make the workflow more efficient." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from tavily import AsyncTavilyClient\n", - "\n", - "\n", - "async def search_web(query: str) -> str:\n", - " \"\"\"Useful for using the web to answer questions.\"\"\"\n", - " client = AsyncTavilyClient(api_key=\"tvly-...\")\n", - " return str(await client.search(query))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With the tool and and LLM defined, we can create an `AgentWorkflow` that uses the tool." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.agent.workflow import FunctionAgent\n", - "\n", - "agent = FunctionAgent(\n", - " tools=[search_web],\n", - " llm=llm,\n", - " system_prompt=\"You are a helpful assistant that can search the web for information.\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running the Agent\n", - "\n", - "Now that our agent is created, we can run it!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "The current weather in San Francisco is as follows:\n", - "\n", - "- **Temperature**: 16.1°C (61°F)\n", - "- **Condition**: Partly cloudy\n", - "- **Wind**: 13.6 mph (22.0 kph) from the west\n", - "- **Humidity**: 64%\n", - "- **Visibility**: 16 km (9 miles)\n", - "- **Pressure**: 1017 mb (30.04 in)\n", - "\n", - "For more details, you can check the full report [here](https://www.weatherapi.com/).\n" - ] - } - ], - "source": [ - "response = await agent.run(user_msg=\"What is the weather in San Francisco?\")\n", - "print(str(response))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The above is the equivalent of the following of using `AgentWorkflow` with a single `FunctionAgent`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.agent.workflow import AgentWorkflow\n", - "\n", - "workflow = AgentWorkflow(agents=[agent])\n", - "\n", - "response = await workflow.run(user_msg=\"What is the weather in San Francisco?\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you were creating a workflow with multiple agents, you can pass in a list of agents to the `AgentWorkflow` constructor. Learn more in our [multi-agent workflow example](https://docs.llamaindex.ai/en/stable/understanding/agent/multi_agent/)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Maintaining State\n", - "\n", - "By default, the `FunctionAgent` will maintain stateless between runs. This means that the agent will not have any memory of previous runs.\n", - "\n", - "To maintain state, we need to keep track of the previous state. Since the `FunctionAgent` is running in a `Workflow`, the state is stored in the `Context`. This can be passed between runs to maintain state and history." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# FunctionAgent / AgentWorkflow Basic Introduction\n", + "\n", + "The `AgentWorkflow` is an orchestrator for running a system of one or more agents. In this example, we'll create a simple workflow with a single `FunctionAgent`, and use that to cover the basic functionality." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Nice to meet you, Logan! How can I assist you today?\n" - ] - } - ], - "source": [ - "response = await agent.run(\n", - " user_msg=\"My name is Logan, nice to meet you!\", ctx=ctx\n", - ")\n", - "print(str(response))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "%pip install llama-index" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Your name is Logan.\n" - ] - } - ], - "source": [ - "response = await agent.run(user_msg=\"What is my name?\", ctx=ctx)\n", - "print(str(response))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The context is serializable, so it can be saved to a database, file, etc. and loaded back in later. \n", - "\n", - "The `JsonSerializer` is a simple serializer that uses `json.dumps` and `json.loads` to serialize and deserialize the context.\n", - "\n", - "The `JsonPickleSerializer` is a serializer that uses `pickle` to serialize and deserialize the context. If you have objects in your context that are not serializable, you can use this serializer." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow import JsonPickleSerializer, JsonSerializer\n", - "\n", - "ctx_dict = ctx.to_dict(serializer=JsonSerializer())\n", - "\n", - "restored_ctx = Context.from_dict(agent, ctx_dict, serializer=JsonSerializer())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "In this example, we will use `OpenAI` as our LLM. For all LLMs, check out the [examples documentation](https://docs.llamaindex.ai/en/stable/examples/llm/openai/) or [LlamaHub](https://llamahub.ai/?tab=llms) for a list of all supported LLMs and how to install/use them." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Yes, I remember your name is Logan.\n" - ] - } - ], - "source": [ - "response = await agent.run(\n", - " user_msg=\"Do you still remember my name?\", ctx=restored_ctx\n", - ")\n", - "print(str(response))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Streaming\n", - "\n", - "The `AgentWorkflow`/`FunctionAgent` also supports streaming. Since the `AgentWorkflow` is a `Workflow`, it can be streamed like any other `Workflow`. This works by using the handler that is returned from the workflow. There are a few key events that are streamed, feel free to explore below.\n", - "\n", - "If you only want to stream the LLM output, you can use the `AgentStream` events." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.llms.openai import OpenAI\n", + "\n", + "llm = OpenAI(model=\"gpt-4o-mini\", api_key=\"sk-...\")" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "The current weather in Saskatoon is as follows:\n", - "\n", - "- **Temperature**: 22.2°C (72°F)\n", - "- **Condition**: Overcast\n", - "- **Humidity**: 25%\n", - "- **Wind Speed**: 6.0 mph (9.7 kph) from the northwest\n", - "- **Visibility**: 4.8 km\n", - "- **Pressure**: 1018 mb\n", - "\n", - "For more details, you can check the full report [here](https://www.weatherapi.com/)." - ] - } - ], - "source": [ - "from llama_index.core.agent.workflow import (\n", - " AgentInput,\n", - " AgentOutput,\n", - " ToolCall,\n", - " ToolCallResult,\n", - " AgentStream,\n", - ")\n", - "\n", - "handler = agent.run(user_msg=\"What is the weather in Saskatoon?\")\n", - "\n", - "async for event in handler.stream_events():\n", - " if isinstance(event, AgentStream):\n", - " print(event.delta, end=\"\", flush=True)\n", - " # print(event.response) # the current full response\n", - " # print(event.raw) # the raw llm api response\n", - " # print(event.current_agent_name) # the current agent name\n", - " # elif isinstance(event, AgentInput):\n", - " # print(event.input) # the current input messages\n", - " # print(event.current_agent_name) # the current agent name\n", - " # elif isinstance(event, AgentOutput):\n", - " # print(event.response) # the current full response\n", - " # print(event.tool_calls) # the selected tool calls, if any\n", - " # print(event.raw) # the raw llm api response\n", - " # elif isinstance(event, ToolCallResult):\n", - " # print(event.tool_name) # the tool name\n", - " # print(event.tool_kwargs) # the tool kwargs\n", - " # print(event.tool_output) # the tool output\n", - " # elif isinstance(event, ToolCall):\n", - " # print(event.tool_name) # the tool name\n", - " # print(event.tool_kwargs) # the tool kwargs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tools and State\n", - "\n", - "Tools can also be defined that have access to the workflow context. This means you can set and retrieve variables from the context and use them in the tool or between tools.\n", - "\n", - "**Note:** The `Context` parameter should be the first parameter of the tool." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To make our agent more useful, we can give it tools/actions to use. In this case, we'll use Tavily to implement a tool that can search the web for information. You can get a free API key from [Tavily](https://tavily.com/)." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Your name has been set to Logan.\n", - "Logan\n" - ] - } - ], - "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "\n", - "async def set_name(ctx: Context, name: str) -> str:\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " ctx_state[\"state\"][\"name\"] = name\n", - " return f\"Name set to {name}\"\n", - "\n", - "\n", - "agent = FunctionAgent(\n", - " tools=[set_name],\n", - " llm=llm,\n", - " system_prompt=\"You are a helpful assistant that can set a name.\",\n", - " initial_state={\"name\": \"unset\"},\n", - ")\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "response = await agent.run(user_msg=\"My name is Logan\", ctx=ctx)\n", - "print(str(response))\n", - "\n", - "state = await ctx.store.get(\"state\")\n", - "print(state[\"name\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Human in the Loop\n", - "\n", - "Tools can also be defined that involve a human in the loop. This is useful for tasks that require human input, such as confirming a tool call or providing feedback.\n", - "\n", - "Using workflow events, we can emit events that require a response from the user. Here, we use the built-in `InputRequiredEvent` and `HumanResponseEvent` to handle the human in the loop, but you can also define your own events.\n", - "\n", - "`wait_for_event` will emit the `waiter_event` and wait until it sees the `HumanResponseEvent` with the specified `requirements`. The `waiter_id` is used to ensure that we only send one `waiter_event` for each `waiter_id`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow import (\n", - " Context,\n", - " InputRequiredEvent,\n", - " HumanResponseEvent,\n", - ")\n", - "\n", - "\n", - "async def dangerous_task(ctx: Context) -> str:\n", - " \"\"\"A dangerous task that requires human confirmation.\"\"\"\n", - "\n", - " question = \"Are you sure you want to proceed?\"\n", - " response = await ctx.wait_for_event(\n", - " HumanResponseEvent,\n", - " waiter_id=question,\n", - " waiter_event=InputRequiredEvent(\n", - " prefix=question,\n", - " user_name=\"Logan\",\n", - " ),\n", - " requirements={\"user_name\": \"Logan\"},\n", - " )\n", - " if response.response == \"yes\":\n", - " return \"Dangerous task completed successfully.\"\n", - " else:\n", - " return \"Dangerous task aborted.\"\n", - "\n", - "\n", - "agent = FunctionAgent(\n", - " tools=[dangerous_task],\n", - " llm=llm,\n", - " system_prompt=\"You are a helpful assistant that can perform dangerous tasks.\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "%pip install tavily-python" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "The dangerous task has been completed successfully. If you need anything else, feel free to ask!\n" - ] - } - ], - "source": [ - "handler = agent.run(user_msg=\"I want to proceed with the dangerous task.\")\n", - "\n", - "async for event in handler.stream_events():\n", - " if isinstance(event, InputRequiredEvent):\n", - " response = input(event.prefix).strip().lower()\n", - " handler.ctx.send_event(\n", - " HumanResponseEvent(\n", - " response=response,\n", - " user_name=event.user_name,\n", - " )\n", - " )\n", - "\n", - "response = await handler\n", - "print(str(response))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In production scenarios, you might handle human-in-the-loop over a websocket or multiple API requests.\n", - "\n", - "As mentioned before, the `Context` object is serializable, and this means we can also save the workflow mid-run and restore it later. \n", - "\n", - "**NOTE:** Any functions/steps that were in-progress will start from the beginning when the workflow is restored." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When creating a tool, its very important to:\n", + "- give the tool a proper name and docstring/description. The LLM uses this to understand what the tool does.\n", + "- annotate the types. This helps the LLM understand the expected input and output types.\n", + "- use async when possible, since this will make the workflow more efficient." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from tavily import AsyncTavilyClient\n", + "\n", + "\n", + "async def search_web(query: str) -> str:\n", + " \"\"\"Useful for using the web to answer questions.\"\"\"\n", + " client = AsyncTavilyClient(api_key=\"tvly-...\")\n", + " return str(await client.search(query))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With the tool and and LLM defined, we can create an `AgentWorkflow` that uses the tool." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.agent.workflow import FunctionAgent\n", + "\n", + "agent = FunctionAgent(\n", + " tools=[search_web],\n", + " llm=llm,\n", + " system_prompt=\"You are a helpful assistant that can search the web for information.\",\n", + ")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running the Agent\n", + "\n", + "Now that our agent is created, we can run it!" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = await agent.run(user_msg=\"What is the weather in San Francisco?\")\n", + "print(str(response))" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The current weather in San Francisco is as follows:\n", + "\n", + "- **Temperature**: 16.1°C (61°F)\n", + "- **Condition**: Partly cloudy\n", + "- **Wind**: 13.6 mph (22.0 kph) from the west\n", + "- **Humidity**: 64%\n", + "- **Visibility**: 16 km (9 miles)\n", + "- **Pressure**: 1017 mb (30.04 in)\n", + "\n", + "For more details, you can check the full report [here](https://www.weatherapi.com/).\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above is the equivalent of the following of using `AgentWorkflow` with a single `FunctionAgent`:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.agent.workflow import AgentWorkflow\n", + "\n", + "workflow = AgentWorkflow(agents=[agent])\n", + "\n", + "response = await workflow.run(user_msg=\"What is the weather in San Francisco?\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you were creating a workflow with multiple agents, you can pass in a list of agents to the `AgentWorkflow` constructor. Learn more in our [multi-agent workflow example](https://docs.llamaindex.ai/en/stable/understanding/agent/multi_agent/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Maintaining State\n", + "\n", + "By default, the `FunctionAgent` will maintain stateless between runs. This means that the agent will not have any memory of previous runs.\n", + "\n", + "To maintain state, we need to keep track of the previous state. Since the `FunctionAgent` is running in a `Workflow`, the state is stored in the `Context`. This can be passed between runs to maintain state and history." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "The dangerous task has been completed successfully. If you need anything else, feel free to ask!\n" - ] + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Context\n\nctx = Context(agent)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = await agent.run(\n", + " user_msg=\"My name is Logan, nice to meet you!\", ctx=ctx\n", + ")\n", + "print(str(response))" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nice to meet you, Logan! How can I assist you today?\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = await agent.run(user_msg=\"What is my name?\", ctx=ctx)\n", + "print(str(response))" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Your name is Logan.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The context is serializable, so it can be saved to a database, file, etc. and loaded back in later. \n", + "\n", + "The `JsonSerializer` is a simple serializer that uses `json.dumps` and `json.loads` to serialize and deserialize the context.\n", + "\n", + "The `JsonPickleSerializer` is a serializer that uses `pickle` to serialize and deserialize the context. If you have objects in your context that are not serializable, you can use this serializer." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows.context import PickleSerializer\n", + "from workflows.context.serializers import JsonSerializer\n", + "\n", + "ctx_dict = ctx.to_dict(serializer=JsonSerializer())\n", + "\n", + "restored_ctx = Context.from_dict(agent, ctx_dict, serializer=JsonSerializer())" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = await agent.run(\n", + " user_msg=\"Do you still remember my name?\", ctx=restored_ctx\n", + ")\n", + "print(str(response))" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Yes, I remember your name is Logan.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Streaming\n", + "\n", + "The `AgentWorkflow`/`FunctionAgent` also supports streaming. Since the `AgentWorkflow` is a `Workflow`, it can be streamed like any other `Workflow`. This works by using the handler that is returned from the workflow. There are a few key events that are streamed, feel free to explore below.\n", + "\n", + "If you only want to stream the LLM output, you can use the `AgentStream` events." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.agent.workflow import (\n", + " AgentInput,\n", + " AgentOutput,\n", + " ToolCall,\n", + " ToolCallResult,\n", + " AgentStream,\n", + ")\n", + "\n", + "handler = agent.run(user_msg=\"What is the weather in Saskatoon?\")\n", + "\n", + "async for event in handler.stream_events():\n", + " if isinstance(event, AgentStream):\n", + " print(event.delta, end=\"\", flush=True)\n", + " # print(event.response) # the current full response\n", + " # print(event.raw) # the raw llm api response\n", + " # print(event.current_agent_name) # the current agent name\n", + " # elif isinstance(event, AgentInput):\n", + " # print(event.input) # the current input messages\n", + " # print(event.current_agent_name) # the current agent name\n", + " # elif isinstance(event, AgentOutput):\n", + " # print(event.response) # the current full response\n", + " # print(event.tool_calls) # the selected tool calls, if any\n", + " # print(event.raw) # the raw llm api response\n", + " # elif isinstance(event, ToolCallResult):\n", + " # print(event.tool_name) # the tool name\n", + " # print(event.tool_kwargs) # the tool kwargs\n", + " # print(event.tool_output) # the tool output\n", + " # elif isinstance(event, ToolCall):\n", + " # print(event.tool_name) # the tool name\n", + " # print(event.tool_kwargs) # the tool kwargs" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The current weather in Saskatoon is as follows:\n", + "\n", + "- **Temperature**: 22.2°C (72°F)\n", + "- **Condition**: Overcast\n", + "- **Humidity**: 25%\n", + "- **Wind Speed**: 6.0 mph (9.7 kph) from the northwest\n", + "- **Visibility**: 4.8 km\n", + "- **Pressure**: 1018 mb\n", + "\n", + "For more details, you can check the full report [here](https://www.weatherapi.com/)." + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tools and State\n", + "\n", + "Tools can also be defined that have access to the workflow context. This means you can set and retrieve variables from the context and use them in the tool or between tools.\n", + "\n", + "**Note:** The `Context` parameter should be the first parameter of the tool." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Context\n\n\nasync def set_name(ctx: Context, name: str) -> str:\n async with ctx.store.edit_state() as ctx_state:\n ctx_state[\"state\"][\"name\"] = name\n return f\"Name set to {name}\"\n\n\nagent = FunctionAgent(\n tools=[set_name],\n llm=llm,\n system_prompt=\"You are a helpful assistant that can set a name.\",\n initial_state={\"name\": \"unset\"},\n)\n\nctx = Context(agent)\n\nresponse = await agent.run(user_msg=\"My name is Logan\", ctx=ctx)\nprint(str(response))\n\nstate = await ctx.store.get(\"state\")\nprint(state[\"name\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Your name has been set to Logan.\n", + "Logan\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Human in the Loop\n", + "\n", + "Tools can also be defined that involve a human in the loop. This is useful for tasks that require human input, such as confirming a tool call or providing feedback.\n", + "\n", + "Using workflow events, we can emit events that require a response from the user. Here, we use the built-in `InputRequiredEvent` and `HumanResponseEvent` to handle the human in the loop, but you can also define your own events.\n", + "\n", + "`wait_for_event` will emit the `waiter_event` and wait until it sees the `HumanResponseEvent` with the specified `requirements`. The `waiter_id` is used to ensure that we only send one `waiter_event` for each `waiter_id`." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Context\nfrom workflows.events import InputRequiredEvent, HumanResponseEvent\n\n\nasync def dangerous_task(ctx: Context) -> str:\n \"\"\"A dangerous task that requires human confirmation.\"\"\"\n\n question = \"Are you sure you want to proceed?\"\n response = await ctx.wait_for_event(\n HumanResponseEvent,\n waiter_id=question,\n waiter_event=InputRequiredEvent(\n prefix=question,\n user_name=\"Logan\",\n ),\n requirements={\"user_name\": \"Logan\"},\n )\n if response.response == \"yes\":\n return \"Dangerous task completed successfully.\"\n else:\n return \"Dangerous task aborted.\"\n\n\nagent = FunctionAgent(\n tools=[dangerous_task],\n llm=llm,\n system_prompt=\"You are a helpful assistant that can perform dangerous tasks.\",\n)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "handler = agent.run(user_msg=\"I want to proceed with the dangerous task.\")\n", + "\n", + "async for event in handler.stream_events():\n", + " if isinstance(event, InputRequiredEvent):\n", + " response = input(event.prefix).strip().lower()\n", + " handler.ctx.send_event(\n", + " HumanResponseEvent(\n", + " response=response,\n", + " user_name=event.user_name,\n", + " )\n", + " )\n", + "\n", + "response = await handler\n", + "print(str(response))" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The dangerous task has been completed successfully. If you need anything else, feel free to ask!\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In production scenarios, you might handle human-in-the-loop over a websocket or multiple API requests.\n", + "\n", + "As mentioned before, the `Context` object is serializable, and this means we can also save the workflow mid-run and restore it later. \n", + "\n", + "**NOTE:** Any functions/steps that were in-progress will start from the beginning when the workflow is restored." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows.context.serializers import JsonSerializer\n", + "\n", + "handler = agent.run(user_msg=\"I want to proceed with the dangerous task.\")\n", + "\n", + "input_ev = None\n", + "async for event in handler.stream_events():\n", + " if isinstance(event, InputRequiredEvent):\n", + " input_ev = event\n", + " break\n", + "\n", + "# save the context somewhere for later\n", + "ctx_dict = handler.ctx.to_dict(serializer=JsonSerializer())\n", + "\n", + "# get the response from the user\n", + "response_str = input(input_ev.prefix).strip().lower()\n", + "\n", + "# restore the workflow\n", + "restored_ctx = Context.from_dict(agent, ctx_dict, serializer=JsonSerializer())\n", + "\n", + "handler = agent.run(ctx=restored_ctx)\n", + "handler.ctx.send_event(\n", + " HumanResponseEvent(\n", + " response=response_str,\n", + " user_name=input_ev.user_name,\n", + " )\n", + ")\n", + "response = await handler\n", + "print(str(response))" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The dangerous task has been completed successfully. If you need anything else, feel free to ask!\n" + ] + } + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } - ], - "source": [ - "from llama_index.core.workflow import JsonSerializer\n", - "\n", - "handler = agent.run(user_msg=\"I want to proceed with the dangerous task.\")\n", - "\n", - "input_ev = None\n", - "async for event in handler.stream_events():\n", - " if isinstance(event, InputRequiredEvent):\n", - " input_ev = event\n", - " break\n", - "\n", - "# save the context somewhere for later\n", - "ctx_dict = handler.ctx.to_dict(serializer=JsonSerializer())\n", - "\n", - "# get the response from the user\n", - "response_str = input(input_ev.prefix).strip().lower()\n", - "\n", - "# restore the workflow\n", - "restored_ctx = Context.from_dict(agent, ctx_dict, serializer=JsonSerializer())\n", - "\n", - "handler = agent.run(ctx=restored_ctx)\n", - "handler.ctx.send_event(\n", - " HumanResponseEvent(\n", - " response=response_str,\n", - " user_name=input_ev.user_name,\n", - " )\n", - ")\n", - "response = await handler\n", - "print(str(response))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/docs/examples/agent/agent_workflow_multi.ipynb b/docs/examples/agent/agent_workflow_multi.ipynb index bb3f97afb8..1ae6ba5210 100644 --- a/docs/examples/agent/agent_workflow_multi.ipynb +++ b/docs/examples/agent/agent_workflow_multi.ipynb @@ -83,37 +83,7 @@ "metadata": {}, "outputs": [], "source": [ - "from tavily import AsyncTavilyClient\n", - "from llama_index.core.workflow import Context\n", - "\n", - "\n", - "async def search_web(query: str) -> str:\n", - " \"\"\"Useful for using the web to answer questions.\"\"\"\n", - " client = AsyncTavilyClient(api_key=\"tvly-...\")\n", - " return str(await client.search(query))\n", - "\n", - "\n", - "async def record_notes(ctx: Context, notes: str, notes_title: str) -> str:\n", - " \"\"\"Useful for recording notes on a given topic. Your input should be notes with a title to save the notes under.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " if \"research_notes\" not in ctx_state[\"state\"]:\n", - " ctx_state[\"state\"][\"research_notes\"] = {}\n", - " ctx_state[\"state\"][\"research_notes\"][notes_title] = notes\n", - " return \"Notes recorded.\"\n", - "\n", - "\n", - "async def write_report(ctx: Context, report_content: str) -> str:\n", - " \"\"\"Useful for writing a report on a given topic. Your input should be a markdown formatted report.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " ctx_state[\"state\"][\"report_content\"] = report_content\n", - " return \"Report written.\"\n", - "\n", - "\n", - "async def review_report(ctx: Context, review: str) -> str:\n", - " \"\"\"Useful for reviewing a report and providing feedback. Your input should be a review of the report.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " ctx_state[\"state\"][\"review\"] = review\n", - " return \"Report reviewed.\"" + "from tavily import AsyncTavilyClient\nfrom workflows import Context\n\n\nasync def search_web(query: str) -> str:\n \"\"\"Useful for using the web to answer questions.\"\"\"\n client = AsyncTavilyClient(api_key=\"tvly-...\")\n return str(await client.search(query))\n\n\nasync def record_notes(ctx: Context, notes: str, notes_title: str) -> str:\n \"\"\"Useful for recording notes on a given topic. Your input should be notes with a title to save the notes under.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n if \"research_notes\" not in ctx_state[\"state\"]:\n ctx_state[\"state\"][\"research_notes\"] = {}\n ctx_state[\"state\"][\"research_notes\"][notes_title] = notes\n return \"Notes recorded.\"\n\n\nasync def write_report(ctx: Context, report_content: str) -> str:\n \"\"\"Useful for writing a report on a given topic. Your input should be a markdown formatted report.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n ctx_state[\"state\"][\"report_content\"] = report_content\n return \"Report written.\"\n\n\nasync def review_report(ctx: Context, review: str) -> str:\n \"\"\"Useful for reviewing a report and providing feedback. Your input should be a review of the report.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n ctx_state[\"state\"][\"review\"] = review\n return \"Report reviewed.\"" ] }, { @@ -387,4 +357,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/agents_as_tools.ipynb b/docs/examples/agent/agents_as_tools.ipynb index 26eb64cc18..916d091bb6 100644 --- a/docs/examples/agent/agents_as_tools.ipynb +++ b/docs/examples/agent/agents_as_tools.ipynb @@ -145,63 +145,7 @@ "metadata": {}, "outputs": [], "source": [ - "import re\n", - "from llama_index.core.workflow import Context\n", - "\n", - "\n", - "async def call_research_agent(ctx: Context, prompt: str) -> str:\n", - " \"\"\"Useful for recording research notes based on a specific prompt.\"\"\"\n", - " result = await research_agent.run(\n", - " user_msg=f\"Write some notes about the following: {prompt}\"\n", - " )\n", - "\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " ctx_state[\"state\"][\"research_notes\"].append(str(result))\n", - "\n", - " return str(result)\n", - "\n", - "\n", - "async def call_write_agent(ctx: Context) -> str:\n", - " \"\"\"Useful for writing a report based on the research notes or revising the report based on feedback.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " notes = ctx_state[\"state\"].get(\"research_notes\", None)\n", - " if not notes:\n", - " return \"No research notes to write from.\"\n", - "\n", - " user_msg = f\"Write a markdown report from the following notes. Be sure to output the report in the following format: ...:\\n\\n\"\n", - "\n", - " # Add the feedback to the user message if it exists\n", - " feedback = ctx_state[\"state\"].get(\"review\", None)\n", - " if feedback:\n", - " user_msg += f\"{feedback}\\n\\n\"\n", - "\n", - " # Add the research notes to the user message\n", - " notes = \"\\n\\n\".join(notes)\n", - " user_msg += f\"{notes}\\n\\n\"\n", - "\n", - " # Run the write agent\n", - " result = await write_agent.run(user_msg=user_msg)\n", - " report = re.search(\n", - " r\"(.*)\", str(result), re.DOTALL\n", - " ).group(1)\n", - " ctx_state[\"state\"][\"report_content\"] = str(report)\n", - "\n", - " return str(report)\n", - "\n", - "\n", - "async def call_review_agent(ctx: Context) -> str:\n", - " \"\"\"Useful for reviewing the report and providing feedback.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " report = ctx_state[\"state\"].get(\"report_content\", None)\n", - " if not report:\n", - " return \"No report content to review.\"\n", - "\n", - " result = await review_agent.run(\n", - " user_msg=f\"Review the following report: {report}\"\n", - " )\n", - " ctx_state[\"state\"][\"review\"] = result\n", - "\n", - " return result" + "import re\nfrom workflows import Context\n\n\nasync def call_research_agent(ctx: Context, prompt: str) -> str:\n \"\"\"Useful for recording research notes based on a specific prompt.\"\"\"\n result = await research_agent.run(\n user_msg=f\"Write some notes about the following: {prompt}\"\n )\n\n async with ctx.store.edit_state() as ctx_state:\n ctx_state[\"state\"][\"research_notes\"].append(str(result))\n\n return str(result)\n\n\nasync def call_write_agent(ctx: Context) -> str:\n \"\"\"Useful for writing a report based on the research notes or revising the report based on feedback.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n notes = ctx_state[\"state\"].get(\"research_notes\", None)\n if not notes:\n return \"No research notes to write from.\"\n\n user_msg = f\"Write a markdown report from the following notes. Be sure to output the report in the following format: ...:\\n\\n\"\n\n # Add the feedback to the user message if it exists\n feedback = ctx_state[\"state\"].get(\"review\", None)\n if feedback:\n user_msg += f\"{feedback}\\n\\n\"\n\n # Add the research notes to the user message\n notes = \"\\n\\n\".join(notes)\n user_msg += f\"{notes}\\n\\n\"\n\n # Run the write agent\n result = await write_agent.run(user_msg=user_msg)\n report = re.search(\n r\"(.*)\", str(result), re.DOTALL\n ).group(1)\n ctx_state[\"state\"][\"report_content\"] = str(report)\n\n return str(report)\n\n\nasync def call_review_agent(ctx: Context) -> str:\n \"\"\"Useful for reviewing the report and providing feedback.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n report = ctx_state[\"state\"].get(\"report_content\", None)\n if not report:\n return \"No report content to review.\"\n\n result = await review_agent.run(\n user_msg=f\"Review the following report: {report}\"\n )\n ctx_state[\"state\"][\"review\"] = result\n\n return result" ] }, { @@ -255,47 +199,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import (\n", - " AgentInput,\n", - " AgentOutput,\n", - " ToolCall,\n", - " ToolCallResult,\n", - " AgentStream,\n", - ")\n", - "from llama_index.core.workflow import Context\n", - "\n", - "# Create a context for the orchestrator to hold history/state\n", - "ctx = Context(orchestrator)\n", - "\n", - "\n", - "async def run_orchestrator(ctx: Context, user_msg: str):\n", - " handler = orchestrator.run(\n", - " user_msg=user_msg,\n", - " ctx=ctx,\n", - " )\n", - "\n", - " async for event in handler.stream_events():\n", - " if isinstance(event, AgentStream):\n", - " if event.delta:\n", - " print(event.delta, end=\"\", flush=True)\n", - " # elif isinstance(event, AgentInput):\n", - " # print(\"📥 Input:\", event.input)\n", - " elif isinstance(event, AgentOutput):\n", - " # Skip printing the output since we are streaming above\n", - " # if event.response.content:\n", - " # print(\"📤 Output:\", event.response.content)\n", - " if event.tool_calls:\n", - " print(\n", - " \"🛠️ Planning to use tools:\",\n", - " [call.tool_name for call in event.tool_calls],\n", - " )\n", - " elif isinstance(event, ToolCallResult):\n", - " print(f\"🔧 Tool Result ({event.tool_name}):\")\n", - " print(f\" Arguments: {event.tool_kwargs}\")\n", - " print(f\" Output: {event.tool_output}\")\n", - " elif isinstance(event, ToolCall):\n", - " print(f\"🔨 Calling Tool: {event.tool_name}\")\n", - " print(f\" With arguments: {event.tool_kwargs}\")" + "from llama_index.core.agent.workflow import (\n AgentInput,\n AgentOutput,\n ToolCall,\n ToolCallResult,\n AgentStream,\n)\nfrom workflows import Context\n\n# Create a context for the orchestrator to hold history/state\nctx = Context(orchestrator)\n\n\nasync def run_orchestrator(ctx: Context, user_msg: str):\n handler = orchestrator.run(\n user_msg=user_msg,\n ctx=ctx,\n )\n\n async for event in handler.stream_events():\n if isinstance(event, AgentStream):\n if event.delta:\n print(event.delta, end=\"\", flush=True)\n # elif isinstance(event, AgentInput):\n # print(\"📥 Input:\", event.input)\n elif isinstance(event, AgentOutput):\n # Skip printing the output since we are streaming above\n # if event.response.content:\n # print(\"📤 Output:\", event.response.content)\n if event.tool_calls:\n print(\n \"🛠️ Planning to use tools:\",\n [call.tool_name for call in event.tool_calls],\n )\n elif isinstance(event, ToolCallResult):\n print(f\"🔧 Tool Result ({event.tool_name}):\")\n print(f\" Arguments: {event.tool_kwargs}\")\n print(f\" Output: {event.tool_output}\")\n elif isinstance(event, ToolCall):\n print(f\"🔨 Calling Tool: {event.tool_name}\")\n print(f\" With arguments: {event.tool_kwargs}\")" ] }, { @@ -568,4 +472,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/anthropic_agent.ipynb b/docs/examples/agent/anthropic_agent.ipynb index 026ea8e253..2299f0fa08 100644 --- a/docs/examples/agent/anthropic_agent.ipynb +++ b/docs/examples/agent/anthropic_agent.ipynb @@ -231,14 +231,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "response = await agent.run(\"My name is John Doe\", ctx=ctx)\n", - "response = await agent.run(\"What is my name?\", ctx=ctx)\n", - "\n", - "print(str(response))" + "from workflows import Context\n\nctx = Context(agent)\n\nresponse = await agent.run(\"My name is John Doe\", ctx=ctx)\nresponse = await agent.run(\"What is my name?\", ctx=ctx)\n\nprint(str(response))" ] }, { @@ -386,4 +379,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/code_act_agent.ipynb b/docs/examples/agent/code_act_agent.ipynb index fc52ca0510..7a9b539ca0 100644 --- a/docs/examples/agent/code_act_agent.ipynb +++ b/docs/examples/agent/code_act_agent.ipynb @@ -225,17 +225,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import CodeActAgent\n", - "from llama_index.core.workflow import Context\n", - "\n", - "agent = CodeActAgent(\n", - " code_execute_fn=code_executor.execute,\n", - " llm=llm,\n", - " tools=[add, subtract, multiply, divide],\n", - ")\n", - "\n", - "# context to hold the agent's session/state/chat history\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import CodeActAgent\nfrom workflows import Context\n\nagent = CodeActAgent(\n code_execute_fn=code_executor.execute,\n llm=llm,\n tools=[add, subtract, multiply, divide],\n)\n\n# context to hold the agent's session/state/chat history\nctx = Context(agent)" ] }, { @@ -491,4 +481,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/custom_multi_agent.ipynb b/docs/examples/agent/custom_multi_agent.ipynb index 4d5247218d..2e57d1da1d 100644 --- a/docs/examples/agent/custom_multi_agent.ipynb +++ b/docs/examples/agent/custom_multi_agent.ipynb @@ -152,63 +152,7 @@ "metadata": {}, "outputs": [], "source": [ - "import re\n", - "from llama_index.core.workflow import Context\n", - "\n", - "\n", - "async def call_research_agent(ctx: Context, prompt: str) -> str:\n", - " \"\"\"Useful for recording research notes based on a specific prompt.\"\"\"\n", - " result = await research_agent.run(\n", - " user_msg=f\"Write some notes about the following: {prompt}\"\n", - " )\n", - "\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " ctx_state[\"state\"][\"research_notes\"].append(str(result))\n", - "\n", - " return str(result)\n", - "\n", - "\n", - "async def call_write_agent(ctx: Context) -> str:\n", - " \"\"\"Useful for writing a report based on the research notes or revising the report based on feedback.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " notes = ctx_state[\"state\"].get(\"research_notes\", None)\n", - " if not notes:\n", - " return \"No research notes to write from.\"\n", - "\n", - " user_msg = f\"Write a markdown report from the following notes. Be sure to output the report in the following format: ...:\\n\\n\"\n", - "\n", - " # Add the feedback to the user message if it exists\n", - " feedback = ctx_state[\"state\"].get(\"review\", None)\n", - " if feedback:\n", - " user_msg += f\"{feedback}\\n\\n\"\n", - "\n", - " # Add the research notes to the user message\n", - " notes = \"\\n\\n\".join(notes)\n", - " user_msg += f\"{notes}\\n\\n\"\n", - "\n", - " # Run the write agent\n", - " result = await write_agent.run(user_msg=user_msg)\n", - " report = re.search(\n", - " r\"(.*)\", str(result), re.DOTALL\n", - " ).group(1)\n", - " ctx_state[\"state\"][\"report_content\"] = str(report)\n", - "\n", - " return str(report)\n", - "\n", - "\n", - "async def call_review_agent(ctx: Context) -> str:\n", - " \"\"\"Useful for reviewing the report and providing feedback.\"\"\"\n", - " async with ctx.store.edit_state() as ctx_state:\n", - " report = ctx_state[\"state\"].get(\"report_content\", None)\n", - " if not report:\n", - " return \"No report content to review.\"\n", - "\n", - " result = await review_agent.run(\n", - " user_msg=f\"Review the following report: {report}\"\n", - " )\n", - " ctx_state[\"state\"][\"review\"] = result\n", - "\n", - " return result" + "import re\nfrom workflows import Context\n\n\nasync def call_research_agent(ctx: Context, prompt: str) -> str:\n \"\"\"Useful for recording research notes based on a specific prompt.\"\"\"\n result = await research_agent.run(\n user_msg=f\"Write some notes about the following: {prompt}\"\n )\n\n async with ctx.store.edit_state() as ctx_state:\n ctx_state[\"state\"][\"research_notes\"].append(str(result))\n\n return str(result)\n\n\nasync def call_write_agent(ctx: Context) -> str:\n \"\"\"Useful for writing a report based on the research notes or revising the report based on feedback.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n notes = ctx_state[\"state\"].get(\"research_notes\", None)\n if not notes:\n return \"No research notes to write from.\"\n\n user_msg = f\"Write a markdown report from the following notes. Be sure to output the report in the following format: ...:\\n\\n\"\n\n # Add the feedback to the user message if it exists\n feedback = ctx_state[\"state\"].get(\"review\", None)\n if feedback:\n user_msg += f\"{feedback}\\n\\n\"\n\n # Add the research notes to the user message\n notes = \"\\n\\n\".join(notes)\n user_msg += f\"{notes}\\n\\n\"\n\n # Run the write agent\n result = await write_agent.run(user_msg=user_msg)\n report = re.search(\n r\"(.*)\", str(result), re.DOTALL\n ).group(1)\n ctx_state[\"state\"][\"report_content\"] = str(report)\n\n return str(report)\n\n\nasync def call_review_agent(ctx: Context) -> str:\n \"\"\"Useful for reviewing the report and providing feedback.\"\"\"\n async with ctx.store.edit_state() as ctx_state:\n report = ctx_state[\"state\"].get(\"report_content\", None)\n if not report:\n return \"No report content to review.\"\n\n result = await review_agent.run(\n user_msg=f\"Review the following report: {report}\"\n )\n ctx_state[\"state\"][\"review\"] = result\n\n return result" ] }, { @@ -228,202 +172,7 @@ "metadata": {}, "outputs": [], "source": [ - "import re\n", - "import xml.etree.ElementTree as ET\n", - "from pydantic import BaseModel, Field\n", - "from typing import Any, Optional\n", - "\n", - "from llama_index.core.llms import ChatMessage\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - ")\n", - "\n", - "PLANNER_PROMPT = \"\"\"You are a planner chatbot. \n", - "\n", - "Given a user request and the current state, break the solution into ordered blocks. Each step must specify the agent to call and the message to send, e.g.\n", - "\n", - " search for …\n", - " draft a report …\n", - " ...\n", - "\n", - "\n", - "\n", - "{state}\n", - "\n", - "\n", - "\n", - "{available_agents}\n", - "\n", - "\n", - "The general flow should be:\n", - "- Record research notes\n", - "- Write a report\n", - "- Review the report\n", - "- Write the report again if the review is not positive enough\n", - "\n", - "If the user request does not require any steps, you can skip the block and respond directly.\n", - "\"\"\"\n", - "\n", - "\n", - "class InputEvent(StartEvent):\n", - " user_msg: Optional[str] = Field(default=None)\n", - " chat_history: list[ChatMessage]\n", - " state: Optional[dict[str, Any]] = Field(default=None)\n", - "\n", - "\n", - "class OutputEvent(StopEvent):\n", - " response: str\n", - " chat_history: list[ChatMessage]\n", - " state: dict[str, Any]\n", - "\n", - "\n", - "class StreamEvent(Event):\n", - " delta: str\n", - "\n", - "\n", - "class PlanEvent(Event):\n", - " step_info: str\n", - "\n", - "\n", - "# Modelling the plan\n", - "class PlanStep(BaseModel):\n", - " agent_name: str\n", - " agent_input: str\n", - "\n", - "\n", - "class Plan(BaseModel):\n", - " steps: list[PlanStep]\n", - "\n", - "\n", - "class ExecuteEvent(Event):\n", - " plan: Plan\n", - " chat_history: list[ChatMessage]\n", - "\n", - "\n", - "class PlannerWorkflow(Workflow):\n", - " llm: OpenAI = OpenAI(\n", - " model=\"o3-mini\",\n", - " api_key=\"sk-...\",\n", - " )\n", - " agents: dict[str, FunctionAgent] = {\n", - " \"ResearchAgent\": research_agent,\n", - " \"WriteAgent\": write_agent,\n", - " \"ReviewAgent\": review_agent,\n", - " }\n", - "\n", - " @step\n", - " async def plan(\n", - " self, ctx: Context, ev: InputEvent\n", - " ) -> ExecuteEvent | OutputEvent:\n", - " # Set initial state if it exists\n", - " if ev.state:\n", - " await ctx.store.set(\"state\", ev.state)\n", - "\n", - " chat_history = ev.chat_history\n", - "\n", - " if ev.user_msg:\n", - " user_msg = ChatMessage(\n", - " role=\"user\",\n", - " content=ev.user_msg,\n", - " )\n", - " chat_history.append(user_msg)\n", - "\n", - " # Inject the system prompt with state and available agents\n", - " state = await ctx.store.get(\"state\")\n", - " available_agents_str = \"\\n\".join(\n", - " [\n", - " f'{agent.description}'\n", - " for agent in self.agents.values()\n", - " ]\n", - " )\n", - " system_prompt = ChatMessage(\n", - " role=\"system\",\n", - " content=PLANNER_PROMPT.format(\n", - " state=str(state),\n", - " available_agents=available_agents_str,\n", - " ),\n", - " )\n", - "\n", - " # Stream the response from the llm\n", - " response = await self.llm.astream_chat(\n", - " messages=[system_prompt] + chat_history,\n", - " )\n", - " full_response = \"\"\n", - " async for chunk in response:\n", - " full_response += chunk.delta or \"\"\n", - " if chunk.delta:\n", - " ctx.write_event_to_stream(\n", - " StreamEvent(delta=chunk.delta),\n", - " )\n", - "\n", - " # Parse the response into a plan and decide whether to execute or output\n", - " xml_match = re.search(r\"(.*)\", full_response, re.DOTALL)\n", - "\n", - " if not xml_match:\n", - " chat_history.append(\n", - " ChatMessage(\n", - " role=\"assistant\",\n", - " content=full_response,\n", - " )\n", - " )\n", - " return OutputEvent(\n", - " response=full_response,\n", - " chat_history=chat_history,\n", - " state=state,\n", - " )\n", - " else:\n", - " xml_str = xml_match.group(1)\n", - " root = ET.fromstring(xml_str)\n", - " plan = Plan(steps=[])\n", - " for step in root.findall(\"step\"):\n", - " plan.steps.append(\n", - " PlanStep(\n", - " agent_name=step.attrib[\"agent\"],\n", - " agent_input=step.text.strip() if step.text else \"\",\n", - " )\n", - " )\n", - "\n", - " return ExecuteEvent(plan=plan, chat_history=chat_history)\n", - "\n", - " @step\n", - " async def execute(self, ctx: Context, ev: ExecuteEvent) -> InputEvent:\n", - " chat_history = ev.chat_history\n", - " plan = ev.plan\n", - "\n", - " for step in plan.steps:\n", - " agent = self.agents[step.agent_name]\n", - " agent_input = step.agent_input\n", - " ctx.write_event_to_stream(\n", - " PlanEvent(\n", - " step_info=f'{step.agent_input}'\n", - " ),\n", - " )\n", - "\n", - " if step.agent_name == \"ResearchAgent\":\n", - " await call_research_agent(ctx, agent_input)\n", - " elif step.agent_name == \"WriteAgent\":\n", - " # Note: we aren't passing the input from the plan since\n", - " # we're using the state to drive the write agent\n", - " await call_write_agent(ctx)\n", - " elif step.agent_name == \"ReviewAgent\":\n", - " await call_review_agent(ctx)\n", - "\n", - " state = await ctx.store.get(\"state\")\n", - " chat_history.append(\n", - " ChatMessage(\n", - " role=\"user\",\n", - " content=f\"I've completed the previous steps, here's the updated state:\\n\\n\\n{state}\\n\\n\\nDo you need to continue and plan more steps?, If not, write a final response.\",\n", - " )\n", - " )\n", - "\n", - " return InputEvent(\n", - " chat_history=chat_history,\n", - " )" + "import re\nimport xml.etree.ElementTree as ET\nfrom pydantic import BaseModel, Field\nfrom typing import Any, Optional\n\nfrom llama_index.core.llms import ChatMessage\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\nPLANNER_PROMPT = \"\"\"You are a planner chatbot. \n\nGiven a user request and the current state, break the solution into ordered blocks. Each step must specify the agent to call and the message to send, e.g.\n\n search for …\n draft a report …\n ...\n\n\n\n{state}\n\n\n\n{available_agents}\n\n\nThe general flow should be:\n- Record research notes\n- Write a report\n- Review the report\n- Write the report again if the review is not positive enough\n\nIf the user request does not require any steps, you can skip the block and respond directly.\n\"\"\"\n\n\nclass InputEvent(StartEvent):\n user_msg: Optional[str] = Field(default=None)\n chat_history: list[ChatMessage]\n state: Optional[dict[str, Any]] = Field(default=None)\n\n\nclass OutputEvent(StopEvent):\n response: str\n chat_history: list[ChatMessage]\n state: dict[str, Any]\n\n\nclass StreamEvent(Event):\n delta: str\n\n\nclass PlanEvent(Event):\n step_info: str\n\n\n# Modelling the plan\nclass PlanStep(BaseModel):\n agent_name: str\n agent_input: str\n\n\nclass Plan(BaseModel):\n steps: list[PlanStep]\n\n\nclass ExecuteEvent(Event):\n plan: Plan\n chat_history: list[ChatMessage]\n\n\nclass PlannerWorkflow(Workflow):\n llm: OpenAI = OpenAI(\n model=\"o3-mini\",\n api_key=\"sk-...\",\n )\n agents: dict[str, FunctionAgent] = {\n \"ResearchAgent\": research_agent,\n \"WriteAgent\": write_agent,\n \"ReviewAgent\": review_agent,\n }\n\n @step\n async def plan(\n self, ctx: Context, ev: InputEvent\n ) -> ExecuteEvent | OutputEvent:\n # Set initial state if it exists\n if ev.state:\n await ctx.store.set(\"state\", ev.state)\n\n chat_history = ev.chat_history\n\n if ev.user_msg:\n user_msg = ChatMessage(\n role=\"user\",\n content=ev.user_msg,\n )\n chat_history.append(user_msg)\n\n # Inject the system prompt with state and available agents\n state = await ctx.store.get(\"state\")\n available_agents_str = \"\\n\".join(\n [\n f'{agent.description}'\n for agent in self.agents.values()\n ]\n )\n system_prompt = ChatMessage(\n role=\"system\",\n content=PLANNER_PROMPT.format(\n state=str(state),\n available_agents=available_agents_str,\n ),\n )\n\n # Stream the response from the llm\n response = await self.llm.astream_chat(\n messages=[system_prompt] + chat_history,\n )\n full_response = \"\"\n async for chunk in response:\n full_response += chunk.delta or \"\"\n if chunk.delta:\n ctx.write_event_to_stream(\n StreamEvent(delta=chunk.delta),\n )\n\n # Parse the response into a plan and decide whether to execute or output\n xml_match = re.search(r\"(.*)\", full_response, re.DOTALL)\n\n if not xml_match:\n chat_history.append(\n ChatMessage(\n role=\"assistant\",\n content=full_response,\n )\n )\n return OutputEvent(\n response=full_response,\n chat_history=chat_history,\n state=state,\n )\n else:\n xml_str = xml_match.group(1)\n root = ET.fromstring(xml_str)\n plan = Plan(steps=[])\n for step in root.findall(\"step\"):\n plan.steps.append(\n PlanStep(\n agent_name=step.attrib[\"agent\"],\n agent_input=step.text.strip() if step.text else \"\",\n )\n )\n\n return ExecuteEvent(plan=plan, chat_history=chat_history)\n\n @step\n async def execute(self, ctx: Context, ev: ExecuteEvent) -> InputEvent:\n chat_history = ev.chat_history\n plan = ev.plan\n\n for step in plan.steps:\n agent = self.agents[step.agent_name]\n agent_input = step.agent_input\n ctx.write_event_to_stream(\n PlanEvent(\n step_info=f'{step.agent_input}'\n ),\n )\n\n if step.agent_name == \"ResearchAgent\":\n await call_research_agent(ctx, agent_input)\n elif step.agent_name == \"WriteAgent\":\n # Note: we aren't passing the input from the plan since\n # we're using the state to drive the write agent\n await call_write_agent(ctx)\n elif step.agent_name == \"ReviewAgent\":\n await call_review_agent(ctx)\n\n state = await ctx.store.get(\"state\")\n chat_history.append(\n ChatMessage(\n role=\"user\",\n content=f\"I've completed the previous steps, here's the updated state:\\n\\n\\n{state}\\n\\n\\nDo you need to continue and plan more steps?, If not, write a final response.\",\n )\n )\n\n return InputEvent(\n chat_history=chat_history,\n )" ] }, { @@ -648,4 +397,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/from_scratch_code_act_agent.ipynb b/docs/examples/agent/from_scratch_code_act_agent.ipynb index c8dc610a68..c9619b6c60 100644 --- a/docs/examples/agent/from_scratch_code_act_agent.ipynb +++ b/docs/examples/agent/from_scratch_code_act_agent.ipynb @@ -223,20 +223,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.llms import ChatMessage\n", - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class InputEvent(Event):\n", - " input: list[ChatMessage]\n", - "\n", - "\n", - "class StreamEvent(Event):\n", - " delta: str\n", - "\n", - "\n", - "class CodeExecutionEvent(Event):\n", - " code: str" + "from llama_index.core.llms import ChatMessage\nfrom workflows.events import Event\n\n\nclass InputEvent(Event):\n input: list[ChatMessage]\n\n\nclass StreamEvent(Event):\n delta: str\n\n\nclass CodeExecutionEvent(Event):\n code: str" ] }, { @@ -252,133 +239,7 @@ "metadata": {}, "outputs": [], "source": [ - "import inspect\n", - "import re\n", - "from typing import Any, Callable, List\n", - "\n", - "from llama_index.core.llms import ChatMessage, LLM\n", - "from llama_index.core.memory import ChatMemoryBuffer\n", - "from llama_index.core.tools.types import BaseTool\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "CODEACT_SYSTEM_PROMPT = \"\"\"\n", - "You are a helpful assistant that can execute code.\n", - "\n", - "Given the chat history, you can write code within ... tags to help the user with their question.\n", - "\n", - "In your code, you can reference any previously used variables or functions.\n", - "\n", - "The user has also provided you with some predefined functions:\n", - "{fn_str}\n", - "\n", - "To execute code, write the code between ... tags.\n", - "\"\"\"\n", - "\n", - "\n", - "class CodeActAgent(Workflow):\n", - " def __init__(\n", - " self,\n", - " fns: List[Callable],\n", - " code_execute_fn: Callable,\n", - " llm: LLM | None = None,\n", - " **workflow_kwargs: Any,\n", - " ) -> None:\n", - " super().__init__(**workflow_kwargs)\n", - " self.fns = fns or []\n", - " self.code_execute_fn = code_execute_fn\n", - " self.llm = llm or OpenAI(model=\"gpt-4o-mini\")\n", - "\n", - " # parse the functions into truncated function strings\n", - " self.fn_str = \"\\n\\n\".join(\n", - " f'def {fn.__name__}{str(inspect.signature(fn))}:\\n \"\"\" {fn.__doc__} \"\"\"\\n ...'\n", - " for fn in self.fns\n", - " )\n", - " self.system_message = ChatMessage(\n", - " role=\"system\",\n", - " content=CODEACT_SYSTEM_PROMPT.format(fn_str=self.fn_str),\n", - " )\n", - "\n", - " def _parse_code(self, response: str) -> str | None:\n", - " # find the code between ... tags\n", - " matches = re.findall(r\"(.*?)\", response, re.DOTALL)\n", - " if matches:\n", - " return \"\\n\\n\".join(matches)\n", - "\n", - " return None\n", - "\n", - " @step\n", - " async def prepare_chat_history(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> InputEvent:\n", - " # check if memory is setup\n", - " memory = await ctx.store.get(\"memory\", default=None)\n", - " if not memory:\n", - " memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\n", - "\n", - " # get user input\n", - " user_input = ev.get(\"user_input\")\n", - " if user_input is None:\n", - " raise ValueError(\"user_input kwarg is required\")\n", - " user_msg = ChatMessage(role=\"user\", content=user_input)\n", - " memory.put(user_msg)\n", - "\n", - " # get chat history\n", - " chat_history = memory.get()\n", - "\n", - " # update context\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " # add the system message to the chat history and return\n", - " return InputEvent(input=[self.system_message, *chat_history])\n", - "\n", - " @step\n", - " async def handle_llm_input(\n", - " self, ctx: Context, ev: InputEvent\n", - " ) -> CodeExecutionEvent | StopEvent:\n", - " chat_history = ev.input\n", - "\n", - " # stream the response\n", - " response_stream = await self.llm.astream_chat(chat_history)\n", - " async for response in response_stream:\n", - " ctx.write_event_to_stream(StreamEvent(delta=response.delta or \"\"))\n", - "\n", - " # save the final response, which should have all content\n", - " memory = await ctx.store.get(\"memory\")\n", - " memory.put(response.message)\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " # get the code to execute\n", - " code = self._parse_code(response.message.content)\n", - "\n", - " if not code:\n", - " return StopEvent(result=response)\n", - " else:\n", - " return CodeExecutionEvent(code=code)\n", - "\n", - " @step\n", - " async def handle_code_execution(\n", - " self, ctx: Context, ev: CodeExecutionEvent\n", - " ) -> InputEvent:\n", - " # execute the code\n", - " ctx.write_event_to_stream(ev)\n", - " output = self.code_execute_fn(ev.code)\n", - "\n", - " # update the memory\n", - " memory = await ctx.store.get(\"memory\")\n", - " memory.put(ChatMessage(role=\"assistant\", content=output))\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " # get the latest chat history and loop back to the start\n", - " chat_history = memory.get()\n", - " return InputEvent(input=[self.system_message, *chat_history])" + "import inspect\nimport re\nfrom typing import Any, Callable, List\n\nfrom llama_index.core.llms import ChatMessage, LLM\nfrom llama_index.core.memory import ChatMemoryBuffer\nfrom llama_index.core.tools.types import BaseTool\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\nfrom llama_index.llms.openai import OpenAI\n\n\nCODEACT_SYSTEM_PROMPT = \"\"\"\nYou are a helpful assistant that can execute code.\n\nGiven the chat history, you can write code within ... tags to help the user with their question.\n\nIn your code, you can reference any previously used variables or functions.\n\nThe user has also provided you with some predefined functions:\n{fn_str}\n\nTo execute code, write the code between ... tags.\n\"\"\"\n\n\nclass CodeActAgent(Workflow):\n def __init__(\n self,\n fns: List[Callable],\n code_execute_fn: Callable,\n llm: LLM | None = None,\n **workflow_kwargs: Any,\n ) -> None:\n super().__init__(**workflow_kwargs)\n self.fns = fns or []\n self.code_execute_fn = code_execute_fn\n self.llm = llm or OpenAI(model=\"gpt-4o-mini\")\n\n # parse the functions into truncated function strings\n self.fn_str = \"\\n\\n\".join(\n f'def {fn.__name__}{str(inspect.signature(fn))}:\\n \"\"\" {fn.__doc__} \"\"\"\\n ...'\n for fn in self.fns\n )\n self.system_message = ChatMessage(\n role=\"system\",\n content=CODEACT_SYSTEM_PROMPT.format(fn_str=self.fn_str),\n )\n\n def _parse_code(self, response: str) -> str | None:\n # find the code between ... tags\n matches = re.findall(r\"(.*?)\", response, re.DOTALL)\n if matches:\n return \"\\n\\n\".join(matches)\n\n return None\n\n @step\n async def prepare_chat_history(\n self, ctx: Context, ev: StartEvent\n ) -> InputEvent:\n # check if memory is setup\n memory = await ctx.store.get(\"memory\", default=None)\n if not memory:\n memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\n\n # get user input\n user_input = ev.get(\"user_input\")\n if user_input is None:\n raise ValueError(\"user_input kwarg is required\")\n user_msg = ChatMessage(role=\"user\", content=user_input)\n memory.put(user_msg)\n\n # get chat history\n chat_history = memory.get()\n\n # update context\n await ctx.store.set(\"memory\", memory)\n\n # add the system message to the chat history and return\n return InputEvent(input=[self.system_message, *chat_history])\n\n @step\n async def handle_llm_input(\n self, ctx: Context, ev: InputEvent\n ) -> CodeExecutionEvent | StopEvent:\n chat_history = ev.input\n\n # stream the response\n response_stream = await self.llm.astream_chat(chat_history)\n async for response in response_stream:\n ctx.write_event_to_stream(StreamEvent(delta=response.delta or \"\"))\n\n # save the final response, which should have all content\n memory = await ctx.store.get(\"memory\")\n memory.put(response.message)\n await ctx.store.set(\"memory\", memory)\n\n # get the code to execute\n code = self._parse_code(response.message.content)\n\n if not code:\n return StopEvent(result=response)\n else:\n return CodeExecutionEvent(code=code)\n\n @step\n async def handle_code_execution(\n self, ctx: Context, ev: CodeExecutionEvent\n ) -> InputEvent:\n # execute the code\n ctx.write_event_to_stream(ev)\n output = self.code_execute_fn(ev.code)\n\n # update the memory\n memory = await ctx.store.get(\"memory\")\n memory.put(ChatMessage(role=\"assistant\", content=output))\n await ctx.store.set(\"memory\", memory)\n\n # get the latest chat history and loop back to the start\n chat_history = memory.get()\n return InputEvent(input=[self.system_message, *chat_history])" ] }, { @@ -398,16 +259,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "agent = CodeActAgent(\n", - " fns=[add, subtract, multiply, divide],\n", - " code_execute_fn=code_executor.execute,\n", - " llm=OpenAI(model=\"gpt-4o-mini\", api_key=\"sk-...\"),\n", - ")\n", - "\n", - "# context to hold the agent's state / memory\n", - "ctx = Context(agent)" + "from workflows import Context\n\nagent = CodeActAgent(\n fns=[add, subtract, multiply, divide],\n code_execute_fn=code_executor.execute,\n llm=OpenAI(model=\"gpt-4o-mini\", api_key=\"sk-...\"),\n)\n\n# context to hold the agent's state / memory\nctx = Context(agent)" ] }, { @@ -629,4 +481,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/gemini_agent.ipynb b/docs/examples/agent/gemini_agent.ipynb index 977ef47042..e23a9b6985 100644 --- a/docs/examples/agent/gemini_agent.ipynb +++ b/docs/examples/agent/gemini_agent.ipynb @@ -238,15 +238,7 @@ } ], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "agent = FunctionAgent(llm=llm)\n", - "ctx = Context(agent)\n", - "\n", - "response = await agent.run(\"My name is John Doe\", ctx=ctx)\n", - "response = await agent.run(\"What is my name?\", ctx=ctx)\n", - "\n", - "print(str(response))" + "from workflows import Context\n\nagent = FunctionAgent(llm=llm)\nctx = Context(agent)\n\nresponse = await agent.run(\"My name is John Doe\", ctx=ctx)\nresponse = await agent.run(\"What is my name?\", ctx=ctx)\n\nprint(str(response))" ] }, { @@ -357,4 +349,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/memory/chat_memory_buffer.ipynb b/docs/examples/agent/memory/chat_memory_buffer.ipynb index 0c6bc57690..0cb41c861b 100644 --- a/docs/examples/agent/memory/chat_memory_buffer.ipynb +++ b/docs/examples/agent/memory/chat_memory_buffer.ipynb @@ -119,17 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import ReActAgent, FunctionAgent\n", - "from llama_index.core.workflow import Context\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "memory = ChatMemoryBuffer.from_defaults(token_limit=40000)\n", - "\n", - "agent = FunctionAgent(tools=[], llm=OpenAI(model=\"gpt-4o-mini\"))\n", - "\n", - "# context to hold the chat history/state\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import ReActAgent, FunctionAgent\nfrom workflows import Context\nfrom llama_index.llms.openai import OpenAI\n\n\nmemory = ChatMemoryBuffer.from_defaults(token_limit=40000)\n\nagent = FunctionAgent(tools=[], llm=OpenAI(model=\"gpt-4o-mini\"))\n\n# context to hold the chat history/state\nctx = Context(agent)" ] }, { @@ -179,4 +169,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/memory/summary_memory_buffer.ipynb b/docs/examples/agent/memory/summary_memory_buffer.ipynb index 90aed20924..b137a42f3b 100644 --- a/docs/examples/agent/memory/summary_memory_buffer.ipynb +++ b/docs/examples/agent/memory/summary_memory_buffer.ipynb @@ -128,17 +128,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import ReActAgent, FunctionAgent\n", - "from llama_index.core.workflow import Context\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "memory = ChatMemoryBuffer.from_defaults(token_limit=40000)\n", - "\n", - "agent = FunctionAgent(tools=[], llm=OpenAI(model=\"gpt-4o-mini\"))\n", - "\n", - "# context to hold the chat history/state\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import ReActAgent, FunctionAgent\nfrom workflows import Context\nfrom llama_index.llms.openai import OpenAI\n\n\nmemory = ChatMemoryBuffer.from_defaults(token_limit=40000)\n\nagent = FunctionAgent(tools=[], llm=OpenAI(model=\"gpt-4o-mini\"))\n\n# context to hold the chat history/state\nctx = Context(agent)" ] }, { @@ -188,4 +178,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/agent/mistral_agent.ipynb b/docs/examples/agent/mistral_agent.ipynb index 1f429d53b3..1426cc2588 100644 --- a/docs/examples/agent/mistral_agent.ipynb +++ b/docs/examples/agent/mistral_agent.ipynb @@ -201,14 +201,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "response = await agent.run(\"My name is John Doe\", ctx=ctx)\n", - "response = await agent.run(\"What is my name?\", ctx=ctx)\n", - "\n", - "print(str(response))" + "from workflows import Context\n\nctx = Context(agent)\n\nresponse = await agent.run(\"My name is John Doe\", ctx=ctx)\nresponse = await agent.run(\"What is my name?\", ctx=ctx)\n\nprint(str(response))" ] }, { @@ -344,4 +337,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/multi_agent_workflow_with_weaviate_queryagent.ipynb b/docs/examples/agent/multi_agent_workflow_with_weaviate_queryagent.ipynb index b264e51616..9b26bd1225 100644 --- a/docs/examples/agent/multi_agent_workflow_with_weaviate_queryagent.ipynb +++ b/docs/examples/agent/multi_agent_workflow_with_weaviate_queryagent.ipynb @@ -56,34 +56,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - " Event,\n", - " Context,\n", - ")\n", - "from llama_index.utils.workflow import draw_all_possible_flows\n", - "from llama_index.readers.web import SimpleWebPageReader\n", - "from llama_index.core.llms import ChatMessage\n", - "from llama_index.core.tools import FunctionTool\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.core.agent.workflow import FunctionAgent\n", - "\n", - "from enum import Enum\n", - "from pydantic import BaseModel, Field\n", - "from llama_index.llms.openai import OpenAI\n", - "from typing import List, Union\n", - "import json\n", - "\n", - "import weaviate\n", - "from weaviate.auth import Auth\n", - "from weaviate.agents.query import QueryAgent\n", - "from weaviate.classes.config import Configure, Property, DataType\n", - "\n", - "import os\n", - "from getpass import getpass" + "from workflows import Workflow, step, Context\nfrom workflows.events import StartEvent, StopEvent, Event\nfrom llama_index.utils.workflow import draw_all_possible_flows\nfrom llama_index.readers.web import SimpleWebPageReader\nfrom llama_index.core.llms import ChatMessage\nfrom llama_index.core.tools import FunctionTool\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.core.agent.workflow import FunctionAgent\n\nfrom enum import Enum\nfrom pydantic import BaseModel, Field\nfrom llama_index.llms.openai import OpenAI\nfrom typing import List, Union\nimport json\n\nimport weaviate\nfrom weaviate.auth import Auth\nfrom weaviate.agents.query import QueryAgent\nfrom weaviate.classes.config import Configure, Property, DataType\n\nimport os\nfrom getpass import getpass" ] }, { @@ -966,4 +939,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/agent/nvidia_document_research_assistant_for_blog_creation.ipynb b/docs/examples/agent/nvidia_document_research_assistant_for_blog_creation.ipynb index 3bc2fa0afe..d76261753b 100644 --- a/docs/examples/agent/nvidia_document_research_assistant_for_blog_creation.ipynb +++ b/docs/examples/agent/nvidia_document_research_assistant_for_blog_creation.ipynb @@ -392,204 +392,7 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import List\n", - "from llama_index.core.workflow import (\n", - " step,\n", - " Event,\n", - " Context,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - ")\n", - "from llama_index.core.agent.workflow import FunctionAgent\n", - "\n", - "\n", - "class OutlineEvent(Event):\n", - " outline: str\n", - "\n", - "\n", - "class QuestionEvent(Event):\n", - " question: str\n", - "\n", - "\n", - "class AnswerEvent(Event):\n", - " question: str\n", - " answer: str\n", - "\n", - "\n", - "class ReviewEvent(Event):\n", - " report: str\n", - "\n", - "\n", - "class ProgressEvent(Event):\n", - " progress: str\n", - "\n", - "\n", - "class DocumentResearchAgent(Workflow):\n", - " # get the initial request and create an outline of the blog post knowing nothing about the topic\n", - " @step\n", - " async def formulate_plan(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> OutlineEvent:\n", - " query = ev.query\n", - " await ctx.store.set(\"original_query\", query)\n", - " await ctx.store.set(\"tools\", ev.tools)\n", - "\n", - " prompt = f\"\"\"You are an expert at writing blog posts. You have been given a topic to write\n", - " a blog post about. Plan an outline for the blog post; it should be detailed and specific.\n", - " Another agent will formulate questions to find the facts necessary to fulfill the outline.\n", - " The topic is: {query}\"\"\"\n", - "\n", - " response = await Settings.llm.acomplete(prompt)\n", - "\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(progress=\"Outline:\\n\" + str(response))\n", - " )\n", - "\n", - " return OutlineEvent(outline=str(response))\n", - "\n", - " # formulate some questions based on the outline\n", - " @step\n", - " async def formulate_questions(\n", - " self, ctx: Context, ev: OutlineEvent\n", - " ) -> QuestionEvent:\n", - " outline = ev.outline\n", - " await ctx.store.set(\"outline\", outline)\n", - "\n", - " prompt = f\"\"\"You are an expert at formulating research questions. You have been given an outline\n", - " for a blog post. Formulate a series of simple questions that will get you the facts necessary\n", - " to fulfill the outline. You cannot assume any existing knowledge; you must ask at least one\n", - " question for every bullet point in the outline. Avoid complex or multi-part questions; break\n", - " them down into a series of simple questions. Your output should be a list of questions, each\n", - " on a new line. Do not include headers or categories or any preamble or explanation; just a\n", - " list of questions. For speed of response, limit yourself to 8 questions. The outline is: {outline}\"\"\"\n", - "\n", - " response = await Settings.llm.acomplete(prompt)\n", - "\n", - " questions = str(response).split(\"\\n\")\n", - " questions = [x for x in questions if x]\n", - "\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(\n", - " progress=\"Formulated questions:\\n\" + \"\\n\".join(questions)\n", - " )\n", - " )\n", - "\n", - " await ctx.store.set(\"num_questions\", len(questions))\n", - "\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(progress=\"Questions:\\n\" + \"\\n\".join(questions))\n", - " )\n", - "\n", - " for question in questions:\n", - " ctx.send_event(QuestionEvent(question=question))\n", - "\n", - " # answer each question in turn\n", - " @step\n", - " async def answer_question(\n", - " self, ctx: Context, ev: QuestionEvent\n", - " ) -> AnswerEvent:\n", - " question = ev.question\n", - " if (\n", - " not question\n", - " or question.isspace()\n", - " or question == \"\"\n", - " or question is None\n", - " ):\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(progress=f\"Skipping empty question.\")\n", - " ) # Log skipping empty question\n", - " return None\n", - " agent = FunctionAgent(\n", - " tools=await ctx.store.get(\"tools\"),\n", - " llm=Settings.llm,\n", - " )\n", - " response = await agent.run(question)\n", - " response = str(response)\n", - "\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(\n", - " progress=f\"To question '{question}' the agent answered: {response}\"\n", - " )\n", - " )\n", - "\n", - " return AnswerEvent(question=question, answer=response)\n", - "\n", - " # given all the answers to all the questions and the outline, write the blog poost\n", - " @step\n", - " async def write_report(self, ctx: Context, ev: AnswerEvent) -> ReviewEvent:\n", - " # wait until we receive as many answers as there are questions\n", - " num_questions = await ctx.store.get(\"num_questions\")\n", - " results = ctx.collect_events(ev, [AnswerEvent] * num_questions)\n", - " if results is None:\n", - " return None\n", - "\n", - " # maintain a list of all questions and answers no matter how many times this step is called\n", - " try:\n", - " previous_questions = await ctx.store.get(\"previous_questions\")\n", - " except:\n", - " previous_questions = []\n", - " previous_questions.extend(results)\n", - " await ctx.store.set(\"previous_questions\", previous_questions)\n", - "\n", - " prompt = f\"\"\"You are an expert at writing blog posts. You are given an outline of a blog post\n", - " and a series of questions and answers that should provide all the data you need to write the\n", - " blog post. Compose the blog post according to the outline, using only the data given in the\n", - " answers. The outline is in and the questions and answers are in and\n", - " .\n", - " {await ctx.store.get('outline')}\"\"\"\n", - "\n", - " for result in previous_questions:\n", - " prompt += f\"{result.question}\\n{result.answer}\\n\"\n", - "\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(progress=\"Writing report with prompt:\\n\" + prompt)\n", - " )\n", - "\n", - " report = await Settings.llm.acomplete(prompt)\n", - "\n", - " return ReviewEvent(report=str(report))\n", - "\n", - " # review the report. If it still needs work, formulate some more questions.\n", - " @step\n", - " async def review_report(\n", - " self, ctx: Context, ev: ReviewEvent\n", - " ) -> StopEvent | QuestionEvent:\n", - " # we re-review a maximum of 3 times\n", - " try:\n", - " num_reviews = await ctx.store.get(\"num_reviews\")\n", - " except:\n", - " num_reviews = 1\n", - " num_reviews += 1\n", - " await ctx.store.set(\"num_reviews\", num_reviews)\n", - "\n", - " report = ev.report\n", - "\n", - " prompt = f\"\"\"You are an expert reviewer of blog posts. You are given an original query,\n", - " and a blog post that was written to satisfy that query. Review the blog post and determine\n", - " if it adequately answers the query and contains enough detail. If it doesn't, come up with\n", - " a set of questions that will get you the facts necessary to expand the blog post. Another\n", - " agent will answer those questions. Your response should just be a list of questions, one\n", - " per line, without any preamble or explanation. For speed, generate a maximum of 4 questions.\n", - " The original query is: '{await ctx.store.get('original_query')}'.\n", - " The blog post is: {report}.\n", - " If the blog post is fine, return just the string 'OKAY'.\"\"\"\n", - "\n", - " response = await Settings.llm.acomplete(prompt)\n", - "\n", - " if response == \"OKAY\" or await ctx.store.get(\"num_reviews\") >= 3:\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(progress=\"Blog post is fine\")\n", - " )\n", - " return StopEvent(result=report)\n", - " else:\n", - " questions = str(response).split(\"\\n\")\n", - " await ctx.store.set(\"num_questions\", len(questions))\n", - " ctx.write_event_to_stream(\n", - " ProgressEvent(progress=\"Formulated some more questions\")\n", - " )\n", - " for question in questions:\n", - " ctx.send_event(QuestionEvent(question=question))" + "from typing import List\nfrom workflows import step, Context, Workflow\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_index.core.agent.workflow import FunctionAgent\n\n\nclass OutlineEvent(Event):\n outline: str\n\n\nclass QuestionEvent(Event):\n question: str\n\n\nclass AnswerEvent(Event):\n question: str\n answer: str\n\n\nclass ReviewEvent(Event):\n report: str\n\n\nclass ProgressEvent(Event):\n progress: str\n\n\nclass DocumentResearchAgent(Workflow):\n # get the initial request and create an outline of the blog post knowing nothing about the topic\n @step\n async def formulate_plan(\n self, ctx: Context, ev: StartEvent\n ) -> OutlineEvent:\n query = ev.query\n await ctx.store.set(\"original_query\", query)\n await ctx.store.set(\"tools\", ev.tools)\n\n prompt = f\"\"\"You are an expert at writing blog posts. You have been given a topic to write\n a blog post about. Plan an outline for the blog post; it should be detailed and specific.\n Another agent will formulate questions to find the facts necessary to fulfill the outline.\n The topic is: {query}\"\"\"\n\n response = await Settings.llm.acomplete(prompt)\n\n ctx.write_event_to_stream(\n ProgressEvent(progress=\"Outline:\\n\" + str(response))\n )\n\n return OutlineEvent(outline=str(response))\n\n # formulate some questions based on the outline\n @step\n async def formulate_questions(\n self, ctx: Context, ev: OutlineEvent\n ) -> QuestionEvent:\n outline = ev.outline\n await ctx.store.set(\"outline\", outline)\n\n prompt = f\"\"\"You are an expert at formulating research questions. You have been given an outline\n for a blog post. Formulate a series of simple questions that will get you the facts necessary\n to fulfill the outline. You cannot assume any existing knowledge; you must ask at least one\n question for every bullet point in the outline. Avoid complex or multi-part questions; break\n them down into a series of simple questions. Your output should be a list of questions, each\n on a new line. Do not include headers or categories or any preamble or explanation; just a\n list of questions. For speed of response, limit yourself to 8 questions. The outline is: {outline}\"\"\"\n\n response = await Settings.llm.acomplete(prompt)\n\n questions = str(response).split(\"\\n\")\n questions = [x for x in questions if x]\n\n ctx.write_event_to_stream(\n ProgressEvent(\n progress=\"Formulated questions:\\n\" + \"\\n\".join(questions)\n )\n )\n\n await ctx.store.set(\"num_questions\", len(questions))\n\n ctx.write_event_to_stream(\n ProgressEvent(progress=\"Questions:\\n\" + \"\\n\".join(questions))\n )\n\n for question in questions:\n ctx.send_event(QuestionEvent(question=question))\n\n # answer each question in turn\n @step\n async def answer_question(\n self, ctx: Context, ev: QuestionEvent\n ) -> AnswerEvent:\n question = ev.question\n if (\n not question\n or question.isspace()\n or question == \"\"\n or question is None\n ):\n ctx.write_event_to_stream(\n ProgressEvent(progress=f\"Skipping empty question.\")\n ) # Log skipping empty question\n return None\n agent = FunctionAgent(\n tools=await ctx.store.get(\"tools\"),\n llm=Settings.llm,\n )\n response = await agent.run(question)\n response = str(response)\n\n ctx.write_event_to_stream(\n ProgressEvent(\n progress=f\"To question '{question}' the agent answered: {response}\"\n )\n )\n\n return AnswerEvent(question=question, answer=response)\n\n # given all the answers to all the questions and the outline, write the blog poost\n @step\n async def write_report(self, ctx: Context, ev: AnswerEvent) -> ReviewEvent:\n # wait until we receive as many answers as there are questions\n num_questions = await ctx.store.get(\"num_questions\")\n results = ctx.collect_events(ev, [AnswerEvent] * num_questions)\n if results is None:\n return None\n\n # maintain a list of all questions and answers no matter how many times this step is called\n try:\n previous_questions = await ctx.store.get(\"previous_questions\")\n except:\n previous_questions = []\n previous_questions.extend(results)\n await ctx.store.set(\"previous_questions\", previous_questions)\n\n prompt = f\"\"\"You are an expert at writing blog posts. You are given an outline of a blog post\n and a series of questions and answers that should provide all the data you need to write the\n blog post. Compose the blog post according to the outline, using only the data given in the\n answers. The outline is in and the questions and answers are in and\n .\n {await ctx.store.get('outline')}\"\"\"\n\n for result in previous_questions:\n prompt += f\"{result.question}\\n{result.answer}\\n\"\n\n ctx.write_event_to_stream(\n ProgressEvent(progress=\"Writing report with prompt:\\n\" + prompt)\n )\n\n report = await Settings.llm.acomplete(prompt)\n\n return ReviewEvent(report=str(report))\n\n # review the report. If it still needs work, formulate some more questions.\n @step\n async def review_report(\n self, ctx: Context, ev: ReviewEvent\n ) -> StopEvent | QuestionEvent:\n # we re-review a maximum of 3 times\n try:\n num_reviews = await ctx.store.get(\"num_reviews\")\n except:\n num_reviews = 1\n num_reviews += 1\n await ctx.store.set(\"num_reviews\", num_reviews)\n\n report = ev.report\n\n prompt = f\"\"\"You are an expert reviewer of blog posts. You are given an original query,\n and a blog post that was written to satisfy that query. Review the blog post and determine\n if it adequately answers the query and contains enough detail. If it doesn't, come up with\n a set of questions that will get you the facts necessary to expand the blog post. Another\n agent will answer those questions. Your response should just be a list of questions, one\n per line, without any preamble or explanation. For speed, generate a maximum of 4 questions.\n The original query is: '{await ctx.store.get('original_query')}'.\n The blog post is: {report}.\n If the blog post is fine, return just the string 'OKAY'.\"\"\"\n\n response = await Settings.llm.acomplete(prompt)\n\n if response == \"OKAY\" or await ctx.store.get(\"num_reviews\") >= 3:\n ctx.write_event_to_stream(\n ProgressEvent(progress=\"Blog post is fine\")\n )\n return StopEvent(result=report)\n else:\n questions = str(response).split(\"\\n\")\n await ctx.store.set(\"num_questions\", len(questions))\n ctx.write_event_to_stream(\n ProgressEvent(progress=\"Formulated some more questions\")\n )\n for question in questions:\n ctx.send_event(QuestionEvent(question=question))" ] }, { @@ -1194,4 +997,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/examples/agent/nvidia_sub_question_query_engine.ipynb b/docs/examples/agent/nvidia_sub_question_query_engine.ipynb index 69157ba5cc..f02261f3e9 100644 --- a/docs/examples/agent/nvidia_sub_question_query_engine.ipynb +++ b/docs/examples/agent/nvidia_sub_question_query_engine.ipynb @@ -74,27 +74,7 @@ "metadata": {}, "outputs": [], "source": [ - "import os, json\n", - "from llama_index.core import (\n", - " SimpleDirectoryReader,\n", - " VectorStoreIndex,\n", - " StorageContext,\n", - " load_index_from_storage,\n", - " Settings,\n", - ")\n", - "from llama_index.core.tools import QueryEngineTool, ToolMetadata\n", - "from llama_index.core.workflow import (\n", - " step,\n", - " Context,\n", - " Workflow,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - ")\n", - "from llama_index.core.agent.workflow import ReActAgent\n", - "from llama_index.llms.nvidia import NVIDIA\n", - "from llama_index.embeddings.nvidia import NVIDIAEmbedding\n", - "from llama_index.utils.workflow import draw_all_possible_flows" + "import os, json\nfrom llama_index.core import (\n SimpleDirectoryReader,\n VectorStoreIndex,\n StorageContext,\n load_index_from_storage,\n Settings,\n)\nfrom llama_index.core.tools import QueryEngineTool, ToolMetadata\nfrom workflows import step, Context, Workflow\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_index.core.agent.workflow import ReActAgent\nfrom llama_index.llms.nvidia import NVIDIA\nfrom llama_index.embeddings.nvidia import NVIDIAEmbedding\nfrom llama_index.utils.workflow import draw_all_possible_flows" ] }, { @@ -389,4 +369,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/agent/openai_agent_context_retrieval.ipynb b/docs/examples/agent/openai_agent_context_retrieval.ipynb index f064eec476..d2734bc90e 100644 --- a/docs/examples/agent/openai_agent_context_retrieval.ipynb +++ b/docs/examples/agent/openai_agent_context_retrieval.ipynb @@ -401,22 +401,7 @@ } ], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "query = \"What is the 'Y' and 'Z' in September 2022?\"\n", - "agent = await get_agent_with_context_awareness(\n", - " query, context_retriever, query_engine_tools\n", - ")\n", - "response = await agent.run(query, ctx=ctx)\n", - "\n", - "query = \"What did I just ask?\"\n", - "agent = await get_agent_with_context_awareness(\n", - " query, context_retriever, query_engine_tools\n", - ")\n", - "response = await agent.run(query, ctx=ctx)\n", - "print(str(response))" + "from workflows import Context\n\nctx = Context(agent)\n\nquery = \"What is the 'Y' and 'Z' in September 2022?\"\nagent = await get_agent_with_context_awareness(\n query, context_retriever, query_engine_tools\n)\nresponse = await agent.run(query, ctx=ctx)\n\nquery = \"What did I just ask?\"\nagent = await get_agent_with_context_awareness(\n query, context_retriever, query_engine_tools\n)\nresponse = await agent.run(query, ctx=ctx)\nprint(str(response))" ] }, { @@ -500,4 +485,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/openai_agent_query_cookbook.ipynb b/docs/examples/agent/openai_agent_query_cookbook.ipynb index eb939be4c3..9495284166 100644 --- a/docs/examples/agent/openai_agent_query_cookbook.ipynb +++ b/docs/examples/agent/openai_agent_query_cookbook.ipynb @@ -474,21 +474,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import FunctionAgent\n", - "from llama_index.core.workflow import Context\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "agent = FunctionAgent(\n", - " tools=[auto_retrieve_tool],\n", - " llm=OpenAI(model=\"gpt-4o\"),\n", - " system_prompt=(\n", - " \"You are a helpful assistant that can answer questions about celebrities by writing a filtered query to a vector database. \"\n", - " \"Unless the user is asking to compare things, you generally only need to make one call to the retriever.\"\n", - " ),\n", - ")\n", - "\n", - "# hold the context/session state for the agent\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import FunctionAgent\nfrom workflows import Context\nfrom llama_index.llms.openai import OpenAI\n\nagent = FunctionAgent(\n tools=[auto_retrieve_tool],\n llm=OpenAI(model=\"gpt-4o\"),\n system_prompt=(\n \"You are a helpful assistant that can answer questions about celebrities by writing a filtered query to a vector database. \"\n \"Unless the user is asking to compare things, you generally only need to make one call to the retriever.\"\n ),\n)\n\n# hold the context/session state for the agent\nctx = Context(agent)" ] }, { @@ -966,17 +952,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import FunctionAgent\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.core.workflow import Context\n", - "\n", - "agent = FunctionAgent(\n", - " tools=[sql_tool, vector_tool],\n", - " llm=OpenAI(model=\"gpt-4o\"),\n", - ")\n", - "\n", - "# hold the context/session state for the agent\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import FunctionAgent\nfrom llama_index.llms.openai import OpenAI\nfrom workflows import Context\n\nagent = FunctionAgent(\n tools=[sql_tool, vector_tool],\n llm=OpenAI(model=\"gpt-4o\"),\n)\n\n# hold the context/session state for the agent\nctx = Context(agent)" ] }, { @@ -1131,4 +1107,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/openai_agent_retrieval.ipynb b/docs/examples/agent/openai_agent_retrieval.ipynb index ddab1bd380..1bf032de98 100644 --- a/docs/examples/agent/openai_agent_retrieval.ipynb +++ b/docs/examples/agent/openai_agent_retrieval.ipynb @@ -232,17 +232,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import FunctionAgent, ReActAgent\n", - "from llama_index.core.workflow import Context\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "agent = FunctionAgent(\n", - " tool_retriever=obj_index.as_retriever(similarity_top_k=2),\n", - " llm=OpenAI(model=\"gpt-4o\"),\n", - ")\n", - "\n", - "# context to hold the session/state\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import FunctionAgent, ReActAgent\nfrom workflows import Context\nfrom llama_index.llms.openai import OpenAI\n\nagent = FunctionAgent(\n tool_retriever=obj_index.as_retriever(similarity_top_k=2),\n llm=OpenAI(model=\"gpt-4o\"),\n)\n\n# context to hold the session/state\nctx = Context(agent)" ] }, { @@ -312,4 +302,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/openai_agent_with_query_engine.ipynb b/docs/examples/agent/openai_agent_with_query_engine.ipynb index 74b2aa70c3..c28fb7e262 100644 --- a/docs/examples/agent/openai_agent_with_query_engine.ipynb +++ b/docs/examples/agent/openai_agent_with_query_engine.ipynb @@ -203,13 +203,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import FunctionAgent, ReActAgent\n", - "from llama_index.core.workflow import Context\n", - "\n", - "agent = FunctionAgent(tools=query_engine_tools, llm=OpenAI(model=\"gpt-4o\"))\n", - "\n", - "# context to hold the session/state\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import FunctionAgent, ReActAgent\nfrom workflows import Context\n\nagent = FunctionAgent(tools=query_engine_tools, llm=OpenAI(model=\"gpt-4o\"))\n\n# context to hold the session/state\nctx = Context(agent)" ] }, { @@ -275,4 +269,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/react_agent.ipynb b/docs/examples/agent/react_agent.ipynb index 66f4a960ea..e032362141 100644 --- a/docs/examples/agent/react_agent.ipynb +++ b/docs/examples/agent/react_agent.ipynb @@ -94,15 +94,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.core.agent.workflow import ReActAgent\n", - "from llama_index.core.workflow import Context\n", - "\n", - "llm = OpenAI(model=\"gpt-4o-mini\")\n", - "agent = ReActAgent(tools=[multiply, add], llm=llm)\n", - "\n", - "# Create a context to store the conversation history/session state\n", - "ctx = Context(agent)" + "from llama_index.llms.openai import OpenAI\nfrom llama_index.core.agent.workflow import ReActAgent\nfrom workflows import Context\n\nllm = OpenAI(model=\"gpt-4o-mini\")\nagent = ReActAgent(tools=[multiply, add], llm=llm)\n\n# Create a context to store the conversation history/session state\nctx = Context(agent)" ] }, { @@ -446,4 +438,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/react_agent_with_query_engine.ipynb b/docs/examples/agent/react_agent_with_query_engine.ipynb index 756d67add0..e1b6e213bf 100644 --- a/docs/examples/agent/react_agent_with_query_engine.ipynb +++ b/docs/examples/agent/react_agent_with_query_engine.ipynb @@ -198,18 +198,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.agent.workflow import ReActAgent\n", - "from llama_index.core.workflow import Context\n", - "\n", - "agent = ReActAgent(\n", - " tools=query_engine_tools,\n", - " llm=OpenAI(model=\"gpt-4o-mini\"),\n", - " # system_prompt=\"...\"\n", - ")\n", - "\n", - "# context to hold this session/state\n", - "\n", - "ctx = Context(agent)" + "from llama_index.core.agent.workflow import ReActAgent\nfrom workflows import Context\n\nagent = ReActAgent(\n tools=query_engine_tools,\n llm=OpenAI(model=\"gpt-4o-mini\"),\n # system_prompt=\"...\"\n)\n\n# context to hold this session/state\n\nctx = Context(agent)" ] }, { @@ -353,4 +342,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/agent/return_direct_agent.ipynb b/docs/examples/agent/return_direct_agent.ipynb index 4807bec534..cf9c0eb568 100644 --- a/docs/examples/agent/return_direct_agent.ipynb +++ b/docs/examples/agent/return_direct_agent.ipynb @@ -136,32 +136,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.llms.anthropic import Anthropic\n", - "from llama_index.core.llms import ChatMessage\n", - "from llama_index.core.agent.workflow import FunctionAgent\n", - "from llama_index.core.workflow import Context\n", - "\n", - "llm = Anthropic(model=\"claude-3-sonnet-20240229\", temperature=0.1)\n", - "\n", - "user = \"user123\"\n", - "system_prompt = f\"\"\"You are now connected to the booking system and helping {user} with making a booking.\n", - "Only enter details that the user has explicitly provided.\n", - "Do not make up any details.\n", - "\"\"\"\n", - "\n", - "agent = FunctionAgent(\n", - " tools=[\n", - " get_booking_state_tool,\n", - " update_booking_tool,\n", - " create_booking_tool,\n", - " confirm_booking_tool,\n", - " ],\n", - " llm=llm,\n", - " system_prompt=system_prompt,\n", - ")\n", - "\n", - "# create a context for the agent to hold the state/history of a session\n", - "ctx = Context(agent)" + "from llama_index.llms.anthropic import Anthropic\nfrom llama_index.core.llms import ChatMessage\nfrom llama_index.core.agent.workflow import FunctionAgent\nfrom workflows import Context\n\nllm = Anthropic(model=\"claude-3-sonnet-20240229\", temperature=0.1)\n\nuser = \"user123\"\nsystem_prompt = f\"\"\"You are now connected to the booking system and helping {user} with making a booking.\nOnly enter details that the user has explicitly provided.\nDo not make up any details.\n\"\"\"\n\nagent = FunctionAgent(\n tools=[\n get_booking_state_tool,\n update_booking_tool,\n create_booking_tool,\n confirm_booking_tool,\n ],\n llm=llm,\n system_prompt=system_prompt,\n)\n\n# create a context for the agent to hold the state/history of a session\nctx = Context(agent)" ] }, { @@ -369,4 +344,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/cookbooks/airtrain.ipynb b/docs/examples/cookbooks/airtrain.ipynb index aa1fa91f3a..6eae5a8d11 100644 --- a/docs/examples/cookbooks/airtrain.ipynb +++ b/docs/examples/cookbooks/airtrain.ipynb @@ -290,20 +290,7 @@ "metadata": {}, "outputs": [], "source": [ - "import asyncio\n", - "\n", - "from llama_index.core.schema import Node\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - ")\n", - "from llama_index.readers.web import AsyncWebPageReader\n", - "\n", - "from airtrain import DatasetMetadata, upload_from_llama_nodes" + "import asyncio\n\nfrom llama_index.core.schema import Node\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_index.readers.web import AsyncWebPageReader\n\nfrom airtrain import DatasetMetadata, upload_from_llama_nodes" ] }, { @@ -494,4 +481,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/cookbooks/build_knowledge_graph_with_neo4j_llamacloud.ipynb b/docs/examples/cookbooks/build_knowledge_graph_with_neo4j_llamacloud.ipynb index eaa5cabe62..91abefa8e9 100644 --- a/docs/examples/cookbooks/build_knowledge_graph_with_neo4j_llamacloud.ipynb +++ b/docs/examples/cookbooks/build_knowledge_graph_with_neo4j_llamacloud.ipynb @@ -555,106 +555,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " Event,\n", - " Context,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "\n", - "\n", - "class ClassifyDocEvent(Event):\n", - " parsed_doc: str\n", - " pdf_path: str\n", - "\n", - "\n", - "class ExtractAffiliate(Event):\n", - " file_path: str\n", - "\n", - "\n", - "class ExtractCoBranding(Event):\n", - " file_path: str\n", - "\n", - "\n", - "class BuildGraph(Event):\n", - " file_path: str\n", - " data: dict\n", - "\n", - "\n", - "class KnowledgeGraphBuilder(Workflow):\n", - " def __init__(\n", - " self,\n", - " classifier: ClassifyClient,\n", - " rules: List[ClassifierRule],\n", - " affiliate_extract_agent: LlamaExtract,\n", - " cobranding_extract_agent: LlamaExtract,\n", - " **kwargs,\n", - " ):\n", - " super().__init__(**kwargs)\n", - " self.classifier = classifier\n", - " self.rules = rules\n", - " self.affiliate_extract_agent = affiliate_extract_agent\n", - " self.cobranding_extract_agent = cobranding_extract_agent\n", - "\n", - " @step\n", - " async def classify_contract(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> ExtractAffiliate | ExtractCoBranding | StopEvent:\n", - " result = await self.classifier.aclassify_file_path(\n", - " rules=self.rules, file_input_path=ev.pdf_path\n", - " )\n", - " contract_type = result.items[0].result.type\n", - " print(contract_type)\n", - " if contract_type == \"affiliate_agreements\":\n", - " return ExtractAffiliate(file_path=ev.pdf_path)\n", - " elif contract_type == \"co_branding\":\n", - " return ExtractCoBranding(file_path=ev.pdf_path)\n", - " else:\n", - " return StopEvent()\n", - "\n", - " @step\n", - " async def extract_affiliate(\n", - " self, ctx: Context, ev: ExtractAffiliate\n", - " ) -> BuildGraph:\n", - " result = await self.affiliate_extract_agent.aextract(ev.file_path)\n", - " return BuildGraph(data=result.data, file_path=ev.file_path)\n", - "\n", - " @step\n", - " async def extract_co_branding(\n", - " self, ctx: Context, ev: ExtractCoBranding\n", - " ) -> BuildGraph:\n", - " result = await self.cobranding_extract_agent.aextract(ev.file_path)\n", - " return BuildGraph(data=result.data, file_path=ev.file_path)\n", - "\n", - " @step\n", - " async def build_graph(self, ctx: Context, ev: BuildGraph) -> StopEvent:\n", - " import_query = \"\"\"\n", - " WITH $contract AS contract\n", - " MERGE (c:Contract {path: $path})\n", - " SET c += apoc.map.clean(contract, [\"parties\", \"agreement_date\", \"effective_date\", \"expiration_date\"], [])\n", - " // Cast to date\n", - " SET c.agreement_date = date(contract.agreement_date),\n", - " c.effective_date = date(contract.effective_date),\n", - " c.expiration_date = date(contract.expiration_date)\n", - "\n", - " // Create parties with their locations\n", - " WITH c, contract\n", - " UNWIND coalesce(contract.parties, []) AS party\n", - " MERGE (p:Party {name: party.name})\n", - " MERGE (c)-[:HAS_PARTY]->(p)\n", - "\n", - " // Create location nodes and link to parties\n", - " WITH p, party\n", - " WHERE party.location IS NOT NULL\n", - " MERGE (p)-[:HAS_LOCATION]->(l:Location)\n", - " SET l += party.location\n", - " \"\"\"\n", - " response = await neo4j_driver.execute_query(\n", - " import_query, contract=ev.data, path=ev.file_path\n", - " )\n", - " return StopEvent(response.summary.counters)" + "from workflows import Workflow, Context, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\nclass ClassifyDocEvent(Event):\n parsed_doc: str\n pdf_path: str\n\n\nclass ExtractAffiliate(Event):\n file_path: str\n\n\nclass ExtractCoBranding(Event):\n file_path: str\n\n\nclass BuildGraph(Event):\n file_path: str\n data: dict\n\n\nclass KnowledgeGraphBuilder(Workflow):\n def __init__(\n self,\n classifier: ClassifyClient,\n rules: List[ClassifierRule],\n affiliate_extract_agent: LlamaExtract,\n cobranding_extract_agent: LlamaExtract,\n **kwargs,\n ):\n super().__init__(**kwargs)\n self.classifier = classifier\n self.rules = rules\n self.affiliate_extract_agent = affiliate_extract_agent\n self.cobranding_extract_agent = cobranding_extract_agent\n\n @step\n async def classify_contract(\n self, ctx: Context, ev: StartEvent\n ) -> ExtractAffiliate | ExtractCoBranding | StopEvent:\n result = await self.classifier.aclassify_file_path(\n rules=self.rules, file_input_path=ev.pdf_path\n )\n contract_type = result.items[0].result.type\n print(contract_type)\n if contract_type == \"affiliate_agreements\":\n return ExtractAffiliate(file_path=ev.pdf_path)\n elif contract_type == \"co_branding\":\n return ExtractCoBranding(file_path=ev.pdf_path)\n else:\n return StopEvent()\n\n @step\n async def extract_affiliate(\n self, ctx: Context, ev: ExtractAffiliate\n ) -> BuildGraph:\n result = await self.affiliate_extract_agent.aextract(ev.file_path)\n return BuildGraph(data=result.data, file_path=ev.file_path)\n\n @step\n async def extract_co_branding(\n self, ctx: Context, ev: ExtractCoBranding\n ) -> BuildGraph:\n result = await self.cobranding_extract_agent.aextract(ev.file_path)\n return BuildGraph(data=result.data, file_path=ev.file_path)\n\n @step\n async def build_graph(self, ctx: Context, ev: BuildGraph) -> StopEvent:\n import_query = \"\"\"\n WITH $contract AS contract\n MERGE (c:Contract {path: $path})\n SET c += apoc.map.clean(contract, [\"parties\", \"agreement_date\", \"effective_date\", \"expiration_date\"], [])\n // Cast to date\n SET c.agreement_date = date(contract.agreement_date),\n c.effective_date = date(contract.effective_date),\n c.expiration_date = date(contract.expiration_date)\n\n // Create parties with their locations\n WITH c, contract\n UNWIND coalesce(contract.parties, []) AS party\n MERGE (p:Party {name: party.name})\n MERGE (c)-[:HAS_PARTY]->(p)\n\n // Create location nodes and link to parties\n WITH p, party\n WHERE party.location IS NOT NULL\n MERGE (p)-[:HAS_LOCATION]->(l:Location)\n SET l += party.location\n \"\"\"\n response = await neo4j_driver.execute_query(\n import_query, contract=ev.data, path=ev.file_path\n )\n return StopEvent(response.summary.counters)" ] }, { @@ -742,4 +643,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/cookbooks/ollama_gpt_oss_cookbook.ipynb b/docs/examples/cookbooks/ollama_gpt_oss_cookbook.ipynb index 1a333e53f8..3a25775f05 100644 --- a/docs/examples/cookbooks/ollama_gpt_oss_cookbook.ipynb +++ b/docs/examples/cookbooks/ollama_gpt_oss_cookbook.ipynb @@ -225,12 +225,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "resp = await agent.run(\"What is 1234 * 5678?\", ctx=ctx)\n", - "resp = await agent.run(\"What was the last question/answer pair?\", ctx=ctx)" + "from workflows import Context\n\nctx = Context(agent)\n\nresp = await agent.run(\"What is 1234 * 5678?\", ctx=ctx)\nresp = await agent.run(\"What was the last question/answer pair?\", ctx=ctx)" ] }, { @@ -276,4 +271,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/cookbooks/toolhouse_llamaindex.ipynb b/docs/examples/cookbooks/toolhouse_llamaindex.ipynb index d923508cb9..6e839b1ca7 100644 --- a/docs/examples/cookbooks/toolhouse_llamaindex.ipynb +++ b/docs/examples/cookbooks/toolhouse_llamaindex.ipynb @@ -86,18 +86,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.llms.groq import Groq\n", - "from llama_index.core.agent import ReActAgent\n", - "from llama_index.core.memory import ChatMemoryBuffer\n", - "from toolhouse import Toolhouse, Provider\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - ")" + "from llama_index.llms.groq import Groq\nfrom llama_index.core.agent import ReActAgent\nfrom llama_index.core.memory import ChatMemoryBuffer\nfrom toolhouse import Toolhouse, Provider\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent" ] }, { @@ -293,4 +282,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/evaluation/step_back_argilla.ipynb b/docs/examples/evaluation/step_back_argilla.ipynb index 2aec520377..e0ab762dee 100644 --- a/docs/examples/evaluation/step_back_argilla.ipynb +++ b/docs/examples/evaluation/step_back_argilla.ipynb @@ -68,29 +68,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core import (\n", - " Settings,\n", - " SimpleDirectoryReader,\n", - " VectorStoreIndex,\n", - ")\n", - "from llama_index.core.instrumentation import get_dispatcher\n", - "from llama_index.core.node_parser import SentenceSplitter\n", - "from llama_index.core.response_synthesizers import ResponseMode\n", - "from llama_index.core.schema import NodeWithScore\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - ")\n", - "\n", - "from llama_index.core import get_response_synthesizer\n", - "from llama_index.core.workflow import Event\n", - "from llama_index.utils.workflow import draw_all_possible_flows\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "from argilla_llama_index import ArgillaHandler" + "from llama_index.core import (\n Settings,\n SimpleDirectoryReader,\n VectorStoreIndex,\n)\nfrom llama_index.core.instrumentation import get_dispatcher\nfrom llama_index.core.node_parser import SentenceSplitter\nfrom llama_index.core.response_synthesizers import ResponseMode\nfrom llama_index.core.schema import NodeWithScore\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nfrom llama_index.core import get_response_synthesizer\nfrom workflows.events import Event\nfrom llama_index.utils.workflow import draw_all_possible_flows\nfrom llama_index.llms.openai import OpenAI\n\nfrom argilla_llama_index import ArgillaHandler" ] }, { @@ -439,4 +417,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/memory/custom_memory.ipynb b/docs/examples/memory/custom_memory.ipynb index 5e7bd611fd..cb78c34290 100644 --- a/docs/examples/memory/custom_memory.ipynb +++ b/docs/examples/memory/custom_memory.ipynb @@ -64,103 +64,7 @@ "metadata": {}, "outputs": [], "source": [ - "import re\n", - "from typing import List, Literal, Optional\n", - "from pydantic import Field\n", - "from llama_index.core.memory import Memory, StaticMemoryBlock\n", - "from llama_index.core.llms import LLM, ChatMessage, TextBlock, ImageBlock\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - ")\n", - "\n", - "\n", - "class InitEvent(StartEvent):\n", - " user_msg: str\n", - " new_file_paths: List[str] = Field(default_factory=list)\n", - " removed_file_paths: List[str] = Field(default_factory=list)\n", - "\n", - "\n", - "class ContextUpdateEvent(Event):\n", - " new_file_paths: List[str] = Field(default_factory=list)\n", - " removed_file_paths: List[str] = Field(default_factory=list)\n", - "\n", - "\n", - "class ChatEvent(Event):\n", - " pass\n", - "\n", - "\n", - "class ResponseEvent(StopEvent):\n", - " response: str\n", - "\n", - "\n", - "class ContextualLLMChat(Workflow):\n", - " def __init__(self, memory: Memory, llm: LLM, **workflow_kwargs):\n", - " super().__init__(**workflow_kwargs)\n", - " self._memory = memory\n", - " self._llm = llm\n", - "\n", - " def _path_to_block_name(self, file_path: str) -> str:\n", - " return re.sub(r\"[^\\w-]\", \"_\", file_path)\n", - "\n", - " @step\n", - " async def init(self, ev: InitEvent) -> ContextUpdateEvent | ChatEvent:\n", - " # Manage memory\n", - " await self._memory.aput(ChatMessage(role=\"user\", content=ev.user_msg))\n", - "\n", - " # Forward to chat or context update\n", - " if ev.new_file_paths or ev.removed_file_paths:\n", - " return ContextUpdateEvent(\n", - " new_file_paths=ev.new_file_paths,\n", - " removed_file_paths=ev.removed_file_paths,\n", - " )\n", - " else:\n", - " return ChatEvent()\n", - "\n", - " @step\n", - " async def update_memory_context(self, ev: ContextUpdateEvent) -> ChatEvent:\n", - " current_blocks = self._memory.memory_blocks\n", - " current_block_names = [block.name for block in current_blocks]\n", - "\n", - " for new_file_path in ev.new_file_paths:\n", - " if new_file_path not in current_block_names:\n", - " if new_file_path.endswith((\".png\", \".jpg\", \".jpeg\")):\n", - " self._memory.memory_blocks.append(\n", - " StaticMemoryBlock(\n", - " name=self._path_to_block_name(new_file_path),\n", - " static_content=[ImageBlock(path=new_file_path)],\n", - " )\n", - " )\n", - " elif new_file_path.endswith((\".txt\", \".md\", \".py\", \".ipynb\")):\n", - " with open(new_file_path, \"r\") as f:\n", - " self._memory.memory_blocks.append(\n", - " StaticMemoryBlock(\n", - " name=self._path_to_block_name(new_file_path),\n", - " static_content=f.read(),\n", - " )\n", - " )\n", - " else:\n", - " raise ValueError(f\"Unsupported file: {new_file_path}\")\n", - " for removed_file_path in ev.removed_file_paths:\n", - " # Remove the block from memory\n", - " named_block = self._path_to_block_name(removed_file_path)\n", - " self._memory.memory_blocks = [\n", - " block\n", - " for block in self._memory.memory_blocks\n", - " if block.name != named_block\n", - " ]\n", - "\n", - " return ChatEvent()\n", - "\n", - " @step\n", - " async def chat(self, ev: ChatEvent) -> ResponseEvent:\n", - " chat_history = await self._memory.aget()\n", - " response = await self._llm.achat(chat_history)\n", - " return ResponseEvent(response=response.message.content)" + "import re\nfrom typing import List, Literal, Optional\nfrom pydantic import Field\nfrom llama_index.core.memory import Memory, StaticMemoryBlock\nfrom llama_index.core.llms import LLM, ChatMessage, TextBlock, ImageBlock\nfrom workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\n\n\nclass InitEvent(StartEvent):\n user_msg: str\n new_file_paths: List[str] = Field(default_factory=list)\n removed_file_paths: List[str] = Field(default_factory=list)\n\n\nclass ContextUpdateEvent(Event):\n new_file_paths: List[str] = Field(default_factory=list)\n removed_file_paths: List[str] = Field(default_factory=list)\n\n\nclass ChatEvent(Event):\n pass\n\n\nclass ResponseEvent(StopEvent):\n response: str\n\n\nclass ContextualLLMChat(Workflow):\n def __init__(self, memory: Memory, llm: LLM, **workflow_kwargs):\n super().__init__(**workflow_kwargs)\n self._memory = memory\n self._llm = llm\n\n def _path_to_block_name(self, file_path: str) -> str:\n return re.sub(r\"[^\\w-]\", \"_\", file_path)\n\n @step\n async def init(self, ev: InitEvent) -> ContextUpdateEvent | ChatEvent:\n # Manage memory\n await self._memory.aput(ChatMessage(role=\"user\", content=ev.user_msg))\n\n # Forward to chat or context update\n if ev.new_file_paths or ev.removed_file_paths:\n return ContextUpdateEvent(\n new_file_paths=ev.new_file_paths,\n removed_file_paths=ev.removed_file_paths,\n )\n else:\n return ChatEvent()\n\n @step\n async def update_memory_context(self, ev: ContextUpdateEvent) -> ChatEvent:\n current_blocks = self._memory.memory_blocks\n current_block_names = [block.name for block in current_blocks]\n\n for new_file_path in ev.new_file_paths:\n if new_file_path not in current_block_names:\n if new_file_path.endswith((\".png\", \".jpg\", \".jpeg\")):\n self._memory.memory_blocks.append(\n StaticMemoryBlock(\n name=self._path_to_block_name(new_file_path),\n static_content=[ImageBlock(path=new_file_path)],\n )\n )\n elif new_file_path.endswith((\".txt\", \".md\", \".py\", \".ipynb\")):\n with open(new_file_path, \"r\") as f:\n self._memory.memory_blocks.append(\n StaticMemoryBlock(\n name=self._path_to_block_name(new_file_path),\n static_content=f.read(),\n )\n )\n else:\n raise ValueError(f\"Unsupported file: {new_file_path}\")\n for removed_file_path in ev.removed_file_paths:\n # Remove the block from memory\n named_block = self._path_to_block_name(removed_file_path)\n self._memory.memory_blocks = [\n block\n for block in self._memory.memory_blocks\n if block.name != named_block\n ]\n\n return ChatEvent()\n\n @step\n async def chat(self, ev: ChatEvent) -> ResponseEvent:\n chat_history = await self._memory.aget()\n response = await self._llm.achat(chat_history)\n return ResponseEvent(response=response.message.content)" ] }, { @@ -340,4 +244,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/tools/mcp.ipynb b/docs/examples/tools/mcp.ipynb index 81c151c9f9..96214251c2 100644 --- a/docs/examples/tools/mcp.ipynb +++ b/docs/examples/tools/mcp.ipynb @@ -87,38 +87,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "from llama_index.tools.mcp.utils import workflow_as_mcp\n", - "\n", - "\n", - "class RunEvent(StartEvent):\n", - " msg: str\n", - "\n", - "\n", - "class InfoEvent(Event):\n", - " msg: str\n", - "\n", - "\n", - "class LoudWorkflow(Workflow):\n", - " \"\"\"Useful for converting strings to uppercase and making them louder.\"\"\"\n", - "\n", - " @step\n", - " def step_one(self, ctx: Context, ev: RunEvent) -> StopEvent:\n", - " ctx.write_event_to_stream(InfoEvent(msg=\"Hello, world!\"))\n", - "\n", - " return StopEvent(result=ev.msg.upper() + \"!\")\n", - "\n", - "\n", - "workflow = LoudWorkflow()\n", - "\n", - "mcp = workflow_as_mcp(workflow)" + "from workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_index.tools.mcp.utils import workflow_as_mcp\n\n\nclass RunEvent(StartEvent):\n msg: str\n\n\nclass InfoEvent(Event):\n msg: str\n\n\nclass LoudWorkflow(Workflow):\n \"\"\"Useful for converting strings to uppercase and making them louder.\"\"\"\n\n @step\n def step_one(self, ctx: Context, ev: RunEvent) -> StopEvent:\n ctx.write_event_to_stream(InfoEvent(msg=\"Hello, world!\"))\n\n return StopEvent(result=ev.msg.upper() + \"!\")\n\n\nworkflow = LoudWorkflow()\n\nmcp = workflow_as_mcp(workflow)" ] }, { @@ -327,4 +296,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/tools/mcp_toolbox.ipynb b/docs/examples/tools/mcp_toolbox.ipynb index db40f1bd70..f684bf302b 100644 --- a/docs/examples/tools/mcp_toolbox.ipynb +++ b/docs/examples/tools/mcp_toolbox.ipynb @@ -104,64 +104,7 @@ } ], "source": [ - "import asyncio\n", - "import os\n", - "from llama_index.core.agent.workflow import AgentWorkflow\n", - "from llama_index.core.workflow import Context\n", - "from llama_index.llms.google_genai import GoogleGenAI\n", - "from toolbox_llamaindex import ToolboxClient\n", - "\n", - "prompt = \"\"\"\n", - " You're a helpful hotel assistant. You handle hotel searching, booking and\n", - " cancellations. When the user searches for a hotel, mention it's name, id,\n", - " location and price tier. Always mention hotel ids while performing any\n", - " searches. This is very important for any operations. For any bookings or\n", - " cancellations, please provide the appropriate confirmation. Be sure to\n", - " update checkin or checkout dates if mentioned by the user.\n", - " Don't ask for confirmations from the user.\n", - "\"\"\"\n", - "\n", - "queries = [\n", - " \"Find hotels in Basel with Basel in it's name.\",\n", - " \"Can you book the Hilton Basel for me?\",\n", - " \"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.\",\n", - " \"My check in dates would be from April 10, 2024 to April 19, 2024.\",\n", - "]\n", - "\n", - "\n", - "async def run_application():\n", - " llm = GoogleGenAI(\n", - " api_key=os.getenv(\"GOOGLE_API_KEY\"),\n", - " model=\"gemini-2.0-flash-001\",\n", - " )\n", - "\n", - " # llm = GoogleGenAI(\n", - " # model=\"gemini-2.0-flash-001\",\n", - " # vertexai_config={\"project\": \"project-id\", \"location\": \"us-central1\"},\n", - " # )\n", - "\n", - " # Load the tools from the Toolbox server\n", - " async with ToolboxClient(\"http://127.0.0.1:5000\") as client:\n", - " tools = await client.aload_toolset()\n", - "\n", - " agent = AgentWorkflow.from_tools_or_functions(\n", - " tools,\n", - " llm=llm,\n", - " )\n", - "\n", - " for tool in tools:\n", - " print(tool.metadata)\n", - "\n", - " ctx = Context(agent)\n", - "\n", - " for query in queries:\n", - " response = await agent.run(user_msg=query, ctx=ctx)\n", - " print()\n", - " print(f\"---- {query} ----\")\n", - " print(str(response))\n", - "\n", - "\n", - "await run_application()" + "import asyncio\nimport os\nfrom llama_index.core.agent.workflow import AgentWorkflow\nfrom workflows import Context\nfrom llama_index.llms.google_genai import GoogleGenAI\nfrom toolbox_llamaindex import ToolboxClient\n\nprompt = \"\"\"\n You're a helpful hotel assistant. You handle hotel searching, booking and\n cancellations. When the user searches for a hotel, mention it's name, id,\n location and price tier. Always mention hotel ids while performing any\n searches. This is very important for any operations. For any bookings or\n cancellations, please provide the appropriate confirmation. Be sure to\n update checkin or checkout dates if mentioned by the user.\n Don't ask for confirmations from the user.\n\"\"\"\n\nqueries = [\n \"Find hotels in Basel with Basel in it's name.\",\n \"Can you book the Hilton Basel for me?\",\n \"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.\",\n \"My check in dates would be from April 10, 2024 to April 19, 2024.\",\n]\n\n\nasync def run_application():\n llm = GoogleGenAI(\n api_key=os.getenv(\"GOOGLE_API_KEY\"),\n model=\"gemini-2.0-flash-001\",\n )\n\n # llm = GoogleGenAI(\n # model=\"gemini-2.0-flash-001\",\n # vertexai_config={\"project\": \"project-id\", \"location\": \"us-central1\"},\n # )\n\n # Load the tools from the Toolbox server\n async with ToolboxClient(\"http://127.0.0.1:5000\") as client:\n tools = await client.aload_toolset()\n\n agent = AgentWorkflow.from_tools_or_functions(\n tools,\n llm=llm,\n )\n\n for tool in tools:\n print(tool.metadata)\n\n ctx = Context(agent)\n\n for query in queries:\n response = await agent.run(user_msg=query, ctx=ctx)\n print()\n print(f\"---- {query} ----\")\n print(str(response))\n\n\nawait run_application()" ] }, { @@ -200,4 +143,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/JSONalyze_query_engine.ipynb b/docs/examples/workflow/JSONalyze_query_engine.ipynb index dc24fa3138..1443038183 100644 --- a/docs/examples/workflow/JSONalyze_query_engine.ipynb +++ b/docs/examples/workflow/JSONalyze_query_engine.ipynb @@ -89,23 +89,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "from typing import Dict, List, Any\n", - "\n", - "\n", - "class JsonAnalyzerEvent(Event):\n", - " \"\"\"\n", - " Event containing results of JSON analysis.\n", - "\n", - " Attributes:\n", - " sql_query (str): The generated SQL query.\n", - " table_schema (Dict[str, Any]): Schema of the analyzed table.\n", - " results (List[Dict[str, Any]]): Query execution results.\n", - " \"\"\"\n", - "\n", - " sql_query: str\n", - " table_schema: Dict[str, Any]\n", - " results: List[Dict[str, Any]]" + "from workflows.events import Event\nfrom typing import Dict, List, Any\n\n\nclass JsonAnalyzerEvent(Event):\n \"\"\"\n Event containing results of JSON analysis.\n\n Attributes:\n sql_query (str): The generated SQL query.\n table_schema (Dict[str, Any]): Schema of the analyzed table.\n results (List[Dict[str, Any]]): Query execution results.\n \"\"\"\n\n sql_query: str\n table_schema: Dict[str, Any]\n results: List[Dict[str, Any]]" ] }, { @@ -162,136 +146,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.base.response.schema import Response\n", - "from llama_index.core.indices.struct_store.sql_retriever import (\n", - " DefaultSQLParser,\n", - ")\n", - "from llama_index.core.prompts.default_prompts import DEFAULT_JSONALYZE_PROMPT\n", - "from llama_index.core.utils import print_text\n", - "\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "from IPython.display import Markdown, display\n", - "\n", - "\n", - "class JSONAnalyzeQueryEngineWorkflow(Workflow):\n", - " @step\n", - " async def jsonalyzer(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> JsonAnalyzerEvent:\n", - " \"\"\"\n", - " Analyze JSON data using a SQL-like query approach.\n", - "\n", - " This asynchronous method sets up an in-memory SQLite database, loads JSON data,\n", - " generates a SQL query based on a natural language question, executes the query,\n", - " and returns the results.\n", - "\n", - " Args:\n", - " ctx (Context): The context object for storing data during execution.\n", - " ev (StartEvent): The event object containing input parameters.\n", - "\n", - " Returns:\n", - " JsonAnalyzerEvent: An event object containing the SQL query, table schema, and query results.\n", - "\n", - " The method performs the following steps:\n", - " 1. Imports the required 'sqlite-utils' package.\n", - " 2. Extracts necessary data from the input event.\n", - " 3. Sets up an in-memory SQLite database and loads the JSON data.\n", - " 4. Generates a SQL query using a LLM based on the input question.\n", - " 5. Executes the SQL query and retrieves the results.\n", - " 6. Returns the results along with the SQL query and table schema.\n", - "\n", - " Note:\n", - " This method requires the 'sqlite-utils' package to be installed.\n", - " \"\"\"\n", - " try:\n", - " import sqlite_utils\n", - " except ImportError as exc:\n", - " IMPORT_ERROR_MSG = (\n", - " \"sqlite-utils is needed to use this Query Engine:\\n\"\n", - " \"pip install sqlite-utils\"\n", - " )\n", - "\n", - " raise ImportError(IMPORT_ERROR_MSG) from exc\n", - "\n", - " await ctx.store.set(\"query\", ev.get(\"query\"))\n", - " await ctx.store.set(\"llm\", ev.get(\"llm\"))\n", - "\n", - " query = ev.get(\"query\")\n", - " table_name = ev.get(\"table_name\")\n", - " list_of_dict = ev.get(\"list_of_dict\")\n", - " prompt = DEFAULT_JSONALYZE_PROMPT\n", - "\n", - " # Instantiate in-memory SQLite database\n", - " db = sqlite_utils.Database(memory=True)\n", - " try:\n", - " # Load list of dictionaries into SQLite database\n", - " db[ev.table_name].insert_all(list_of_dict)\n", - " except sqlite_utils.utils.sqlite3.IntegrityError as exc:\n", - " print_text(\n", - " f\"Error inserting into table {table_name}, expected format:\"\n", - " )\n", - " print_text(\"[{col1: val1, col2: val2, ...}, ...]\")\n", - " raise ValueError(\"Invalid list_of_dict\") from exc\n", - "\n", - " # Get the table schema\n", - " table_schema = db[table_name].columns_dict\n", - "\n", - " # Get the SQL query with text-to-SQL prompt\n", - " response_str = await ev.llm.apredict(\n", - " prompt=prompt,\n", - " table_name=table_name,\n", - " table_schema=table_schema,\n", - " question=query,\n", - " )\n", - "\n", - " sql_parser = DefaultSQLParser()\n", - "\n", - " sql_query = sql_parser.parse_response_to_sql(response_str, ev.query)\n", - "\n", - " try:\n", - " # Execute the SQL query\n", - " results = list(db.query(sql_query))\n", - " except sqlite_utils.utils.sqlite3.OperationalError as exc:\n", - " print_text(f\"Error executing query: {sql_query}\")\n", - " raise ValueError(\"Invalid query\") from exc\n", - "\n", - " return JsonAnalyzerEvent(\n", - " sql_query=sql_query, table_schema=table_schema, results=results\n", - " )\n", - "\n", - " @step\n", - " async def synthesize(\n", - " self, ctx: Context, ev: JsonAnalyzerEvent\n", - " ) -> StopEvent:\n", - " \"\"\"Synthesize the response.\"\"\"\n", - " llm = await ctx.store.get(\"llm\", default=None)\n", - " query = await ctx.store.get(\"query\", default=None)\n", - "\n", - " response_str = llm.predict(\n", - " DEFAULT_RESPONSE_SYNTHESIS_PROMPT,\n", - " sql_query=ev.sql_query,\n", - " table_schema=ev.table_schema,\n", - " sql_response=ev.results,\n", - " query_str=query,\n", - " )\n", - "\n", - " response_metadata = {\n", - " \"sql_query\": ev.sql_query,\n", - " \"table_schema\": str(ev.table_schema),\n", - " }\n", - "\n", - " response = Response(response=response_str, metadata=response_metadata)\n", - "\n", - " return StopEvent(result=response)" + "from llama_index.core.base.response.schema import Response\nfrom llama_index.core.indices.struct_store.sql_retriever import (\n DefaultSQLParser,\n)\nfrom llama_index.core.prompts.default_prompts import DEFAULT_JSONALYZE_PROMPT\nfrom llama_index.core.utils import print_text\n\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nfrom llama_index.llms.openai import OpenAI\n\nfrom IPython.display import Markdown, display\n\n\nclass JSONAnalyzeQueryEngineWorkflow(Workflow):\n @step\n async def jsonalyzer(\n self, ctx: Context, ev: StartEvent\n ) -> JsonAnalyzerEvent:\n \"\"\"\n Analyze JSON data using a SQL-like query approach.\n\n This asynchronous method sets up an in-memory SQLite database, loads JSON data,\n generates a SQL query based on a natural language question, executes the query,\n and returns the results.\n\n Args:\n ctx (Context): The context object for storing data during execution.\n ev (StartEvent): The event object containing input parameters.\n\n Returns:\n JsonAnalyzerEvent: An event object containing the SQL query, table schema, and query results.\n\n The method performs the following steps:\n 1. Imports the required 'sqlite-utils' package.\n 2. Extracts necessary data from the input event.\n 3. Sets up an in-memory SQLite database and loads the JSON data.\n 4. Generates a SQL query using a LLM based on the input question.\n 5. Executes the SQL query and retrieves the results.\n 6. Returns the results along with the SQL query and table schema.\n\n Note:\n This method requires the 'sqlite-utils' package to be installed.\n \"\"\"\n try:\n import sqlite_utils\n except ImportError as exc:\n IMPORT_ERROR_MSG = (\n \"sqlite-utils is needed to use this Query Engine:\\n\"\n \"pip install sqlite-utils\"\n )\n\n raise ImportError(IMPORT_ERROR_MSG) from exc\n\n await ctx.store.set(\"query\", ev.get(\"query\"))\n await ctx.store.set(\"llm\", ev.get(\"llm\"))\n\n query = ev.get(\"query\")\n table_name = ev.get(\"table_name\")\n list_of_dict = ev.get(\"list_of_dict\")\n prompt = DEFAULT_JSONALYZE_PROMPT\n\n # Instantiate in-memory SQLite database\n db = sqlite_utils.Database(memory=True)\n try:\n # Load list of dictionaries into SQLite database\n db[ev.table_name].insert_all(list_of_dict)\n except sqlite_utils.utils.sqlite3.IntegrityError as exc:\n print_text(\n f\"Error inserting into table {table_name}, expected format:\"\n )\n print_text(\"[{col1: val1, col2: val2, ...}, ...]\")\n raise ValueError(\"Invalid list_of_dict\") from exc\n\n # Get the table schema\n table_schema = db[table_name].columns_dict\n\n # Get the SQL query with text-to-SQL prompt\n response_str = await ev.llm.apredict(\n prompt=prompt,\n table_name=table_name,\n table_schema=table_schema,\n question=query,\n )\n\n sql_parser = DefaultSQLParser()\n\n sql_query = sql_parser.parse_response_to_sql(response_str, ev.query)\n\n try:\n # Execute the SQL query\n results = list(db.query(sql_query))\n except sqlite_utils.utils.sqlite3.OperationalError as exc:\n print_text(f\"Error executing query: {sql_query}\")\n raise ValueError(\"Invalid query\") from exc\n\n return JsonAnalyzerEvent(\n sql_query=sql_query, table_schema=table_schema, results=results\n )\n\n @step\n async def synthesize(\n self, ctx: Context, ev: JsonAnalyzerEvent\n ) -> StopEvent:\n \"\"\"Synthesize the response.\"\"\"\n llm = await ctx.store.get(\"llm\", default=None)\n query = await ctx.store.get(\"query\", default=None)\n\n response_str = llm.predict(\n DEFAULT_RESPONSE_SYNTHESIS_PROMPT,\n sql_query=ev.sql_query,\n table_schema=ev.table_schema,\n sql_response=ev.results,\n query_str=query,\n )\n\n response_metadata = {\n \"sql_query\": ev.sql_query,\n \"table_schema\": str(ev.table_schema),\n }\n\n response = Response(response=response_str, metadata=response_metadata)\n\n return StopEvent(result=response)" ] }, { @@ -773,4 +628,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/advanced_text_to_sql.ipynb b/docs/examples/workflow/advanced_text_to_sql.ipynb index 86767c93e3..85650b554f 100644 --- a/docs/examples/workflow/advanced_text_to_sql.ipynb +++ b/docs/examples/workflow/advanced_text_to_sql.ipynb @@ -506,85 +506,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - " Context,\n", - " Event,\n", - ")\n", - "\n", - "\n", - "class TableRetrieveEvent(Event):\n", - " \"\"\"Result of running table retrieval.\"\"\"\n", - "\n", - " table_context_str: str\n", - " query: str\n", - "\n", - "\n", - "class TextToSQLEvent(Event):\n", - " \"\"\"Text-to-SQL event.\"\"\"\n", - "\n", - " sql: str\n", - " query: str\n", - "\n", - "\n", - "class TextToSQLWorkflow1(Workflow):\n", - " \"\"\"Text-to-SQL Workflow that does query-time table retrieval.\"\"\"\n", - "\n", - " def __init__(\n", - " self,\n", - " obj_retriever,\n", - " text2sql_prompt,\n", - " sql_retriever,\n", - " response_synthesis_prompt,\n", - " llm,\n", - " *args,\n", - " **kwargs,\n", - " ) -> None:\n", - " \"\"\"Init params.\"\"\"\n", - " super().__init__(*args, **kwargs)\n", - " self.obj_retriever = obj_retriever\n", - " self.text2sql_prompt = text2sql_prompt\n", - " self.sql_retriever = sql_retriever\n", - " self.response_synthesis_prompt = response_synthesis_prompt\n", - " self.llm = llm\n", - "\n", - " @step\n", - " def retrieve_tables(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> TableRetrieveEvent:\n", - " \"\"\"Retrieve tables.\"\"\"\n", - " table_schema_objs = self.obj_retriever.retrieve(ev.query)\n", - " table_context_str = get_table_context_str(table_schema_objs)\n", - " return TableRetrieveEvent(\n", - " table_context_str=table_context_str, query=ev.query\n", - " )\n", - "\n", - " @step\n", - " def generate_sql(\n", - " self, ctx: Context, ev: TableRetrieveEvent\n", - " ) -> TextToSQLEvent:\n", - " \"\"\"Generate SQL statement.\"\"\"\n", - " fmt_messages = self.text2sql_prompt.format_messages(\n", - " query_str=ev.query, schema=ev.table_context_str\n", - " )\n", - " chat_response = self.llm.chat(fmt_messages)\n", - " sql = parse_response_to_sql(chat_response)\n", - " return TextToSQLEvent(sql=sql, query=ev.query)\n", - "\n", - " @step\n", - " def generate_response(self, ctx: Context, ev: TextToSQLEvent) -> StopEvent:\n", - " \"\"\"Run SQL retrieval and generate response.\"\"\"\n", - " retrieved_rows = self.sql_retriever.retrieve(ev.sql)\n", - " fmt_messages = self.response_synthesis_prompt.format_messages(\n", - " sql_query=ev.sql,\n", - " context_str=str(retrieved_rows),\n", - " query_str=ev.query,\n", - " )\n", - " chat_response = llm.chat(fmt_messages)\n", - " return StopEvent(result=chat_response)" + "from workflows import Workflow, step, Context\nfrom workflows.events import StartEvent, StopEvent, Event\n\n\nclass TableRetrieveEvent(Event):\n \"\"\"Result of running table retrieval.\"\"\"\n\n table_context_str: str\n query: str\n\n\nclass TextToSQLEvent(Event):\n \"\"\"Text-to-SQL event.\"\"\"\n\n sql: str\n query: str\n\n\nclass TextToSQLWorkflow1(Workflow):\n \"\"\"Text-to-SQL Workflow that does query-time table retrieval.\"\"\"\n\n def __init__(\n self,\n obj_retriever,\n text2sql_prompt,\n sql_retriever,\n response_synthesis_prompt,\n llm,\n *args,\n **kwargs,\n ) -> None:\n \"\"\"Init params.\"\"\"\n super().__init__(*args, **kwargs)\n self.obj_retriever = obj_retriever\n self.text2sql_prompt = text2sql_prompt\n self.sql_retriever = sql_retriever\n self.response_synthesis_prompt = response_synthesis_prompt\n self.llm = llm\n\n @step\n def retrieve_tables(\n self, ctx: Context, ev: StartEvent\n ) -> TableRetrieveEvent:\n \"\"\"Retrieve tables.\"\"\"\n table_schema_objs = self.obj_retriever.retrieve(ev.query)\n table_context_str = get_table_context_str(table_schema_objs)\n return TableRetrieveEvent(\n table_context_str=table_context_str, query=ev.query\n )\n\n @step\n def generate_sql(\n self, ctx: Context, ev: TableRetrieveEvent\n ) -> TextToSQLEvent:\n \"\"\"Generate SQL statement.\"\"\"\n fmt_messages = self.text2sql_prompt.format_messages(\n query_str=ev.query, schema=ev.table_context_str\n )\n chat_response = self.llm.chat(fmt_messages)\n sql = parse_response_to_sql(chat_response)\n return TextToSQLEvent(sql=sql, query=ev.query)\n\n @step\n def generate_response(self, ctx: Context, ev: TextToSQLEvent) -> StopEvent:\n \"\"\"Run SQL retrieval and generate response.\"\"\"\n retrieved_rows = self.sql_retriever.retrieve(ev.sql)\n fmt_messages = self.response_synthesis_prompt.format_messages(\n sql_query=ev.sql,\n context_str=str(retrieved_rows),\n query_str=ev.query,\n )\n chat_response = llm.chat(fmt_messages)\n return StopEvent(result=chat_response)" ] }, { @@ -974,31 +896,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - " Context,\n", - " Event,\n", - ")\n", - "\n", - "\n", - "class TextToSQLWorkflow2(TextToSQLWorkflow1):\n", - " \"\"\"Text-to-SQL Workflow that does query-time row AND table retrieval.\"\"\"\n", - "\n", - " @step\n", - " def retrieve_tables(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> TableRetrieveEvent:\n", - " \"\"\"Retrieve tables.\"\"\"\n", - " table_schema_objs = self.obj_retriever.retrieve(ev.query)\n", - " table_context_str = get_table_context_and_rows_str(\n", - " ev.query, table_schema_objs, verbose=self._verbose\n", - " )\n", - " return TableRetrieveEvent(\n", - " table_context_str=table_context_str, query=ev.query\n", - " )" + "from workflows import Workflow, step, Context\nfrom workflows.events import StartEvent, StopEvent, Event\n\n\nclass TextToSQLWorkflow2(TextToSQLWorkflow1):\n \"\"\"Text-to-SQL Workflow that does query-time row AND table retrieval.\"\"\"\n\n @step\n def retrieve_tables(\n self, ctx: Context, ev: StartEvent\n ) -> TableRetrieveEvent:\n \"\"\"Retrieve tables.\"\"\"\n table_schema_objs = self.obj_retriever.retrieve(ev.query)\n table_context_str = get_table_context_and_rows_str(\n ev.query, table_schema_objs, verbose=self._verbose\n )\n return TableRetrieveEvent(\n table_context_str=table_context_str, query=ev.query\n )" ] }, { @@ -1119,4 +1017,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/checkpointing_workflows.ipynb b/docs/examples/workflow/checkpointing_workflows.ipynb index 286c3a9ece..a4c0e33ed9 100644 --- a/docs/examples/workflow/checkpointing_workflows.ipynb +++ b/docs/examples/workflow/checkpointing_workflows.ipynb @@ -1,526 +1,520 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Checkpointing Workflow Runs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this notebook, we demonstrate how to checkpoint `Workflow` runs via a `WorkflowCheckpointer` object. We also show how we can view all of the checkpoints that are stored in this object and finally how we can use a checkpoint as the starting point of a new run." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define a Workflow" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "api_key = os.environ.get(\"OPENAI_API_KEY\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " step,\n", - " StartEvent,\n", - " StopEvent,\n", - " Event,\n", - " Context,\n", - ")\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "class JokeEvent(Event):\n", - " joke: str\n", - "\n", - "\n", - "class JokeFlow(Workflow):\n", - " llm = OpenAI(api_key=api_key)\n", - "\n", - " @step\n", - " async def generate_joke(self, ev: StartEvent) -> JokeEvent:\n", - " topic = ev.topic\n", - "\n", - " prompt = f\"Write your best joke about {topic}.\"\n", - " response = await self.llm.acomplete(prompt)\n", - " return JokeEvent(joke=str(response))\n", - "\n", - " @step\n", - " async def critique_joke(self, ev: JokeEvent) -> StopEvent:\n", - " joke = ev.joke\n", - "\n", - " prompt = f\"Give a thorough analysis and critique of the following joke: {joke}\"\n", - " response = await self.llm.acomplete(prompt)\n", - " return StopEvent(result=str(response))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define a WorkflowCheckpointer Object" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow.checkpointer import WorkflowCheckpointer" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# instantiate Jokeflow\n", - "workflow = JokeFlow()\n", - "wflow_ckptr = WorkflowCheckpointer(workflow=workflow)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run the Workflow from the WorkflowCheckpointer" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `WorkflowCheckpointer.run()` method is a wrapper over the `Workflow.run()` method, which injects a checkpointer callback in order to create and store checkpoints. Note that checkpoints are created at the completion of a step, and that the data stored in checkpoints are:\n", - "\n", - "- `last_completed_step`: The name of the last completed step\n", - "- `input_event`: The input event to this last completed step\n", - "- `output_event`: The event outputted by this last completed step\n", - "- `ctx_state`: a snapshot of the attached `Context`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'This joke plays on the double meaning of the word \"rates,\" which can refer to both the cost of something and the passage of time. The punchline suggests that chemists prefer nitrates because they are less expensive than day rates, implying that chemists are frugal or cost-conscious individuals.\\n\\nOverall, the joke is clever and plays on a pun that is likely to be appreciated by those familiar with chemistry and the concept of nitrates. However, the humor may be lost on those who are not well-versed in chemistry terminology. Additionally, the joke relies on a somewhat simplistic play on words, which may not be as engaging or humorous to some audiences.\\n\\nIn terms of structure, the joke follows a classic setup and punchline format, with the punchline providing a surprising twist on the initial premise. The delivery of the joke may also play a role in its effectiveness, as timing and tone can greatly impact the humor of a joke.\\n\\nOverall, while the joke may appeal to a specific audience and demonstrate some clever wordplay, it may not have universal appeal and may be considered somewhat niche in its humor.'" + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Checkpointing Workflow Runs" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "handler = wflow_ckptr.run(\n", - " topic=\"chemistry\",\n", - ")\n", - "await handler" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can view all of the checkpoints via the `.checkpoints` attribute, which is dictionary with keys representing the `run_id` of the run and whose values are the list of checkpoints stored for the run." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'483eccdd-a035-42cc-b596-cd33d42938a7': [Checkpoint(id_='d5acd098-47e2-4acf-9520-9ca06ee4e238', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke=\"Why do chemists like nitrates so much?\\n\\nBecause they're cheaper than day rates!\"), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"chemistry\", \"store_checkpoints\": false}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True}),\n", - " Checkpoint(id_='288c3e54-292b-4c7e-aed8-662537508b46', last_completed_step='critique_joke', input_event=JokeEvent(joke=\"Why do chemists like nitrates so much?\\n\\nBecause they're cheaper than day rates!\"), output_event=StopEvent(result='This joke plays on the double meaning of the word \"rates,\" which can refer to both the cost of something and the passage of time. The punchline suggests that chemists prefer nitrates because they are less expensive than day rates, implying that chemists are frugal or cost-conscious individuals.\\n\\nOverall, the joke is clever and plays on a pun that is likely to be appreciated by those familiar with chemistry and the concept of nitrates. However, the humor may be lost on those who are not well-versed in chemistry terminology. Additionally, the joke relies on a somewhat simplistic play on words, which may not be as engaging or humorous to some audiences.\\n\\nIn terms of structure, the joke follows a classic setup and punchline format, with the punchline providing a surprising twist on the initial premise. The delivery of the joke may also play a role in its effectiveness, as timing and tone can greatly impact the humor of a joke.\\n\\nOverall, while the joke may appeal to a specific audience and demonstrate some clever wordplay, it may not have universal appeal and may be considered somewhat niche in its humor.'), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': [], 'critique_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"chemistry\", \"store_checkpoints\": false}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}', '{\"__is_pydantic\": true, \"value\": {\"joke\": \"Why do chemists like nitrates so much?\\\\n\\\\nBecause they\\'re cheaper than day rates!\"}, \"qualified_name\": \"__main__.JokeEvent\"}'], 'is_running': True})]}" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this notebook, we demonstrate how to checkpoint `Workflow` runs via a `WorkflowCheckpointer` object. We also show how we can view all of the checkpoints that are stored in this object and finally how we can use a checkpoint as the starting point of a new run." ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "wflow_ckptr.checkpoints" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has 2 stored checkpoints\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {len(ckpts)} stored checkpoints\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Filtering the Checkpoints" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `WorkflowCheckpointer` object also has a `.filter_checkpoints()` method that allows us to filter via:\n", - "\n", - "- The name of the last completed step by speciying the param `last_completed_step`\n", - "- The event type of the last completed step's output event by specifying `output_event_type`\n", - "- Similarly, the event type of the last completed step's input event by specifying `input_event_type`\n", - "\n", - "Specifying multiple of these filters will be combined by the \"AND\" operator." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's test this functionality out, but first we'll make things a bit more interesting by running a couple of more runs with our `Workflow`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "additional_topics = [\"biology\", \"history\"]\n", - "\n", - "for topic in additional_topics:\n", - " handler = wflow_ckptr.run(topic=topic)\n", - " await handler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has 2 stored checkpoints\n", - "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has 2 stored checkpoints\n", - "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has 2 stored checkpoints\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {len(ckpts)} stored checkpoints\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Checkpoint(id_='d5acd098-47e2-4acf-9520-9ca06ee4e238', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke=\"Why do chemists like nitrates so much?\\n\\nBecause they're cheaper than day rates!\"), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"chemistry\", \"store_checkpoints\": false}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True}),\n", - " Checkpoint(id_='87865641-14e7-4eb0-bb62-4c211567acfc', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke=\"Why did the biologist break up with the mathematician?\\n\\nBecause they couldn't find a common denominator!\"), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"biology\"}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True}),\n", - " Checkpoint(id_='69a99535-d45c-46b4-a1f4-d3ecc128cb08', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke='Why did the history teacher go to the beach?\\n\\nTo catch some waves of the past!'), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"history\"}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True})]" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define a Workflow" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Filter by the name of last completed step\n", - "checkpoints_right_after_generate_joke_step = wflow_ckptr.filter_checkpoints(\n", - " last_completed_step=\"generate_joke\",\n", - ")\n", - "\n", - "# checkpoint ids\n", - "[ckpt for ckpt in checkpoints_right_after_generate_joke_step]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Re-Run Workflow from a specific checkpoint" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To run from a chosen `Checkpoint` we can use the `WorkflowCheckpointer.run_from()` method. NOTE that doing so will lead to a new `run` and it's checkpoints if enabled will be stored under the newly assigned `run_id`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Analysis:\\nThis joke plays on the double meaning of the word \"rates,\" which can refer to both the cost of something and the passage of time. In this case, the joke suggests that chemists prefer nitrates because they are less expensive than day rates, implying that chemists are frugal or cost-conscious individuals.\\n\\nCritique:\\n- Clever wordplay: The joke relies on a clever play on words, which can be entertaining for those who appreciate puns and linguistic humor.\\n- Niche audience: The humor in this joke may be more appreciated by individuals with a background in chemistry or a specific interest in science, as the punchline relies on knowledge of chemical compounds.\\n- Lack of universal appeal: The joke may not resonate with a general audience who may not understand the reference to nitrates or the significance of their cost compared to day rates.\\n- Lack of depth: While the joke is amusing on a surface level, it may be considered somewhat shallow or simplistic compared to more nuanced or thought-provoking humor.\\n\\nOverall, the joke is a light-hearted play on words that may appeal to individuals with a specific interest in chemistry or wordplay. However, its niche appeal and lack of universal relevance may limit its effectiveness as a joke for a broader audience.'" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import os\n", + "\n", + "api_key = os.environ.get(\"OPENAI_API_KEY\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Workflow, step, Context\n", + "from workflows.events import StartEvent, StopEvent, Event\n", + "from llama_index.llms.openai import OpenAI\n", + "\n", + "\n", + "class JokeEvent(Event):\n", + " joke: str\n", + "\n", + "\n", + "class JokeFlow(Workflow):\n", + " llm = OpenAI(api_key=api_key)\n", + "\n", + " @step\n", + " async def generate_joke(self, ev: StartEvent) -> JokeEvent:\n", + " topic = ev.topic\n", + "\n", + " prompt = f\"Write your best joke about {topic}.\"\n", + " response = await self.llm.acomplete(prompt)\n", + " return JokeEvent(joke=str(response))\n", + "\n", + " @step\n", + " async def critique_joke(self, ev: JokeEvent) -> StopEvent:\n", + " joke = ev.joke\n", + "\n", + " prompt = f\"Give a thorough analysis and critique of the following joke: {joke}\"\n", + " response = await self.llm.acomplete(prompt)\n", + " return StopEvent(result=str(response))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define a WorkflowCheckpointer Object" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# can work with a new instance\n", - "new_workflow_instance = JokeFlow()\n", - "wflow_ckptr.workflow = new_workflow_instance\n", - "\n", - "ckpt = checkpoints_right_after_generate_joke_step[0]\n", - "\n", - "handler = wflow_ckptr.run_from(checkpoint=ckpt)\n", - "await handler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has 2 stored checkpoints\n", - "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has 2 stored checkpoints\n", - "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has 2 stored checkpoints\n", - "Run: 9dccfda3-b5bf-4771-8293-efd3e3a275a6 has 1 stored checkpoints\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {len(ckpts)} stored checkpoints\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since we've executed from the checkpoint that represents the end of \"generate_joke\" step, there is only one additional checkpoint (i.e., that for the completion of step \"critique_joke\") that gets stored in the last partial run." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Specifying Which Steps To Checkpoint" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By default all steps of the attached workflow (excluding the \"_done\" step) will be checkpointed. You can see which steps are enabled for checkpointing via the `enabled_checkpoints` attribute." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'critique_joke', 'generate_joke'}" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# WorkflowCheckpointer has been removed (deprecated). See CHANGELOG." + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# instantiate Jokeflow\n", + "workflow = JokeFlow()\n", + "wflow_ckptr = WorkflowCheckpointer(workflow=workflow)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run the Workflow from the WorkflowCheckpointer" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "wflow_ckptr.enabled_checkpoints" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To disable a step for checkpointing, we can use the `.disable_checkpoint()` method" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "wflow_ckptr.disable_checkpoint(step=\"critique_joke\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Analysis:\\nThis joke plays on the common stereotype that mechanics are overly critical and nitpicky when it comes to fixing cars. The humor comes from the unexpected twist of the car being the one to break up with the mechanic, rather than the other way around. The punchline is clever and plays on the idea of a relationship ending due to constant criticism.\\n\\nCritique:\\nOverall, this joke is light-hearted and easy to understand. It relies on a simple pun and doesn't require much thought to appreciate. However, the humor may be seen as somewhat predictable and not particularly original. The joke also perpetuates the stereotype of mechanics being overly critical, which may not sit well with some people in the automotive industry. Additionally, the joke may not be as universally funny as some other jokes, as it relies on a specific understanding of the relationship between cars and mechanics. Overall, while the joke is amusing, it may not be considered particularly groundbreaking or memorable.\"" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `WorkflowCheckpointer.run()` method is a wrapper over the `Workflow.run()` method, which injects a checkpointer callback in order to create and store checkpoints. Note that checkpoints are created at the completion of a step, and that the data stored in checkpoints are:\n", + "\n", + "- `last_completed_step`: The name of the last completed step\n", + "- `input_event`: The input event to this last completed step\n", + "- `output_event`: The event outputted by this last completed step\n", + "- `ctx_state`: a snapshot of the attached `Context`" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "handler = wflow_ckptr.run(topic=\"cars\")\n", - "await handler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", - "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", - "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", - "Run: 9dccfda3-b5bf-4771-8293-efd3e3a275a6 has stored checkpoints for steps ['critique_joke']\n", - "Run: 6390e2ce-63f4-44a3-8a75-64ccb765abfd has stored checkpoints for steps ['generate_joke']\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(\n", - " f\"Run: {run_id} has stored checkpoints for steps {[c.last_completed_step for c in ckpts]}\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And we can turn checkpointing back on by using the `.enable_checkpoint()` method" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "wflow_ckptr.enable_checkpoint(step=\"critique_joke\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Analysis:\\nThis joke plays on the common stereotype that mechanics are overly critical and nitpicky when it comes to fixing cars. The humor comes from the unexpected twist of the car being the one to break up with the mechanic, rather than the other way around. The joke also cleverly uses the term \"nitpicking\" in a literal sense, as in picking at the nit (small details) of the car.\\n\\nCritique:\\nWhile the joke is clever and plays on a well-known stereotype, it may not be the most original or groundbreaking joke. The punchline is somewhat predictable and relies on a common trope about mechanics. Additionally, the joke may not be universally relatable or understood by all audiences, as it requires some knowledge of the stereotype about mechanics being nitpicky.\\n\\nOverall, the joke is light-hearted and humorous, but it may not be the most memorable or impactful joke due to its reliance on a common stereotype. It could be improved by adding a more unexpected or unique twist to the punchline.'" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "handler = wflow_ckptr.run(\n", + " topic=\"chemistry\",\n", + ")\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "'This joke plays on the double meaning of the word \"rates,\" which can refer to both the cost of something and the passage of time. The punchline suggests that chemists prefer nitrates because they are less expensive than day rates, implying that chemists are frugal or cost-conscious individuals.\\n\\nOverall, the joke is clever and plays on a pun that is likely to be appreciated by those familiar with chemistry and the concept of nitrates. However, the humor may be lost on those who are not well-versed in chemistry terminology. Additionally, the joke relies on a somewhat simplistic play on words, which may not be as engaging or humorous to some audiences.\\n\\nIn terms of structure, the joke follows a classic setup and punchline format, with the punchline providing a surprising twist on the initial premise. The delivery of the joke may also play a role in its effectiveness, as timing and tone can greatly impact the humor of a joke.\\n\\nOverall, while the joke may appeal to a specific audience and demonstrate some clever wordplay, it may not have universal appeal and may be considered somewhat niche in its humor.'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can view all of the checkpoints via the `.checkpoints` attribute, which is dictionary with keys representing the `run_id` of the run and whose values are the list of checkpoints stored for the run." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "wflow_ckptr.checkpoints" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "{'483eccdd-a035-42cc-b596-cd33d42938a7': [Checkpoint(id_='d5acd098-47e2-4acf-9520-9ca06ee4e238', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke=\"Why do chemists like nitrates so much?\\n\\nBecause they're cheaper than day rates!\"), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"chemistry\", \"store_checkpoints\": false}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True}),\n", + " Checkpoint(id_='288c3e54-292b-4c7e-aed8-662537508b46', last_completed_step='critique_joke', input_event=JokeEvent(joke=\"Why do chemists like nitrates so much?\\n\\nBecause they're cheaper than day rates!\"), output_event=StopEvent(result='This joke plays on the double meaning of the word \"rates,\" which can refer to both the cost of something and the passage of time. The punchline suggests that chemists prefer nitrates because they are less expensive than day rates, implying that chemists are frugal or cost-conscious individuals.\\n\\nOverall, the joke is clever and plays on a pun that is likely to be appreciated by those familiar with chemistry and the concept of nitrates. However, the humor may be lost on those who are not well-versed in chemistry terminology. Additionally, the joke relies on a somewhat simplistic play on words, which may not be as engaging or humorous to some audiences.\\n\\nIn terms of structure, the joke follows a classic setup and punchline format, with the punchline providing a surprising twist on the initial premise. The delivery of the joke may also play a role in its effectiveness, as timing and tone can greatly impact the humor of a joke.\\n\\nOverall, while the joke may appeal to a specific audience and demonstrate some clever wordplay, it may not have universal appeal and may be considered somewhat niche in its humor.'), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': [], 'critique_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"chemistry\", \"store_checkpoints\": false}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}', '{\"__is_pydantic\": true, \"value\": {\"joke\": \"Why do chemists like nitrates so much?\\\\n\\\\nBecause they\\'re cheaper than day rates!\"}, \"qualified_name\": \"__main__.JokeEvent\"}'], 'is_running': True})]}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {len(ckpts)} stored checkpoints\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has 2 stored checkpoints\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Filtering the Checkpoints" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `WorkflowCheckpointer` object also has a `.filter_checkpoints()` method that allows us to filter via:\n", + "\n", + "- The name of the last completed step by speciying the param `last_completed_step`\n", + "- The event type of the last completed step's output event by specifying `output_event_type`\n", + "- Similarly, the event type of the last completed step's input event by specifying `input_event_type`\n", + "\n", + "Specifying multiple of these filters will be combined by the \"AND\" operator." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test this functionality out, but first we'll make things a bit more interesting by running a couple of more runs with our `Workflow`." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "additional_topics = [\"biology\", \"history\"]\n", + "\n", + "for topic in additional_topics:\n", + " handler = wflow_ckptr.run(topic=topic)\n", + " await handler" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {len(ckpts)} stored checkpoints\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has 2 stored checkpoints\n", + "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has 2 stored checkpoints\n", + "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has 2 stored checkpoints\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Filter by the name of last completed step\n", + "checkpoints_right_after_generate_joke_step = wflow_ckptr.filter_checkpoints(\n", + " last_completed_step=\"generate_joke\",\n", + ")\n", + "\n", + "# checkpoint ids\n", + "[ckpt for ckpt in checkpoints_right_after_generate_joke_step]" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "[Checkpoint(id_='d5acd098-47e2-4acf-9520-9ca06ee4e238', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke=\"Why do chemists like nitrates so much?\\n\\nBecause they're cheaper than day rates!\"), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"chemistry\", \"store_checkpoints\": false}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True}),\n", + " Checkpoint(id_='87865641-14e7-4eb0-bb62-4c211567acfc', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke=\"Why did the biologist break up with the mathematician?\\n\\nBecause they couldn't find a common denominator!\"), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"biology\"}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True}),\n", + " Checkpoint(id_='69a99535-d45c-46b4-a1f4-d3ecc128cb08', last_completed_step='generate_joke', input_event=StartEvent(), output_event=JokeEvent(joke='Why did the history teacher go to the beach?\\n\\nTo catch some waves of the past!'), ctx_state={'globals': {}, 'streaming_queue': '[]', 'queues': {'_done': '[]', 'critique_joke': '[]', 'generate_joke': '[]'}, 'stepwise': False, 'events_buffer': {}, 'in_progress': {'generate_joke': []}, 'accepted_events': [('generate_joke', 'StartEvent'), ('critique_joke', 'JokeEvent')], 'broker_log': ['{\"__is_pydantic\": true, \"value\": {\"_data\": {\"topic\": \"history\"}}, \"qualified_name\": \"llama_index.core.workflow.events.StartEvent\"}'], 'is_running': True})]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Re-Run Workflow from a specific checkpoint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run from a chosen `Checkpoint` we can use the `WorkflowCheckpointer.run_from()` method. NOTE that doing so will lead to a new `run` and it's checkpoints if enabled will be stored under the newly assigned `run_id`." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# can work with a new instance\n", + "new_workflow_instance = JokeFlow()\n", + "wflow_ckptr.workflow = new_workflow_instance\n", + "\n", + "ckpt = checkpoints_right_after_generate_joke_step[0]\n", + "\n", + "handler = wflow_ckptr.run_from(checkpoint=ckpt)\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "'Analysis:\\nThis joke plays on the double meaning of the word \"rates,\" which can refer to both the cost of something and the passage of time. In this case, the joke suggests that chemists prefer nitrates because they are less expensive than day rates, implying that chemists are frugal or cost-conscious individuals.\\n\\nCritique:\\n- Clever wordplay: The joke relies on a clever play on words, which can be entertaining for those who appreciate puns and linguistic humor.\\n- Niche audience: The humor in this joke may be more appreciated by individuals with a background in chemistry or a specific interest in science, as the punchline relies on knowledge of chemical compounds.\\n- Lack of universal appeal: The joke may not resonate with a general audience who may not understand the reference to nitrates or the significance of their cost compared to day rates.\\n- Lack of depth: While the joke is amusing on a surface level, it may be considered somewhat shallow or simplistic compared to more nuanced or thought-provoking humor.\\n\\nOverall, the joke is a light-hearted play on words that may appeal to individuals with a specific interest in chemistry or wordplay. However, its niche appeal and lack of universal relevance may limit its effectiveness as a joke for a broader audience.'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {len(ckpts)} stored checkpoints\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has 2 stored checkpoints\n", + "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has 2 stored checkpoints\n", + "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has 2 stored checkpoints\n", + "Run: 9dccfda3-b5bf-4771-8293-efd3e3a275a6 has 1 stored checkpoints\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since we've executed from the checkpoint that represents the end of \"generate_joke\" step, there is only one additional checkpoint (i.e., that for the completion of step \"critique_joke\") that gets stored in the last partial run." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Specifying Which Steps To Checkpoint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default all steps of the attached workflow (excluding the \"_done\" step) will be checkpointed. You can see which steps are enabled for checkpointing via the `enabled_checkpoints` attribute." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "wflow_ckptr.enabled_checkpoints" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "{'critique_joke', 'generate_joke'}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To disable a step for checkpointing, we can use the `.disable_checkpoint()` method" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "wflow_ckptr.disable_checkpoint(step=\"critique_joke\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "handler = wflow_ckptr.run(topic=\"cars\")\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Analysis:\\nThis joke plays on the common stereotype that mechanics are overly critical and nitpicky when it comes to fixing cars. The humor comes from the unexpected twist of the car being the one to break up with the mechanic, rather than the other way around. The punchline is clever and plays on the idea of a relationship ending due to constant criticism.\\n\\nCritique:\\nOverall, this joke is light-hearted and easy to understand. It relies on a simple pun and doesn't require much thought to appreciate. However, the humor may be seen as somewhat predictable and not particularly original. The joke also perpetuates the stereotype of mechanics being overly critical, which may not sit well with some people in the automotive industry. Additionally, the joke may not be as universally funny as some other jokes, as it relies on a specific understanding of the relationship between cars and mechanics. Overall, while the joke is amusing, it may not be considered particularly groundbreaking or memorable.\"" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(\n", + " f\"Run: {run_id} has stored checkpoints for steps {[c.last_completed_step for c in ckpts]}\"\n", + " )" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", + "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", + "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", + "Run: 9dccfda3-b5bf-4771-8293-efd3e3a275a6 has stored checkpoints for steps ['critique_joke']\n", + "Run: 6390e2ce-63f4-44a3-8a75-64ccb765abfd has stored checkpoints for steps ['generate_joke']\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And we can turn checkpointing back on by using the `.enable_checkpoint()` method" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "wflow_ckptr.enable_checkpoint(step=\"critique_joke\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "handler = wflow_ckptr.run(topic=\"cars\")\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "'Analysis:\\nThis joke plays on the common stereotype that mechanics are overly critical and nitpicky when it comes to fixing cars. The humor comes from the unexpected twist of the car being the one to break up with the mechanic, rather than the other way around. The joke also cleverly uses the term \"nitpicking\" in a literal sense, as in picking at the nit (small details) of the car.\\n\\nCritique:\\nWhile the joke is clever and plays on a well-known stereotype, it may not be the most original or groundbreaking joke. The punchline is somewhat predictable and relies on a common trope about mechanics. Additionally, the joke may not be universally relatable or understood by all audiences, as it requires some knowledge of the stereotype about mechanics being nitpicky.\\n\\nOverall, the joke is light-hearted and humorous, but it may not be the most memorable or impactful joke due to its reliance on a common stereotype. It could be improved by adding a more unexpected or unique twist to the punchline.'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(\n", + " f\"Run: {run_id} has stored checkpoints for steps {[c.last_completed_step for c in ckpts]}\"\n", + " )" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", + "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", + "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", + "Run: 9dccfda3-b5bf-4771-8293-efd3e3a275a6 has stored checkpoints for steps ['critique_joke']\n", + "Run: 6390e2ce-63f4-44a3-8a75-64ccb765abfd has stored checkpoints for steps ['generate_joke']\n", + "Run: e3291623-6eb8-43c6-b102-dc6f88a42f4d has stored checkpoints for steps ['generate_joke', 'critique_joke']\n" + ] + } ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" } - ], - "source": [ - "handler = wflow_ckptr.run(topic=\"cars\")\n", - "await handler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 483eccdd-a035-42cc-b596-cd33d42938a7 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", - "Run: e112bca9-637c-4492-a8aa-926c302c99d4 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", - "Run: 7a59c918-90a3-47f8-a818-71e45897ae39 has stored checkpoints for steps ['generate_joke', 'critique_joke']\n", - "Run: 9dccfda3-b5bf-4771-8293-efd3e3a275a6 has stored checkpoints for steps ['critique_joke']\n", - "Run: 6390e2ce-63f4-44a3-8a75-64ccb765abfd has stored checkpoints for steps ['generate_joke']\n", - "Run: e3291623-6eb8-43c6-b102-dc6f88a42f4d has stored checkpoints for steps ['generate_joke', 'critique_joke']\n" - ] + ], + "metadata": { + "kernelspec": { + "display_name": "llama-index-core-nrCJU7vQ-py3.10", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(\n", - " f\"Run: {run_id} has stored checkpoints for steps {[c.last_completed_step for c in ckpts]}\"\n", - " )" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "llama-index-core-nrCJU7vQ-py3.10", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/docs/examples/workflow/citation_query_engine.ipynb b/docs/examples/workflow/citation_query_engine.ipynb index 542a309d90..1689571922 100644 --- a/docs/examples/workflow/citation_query_engine.ipynb +++ b/docs/examples/workflow/citation_query_engine.ipynb @@ -118,20 +118,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "from llama_index.core.schema import NodeWithScore\n", - "\n", - "\n", - "class RetrieverEvent(Event):\n", - " \"\"\"Result of running retrieval\"\"\"\n", - "\n", - " nodes: list[NodeWithScore]\n", - "\n", - "\n", - "class CreateCitationsEvent(Event):\n", - " \"\"\"Add citations to the nodes.\"\"\"\n", - "\n", - " nodes: list[NodeWithScore]" + "from workflows.events import Event\nfrom llama_index.core.schema import NodeWithScore\n\n\nclass RetrieverEvent(Event):\n \"\"\"Result of running retrieval\"\"\"\n\n nodes: list[NodeWithScore]\n\n\nclass CreateCitationsEvent(Event):\n \"\"\"Add citations to the nodes.\"\"\"\n\n nodes: list[NodeWithScore]" ] }, { @@ -223,118 +210,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core import SimpleDirectoryReader, VectorStoreIndex\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.embeddings.openai import OpenAIEmbedding\n", - "\n", - "from llama_index.core.schema import (\n", - " MetadataMode,\n", - " NodeWithScore,\n", - " TextNode,\n", - ")\n", - "\n", - "from llama_index.core.response_synthesizers import (\n", - " ResponseMode,\n", - " get_response_synthesizer,\n", - ")\n", - "\n", - "from typing import Union, List\n", - "from llama_index.core.node_parser import SentenceSplitter\n", - "\n", - "\n", - "class CitationQueryEngineWorkflow(Workflow):\n", - " @step\n", - " async def retrieve(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> Union[RetrieverEvent, None]:\n", - " \"Entry point for RAG, triggered by a StartEvent with `query`.\"\n", - " query = ev.get(\"query\")\n", - " if not query:\n", - " return None\n", - "\n", - " print(f\"Query the database with: {query}\")\n", - "\n", - " # store the query in the global context\n", - " await ctx.store.set(\"query\", query)\n", - "\n", - " if ev.index is None:\n", - " print(\"Index is empty, load some documents before querying!\")\n", - " return None\n", - "\n", - " retriever = ev.index.as_retriever(similarity_top_k=2)\n", - " nodes = retriever.retrieve(query)\n", - " print(f\"Retrieved {len(nodes)} nodes.\")\n", - " return RetrieverEvent(nodes=nodes)\n", - "\n", - " @step\n", - " async def create_citation_nodes(\n", - " self, ev: RetrieverEvent\n", - " ) -> CreateCitationsEvent:\n", - " \"\"\"\n", - " Modify retrieved nodes to create granular sources for citations.\n", - "\n", - " Takes a list of NodeWithScore objects and splits their content\n", - " into smaller chunks, creating new NodeWithScore objects for each chunk.\n", - " Each new node is labeled as a numbered source, allowing for more precise\n", - " citation in query results.\n", - "\n", - " Args:\n", - " nodes (List[NodeWithScore]): A list of NodeWithScore objects to be processed.\n", - "\n", - " Returns:\n", - " List[NodeWithScore]: A new list of NodeWithScore objects, where each object\n", - " represents a smaller chunk of the original nodes, labeled as a source.\n", - " \"\"\"\n", - " nodes = ev.nodes\n", - "\n", - " new_nodes: List[NodeWithScore] = []\n", - "\n", - " text_splitter = SentenceSplitter(\n", - " chunk_size=DEFAULT_CITATION_CHUNK_SIZE,\n", - " chunk_overlap=DEFAULT_CITATION_CHUNK_OVERLAP,\n", - " )\n", - "\n", - " for node in nodes:\n", - " text_chunks = text_splitter.split_text(\n", - " node.node.get_content(metadata_mode=MetadataMode.NONE)\n", - " )\n", - "\n", - " for text_chunk in text_chunks:\n", - " text = f\"Source {len(new_nodes)+1}:\\n{text_chunk}\\n\"\n", - "\n", - " new_node = NodeWithScore(\n", - " node=TextNode.parse_obj(node.node), score=node.score\n", - " )\n", - " new_node.node.text = text\n", - " new_nodes.append(new_node)\n", - " return CreateCitationsEvent(nodes=new_nodes)\n", - "\n", - " @step\n", - " async def synthesize(\n", - " self, ctx: Context, ev: CreateCitationsEvent\n", - " ) -> StopEvent:\n", - " \"\"\"Return a streaming response using the retrieved nodes.\"\"\"\n", - " llm = OpenAI(model=\"gpt-4o-mini\")\n", - " query = await ctx.store.get(\"query\", default=None)\n", - "\n", - " synthesizer = get_response_synthesizer(\n", - " llm=llm,\n", - " text_qa_template=CITATION_QA_TEMPLATE,\n", - " refine_template=CITATION_REFINE_TEMPLATE,\n", - " response_mode=ResponseMode.COMPACT,\n", - " use_async=True,\n", - " )\n", - "\n", - " response = await synthesizer.asynthesize(query, nodes=ev.nodes)\n", - " return StopEvent(result=response)" + "from llama_index.core import SimpleDirectoryReader, VectorStoreIndex\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.embeddings.openai import OpenAIEmbedding\n\nfrom llama_index.core.schema import (\n MetadataMode,\n NodeWithScore,\n TextNode,\n)\n\nfrom llama_index.core.response_synthesizers import (\n ResponseMode,\n get_response_synthesizer,\n)\n\nfrom typing import Union, List\nfrom llama_index.core.node_parser import SentenceSplitter\n\n\nclass CitationQueryEngineWorkflow(Workflow):\n @step\n async def retrieve(\n self, ctx: Context, ev: StartEvent\n ) -> Union[RetrieverEvent, None]:\n \"Entry point for RAG, triggered by a StartEvent with `query`.\"\n query = ev.get(\"query\")\n if not query:\n return None\n\n print(f\"Query the database with: {query}\")\n\n # store the query in the global context\n await ctx.store.set(\"query\", query)\n\n if ev.index is None:\n print(\"Index is empty, load some documents before querying!\")\n return None\n\n retriever = ev.index.as_retriever(similarity_top_k=2)\n nodes = retriever.retrieve(query)\n print(f\"Retrieved {len(nodes)} nodes.\")\n return RetrieverEvent(nodes=nodes)\n\n @step\n async def create_citation_nodes(\n self, ev: RetrieverEvent\n ) -> CreateCitationsEvent:\n \"\"\"\n Modify retrieved nodes to create granular sources for citations.\n\n Takes a list of NodeWithScore objects and splits their content\n into smaller chunks, creating new NodeWithScore objects for each chunk.\n Each new node is labeled as a numbered source, allowing for more precise\n citation in query results.\n\n Args:\n nodes (List[NodeWithScore]): A list of NodeWithScore objects to be processed.\n\n Returns:\n List[NodeWithScore]: A new list of NodeWithScore objects, where each object\n represents a smaller chunk of the original nodes, labeled as a source.\n \"\"\"\n nodes = ev.nodes\n\n new_nodes: List[NodeWithScore] = []\n\n text_splitter = SentenceSplitter(\n chunk_size=DEFAULT_CITATION_CHUNK_SIZE,\n chunk_overlap=DEFAULT_CITATION_CHUNK_OVERLAP,\n )\n\n for node in nodes:\n text_chunks = text_splitter.split_text(\n node.node.get_content(metadata_mode=MetadataMode.NONE)\n )\n\n for text_chunk in text_chunks:\n text = f\"Source {len(new_nodes)+1}:\\n{text_chunk}\\n\"\n\n new_node = NodeWithScore(\n node=TextNode.parse_obj(node.node), score=node.score\n )\n new_node.node.text = text\n new_nodes.append(new_node)\n return CreateCitationsEvent(nodes=new_nodes)\n\n @step\n async def synthesize(\n self, ctx: Context, ev: CreateCitationsEvent\n ) -> StopEvent:\n \"\"\"Return a streaming response using the retrieved nodes.\"\"\"\n llm = OpenAI(model=\"gpt-4o-mini\")\n query = await ctx.store.get(\"query\", default=None)\n\n synthesizer = get_response_synthesizer(\n llm=llm,\n text_qa_template=CITATION_QA_TEMPLATE,\n refine_template=CITATION_REFINE_TEMPLATE,\n response_mode=ResponseMode.COMPACT,\n use_async=True,\n )\n\n response = await synthesizer.asynthesize(query, nodes=ev.nodes)\n return StopEvent(result=response)" ] }, { @@ -521,4 +397,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/corrective_rag_pack.ipynb b/docs/examples/workflow/corrective_rag_pack.ipynb index 1edeef2e43..a005de11b0 100644 --- a/docs/examples/workflow/corrective_rag_pack.ipynb +++ b/docs/examples/workflow/corrective_rag_pack.ipynb @@ -107,39 +107,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "from llama_index.core.schema import NodeWithScore\n", - "\n", - "\n", - "class PrepEvent(Event):\n", - " \"\"\"Prep event (prepares for retrieval).\"\"\"\n", - "\n", - " pass\n", - "\n", - "\n", - "class RetrieveEvent(Event):\n", - " \"\"\"Retrieve event (gets retrieved nodes).\"\"\"\n", - "\n", - " retrieved_nodes: list[NodeWithScore]\n", - "\n", - "\n", - "class RelevanceEvalEvent(Event):\n", - " \"\"\"Relevance evaluation event (gets results of relevance evaluation).\"\"\"\n", - "\n", - " relevant_results: list[str]\n", - "\n", - "\n", - "class TextExtractEvent(Event):\n", - " \"\"\"Text extract event. Extracts relevant text and concatenates.\"\"\"\n", - "\n", - " relevant_text: str\n", - "\n", - "\n", - "class QueryEvent(Event):\n", - " \"\"\"Query event. Queries given relevant text and search text.\"\"\"\n", - "\n", - " relevant_text: str\n", - " search_text: str" + "from workflows.events import Event\nfrom llama_index.core.schema import NodeWithScore\n\n\nclass PrepEvent(Event):\n \"\"\"Prep event (prepares for retrieval).\"\"\"\n\n pass\n\n\nclass RetrieveEvent(Event):\n \"\"\"Retrieve event (gets retrieved nodes).\"\"\"\n\n retrieved_nodes: list[NodeWithScore]\n\n\nclass RelevanceEvalEvent(Event):\n \"\"\"Relevance evaluation event (gets results of relevance evaluation).\"\"\"\n\n relevant_results: list[str]\n\n\nclass TextExtractEvent(Event):\n \"\"\"Text extract event. Extracts relevant text and concatenates.\"\"\"\n\n relevant_text: str\n\n\nclass QueryEvent(Event):\n \"\"\"Query event. Queries given relevant text and search text.\"\"\"\n\n relevant_text: str\n search_text: str" ] }, { @@ -155,199 +123,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " step,\n", - " Context,\n", - " StartEvent,\n", - " StopEvent,\n", - ")\n", - "from llama_index.core import (\n", - " VectorStoreIndex,\n", - " Document,\n", - " PromptTemplate,\n", - " SummaryIndex,\n", - ")\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.tools.tavily_research.base import TavilyToolSpec\n", - "from llama_index.core.base.base_retriever import BaseRetriever\n", - "\n", - "DEFAULT_RELEVANCY_PROMPT_TEMPLATE = PromptTemplate(\n", - " template=\"\"\"As a grader, your task is to evaluate the relevance of a document retrieved in response to a user's question.\n", - "\n", - " Retrieved Document:\n", - " -------------------\n", - " {context_str}\n", - "\n", - " User Question:\n", - " --------------\n", - " {query_str}\n", - "\n", - " Evaluation Criteria:\n", - " - Consider whether the document contains keywords or topics related to the user's question.\n", - " - The evaluation should not be overly stringent; the primary objective is to identify and filter out clearly irrelevant retrievals.\n", - "\n", - " Decision:\n", - " - Assign a binary score to indicate the document's relevance.\n", - " - Use 'yes' if the document is relevant to the question, or 'no' if it is not.\n", - "\n", - " Please provide your binary score ('yes' or 'no') below to indicate the document's relevance to the user question.\"\"\"\n", - ")\n", - "\n", - "DEFAULT_TRANSFORM_QUERY_TEMPLATE = PromptTemplate(\n", - " template=\"\"\"Your task is to refine a query to ensure it is highly effective for retrieving relevant search results. \\n\n", - " Analyze the given input to grasp the core semantic intent or meaning. \\n\n", - " Original Query:\n", - " \\n ------- \\n\n", - " {query_str}\n", - " \\n ------- \\n\n", - " Your goal is to rephrase or enhance this query to improve its search performance. Ensure the revised query is concise and directly aligned with the intended search objective. \\n\n", - " Respond with the optimized query only:\"\"\"\n", - ")\n", - "\n", - "\n", - "class CorrectiveRAGWorkflow(Workflow):\n", - " @step\n", - " async def ingest(self, ctx: Context, ev: StartEvent) -> StopEvent | None:\n", - " \"\"\"Ingest step (for ingesting docs and initializing index).\"\"\"\n", - " documents: list[Document] | None = ev.get(\"documents\")\n", - "\n", - " if documents is None:\n", - " return None\n", - "\n", - " index = VectorStoreIndex.from_documents(documents)\n", - "\n", - " return StopEvent(result=index)\n", - "\n", - " @step\n", - " async def prepare_for_retrieval(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> PrepEvent | None:\n", - " \"\"\"Prepare for retrieval.\"\"\"\n", - "\n", - " query_str: str | None = ev.get(\"query_str\")\n", - " retriever_kwargs: dict | None = ev.get(\"retriever_kwargs\", {})\n", - "\n", - " if query_str is None:\n", - " return None\n", - "\n", - " tavily_ai_apikey: str | None = ev.get(\"tavily_ai_apikey\")\n", - " index = ev.get(\"index\")\n", - "\n", - " llm = OpenAI(model=\"gpt-4\")\n", - "\n", - " await ctx.store.set(\"llm\", llm)\n", - " await ctx.store.set(\"index\", index)\n", - " await ctx.store.set(\n", - " \"tavily_tool\", TavilyToolSpec(api_key=tavily_ai_apikey)\n", - " )\n", - "\n", - " await ctx.store.set(\"query_str\", query_str)\n", - " await ctx.store.set(\"retriever_kwargs\", retriever_kwargs)\n", - "\n", - " return PrepEvent()\n", - "\n", - " @step\n", - " async def retrieve(\n", - " self, ctx: Context, ev: PrepEvent\n", - " ) -> RetrieveEvent | None:\n", - " \"\"\"Retrieve the relevant nodes for the query.\"\"\"\n", - " query_str = await ctx.store.get(\"query_str\")\n", - " retriever_kwargs = await ctx.store.get(\"retriever_kwargs\")\n", - "\n", - " if query_str is None:\n", - " return None\n", - "\n", - " index = await ctx.store.get(\"index\", default=None)\n", - " tavily_tool = await ctx.store.get(\"tavily_tool\", default=None)\n", - " if not (index or tavily_tool):\n", - " raise ValueError(\n", - " \"Index and tavily tool must be constructed. Run with 'documents' and 'tavily_ai_apikey' params first.\"\n", - " )\n", - "\n", - " retriever: BaseRetriever = index.as_retriever(**retriever_kwargs)\n", - " result = retriever.retrieve(query_str)\n", - " await ctx.store.set(\"retrieved_nodes\", result)\n", - " await ctx.store.set(\"query_str\", query_str)\n", - " return RetrieveEvent(retrieved_nodes=result)\n", - "\n", - " @step\n", - " async def eval_relevance(\n", - " self, ctx: Context, ev: RetrieveEvent\n", - " ) -> RelevanceEvalEvent:\n", - " \"\"\"Evaluate relevancy of retrieved documents with the query.\"\"\"\n", - " retrieved_nodes = ev.retrieved_nodes\n", - " query_str = await ctx.store.get(\"query_str\")\n", - "\n", - " relevancy_results = []\n", - " for node in retrieved_nodes:\n", - " llm = await ctx.store.get(\"llm\")\n", - " resp = await llm.acomplete(\n", - " DEFAULT_RELEVANCY_PROMPT_TEMPLATE.format(\n", - " context_str=node.text, query_str=query_str\n", - " )\n", - " )\n", - " relevancy_results.append(resp.text.lower().strip())\n", - "\n", - " await ctx.store.set(\"relevancy_results\", relevancy_results)\n", - " return RelevanceEvalEvent(relevant_results=relevancy_results)\n", - "\n", - " @step\n", - " async def extract_relevant_texts(\n", - " self, ctx: Context, ev: RelevanceEvalEvent\n", - " ) -> TextExtractEvent:\n", - " \"\"\"Extract relevant texts from retrieved documents.\"\"\"\n", - " retrieved_nodes = await ctx.store.get(\"retrieved_nodes\")\n", - " relevancy_results = ev.relevant_results\n", - "\n", - " relevant_texts = [\n", - " retrieved_nodes[i].text\n", - " for i, result in enumerate(relevancy_results)\n", - " if result == \"yes\"\n", - " ]\n", - "\n", - " result = \"\\n\".join(relevant_texts)\n", - " return TextExtractEvent(relevant_text=result)\n", - "\n", - " @step\n", - " async def transform_query(\n", - " self, ctx: Context, ev: TextExtractEvent\n", - " ) -> QueryEvent:\n", - " \"\"\"Search the transformed query with Tavily API.\"\"\"\n", - " relevant_text = ev.relevant_text\n", - " relevancy_results = await ctx.store.get(\"relevancy_results\")\n", - " query_str = await ctx.store.get(\"query_str\")\n", - "\n", - " # If any document is found irrelevant, transform the query string for better search results.\n", - " if \"no\" in relevancy_results:\n", - " llm = await ctx.store.get(\"llm\")\n", - " resp = await llm.acomplete(\n", - " DEFAULT_TRANSFORM_QUERY_TEMPLATE.format(query_str=query_str)\n", - " )\n", - " transformed_query_str = resp.text\n", - " # Conduct a search with the transformed query string and collect the results.\n", - " tavily_tool = await ctx.store.get(\"tavily_tool\")\n", - " search_results = tavily_tool.search(\n", - " transformed_query_str, max_results=5\n", - " )\n", - " search_text = \"\\n\".join([result.text for result in search_results])\n", - " else:\n", - " search_text = \"\"\n", - "\n", - " return QueryEvent(relevant_text=relevant_text, search_text=search_text)\n", - "\n", - " @step\n", - " async def query_result(self, ctx: Context, ev: QueryEvent) -> StopEvent:\n", - " \"\"\"Get result with relevant text.\"\"\"\n", - " relevant_text = ev.relevant_text\n", - " search_text = ev.search_text\n", - " query_str = await ctx.store.get(\"query_str\")\n", - "\n", - " documents = [Document(text=relevant_text + \"\\n\" + search_text)]\n", - " index = SummaryIndex.from_documents(documents)\n", - " query_engine = index.as_query_engine()\n", - " result = query_engine.query(query_str)\n", - " return StopEvent(result=result)" + "from workflows import Workflow, step, Context\nfrom workflows.events import StartEvent, StopEvent\nfrom llama_index.core import (\n VectorStoreIndex,\n Document,\n PromptTemplate,\n SummaryIndex,\n)\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.tools.tavily_research.base import TavilyToolSpec\nfrom llama_index.core.base.base_retriever import BaseRetriever\n\nDEFAULT_RELEVANCY_PROMPT_TEMPLATE = PromptTemplate(\n template=\"\"\"As a grader, your task is to evaluate the relevance of a document retrieved in response to a user's question.\n\n Retrieved Document:\n -------------------\n {context_str}\n\n User Question:\n --------------\n {query_str}\n\n Evaluation Criteria:\n - Consider whether the document contains keywords or topics related to the user's question.\n - The evaluation should not be overly stringent; the primary objective is to identify and filter out clearly irrelevant retrievals.\n\n Decision:\n - Assign a binary score to indicate the document's relevance.\n - Use 'yes' if the document is relevant to the question, or 'no' if it is not.\n\n Please provide your binary score ('yes' or 'no') below to indicate the document's relevance to the user question.\"\"\"\n)\n\nDEFAULT_TRANSFORM_QUERY_TEMPLATE = PromptTemplate(\n template=\"\"\"Your task is to refine a query to ensure it is highly effective for retrieving relevant search results. \\n\n Analyze the given input to grasp the core semantic intent or meaning. \\n\n Original Query:\n \\n ------- \\n\n {query_str}\n \\n ------- \\n\n Your goal is to rephrase or enhance this query to improve its search performance. Ensure the revised query is concise and directly aligned with the intended search objective. \\n\n Respond with the optimized query only:\"\"\"\n)\n\n\nclass CorrectiveRAGWorkflow(Workflow):\n @step\n async def ingest(self, ctx: Context, ev: StartEvent) -> StopEvent | None:\n \"\"\"Ingest step (for ingesting docs and initializing index).\"\"\"\n documents: list[Document] | None = ev.get(\"documents\")\n\n if documents is None:\n return None\n\n index = VectorStoreIndex.from_documents(documents)\n\n return StopEvent(result=index)\n\n @step\n async def prepare_for_retrieval(\n self, ctx: Context, ev: StartEvent\n ) -> PrepEvent | None:\n \"\"\"Prepare for retrieval.\"\"\"\n\n query_str: str | None = ev.get(\"query_str\")\n retriever_kwargs: dict | None = ev.get(\"retriever_kwargs\", {})\n\n if query_str is None:\n return None\n\n tavily_ai_apikey: str | None = ev.get(\"tavily_ai_apikey\")\n index = ev.get(\"index\")\n\n llm = OpenAI(model=\"gpt-4\")\n\n await ctx.store.set(\"llm\", llm)\n await ctx.store.set(\"index\", index)\n await ctx.store.set(\n \"tavily_tool\", TavilyToolSpec(api_key=tavily_ai_apikey)\n )\n\n await ctx.store.set(\"query_str\", query_str)\n await ctx.store.set(\"retriever_kwargs\", retriever_kwargs)\n\n return PrepEvent()\n\n @step\n async def retrieve(\n self, ctx: Context, ev: PrepEvent\n ) -> RetrieveEvent | None:\n \"\"\"Retrieve the relevant nodes for the query.\"\"\"\n query_str = await ctx.store.get(\"query_str\")\n retriever_kwargs = await ctx.store.get(\"retriever_kwargs\")\n\n if query_str is None:\n return None\n\n index = await ctx.store.get(\"index\", default=None)\n tavily_tool = await ctx.store.get(\"tavily_tool\", default=None)\n if not (index or tavily_tool):\n raise ValueError(\n \"Index and tavily tool must be constructed. Run with 'documents' and 'tavily_ai_apikey' params first.\"\n )\n\n retriever: BaseRetriever = index.as_retriever(**retriever_kwargs)\n result = retriever.retrieve(query_str)\n await ctx.store.set(\"retrieved_nodes\", result)\n await ctx.store.set(\"query_str\", query_str)\n return RetrieveEvent(retrieved_nodes=result)\n\n @step\n async def eval_relevance(\n self, ctx: Context, ev: RetrieveEvent\n ) -> RelevanceEvalEvent:\n \"\"\"Evaluate relevancy of retrieved documents with the query.\"\"\"\n retrieved_nodes = ev.retrieved_nodes\n query_str = await ctx.store.get(\"query_str\")\n\n relevancy_results = []\n for node in retrieved_nodes:\n llm = await ctx.store.get(\"llm\")\n resp = await llm.acomplete(\n DEFAULT_RELEVANCY_PROMPT_TEMPLATE.format(\n context_str=node.text, query_str=query_str\n )\n )\n relevancy_results.append(resp.text.lower().strip())\n\n await ctx.store.set(\"relevancy_results\", relevancy_results)\n return RelevanceEvalEvent(relevant_results=relevancy_results)\n\n @step\n async def extract_relevant_texts(\n self, ctx: Context, ev: RelevanceEvalEvent\n ) -> TextExtractEvent:\n \"\"\"Extract relevant texts from retrieved documents.\"\"\"\n retrieved_nodes = await ctx.store.get(\"retrieved_nodes\")\n relevancy_results = ev.relevant_results\n\n relevant_texts = [\n retrieved_nodes[i].text\n for i, result in enumerate(relevancy_results)\n if result == \"yes\"\n ]\n\n result = \"\\n\".join(relevant_texts)\n return TextExtractEvent(relevant_text=result)\n\n @step\n async def transform_query(\n self, ctx: Context, ev: TextExtractEvent\n ) -> QueryEvent:\n \"\"\"Search the transformed query with Tavily API.\"\"\"\n relevant_text = ev.relevant_text\n relevancy_results = await ctx.store.get(\"relevancy_results\")\n query_str = await ctx.store.get(\"query_str\")\n\n # If any document is found irrelevant, transform the query string for better search results.\n if \"no\" in relevancy_results:\n llm = await ctx.store.get(\"llm\")\n resp = await llm.acomplete(\n DEFAULT_TRANSFORM_QUERY_TEMPLATE.format(query_str=query_str)\n )\n transformed_query_str = resp.text\n # Conduct a search with the transformed query string and collect the results.\n tavily_tool = await ctx.store.get(\"tavily_tool\")\n search_results = tavily_tool.search(\n transformed_query_str, max_results=5\n )\n search_text = \"\\n\".join([result.text for result in search_results])\n else:\n search_text = \"\"\n\n return QueryEvent(relevant_text=relevant_text, search_text=search_text)\n\n @step\n async def query_result(self, ctx: Context, ev: QueryEvent) -> StopEvent:\n \"\"\"Get result with relevant text.\"\"\"\n relevant_text = ev.relevant_text\n search_text = ev.search_text\n query_str = await ctx.store.get(\"query_str\")\n\n documents = [Document(text=relevant_text + \"\\n\" + search_text)]\n index = SummaryIndex.from_documents(documents)\n query_engine = index.as_query_engine()\n result = query_engine.query(query_str)\n return StopEvent(result=result)" ] }, { @@ -445,4 +221,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/function_calling_agent.ipynb b/docs/examples/workflow/function_calling_agent.ipynb index e989966f05..b400157da5 100644 --- a/docs/examples/workflow/function_calling_agent.ipynb +++ b/docs/examples/workflow/function_calling_agent.ipynb @@ -1,498 +1,493 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Workflow for a Function Calling Agent\n", - "\n", - "This notebook walks through setting up a `Workflow` to construct a function calling agent from scratch.\n", - "\n", - "Function calling agents work by using an LLM that supports tools/functions in its API (OpenAI, Ollama, Anthropic, etc.) to call functions an use tools.\n", - "\n", - "Our workflow will be stateful with memory, and will be able to call the LLM to select tools and process incoming user messages." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install -U llama-index" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### [Optional] Set up observability with Llamatrace\n", - "\n", - "Set up tracing to visualize each step in the workflow." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since workflows are async first, this all runs fine in a notebook. If you were running in your own code, you would want to use `asyncio.run()` to start an async event loop if one isn't already running.\n", - "\n", - "```python\n", - "async def main():\n", - " \n", - "\n", - "if __name__ == \"__main__\":\n", - " import asyncio\n", - " asyncio.run(main())\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Designing the Workflow\n", - "\n", - "An agent consists of several steps\n", - "1. Handling the latest incoming user message, including adding to memory and getting the latest chat history\n", - "2. Calling the LLM with tools + chat history\n", - "3. Parsing out tool calls (if any)\n", - "4. If there are tool calls, call them, and loop until there are none\n", - "5. When there is no tool calls, return the LLM response\n", - "\n", - "### The Workflow Events\n", - "\n", - "To handle these steps, we need to define a few events:\n", - "1. An event to handle new messages and prepare the chat history\n", - "2. An event to handle streaming responses\n", - "3. An event to trigger tool calls\n", - "4. An event to handle the results of tool calls\n", - "\n", - "The other steps will use the built-in `StartEvent` and `StopEvent` events." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.llms import ChatMessage\n", - "from llama_index.core.tools import ToolSelection, ToolOutput\n", - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class InputEvent(Event):\n", - " input: list[ChatMessage]\n", - "\n", - "\n", - "class StreamEvent(Event):\n", - " delta: str\n", - "\n", - "\n", - "class ToolCallEvent(Event):\n", - " tool_calls: list[ToolSelection]\n", - "\n", - "\n", - "class FunctionOutputEvent(Event):\n", - " output: ToolOutput" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### The Workflow Itself\n", - "\n", - "With our events defined, we can construct our workflow and steps. \n", - "\n", - "Note that the workflow automatically validates itself using type annotations, so the type annotations on our steps are very helpful!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Any, List\n", - "\n", - "from llama_index.core.llms.function_calling import FunctionCallingLLM\n", - "from llama_index.core.memory import ChatMemoryBuffer\n", - "from llama_index.core.tools.types import BaseTool\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "class FuncationCallingAgent(Workflow):\n", - " def __init__(\n", - " self,\n", - " *args: Any,\n", - " llm: FunctionCallingLLM | None = None,\n", - " tools: List[BaseTool] | None = None,\n", - " **kwargs: Any,\n", - " ) -> None:\n", - " super().__init__(*args, **kwargs)\n", - " self.tools = tools or []\n", - "\n", - " self.llm = llm or OpenAI()\n", - " assert self.llm.metadata.is_function_calling_model\n", - "\n", - " @step\n", - " async def prepare_chat_history(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> InputEvent:\n", - " # clear sources\n", - " await ctx.store.set(\"sources\", [])\n", - "\n", - " # check if memory is setup\n", - " memory = await ctx.store.get(\"memory\", default=None)\n", - " if not memory:\n", - " memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\n", - "\n", - " # get user input\n", - " user_input = ev.input\n", - " user_msg = ChatMessage(role=\"user\", content=user_input)\n", - " memory.put(user_msg)\n", - "\n", - " # get chat history\n", - " chat_history = memory.get()\n", - "\n", - " # update context\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " return InputEvent(input=chat_history)\n", - "\n", - " @step\n", - " async def handle_llm_input(\n", - " self, ctx: Context, ev: InputEvent\n", - " ) -> ToolCallEvent | StopEvent:\n", - " chat_history = ev.input\n", - "\n", - " # stream the response\n", - " response_stream = await self.llm.astream_chat_with_tools(\n", - " self.tools, chat_history=chat_history\n", - " )\n", - " async for response in response_stream:\n", - " ctx.write_event_to_stream(StreamEvent(delta=response.delta or \"\"))\n", - "\n", - " # save the final response, which should have all content\n", - " memory = await ctx.store.get(\"memory\")\n", - " memory.put(response.message)\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " # get tool calls\n", - " tool_calls = self.llm.get_tool_calls_from_response(\n", - " response, error_on_no_tool_call=False\n", - " )\n", - "\n", - " if not tool_calls:\n", - " sources = await ctx.store.get(\"sources\", default=[])\n", - " return StopEvent(\n", - " result={\"response\": response, \"sources\": [*sources]}\n", - " )\n", - " else:\n", - " return ToolCallEvent(tool_calls=tool_calls)\n", - "\n", - " @step\n", - " async def handle_tool_calls(\n", - " self, ctx: Context, ev: ToolCallEvent\n", - " ) -> InputEvent:\n", - " tool_calls = ev.tool_calls\n", - " tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}\n", - "\n", - " tool_msgs = []\n", - " sources = await ctx.store.get(\"sources\", default=[])\n", - "\n", - " # call tools -- safely!\n", - " for tool_call in tool_calls:\n", - " tool = tools_by_name.get(tool_call.tool_name)\n", - " additional_kwargs = {\n", - " \"tool_call_id\": tool_call.tool_id,\n", - " \"name\": tool.metadata.get_name(),\n", - " }\n", - " if not tool:\n", - " tool_msgs.append(\n", - " ChatMessage(\n", - " role=\"tool\",\n", - " content=f\"Tool {tool_call.tool_name} does not exist\",\n", - " additional_kwargs=additional_kwargs,\n", - " )\n", - " )\n", - " continue\n", - "\n", - " try:\n", - " tool_output = tool(**tool_call.tool_kwargs)\n", - " sources.append(tool_output)\n", - " tool_msgs.append(\n", - " ChatMessage(\n", - " role=\"tool\",\n", - " content=tool_output.content,\n", - " additional_kwargs=additional_kwargs,\n", - " )\n", - " )\n", - " except Exception as e:\n", - " tool_msgs.append(\n", - " ChatMessage(\n", - " role=\"tool\",\n", - " content=f\"Encountered error in tool call: {e}\",\n", - " additional_kwargs=additional_kwargs,\n", - " )\n", - " )\n", - "\n", - " # update memory\n", - " memory = await ctx.store.get(\"memory\")\n", - " for msg in tool_msgs:\n", - " memory.put(msg)\n", - "\n", - " await ctx.store.set(\"sources\", sources)\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " chat_history = memory.get()\n", - " return InputEvent(input=chat_history)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And thats it! Let's explore the workflow we wrote a bit.\n", - "\n", - "`prepare_chat_history()`:\n", - "This is our main entry point. It handles adding the user message to memory, and uses the memory to get the latest chat history. It returns an `InputEvent`.\n", - "\n", - "`handle_llm_input()`:\n", - "Triggered by an `InputEvent`, it uses the chat history and tools to prompt the llm. If tool calls are found, a `ToolCallEvent` is emitted. Otherwise, we say the workflow is done an emit a `StopEvent`\n", - "\n", - "`handle_tool_calls()`:\n", - "Triggered by `ToolCallEvent`, it calls tools with error handling and returns tool outputs. This event triggers a **loop** since it emits an `InputEvent`, which takes us back to `handle_llm_input()`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run the Workflow!\n", - "\n", - "**NOTE:** With loops, we need to be mindful of runtime. Here, we set a timeout of 120s." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n" - ] - } - ], - "source": [ - "from llama_index.core.tools import FunctionTool\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "def add(x: int, y: int) -> int:\n", - " \"\"\"Useful function to add two numbers.\"\"\"\n", - " return x + y\n", - "\n", - "\n", - "def multiply(x: int, y: int) -> int:\n", - " \"\"\"Useful function to multiply two numbers.\"\"\"\n", - " return x * y\n", - "\n", - "\n", - "tools = [\n", - " FunctionTool.from_defaults(add),\n", - " FunctionTool.from_defaults(multiply),\n", - "]\n", - "\n", - "agent = FuncationCallingAgent(\n", - " llm=OpenAI(model=\"gpt-4o-mini\"), tools=tools, timeout=120, verbose=True\n", - ")\n", - "\n", - "ret = await agent.run(input=\"Hello!\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Workflow for a Function Calling Agent\n", + "\n", + "This notebook walks through setting up a `Workflow` to construct a function calling agent from scratch.\n", + "\n", + "Function calling agents work by using an LLM that supports tools/functions in its API (OpenAI, Ollama, Anthropic, etc.) to call functions an use tools.\n", + "\n", + "Our workflow will be stateful with memory, and will be able to call the LLM to select tools and process incoming user messages." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "assistant: Hello! How can I assist you today?\n" - ] - } - ], - "source": [ - "print(ret[\"response\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "!pip install -U llama-index" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event ToolCallEvent\n", - "Running step handle_tool_calls\n", - "Step handle_tool_calls produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event ToolCallEvent\n", - "Running step handle_tool_calls\n", - "Step handle_tool_calls produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n" - ] - } - ], - "source": [ - "ret = await agent.run(input=\"What is (2123 + 2321) * 312?\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Chat History\n", - "\n", - "By default, the workflow is creating a fresh `Context` for each run. This means that the chat history is not preserved between runs. However, we can pass our own `Context` to the workflow to preserve chat history." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n", - "assistant: Hello, Logan! How can I assist you today?\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n", - "assistant: Your name is Logan.\n" - ] - } - ], - "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "ret = await agent.run(input=\"Hello! My name is Logan.\", ctx=ctx)\n", - "print(ret[\"response\"])\n", - "\n", - "ret = await agent.run(input=\"What is my name?\", ctx=ctx)\n", - "print(ret[\"response\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Streaming\n", - "\n", - "Using the `handler` returned from the `.run()` method, we can also access the streaming events." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### [Optional] Set up observability with Llamatrace\n", + "\n", + "Set up tracing to visualize each step in the workflow." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since workflows are async first, this all runs fine in a notebook. If you were running in your own code, you would want to use `asyncio.run()` to start an async event loop if one isn't already running.\n", + "\n", + "```python\n", + "async def main():\n", + " \n", + "\n", + "if __name__ == \"__main__\":\n", + " import asyncio\n", + " asyncio.run(main())\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Designing the Workflow\n", + "\n", + "An agent consists of several steps\n", + "1. Handling the latest incoming user message, including adding to memory and getting the latest chat history\n", + "2. Calling the LLM with tools + chat history\n", + "3. Parsing out tool calls (if any)\n", + "4. If there are tool calls, call them, and loop until there are none\n", + "5. When there is no tool calls, return the LLM response\n", + "\n", + "### The Workflow Events\n", + "\n", + "To handle these steps, we need to define a few events:\n", + "1. An event to handle new messages and prepare the chat history\n", + "2. An event to handle streaming responses\n", + "3. An event to trigger tool calls\n", + "4. An event to handle the results of tool calls\n", + "\n", + "The other steps will use the built-in `StartEvent` and `StopEvent` events." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.llms import ChatMessage\n", + "from llama_index.core.tools import ToolSelection, ToolOutput\n", + "from workflows.events import Event\n", + "\n", + "\n", + "class InputEvent(Event):\n", + " input: list[ChatMessage]\n", + "\n", + "\n", + "class StreamEvent(Event):\n", + " delta: str\n", + "\n", + "\n", + "class ToolCallEvent(Event):\n", + " tool_calls: list[ToolSelection]\n", + "\n", + "\n", + "class FunctionOutputEvent(Event):\n", + " output: ToolOutput" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Workflow Itself\n", + "\n", + "With our events defined, we can construct our workflow and steps. \n", + "\n", + "Note that the workflow automatically validates itself using type annotations, so the type annotations on our steps are very helpful!" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from typing import Any, List\n", + "\n", + "from llama_index.core.llms.function_calling import FunctionCallingLLM\n", + "from llama_index.core.memory import ChatMemoryBuffer\n", + "from llama_index.core.tools.types import BaseTool\n", + "from workflows import Context, Workflow, step\n", + "from workflows.events import StartEvent, StopEvent\n", + "from llama_index.llms.openai import OpenAI\n", + "\n", + "\n", + "class FuncationCallingAgent(Workflow):\n", + " def __init__(\n", + " self,\n", + " *args: Any,\n", + " llm: FunctionCallingLLM | None = None,\n", + " tools: List[BaseTool] | None = None,\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " super().__init__(*args, **kwargs)\n", + " self.tools = tools or []\n", + "\n", + " self.llm = llm or OpenAI()\n", + " assert self.llm.metadata.is_function_calling_model\n", + "\n", + " @step\n", + " async def prepare_chat_history(\n", + " self, ctx: Context, ev: StartEvent\n", + " ) -> InputEvent:\n", + " # clear sources\n", + " await ctx.store.set(\"sources\", [])\n", + "\n", + " # check if memory is setup\n", + " memory = await ctx.store.get(\"memory\", default=None)\n", + " if not memory:\n", + " memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\n", + "\n", + " # get user input\n", + " user_input = ev.input\n", + " user_msg = ChatMessage(role=\"user\", content=user_input)\n", + " memory.put(user_msg)\n", + "\n", + " # get chat history\n", + " chat_history = memory.get()\n", + "\n", + " # update context\n", + " await ctx.store.set(\"memory\", memory)\n", + "\n", + " return InputEvent(input=chat_history)\n", + "\n", + " @step\n", + " async def handle_llm_input(\n", + " self, ctx: Context, ev: InputEvent\n", + " ) -> ToolCallEvent | StopEvent:\n", + " chat_history = ev.input\n", + "\n", + " # stream the response\n", + " response_stream = await self.llm.astream_chat_with_tools(\n", + " self.tools, chat_history=chat_history\n", + " )\n", + " async for response in response_stream:\n", + " ctx.write_event_to_stream(StreamEvent(delta=response.delta or \"\"))\n", + "\n", + " # save the final response, which should have all content\n", + " memory = await ctx.store.get(\"memory\")\n", + " memory.put(response.message)\n", + " await ctx.store.set(\"memory\", memory)\n", + "\n", + " # get tool calls\n", + " tool_calls = self.llm.get_tool_calls_from_response(\n", + " response, error_on_no_tool_call=False\n", + " )\n", + "\n", + " if not tool_calls:\n", + " sources = await ctx.store.get(\"sources\", default=[])\n", + " return StopEvent(\n", + " result={\"response\": response, \"sources\": [*sources]}\n", + " )\n", + " else:\n", + " return ToolCallEvent(tool_calls=tool_calls)\n", + "\n", + " @step\n", + " async def handle_tool_calls(\n", + " self, ctx: Context, ev: ToolCallEvent\n", + " ) -> InputEvent:\n", + " tool_calls = ev.tool_calls\n", + " tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}\n", + "\n", + " tool_msgs = []\n", + " sources = await ctx.store.get(\"sources\", default=[])\n", + "\n", + " # call tools -- safely!\n", + " for tool_call in tool_calls:\n", + " tool = tools_by_name.get(tool_call.tool_name)\n", + " additional_kwargs = {\n", + " \"tool_call_id\": tool_call.tool_id,\n", + " \"name\": tool.metadata.get_name(),\n", + " }\n", + " if not tool:\n", + " tool_msgs.append(\n", + " ChatMessage(\n", + " role=\"tool\",\n", + " content=f\"Tool {tool_call.tool_name} does not exist\",\n", + " additional_kwargs=additional_kwargs,\n", + " )\n", + " )\n", + " continue\n", + "\n", + " try:\n", + " tool_output = tool(**tool_call.tool_kwargs)\n", + " sources.append(tool_output)\n", + " tool_msgs.append(\n", + " ChatMessage(\n", + " role=\"tool\",\n", + " content=tool_output.content,\n", + " additional_kwargs=additional_kwargs,\n", + " )\n", + " )\n", + " except Exception as e:\n", + " tool_msgs.append(\n", + " ChatMessage(\n", + " role=\"tool\",\n", + " content=f\"Encountered error in tool call: {e}\",\n", + " additional_kwargs=additional_kwargs,\n", + " )\n", + " )\n", + "\n", + " # update memory\n", + " memory = await ctx.store.get(\"memory\")\n", + " for msg in tool_msgs:\n", + " memory.put(msg)\n", + "\n", + " await ctx.store.set(\"sources\", sources)\n", + " await ctx.store.set(\"memory\", memory)\n", + "\n", + " chat_history = memory.get()\n", + " return InputEvent(input=chat_history)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And thats it! Let's explore the workflow we wrote a bit.\n", + "\n", + "`prepare_chat_history()`:\n", + "This is our main entry point. It handles adding the user message to memory, and uses the memory to get the latest chat history. It returns an `InputEvent`.\n", + "\n", + "`handle_llm_input()`:\n", + "Triggered by an `InputEvent`, it uses the chat history and tools to prompt the llm. If tool calls are found, a `ToolCallEvent` is emitted. Otherwise, we say the workflow is done an emit a `StopEvent`\n", + "\n", + "`handle_tool_calls()`:\n", + "Triggered by `ToolCallEvent`, it calls tools with error handling and returns tool outputs. This event triggers a **loop** since it emits an `InputEvent`, which takes us back to `handle_llm_input()`" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Once upon a time in a quaint little village, there lived a curious cat named Whiskers. Whiskers was no ordinary cat; he had a beautiful coat of orange and white fur that shimmered in the sunlight, and his emerald green eyes sparkled with mischief.\n", - "\n", - "Every day, Whiskers would explore the village, visiting the bakery for a whiff of freshly baked bread and the flower shop to sniff the colorful blooms. The villagers adored him, often leaving out little treats for their favorite feline.\n", - "\n", - "One sunny afternoon, while wandering near the edge of the village, Whiskers stumbled upon a hidden path that led into the woods. His curiosity piqued, he decided to follow the path, which was lined with tall trees and vibrant wildflowers. As he ventured deeper, he heard a soft, melodic sound that seemed to beckon him.\n", - "\n", - "Following the enchanting music, Whiskers soon found himself in a clearing where a group of woodland creatures had gathered. They were having a grand celebration, complete with dancing, singing, and a feast of berries and nuts. The animals welcomed Whiskers with open paws, inviting him to join their festivities.\n", - "\n", - "Whiskers, delighted by the warmth and joy of his new friends, danced and played until the sun began to set. As the sky turned shades of pink and orange, he realized it was time to return home. The woodland creatures gifted him a small, sparkling acorn as a token of their friendship.\n", - "\n", - "From that day on, Whiskers would often visit the clearing, sharing stories of the village and enjoying the company of his woodland friends. He learned that adventure and friendship could be found in the most unexpected places, and he cherished every moment spent in the magical woods.\n", - "\n", - "And so, Whiskers continued to live his life filled with curiosity, laughter, and the warmth of friendship, reminding everyone that sometimes, the best adventures are just a whisker away." - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run the Workflow!\n", + "\n", + "**NOTE:** With loops, we need to be mindful of runtime. Here, we set a timeout of 120s." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.tools import FunctionTool\n", + "from llama_index.llms.openai import OpenAI\n", + "\n", + "\n", + "def add(x: int, y: int) -> int:\n", + " \"\"\"Useful function to add two numbers.\"\"\"\n", + " return x + y\n", + "\n", + "\n", + "def multiply(x: int, y: int) -> int:\n", + " \"\"\"Useful function to multiply two numbers.\"\"\"\n", + " return x * y\n", + "\n", + "\n", + "tools = [\n", + " FunctionTool.from_defaults(add),\n", + " FunctionTool.from_defaults(multiply),\n", + "]\n", + "\n", + "agent = FuncationCallingAgent(\n", + " llm=OpenAI(model=\"gpt-4o-mini\"), tools=tools, timeout=120, verbose=True\n", + ")\n", + "\n", + "ret = await agent.run(input=\"Hello!\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "print(ret[\"response\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "assistant: Hello! How can I assist you today?\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "ret = await agent.run(input=\"What is (2123 + 2321) * 312?\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event ToolCallEvent\n", + "Running step handle_tool_calls\n", + "Step handle_tool_calls produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event ToolCallEvent\n", + "Running step handle_tool_calls\n", + "Step handle_tool_calls produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chat History\n", + "\n", + "By default, the workflow is creating a fresh `Context` for each run. This means that the chat history is not preserved between runs. However, we can pass our own `Context` to the workflow to preserve chat history." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Context\n", + "\n", + "ctx = Context(agent)\n", + "\n", + "ret = await agent.run(input=\"Hello! My name is Logan.\", ctx=ctx)\n", + "print(ret[\"response\"])\n", + "\n", + "ret = await agent.run(input=\"What is my name?\", ctx=ctx)\n", + "print(ret[\"response\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n", + "assistant: Hello, Logan! How can I assist you today?\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n", + "assistant: Your name is Logan.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Streaming\n", + "\n", + "Using the `handler` returned from the `.run()` method, we can also access the streaming events." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "agent = FuncationCallingAgent(\n", + " llm=OpenAI(model=\"gpt-4o-mini\"), tools=tools, timeout=120, verbose=False\n", + ")\n", + "\n", + "handler = agent.run(input=\"Hello! Write me a short story about a cat.\")\n", + "\n", + "async for event in handler.stream_events():\n", + " if isinstance(event, StreamEvent):\n", + " print(event.delta, end=\"\", flush=True)\n", + "\n", + "response = await handler\n", + "# print(ret[\"response\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Once upon a time in a quaint little village, there lived a curious cat named Whiskers. Whiskers was no ordinary cat; he had a beautiful coat of orange and white fur that shimmered in the sunlight, and his emerald green eyes sparkled with mischief.\n", + "\n", + "Every day, Whiskers would explore the village, visiting the bakery for a whiff of freshly baked bread and the flower shop to sniff the colorful blooms. The villagers adored him, often leaving out little treats for their favorite feline.\n", + "\n", + "One sunny afternoon, while wandering near the edge of the village, Whiskers stumbled upon a hidden path that led into the woods. His curiosity piqued, he decided to follow the path, which was lined with tall trees and vibrant wildflowers. As he ventured deeper, he heard a soft, melodic sound that seemed to beckon him.\n", + "\n", + "Following the enchanting music, Whiskers soon found himself in a clearing where a group of woodland creatures had gathered. They were having a grand celebration, complete with dancing, singing, and a feast of berries and nuts. The animals welcomed Whiskers with open paws, inviting him to join their festivities.\n", + "\n", + "Whiskers, delighted by the warmth and joy of his new friends, danced and played until the sun began to set. As the sky turned shades of pink and orange, he realized it was time to return home. The woodland creatures gifted him a small, sparkling acorn as a token of their friendship.\n", + "\n", + "From that day on, Whiskers would often visit the clearing, sharing stories of the village and enjoying the company of his woodland friends. He learned that adventure and friendship could be found in the most unexpected places, and he cherished every moment spent in the magical woods.\n", + "\n", + "And so, Whiskers continued to live his life filled with curiosity, laughter, and the warmth of friendship, reminding everyone that sometimes, the best adventures are just a whisker away." + ] + } + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "llama-index-caVs7DDe-py3.10", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } - ], - "source": [ - "agent = FuncationCallingAgent(\n", - " llm=OpenAI(model=\"gpt-4o-mini\"), tools=tools, timeout=120, verbose=False\n", - ")\n", - "\n", - "handler = agent.run(input=\"Hello! Write me a short story about a cat.\")\n", - "\n", - "async for event in handler.stream_events():\n", - " if isinstance(event, StreamEvent):\n", - " print(event.delta, end=\"\", flush=True)\n", - "\n", - "response = await handler\n", - "# print(ret[\"response\"])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "llama-index-caVs7DDe-py3.10", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/docs/examples/workflow/human_in_the_loop_story_crafting.ipynb b/docs/examples/workflow/human_in_the_loop_story_crafting.ipynb index b9f7461763..5eb58f57d9 100644 --- a/docs/examples/workflow/human_in_the_loop_story_crafting.ipynb +++ b/docs/examples/workflow/human_in_the_loop_story_crafting.ipynb @@ -265,14 +265,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Context,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - ")" + "from workflows import Context, Workflow, step\nfrom workflows.events import Event, StartEvent, StopEvent" ] }, { @@ -518,4 +511,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/long_rag_pack.ipynb b/docs/examples/workflow/long_rag_pack.ipynb index 5995ed63fc..67a49d26f4 100644 --- a/docs/examples/workflow/long_rag_pack.ipynb +++ b/docs/examples/workflow/long_rag_pack.ipynb @@ -311,21 +311,7 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Iterable\n", - "\n", - "from llama_index.core import VectorStoreIndex\n", - "from llama_index.core.llms import LLM\n", - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class LoadNodeEvent(Event):\n", - " \"\"\"Event for loading nodes.\"\"\"\n", - "\n", - " small_nodes: Iterable[TextNode]\n", - " grouped_nodes: list[TextNode]\n", - " index: VectorStoreIndex\n", - " similarity_top_k: int\n", - " llm: LLM" + "from typing import Iterable\n\nfrom llama_index.core import VectorStoreIndex\nfrom llama_index.core.llms import LLM\nfrom workflows.events import Event\n\n\nclass LoadNodeEvent(Event):\n \"\"\"Event for loading nodes.\"\"\"\n\n small_nodes: Iterable[TextNode]\n grouped_nodes: list[TextNode]\n index: VectorStoreIndex\n similarity_top_k: int\n llm: LLM" ] }, { @@ -341,124 +327,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " step,\n", - " StartEvent,\n", - " StopEvent,\n", - " Context,\n", - ")\n", - "from llama_index.core import SimpleDirectoryReader\n", - "from llama_index.core.query_engine import RetrieverQueryEngine\n", - "\n", - "\n", - "class LongRAGWorkflow(Workflow):\n", - " \"\"\"Long RAG Workflow.\"\"\"\n", - "\n", - " @step\n", - " async def ingest(self, ev: StartEvent) -> LoadNodeEvent | None:\n", - " \"\"\"Ingestion step.\n", - "\n", - " Args:\n", - " ctx (Context): Context\n", - " ev (StartEvent): start event\n", - "\n", - " Returns:\n", - " StopEvent | None: stop event with result\n", - " \"\"\"\n", - " data_dir: str = ev.get(\"data_dir\")\n", - " llm: LLM = ev.get(\"llm\")\n", - " chunk_size: int | None = ev.get(\"chunk_size\")\n", - " similarity_top_k: int = ev.get(\"similarity_top_k\")\n", - " small_chunk_size: int = ev.get(\"small_chunk_size\")\n", - " index: VectorStoreIndex | None = ev.get(\"index\")\n", - " index_kwargs: dict[str, t.Any] | None = ev.get(\"index_kwargs\")\n", - "\n", - " if any(\n", - " i is None\n", - " for i in [data_dir, llm, similarity_top_k, small_chunk_size]\n", - " ):\n", - " return None\n", - "\n", - " if not index:\n", - " docs = SimpleDirectoryReader(data_dir).load_data()\n", - " if chunk_size is not None:\n", - " nodes = split_doc(\n", - " chunk_size, docs\n", - " ) # split documents into chunks of chunk_size\n", - " grouped_nodes = get_grouped_docs(\n", - " nodes\n", - " ) # get list of nodes after grouping (groups are combined into one node), these are long retrieval units\n", - " else:\n", - " grouped_nodes = docs\n", - "\n", - " # split large retrieval units into smaller nodes\n", - " small_nodes = split_doc(small_chunk_size, grouped_nodes)\n", - "\n", - " index_kwargs = index_kwargs or {}\n", - " index = VectorStoreIndex(small_nodes, **index_kwargs)\n", - " else:\n", - " # get smaller nodes from index and form large retrieval units from these nodes\n", - " small_nodes = index.docstore.docs.values()\n", - " grouped_nodes = get_grouped_docs(small_nodes, None)\n", - "\n", - " return LoadNodeEvent(\n", - " small_nodes=small_nodes,\n", - " grouped_nodes=grouped_nodes,\n", - " index=index,\n", - " similarity_top_k=similarity_top_k,\n", - " llm=llm,\n", - " )\n", - "\n", - " @step\n", - " async def make_query_engine(\n", - " self, ctx: Context, ev: LoadNodeEvent\n", - " ) -> StopEvent:\n", - " \"\"\"Query engine construction step.\n", - "\n", - " Args:\n", - " ctx (Context): context\n", - " ev (LoadNodeEvent): event\n", - "\n", - " Returns:\n", - " StopEvent: stop event\n", - " \"\"\"\n", - " # make retriever and query engine\n", - " retriever = LongRAGRetriever(\n", - " grouped_nodes=ev.grouped_nodes,\n", - " small_toks=ev.small_nodes,\n", - " similarity_top_k=ev.similarity_top_k,\n", - " vector_store=ev.index.vector_store,\n", - " )\n", - " query_eng = RetrieverQueryEngine.from_args(retriever, ev.llm)\n", - "\n", - " return StopEvent(\n", - " result={\n", - " \"retriever\": retriever,\n", - " \"query_engine\": query_eng,\n", - " \"index\": ev.index,\n", - " }\n", - " )\n", - "\n", - " @step\n", - " async def query(self, ctx: Context, ev: StartEvent) -> StopEvent | None:\n", - " \"\"\"Query step.\n", - "\n", - " Args:\n", - " ctx (Context): context\n", - " ev (StartEvent): start event\n", - "\n", - " Returns:\n", - " StopEvent | None: stop event with result\n", - " \"\"\"\n", - " query_str: str | None = ev.get(\"query_str\")\n", - " query_eng = ev.get(\"query_eng\")\n", - "\n", - " if query_str is None:\n", - " return None\n", - "\n", - " result = query_eng.query(query_str)\n", - " return StopEvent(result=result)" + "from workflows import Workflow, step, Context\nfrom workflows.events import StartEvent, StopEvent\nfrom llama_index.core import SimpleDirectoryReader\nfrom llama_index.core.query_engine import RetrieverQueryEngine\n\n\nclass LongRAGWorkflow(Workflow):\n \"\"\"Long RAG Workflow.\"\"\"\n\n @step\n async def ingest(self, ev: StartEvent) -> LoadNodeEvent | None:\n \"\"\"Ingestion step.\n\n Args:\n ctx (Context): Context\n ev (StartEvent): start event\n\n Returns:\n StopEvent | None: stop event with result\n \"\"\"\n data_dir: str = ev.get(\"data_dir\")\n llm: LLM = ev.get(\"llm\")\n chunk_size: int | None = ev.get(\"chunk_size\")\n similarity_top_k: int = ev.get(\"similarity_top_k\")\n small_chunk_size: int = ev.get(\"small_chunk_size\")\n index: VectorStoreIndex | None = ev.get(\"index\")\n index_kwargs: dict[str, t.Any] | None = ev.get(\"index_kwargs\")\n\n if any(\n i is None\n for i in [data_dir, llm, similarity_top_k, small_chunk_size]\n ):\n return None\n\n if not index:\n docs = SimpleDirectoryReader(data_dir).load_data()\n if chunk_size is not None:\n nodes = split_doc(\n chunk_size, docs\n ) # split documents into chunks of chunk_size\n grouped_nodes = get_grouped_docs(\n nodes\n ) # get list of nodes after grouping (groups are combined into one node), these are long retrieval units\n else:\n grouped_nodes = docs\n\n # split large retrieval units into smaller nodes\n small_nodes = split_doc(small_chunk_size, grouped_nodes)\n\n index_kwargs = index_kwargs or {}\n index = VectorStoreIndex(small_nodes, **index_kwargs)\n else:\n # get smaller nodes from index and form large retrieval units from these nodes\n small_nodes = index.docstore.docs.values()\n grouped_nodes = get_grouped_docs(small_nodes, None)\n\n return LoadNodeEvent(\n small_nodes=small_nodes,\n grouped_nodes=grouped_nodes,\n index=index,\n similarity_top_k=similarity_top_k,\n llm=llm,\n )\n\n @step\n async def make_query_engine(\n self, ctx: Context, ev: LoadNodeEvent\n ) -> StopEvent:\n \"\"\"Query engine construction step.\n\n Args:\n ctx (Context): context\n ev (LoadNodeEvent): event\n\n Returns:\n StopEvent: stop event\n \"\"\"\n # make retriever and query engine\n retriever = LongRAGRetriever(\n grouped_nodes=ev.grouped_nodes,\n small_toks=ev.small_nodes,\n similarity_top_k=ev.similarity_top_k,\n vector_store=ev.index.vector_store,\n )\n query_eng = RetrieverQueryEngine.from_args(retriever, ev.llm)\n\n return StopEvent(\n result={\n \"retriever\": retriever,\n \"query_engine\": query_eng,\n \"index\": ev.index,\n }\n )\n\n @step\n async def query(self, ctx: Context, ev: StartEvent) -> StopEvent | None:\n \"\"\"Query step.\n\n Args:\n ctx (Context): context\n ev (StartEvent): start event\n\n Returns:\n StopEvent | None: stop event with result\n \"\"\"\n query_str: str | None = ev.get(\"query_str\")\n query_eng = ev.get(\"query_eng\")\n\n if query_str is None:\n return None\n\n result = query_eng.query(query_str)\n return StopEvent(result=result)" ] }, { @@ -553,4 +422,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/multi_step_query_engine.ipynb b/docs/examples/workflow/multi_step_query_engine.ipynb index 9d4ab69956..fbc000b414 100644 --- a/docs/examples/workflow/multi_step_query_engine.ipynb +++ b/docs/examples/workflow/multi_step_query_engine.ipynb @@ -107,24 +107,7 @@ } ], "source": [ - "from llama_index.core.workflow import Event\n", - "from typing import Dict, List, Any\n", - "from llama_index.core.schema import NodeWithScore\n", - "\n", - "\n", - "class QueryMultiStepEvent(Event):\n", - " \"\"\"\n", - " Event containing results of a multi-step query process.\n", - "\n", - " Attributes:\n", - " nodes (List[NodeWithScore]): List of nodes with their associated scores.\n", - " source_nodes (List[NodeWithScore]): List of source nodes with their scores.\n", - " final_response_metadata (Dict[str, Any]): Metadata associated with the final response.\n", - " \"\"\"\n", - "\n", - " nodes: List[NodeWithScore]\n", - " source_nodes: List[NodeWithScore]\n", - " final_response_metadata: Dict[str, Any]" + "from workflows.events import Event\nfrom typing import Dict, List, Any\nfrom llama_index.core.schema import NodeWithScore\n\n\nclass QueryMultiStepEvent(Event):\n \"\"\"\n Event containing results of a multi-step query process.\n\n Attributes:\n nodes (List[NodeWithScore]): List of nodes with their associated scores.\n source_nodes (List[NodeWithScore]): List of source nodes with their scores.\n final_response_metadata (Dict[str, Any]): Metadata associated with the final response.\n \"\"\"\n\n nodes: List[NodeWithScore]\n source_nodes: List[NodeWithScore]\n final_response_metadata: Dict[str, Any]" ] }, { @@ -140,149 +123,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.indices.query.query_transform.base import (\n", - " StepDecomposeQueryTransform,\n", - ")\n", - "from llama_index.core.response_synthesizers import (\n", - " get_response_synthesizer,\n", - ")\n", - "\n", - "from llama_index.core.schema import QueryBundle, TextNode\n", - "\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "\n", - "from llama_index.core import Settings\n", - "from llama_index.core.llms import LLM\n", - "\n", - "from typing import cast\n", - "from IPython.display import Markdown, display\n", - "\n", - "\n", - "class MultiStepQueryEngineWorkflow(Workflow):\n", - " def combine_queries(\n", - " self,\n", - " query_bundle: QueryBundle,\n", - " prev_reasoning: str,\n", - " index_summary: str,\n", - " llm: LLM,\n", - " ) -> QueryBundle:\n", - " \"\"\"Combine queries using StepDecomposeQueryTransform.\"\"\"\n", - " transform_metadata = {\n", - " \"prev_reasoning\": prev_reasoning,\n", - " \"index_summary\": index_summary,\n", - " }\n", - " return StepDecomposeQueryTransform(llm=llm)(\n", - " query_bundle, metadata=transform_metadata\n", - " )\n", - "\n", - " def default_stop_fn(self, stop_dict: Dict) -> bool:\n", - " \"\"\"Stop function for multi-step query combiner.\"\"\"\n", - " query_bundle = cast(QueryBundle, stop_dict.get(\"query_bundle\"))\n", - " if query_bundle is None:\n", - " raise ValueError(\"Response must be provided to stop function.\")\n", - "\n", - " return \"none\" in query_bundle.query_str.lower()\n", - "\n", - " @step\n", - " async def query_multistep(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> QueryMultiStepEvent:\n", - " \"\"\"Execute multi-step query process.\"\"\"\n", - " prev_reasoning = \"\"\n", - " cur_response = None\n", - " should_stop = False\n", - " cur_steps = 0\n", - "\n", - " # use response\n", - " final_response_metadata: Dict[str, Any] = {\"sub_qa\": []}\n", - "\n", - " text_chunks = []\n", - " source_nodes = []\n", - "\n", - " query = ev.get(\"query\")\n", - " await ctx.store.set(\"query\", ev.get(\"query\"))\n", - "\n", - " llm = Settings.llm\n", - " stop_fn = self.default_stop_fn\n", - "\n", - " num_steps = ev.get(\"num_steps\")\n", - " query_engine = ev.get(\"query_engine\")\n", - " index_summary = ev.get(\"index_summary\")\n", - "\n", - " while not should_stop:\n", - " if num_steps is not None and cur_steps >= num_steps:\n", - " should_stop = True\n", - " break\n", - " elif should_stop:\n", - " break\n", - "\n", - " updated_query_bundle = self.combine_queries(\n", - " QueryBundle(query_str=query),\n", - " prev_reasoning,\n", - " index_summary,\n", - " llm,\n", - " )\n", - "\n", - " print(\n", - " f\"Created query for the step - {cur_steps} is: {updated_query_bundle}\"\n", - " )\n", - "\n", - " stop_dict = {\"query_bundle\": updated_query_bundle}\n", - " if stop_fn(stop_dict):\n", - " should_stop = True\n", - " break\n", - "\n", - " cur_response = query_engine.query(updated_query_bundle)\n", - "\n", - " # append to response builder\n", - " cur_qa_text = (\n", - " f\"\\nQuestion: {updated_query_bundle.query_str}\\n\"\n", - " f\"Answer: {cur_response!s}\"\n", - " )\n", - " text_chunks.append(cur_qa_text)\n", - " for source_node in cur_response.source_nodes:\n", - " source_nodes.append(source_node)\n", - " # update metadata\n", - " final_response_metadata[\"sub_qa\"].append(\n", - " (updated_query_bundle.query_str, cur_response)\n", - " )\n", - "\n", - " prev_reasoning += (\n", - " f\"- {updated_query_bundle.query_str}\\n\" f\"- {cur_response!s}\\n\"\n", - " )\n", - " cur_steps += 1\n", - "\n", - " nodes = [\n", - " NodeWithScore(node=TextNode(text=text_chunk))\n", - " for text_chunk in text_chunks\n", - " ]\n", - " return QueryMultiStepEvent(\n", - " nodes=nodes,\n", - " source_nodes=source_nodes,\n", - " final_response_metadata=final_response_metadata,\n", - " )\n", - "\n", - " @step\n", - " async def synthesize(\n", - " self, ctx: Context, ev: QueryMultiStepEvent\n", - " ) -> StopEvent:\n", - " \"\"\"Synthesize the response.\"\"\"\n", - " response_synthesizer = get_response_synthesizer()\n", - " query = await ctx.store.get(\"query\", default=None)\n", - " final_response = await response_synthesizer.asynthesize(\n", - " query=query,\n", - " nodes=ev.nodes,\n", - " additional_source_nodes=ev.source_nodes,\n", - " )\n", - " final_response.metadata = ev.final_response_metadata\n", - "\n", - " return StopEvent(result=final_response)" + "from llama_index.core.indices.query.query_transform.base import (\n StepDecomposeQueryTransform,\n)\nfrom llama_index.core.response_synthesizers import (\n get_response_synthesizer,\n)\n\nfrom llama_index.core.schema import QueryBundle, TextNode\n\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nfrom llama_index.core import Settings\nfrom llama_index.core.llms import LLM\n\nfrom typing import cast\nfrom IPython.display import Markdown, display\n\n\nclass MultiStepQueryEngineWorkflow(Workflow):\n def combine_queries(\n self,\n query_bundle: QueryBundle,\n prev_reasoning: str,\n index_summary: str,\n llm: LLM,\n ) -> QueryBundle:\n \"\"\"Combine queries using StepDecomposeQueryTransform.\"\"\"\n transform_metadata = {\n \"prev_reasoning\": prev_reasoning,\n \"index_summary\": index_summary,\n }\n return StepDecomposeQueryTransform(llm=llm)(\n query_bundle, metadata=transform_metadata\n )\n\n def default_stop_fn(self, stop_dict: Dict) -> bool:\n \"\"\"Stop function for multi-step query combiner.\"\"\"\n query_bundle = cast(QueryBundle, stop_dict.get(\"query_bundle\"))\n if query_bundle is None:\n raise ValueError(\"Response must be provided to stop function.\")\n\n return \"none\" in query_bundle.query_str.lower()\n\n @step\n async def query_multistep(\n self, ctx: Context, ev: StartEvent\n ) -> QueryMultiStepEvent:\n \"\"\"Execute multi-step query process.\"\"\"\n prev_reasoning = \"\"\n cur_response = None\n should_stop = False\n cur_steps = 0\n\n # use response\n final_response_metadata: Dict[str, Any] = {\"sub_qa\": []}\n\n text_chunks = []\n source_nodes = []\n\n query = ev.get(\"query\")\n await ctx.store.set(\"query\", ev.get(\"query\"))\n\n llm = Settings.llm\n stop_fn = self.default_stop_fn\n\n num_steps = ev.get(\"num_steps\")\n query_engine = ev.get(\"query_engine\")\n index_summary = ev.get(\"index_summary\")\n\n while not should_stop:\n if num_steps is not None and cur_steps >= num_steps:\n should_stop = True\n break\n elif should_stop:\n break\n\n updated_query_bundle = self.combine_queries(\n QueryBundle(query_str=query),\n prev_reasoning,\n index_summary,\n llm,\n )\n\n print(\n f\"Created query for the step - {cur_steps} is: {updated_query_bundle}\"\n )\n\n stop_dict = {\"query_bundle\": updated_query_bundle}\n if stop_fn(stop_dict):\n should_stop = True\n break\n\n cur_response = query_engine.query(updated_query_bundle)\n\n # append to response builder\n cur_qa_text = (\n f\"\\nQuestion: {updated_query_bundle.query_str}\\n\"\n f\"Answer: {cur_response!s}\"\n )\n text_chunks.append(cur_qa_text)\n for source_node in cur_response.source_nodes:\n source_nodes.append(source_node)\n # update metadata\n final_response_metadata[\"sub_qa\"].append(\n (updated_query_bundle.query_str, cur_response)\n )\n\n prev_reasoning += (\n f\"- {updated_query_bundle.query_str}\\n\" f\"- {cur_response!s}\\n\"\n )\n cur_steps += 1\n\n nodes = [\n NodeWithScore(node=TextNode(text=text_chunk))\n for text_chunk in text_chunks\n ]\n return QueryMultiStepEvent(\n nodes=nodes,\n source_nodes=source_nodes,\n final_response_metadata=final_response_metadata,\n )\n\n @step\n async def synthesize(\n self, ctx: Context, ev: QueryMultiStepEvent\n ) -> StopEvent:\n \"\"\"Synthesize the response.\"\"\"\n response_synthesizer = get_response_synthesizer()\n query = await ctx.store.get(\"query\", default=None)\n final_response = await response_synthesizer.asynthesize(\n query=query,\n nodes=ev.nodes,\n additional_source_nodes=ev.source_nodes,\n )\n final_response.metadata = ev.final_response_metadata\n\n return StopEvent(result=final_response)" ] }, { @@ -546,4 +387,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/multi_strategy_workflow.ipynb b/docs/examples/workflow/multi_strategy_workflow.ipynb index 804da4e9ab..16d32445ef 100644 --- a/docs/examples/workflow/multi_strategy_workflow.ipynb +++ b/docs/examples/workflow/multi_strategy_workflow.ipynb @@ -76,26 +76,7 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "from llama_index.core import (\n", - " SimpleDirectoryReader,\n", - " VectorStoreIndex,\n", - " StorageContext,\n", - " load_index_from_storage,\n", - ")\n", - "from llama_index.core.workflow import (\n", - " step,\n", - " Context,\n", - " Workflow,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - ")\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.core.postprocessor.rankGPT_rerank import RankGPTRerank\n", - "from llama_index.core.query_engine import RetrieverQueryEngine\n", - "from llama_index.core.chat_engine import SimpleChatEngine\n", - "from llama_index.utils.workflow import draw_all_possible_flows" + "import os\nfrom llama_index.core import (\n SimpleDirectoryReader,\n VectorStoreIndex,\n StorageContext,\n load_index_from_storage,\n)\nfrom workflows import step, Context, Workflow\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.core.postprocessor.rankGPT_rerank import RankGPTRerank\nfrom llama_index.core.query_engine import RetrieverQueryEngine\nfrom llama_index.core.chat_engine import SimpleChatEngine\nfrom llama_index.utils.workflow import draw_all_possible_flows" ] }, { @@ -415,4 +396,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/parallel_execution.ipynb b/docs/examples/workflow/parallel_execution.ipynb index c3ece15e5c..b20c629295 100644 --- a/docs/examples/workflow/parallel_execution.ipynb +++ b/docs/examples/workflow/parallel_execution.ipynb @@ -1,553 +1,547 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Parallel Execution of Same Event Example\n", - "\n", - "In this example, we'll demonstrate how to use the workflow functionality to achieve similar capabilities while allowing parallel execution of multiple events of the same type. \n", - "By setting the `num_workers` parameter in `@step` decorator, we can control the number of steps executed simultaneously, enabling efficient parallel processing.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Installing Dependencies\n", - "\n", - "First, we need to install the necessary dependencies:\n", - "\n", - "* LlamaIndex core for most functionalities\n", - "* llama-index-utils-workflow for workflow capabilities" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# %pip install llama-index-core llama-index-utils-workflow -q" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Importing Required Libraries\n", - "After installing the dependencies, we can import the required libraries:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "from llama_index.core.workflow import (\n", - " step,\n", - " Context,\n", - " Workflow,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will create two workflows: one that can process multiple data items in parallel by using the `@step(num_workers=N)` decorator, and another without setting num_workers, for comparison. \n", - "By using the `num_workers` parameter in the `@step` decorator, we can limit the number of steps executed simultaneously, thus controlling the level of parallelism. This approach is particularly suitable for scenarios that require processing similar tasks while managing resource usage. \n", - "For example, you can execute multiple sub-queries at once, but please note that num_workers cannot be set without limits. It depends on your workload or token limits.\n", - "# Defining Event Types\n", - "We'll define two event types: one for input events to be processed, and another for processing results:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class ProcessEvent(Event):\n", - " data: str\n", - "\n", - "\n", - "class ResultEvent(Event):\n", - " result: str" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Creating Sequential and Parallel Workflows\n", - "Now, we'll create a SequentialWorkflow and a ParallelWorkflow class that includes three main steps:\n", - "\n", - "- start: Initialize and send multiple parallel events\n", - "- process_data: Process data\n", - "- combine_results: Collect and merge all processing results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "\n", - "\n", - "class SequentialWorkflow(Workflow):\n", - " @step\n", - " async def start(self, ctx: Context, ev: StartEvent) -> ProcessEvent:\n", - " data_list = [\"A\", \"B\", \"C\"]\n", - " await ctx.store.set(\"num_to_collect\", len(data_list))\n", - " for item in data_list:\n", - " ctx.send_event(ProcessEvent(data=item))\n", - " return None\n", - "\n", - " @step(num_workers=1)\n", - " async def process_data(self, ev: ProcessEvent) -> ResultEvent:\n", - " # Simulate some time-consuming processing\n", - " processing_time = 2 + random.random()\n", - " await asyncio.sleep(processing_time)\n", - " result = f\"Processed: {ev.data}\"\n", - " print(f\"Completed processing: {ev.data}\")\n", - " return ResultEvent(result=result)\n", - "\n", - " @step\n", - " async def combine_results(\n", - " self, ctx: Context, ev: ResultEvent\n", - " ) -> StopEvent | None:\n", - " num_to_collect = await ctx.store.get(\"num_to_collect\")\n", - " results = ctx.collect_events(ev, [ResultEvent] * num_to_collect)\n", - " if results is None:\n", - " return None\n", - "\n", - " combined_result = \", \".join([event.result for event in results])\n", - " return StopEvent(result=combined_result)\n", - "\n", - "\n", - "class ParallelWorkflow(Workflow):\n", - " @step\n", - " async def start(self, ctx: Context, ev: StartEvent) -> ProcessEvent:\n", - " data_list = [\"A\", \"B\", \"C\"]\n", - " await ctx.store.set(\"num_to_collect\", len(data_list))\n", - " for item in data_list:\n", - " ctx.send_event(ProcessEvent(data=item))\n", - " return None\n", - "\n", - " @step(num_workers=3)\n", - " async def process_data(self, ev: ProcessEvent) -> ResultEvent:\n", - " # Simulate some time-consuming processing\n", - " processing_time = 2 + random.random()\n", - " await asyncio.sleep(processing_time)\n", - " result = f\"Processed: {ev.data}\"\n", - " print(f\"Completed processing: {ev.data}\")\n", - " return ResultEvent(result=result)\n", - "\n", - " @step\n", - " async def combine_results(\n", - " self, ctx: Context, ev: ResultEvent\n", - " ) -> StopEvent | None:\n", - " num_to_collect = await ctx.store.get(\"num_to_collect\")\n", - " results = ctx.collect_events(ev, [ResultEvent] * num_to_collect)\n", - " if results is None:\n", - " return None\n", - "\n", - " combined_result = \", \".join([event.result for event in results])\n", - " return StopEvent(result=combined_result)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In these two workflows:\n", - "\n", - "- The start method initializes and sends multiple ProcessEvent.\n", - "- The process_data method uses\n", - " - only the `@step` decorator in SequentialWorkflow\n", - " - uses the `@step(num_workers=3)` decorator in ParallelWorkflow to limit the number of simultaneously executing workers to 3.\n", - "- The combine_results method collects all processing results and merges them.\n", - "\n", - "# Running the Workflow\n", - "Finally, we can create a main function to run our workflow:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Start a sequential workflow without setting num_workers in the step of process_data\n", - "Completed processing: A\n", - "Completed processing: B\n", - "Completed processing: C\n", - "Workflow result: Processed: A, Processed: B, Processed: C\n", - "Time taken: 7.439495086669922 seconds\n", - "------------------------------\n", - "Start a parallel workflow with setting num_workers in the step of process_data\n", - "Completed processing: C\n", - "Completed processing: A\n", - "Completed processing: B\n", - "Workflow result: Processed: C, Processed: A, Processed: B\n", - "Time taken: 2.5881590843200684 seconds\n" - ] - } - ], - "source": [ - "import time\n", - "\n", - "sequential_workflow = SequentialWorkflow()\n", - "\n", - "print(\n", - " \"Start a sequential workflow without setting num_workers in the step of process_data\"\n", - ")\n", - "start_time = time.time()\n", - "result = await sequential_workflow.run()\n", - "end_time = time.time()\n", - "print(f\"Workflow result: {result}\")\n", - "print(f\"Time taken: {end_time - start_time} seconds\")\n", - "print(\"-\" * 30)\n", - "\n", - "parallel_workflow = ParallelWorkflow()\n", - "\n", - "print(\n", - " \"Start a parallel workflow with setting num_workers in the step of process_data\"\n", - ")\n", - "start_time = time.time()\n", - "result = await parallel_workflow.run()\n", - "end_time = time.time()\n", - "print(f\"Workflow result: {result}\")\n", - "print(f\"Time taken: {end_time - start_time} seconds\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Note\n", - "\n", - "- Without setting `num_workers=1`, it might take a total of 6-9 seconds. By setting `num_workers=3`, the processing occurs in parallel, handling 3 items at a time, and only takes 2-3 seconds total.\n", - "- In ParallelWorkflow, the order of the completed results may differ from the input order, depending on the completion time of the tasks.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This example demonstrates the execution speed with and without using num_workers, and how to implement parallel processing in a workflow. By setting num_workers, we can control the degree of parallelism, which is very useful for scenarios that need to balance performance and resource usage." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Checkpointing" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Checkpointing a parallel execution Workflow like the one defined above is also possible. To do so, we must wrap the `Workflow` with a `WorkflowCheckpointer` object and perfrom the runs with these instances. During the execution of the workflow, checkpoints are stored in this wrapper object and can be used for inspection and as starting points for run executions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow.checkpointer import WorkflowCheckpointer" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Parallel Execution of Same Event Example\n", + "\n", + "In this example, we'll demonstrate how to use the workflow functionality to achieve similar capabilities while allowing parallel execution of multiple events of the same type. \n", + "By setting the `num_workers` parameter in `@step` decorator, we can control the number of steps executed simultaneously, enabling efficient parallel processing.\n", + "\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Completed processing: C\n", - "Completed processing: A\n", - "Completed processing: B\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Installing Dependencies\n", + "\n", + "First, we need to install the necessary dependencies:\n", + "\n", + "* LlamaIndex core for most functionalities\n", + "* llama-index-utils-workflow for workflow capabilities" + ] }, { - "data": { - "text/plain": [ - "'Processed: C, Processed: A, Processed: B'" + "cell_type": "code", + "metadata": {}, + "source": [ + "# %pip install llama-index-core llama-index-utils-workflow -q" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Importing Required Libraries\n", + "After installing the dependencies, we can import the required libraries:" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "wflow_ckptr = WorkflowCheckpointer(workflow=parallel_workflow)\n", - "handler = wflow_ckptr.run()\n", - "await handler" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Checkpoints for the above run are stored in the `WorkflowCheckpointer.checkpoints` Dict attribute." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can run from any of the checkpoints stored, using `WorkflowCheckpointer.run_from(checkpoint=...)` method. Let's take the first checkpoint that was stored after the first completion of \"process_data\" and run from it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "import asyncio\n", + "from workflows import Workflow, step, Context\n", + "from workflows.events import Event, StartEvent, StopEvent" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Completed processing: B\n", - "Completed processing: A\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will create two workflows: one that can process multiple data items in parallel by using the `@step(num_workers=N)` decorator, and another without setting num_workers, for comparison. \n", + "By using the `num_workers` parameter in the `@step` decorator, we can limit the number of steps executed simultaneously, thus controlling the level of parallelism. This approach is particularly suitable for scenarios that require processing similar tasks while managing resource usage. \n", + "For example, you can execute multiple sub-queries at once, but please note that num_workers cannot be set without limits. It depends on your workload or token limits.\n", + "# Defining Event Types\n", + "We'll define two event types: one for input events to be processed, and another for processing results:" + ] }, { - "data": { - "text/plain": [ - "'Processed: C, Processed: B, Processed: A'" + "cell_type": "code", + "metadata": {}, + "source": [ + "class ProcessEvent(Event):\n", + " data: str\n", + "\n", + "\n", + "class ResultEvent(Event):\n", + " result: str" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating Sequential and Parallel Workflows\n", + "Now, we'll create a SequentialWorkflow and a ParallelWorkflow class that includes three main steps:\n", + "\n", + "- start: Initialize and send multiple parallel events\n", + "- process_data: Process data\n", + "- combine_results: Collect and merge all processing results" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ckpt = wflow_ckptr.checkpoints[run_id][0]\n", - "handler = wflow_ckptr.run_from(ckpt)\n", - "await handler" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Invoking a `run_from` or `run` will create a new run entry in the `checkpoints` attribute. In the latest run from the specified checkpoint, we can see that only two more \"process_data\" steps and the final \"combine_results\" steps were left to be completed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n", - "Run: 4e1d24cd-c672-4ed1-bb5b-b9f1a252abed has ['process_data', 'process_data', 'combine_results']\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, if we use the checkpoint associated with the second completion of \"process_data\" of the same initial run as the starting point, then we should see a new entry that only has two steps: \"process_data\" and \"combine_results\"." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "import random\n", + "\n", + "\n", + "class SequentialWorkflow(Workflow):\n", + " @step\n", + " async def start(self, ctx: Context, ev: StartEvent) -> ProcessEvent:\n", + " data_list = [\"A\", \"B\", \"C\"]\n", + " await ctx.store.set(\"num_to_collect\", len(data_list))\n", + " for item in data_list:\n", + " ctx.send_event(ProcessEvent(data=item))\n", + " return None\n", + "\n", + " @step(num_workers=1)\n", + " async def process_data(self, ev: ProcessEvent) -> ResultEvent:\n", + " # Simulate some time-consuming processing\n", + " processing_time = 2 + random.random()\n", + " await asyncio.sleep(processing_time)\n", + " result = f\"Processed: {ev.data}\"\n", + " print(f\"Completed processing: {ev.data}\")\n", + " return ResultEvent(result=result)\n", + "\n", + " @step\n", + " async def combine_results(\n", + " self, ctx: Context, ev: ResultEvent\n", + " ) -> StopEvent | None:\n", + " num_to_collect = await ctx.store.get(\"num_to_collect\")\n", + " results = ctx.collect_events(ev, [ResultEvent] * num_to_collect)\n", + " if results is None:\n", + " return None\n", + "\n", + " combined_result = \", \".join([event.result for event in results])\n", + " return StopEvent(result=combined_result)\n", + "\n", + "\n", + "class ParallelWorkflow(Workflow):\n", + " @step\n", + " async def start(self, ctx: Context, ev: StartEvent) -> ProcessEvent:\n", + " data_list = [\"A\", \"B\", \"C\"]\n", + " await ctx.store.set(\"num_to_collect\", len(data_list))\n", + " for item in data_list:\n", + " ctx.send_event(ProcessEvent(data=item))\n", + " return None\n", + "\n", + " @step(num_workers=3)\n", + " async def process_data(self, ev: ProcessEvent) -> ResultEvent:\n", + " # Simulate some time-consuming processing\n", + " processing_time = 2 + random.random()\n", + " await asyncio.sleep(processing_time)\n", + " result = f\"Processed: {ev.data}\"\n", + " print(f\"Completed processing: {ev.data}\")\n", + " return ResultEvent(result=result)\n", + "\n", + " @step\n", + " async def combine_results(\n", + " self, ctx: Context, ev: ResultEvent\n", + " ) -> StopEvent | None:\n", + " num_to_collect = await ctx.store.get(\"num_to_collect\")\n", + " results = ctx.collect_events(ev, [ResultEvent] * num_to_collect)\n", + " if results is None:\n", + " return None\n", + "\n", + " combined_result = \", \".join([event.result for event in results])\n", + " return StopEvent(result=combined_result)" + ], + "execution_count": null, + "outputs": [] + }, { - "data": { - "text/plain": [ - "'90812bec-b571-4513-8ad5-aa957ad7d4fb'" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In these two workflows:\n", + "\n", + "- The start method initializes and sends multiple ProcessEvent.\n", + "- The process_data method uses\n", + " - only the `@step` decorator in SequentialWorkflow\n", + " - uses the `@step(num_workers=3)` decorator in ParallelWorkflow to limit the number of simultaneously executing workers to 3.\n", + "- The combine_results method collects all processing results and merges them.\n", + "\n", + "# Running the Workflow\n", + "Finally, we can create a main function to run our workflow:" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get the run_id of the first initial run\n", - "first_run_id = next(iter(wflow_ckptr.checkpoints.keys()))\n", - "first_run_id" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Completed processing: B\n" - ] + "cell_type": "code", + "metadata": {}, + "source": [ + "import time\n", + "\n", + "sequential_workflow = SequentialWorkflow()\n", + "\n", + "print(\n", + " \"Start a sequential workflow without setting num_workers in the step of process_data\"\n", + ")\n", + "start_time = time.time()\n", + "result = await sequential_workflow.run()\n", + "end_time = time.time()\n", + "print(f\"Workflow result: {result}\")\n", + "print(f\"Time taken: {end_time - start_time} seconds\")\n", + "print(\"-\" * 30)\n", + "\n", + "parallel_workflow = ParallelWorkflow()\n", + "\n", + "print(\n", + " \"Start a parallel workflow with setting num_workers in the step of process_data\"\n", + ")\n", + "start_time = time.time()\n", + "result = await parallel_workflow.run()\n", + "end_time = time.time()\n", + "print(f\"Workflow result: {result}\")\n", + "print(f\"Time taken: {end_time - start_time} seconds\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Start a sequential workflow without setting num_workers in the step of process_data\n", + "Completed processing: A\n", + "Completed processing: B\n", + "Completed processing: C\n", + "Workflow result: Processed: A, Processed: B, Processed: C\n", + "Time taken: 7.439495086669922 seconds\n", + "------------------------------\n", + "Start a parallel workflow with setting num_workers in the step of process_data\n", + "Completed processing: C\n", + "Completed processing: A\n", + "Completed processing: B\n", + "Workflow result: Processed: C, Processed: A, Processed: B\n", + "Time taken: 2.5881590843200684 seconds\n" + ] + } + ] }, { - "data": { - "text/plain": [ - "'Processed: C, Processed: A, Processed: B'" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Note\n", + "\n", + "- Without setting `num_workers=1`, it might take a total of 6-9 seconds. By setting `num_workers=3`, the processing occurs in parallel, handling 3 items at a time, and only takes 2-3 seconds total.\n", + "- In ParallelWorkflow, the order of the completed results may differ from the input order, depending on the completion time of the tasks.\n" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ckpt = wflow_ckptr.checkpoints[first_run_id][\n", - " 1\n", - "] # checkpoint after the second \"process_data\" step\n", - "handler = wflow_ckptr.run_from(ckpt)\n", - "await handler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n", - "Run: 4e1d24cd-c672-4ed1-bb5b-b9f1a252abed has ['process_data', 'process_data', 'combine_results']\n", - "Run: e4f94fcd-9b78-4e28-8981-e0232d068f6e has ['process_data', 'combine_results']\n" - ] - } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Similarly, if we start with the checkpoint for the third completion of \"process_data\" of the initial run, then we should only see the final \"combine_results\" step." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example demonstrates the execution speed with and without using num_workers, and how to implement parallel processing in a workflow. By setting num_workers, we can control the degree of parallelism, which is very useful for scenarios that need to balance performance and resource usage." + ] + }, { - "data": { - "text/plain": [ - "'Processed: C, Processed: A, Processed: B'" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Checkpointing" ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ckpt = wflow_ckptr.checkpoints[first_run_id][\n", - " 2\n", - "] # checkpoint after the third \"process_data\" step\n", - "handler = wflow_ckptr.run_from(ckpt)\n", - "await handler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Checkpointing a parallel execution Workflow like the one defined above is also possible. To do so, we must wrap the `Workflow` with a `WorkflowCheckpointer` object and perfrom the runs with these instances. During the execution of the workflow, checkpoints are stored in this wrapper object and can be used for inspection and as starting points for run executions." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# WorkflowCheckpointer has been removed (deprecated). See CHANGELOG." + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "wflow_ckptr = WorkflowCheckpointer(workflow=parallel_workflow)\n", + "handler = wflow_ckptr.run()\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed processing: C\n", + "Completed processing: A\n", + "Completed processing: B\n" + ] + }, + { + "data": { + "text/plain": [ + "'Processed: C, Processed: A, Processed: B'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Checkpoints for the above run are stored in the `WorkflowCheckpointer.checkpoints` Dict attribute." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can run from any of the checkpoints stored, using `WorkflowCheckpointer.run_from(checkpoint=...)` method. Let's take the first checkpoint that was stored after the first completion of \"process_data\" and run from it." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "ckpt = wflow_ckptr.checkpoints[run_id][0]\n", + "handler = wflow_ckptr.run_from(ckpt)\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed processing: B\n", + "Completed processing: A\n" + ] + }, + { + "data": { + "text/plain": [ + "'Processed: C, Processed: B, Processed: A'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Invoking a `run_from` or `run` will create a new run entry in the `checkpoints` attribute. In the latest run from the specified checkpoint, we can see that only two more \"process_data\" steps and the final \"combine_results\" steps were left to be completed." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n", - "Run: 4e1d24cd-c672-4ed1-bb5b-b9f1a252abed has ['process_data', 'process_data', 'combine_results']\n", - "Run: e4f94fcd-9b78-4e28-8981-e0232d068f6e has ['process_data', 'combine_results']\n", - "Run: c498a1a0-cf4c-4d80-a1e2-a175bb90b66d has ['combine_results']\n" - ] + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n", + "Run: 4e1d24cd-c672-4ed1-bb5b-b9f1a252abed has ['process_data', 'process_data', 'combine_results']\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, if we use the checkpoint associated with the second completion of \"process_data\" of the same initial run as the starting point, then we should see a new entry that only has two steps: \"process_data\" and \"combine_results\"." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# get the run_id of the first initial run\n", + "first_run_id = next(iter(wflow_ckptr.checkpoints.keys()))\n", + "first_run_id" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "'90812bec-b571-4513-8ad5-aa957ad7d4fb'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "ckpt = wflow_ckptr.checkpoints[first_run_id][\n", + " 1\n", + "] # checkpoint after the second \"process_data\" step\n", + "handler = wflow_ckptr.run_from(ckpt)\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed processing: B\n" + ] + }, + { + "data": { + "text/plain": [ + "'Processed: C, Processed: A, Processed: B'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n", + "Run: 4e1d24cd-c672-4ed1-bb5b-b9f1a252abed has ['process_data', 'process_data', 'combine_results']\n", + "Run: e4f94fcd-9b78-4e28-8981-e0232d068f6e has ['process_data', 'combine_results']\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, if we start with the checkpoint for the third completion of \"process_data\" of the initial run, then we should only see the final \"combine_results\" step." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "ckpt = wflow_ckptr.checkpoints[first_run_id][\n", + " 2\n", + "] # checkpoint after the third \"process_data\" step\n", + "handler = wflow_ckptr.run_from(ckpt)\n", + "await handler" + ], + "execution_count": null, + "outputs": [ + { + "data": { + "text/plain": [ + "'Processed: C, Processed: A, Processed: B'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", + " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Run: 90812bec-b571-4513-8ad5-aa957ad7d4fb has ['process_data', 'process_data', 'process_data', 'combine_results']\n", + "Run: 4e1d24cd-c672-4ed1-bb5b-b9f1a252abed has ['process_data', 'process_data', 'combine_results']\n", + "Run: e4f94fcd-9b78-4e28-8981-e0232d068f6e has ['process_data', 'combine_results']\n", + "Run: c498a1a0-cf4c-4d80-a1e2-a175bb90b66d has ['combine_results']\n" + ] + } + ] + } + ], + "metadata": { + "colab": { + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "llama-index-core", + "language": "python", + "name": "llama-index-core" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } - ], - "source": [ - "for run_id, ckpts in wflow_ckptr.checkpoints.items():\n", - " print(f\"Run: {run_id} has {[c.last_completed_step for c in ckpts]}\")" - ] - } - ], - "metadata": { - "colab": { - "provenance": [], - "toc_visible": true - }, - "kernelspec": { - "display_name": "llama-index-core", - "language": "python", - "name": "llama-index-core" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/docs/examples/workflow/planning_workflow.ipynb b/docs/examples/workflow/planning_workflow.ipynb index 3a9737cdd8..a2cc5f58eb 100644 --- a/docs/examples/workflow/planning_workflow.ipynb +++ b/docs/examples/workflow/planning_workflow.ipynb @@ -69,25 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "from pydantic import BaseModel, Field\n", - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class QueryPlanItem(Event):\n", - " \"\"\"A single step in an execution plan for a RAG system.\"\"\"\n", - "\n", - " name: str = Field(description=\"The name of the tool to use.\")\n", - " query: str = Field(\n", - " description=\"A natural language search query for a RAG system.\"\n", - " )\n", - "\n", - "\n", - "class QueryPlan(BaseModel):\n", - " \"\"\"A plan for a RAG system. After running the plan, we should have either enough information to answer the user's original query, or enough information to form a new query plan.\"\"\"\n", - "\n", - " items: list[QueryPlanItem] = Field(\n", - " description=\"A list of the QueryPlanItem objects in the plan.\"\n", - " )" + "from pydantic import BaseModel, Field\nfrom workflows.events import Event\n\n\nclass QueryPlanItem(Event):\n \"\"\"A single step in an execution plan for a RAG system.\"\"\"\n\n name: str = Field(description=\"The name of the tool to use.\")\n query: str = Field(\n description=\"A natural language search query for a RAG system.\"\n )\n\n\nclass QueryPlan(BaseModel):\n \"\"\"A plan for a RAG system. After running the plan, we should have either enough information to answer the user's original query, or enough information to form a new query plan.\"\"\"\n\n items: list[QueryPlanItem] = Field(\n description=\"A list of the QueryPlanItem objects in the plan.\"\n )" ] }, { @@ -131,144 +113,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " StopEvent,\n", - " StartEvent,\n", - " Context,\n", - " step,\n", - ")\n", - "from llama_index.core.prompts import PromptTemplate\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "class QueryPlanningWorkflow(Workflow):\n", - " llm = OpenAI(model=\"gpt-4o\")\n", - " planning_prompt = PromptTemplate(\n", - " \"Think step by step. Given an initial query, as well as information about the indexes you can query, return a plan for a RAG system.\\n\"\n", - " \"The plan should be a list of QueryPlanItem objects, where each object contains a query.\\n\"\n", - " \"The result of executing an entire plan should provide a result that is a substantial answer to the initial query, \"\n", - " \"or enough information to form a new query plan.\\n\"\n", - " \"Sources you can query: {context}\\n\"\n", - " \"Initial query: {query}\\n\"\n", - " \"Plan:\"\n", - " )\n", - " decision_prompt = PromptTemplate(\n", - " \"Given the following information, return a final response that satisfies the original query, or return 'PLAN' if you need to continue planning.\\n\"\n", - " \"Original query: {query}\\n\"\n", - " \"Current results: {results}\\n\"\n", - " )\n", - "\n", - " @step\n", - " async def planning_step(\n", - " self, ctx: Context, ev: StartEvent | ExecutedPlanEvent\n", - " ) -> QueryPlanItem | StopEvent:\n", - " if isinstance(ev, StartEvent):\n", - " # Initially, we need to plan\n", - " query = ev.get(\"query\")\n", - "\n", - " tools = ev.get(\"tools\")\n", - "\n", - " await ctx.store.set(\"tools\", {t.metadata.name: t for t in tools})\n", - " await ctx.store.set(\"original_query\", query)\n", - "\n", - " context_str = \"\\n\".join(\n", - " [\n", - " f\"{i+1}. {tool.metadata.name}: {tool.metadata.description}\"\n", - " for i, tool in enumerate(tools)\n", - " ]\n", - " )\n", - " await ctx.store.set(\"context\", context_str)\n", - "\n", - " query_plan = await self.llm.astructured_predict(\n", - " QueryPlan,\n", - " self.planning_prompt,\n", - " context=context_str,\n", - " query=query,\n", - " )\n", - "\n", - " ctx.write_event_to_stream(\n", - " Event(msg=f\"Planning step: {query_plan}\")\n", - " )\n", - "\n", - " num_items = len(query_plan.items)\n", - " await ctx.store.set(\"num_items\", num_items)\n", - " for item in query_plan.items:\n", - " ctx.send_event(item)\n", - " else:\n", - " # If we've already gone through planning and executing, we need to decide\n", - " # if we should continue planning or if we can stop and return a result.\n", - " query = await ctx.store.get(\"original_query\")\n", - " current_results_str = ev.result\n", - "\n", - " decision = await self.llm.apredict(\n", - " self.decision_prompt,\n", - " query=query,\n", - " results=current_results_str,\n", - " )\n", - "\n", - " # Simple string matching to see if we need to keep planning or if we can stop.\n", - " if \"PLAN\" in decision:\n", - " context_str = await ctx.store.get(\"context\")\n", - " query_plan = await self.llm.astructured_predict(\n", - " QueryPlan,\n", - " self.planning_prompt,\n", - " context=context_str,\n", - " query=query,\n", - " )\n", - "\n", - " ctx.write_event_to_stream(\n", - " Event(msg=f\"Re-Planning step: {query_plan}\")\n", - " )\n", - "\n", - " num_items = len(query_plan.items)\n", - " await ctx.store.set(\"num_items\", num_items)\n", - " for item in query_plan.items:\n", - " ctx.send_event(item)\n", - " else:\n", - " return StopEvent(result=decision)\n", - "\n", - " @step(num_workers=4)\n", - " async def execute_item(\n", - " self, ctx: Context, ev: QueryPlanItem\n", - " ) -> QueryPlanItemResult:\n", - " tools = await ctx.store.get(\"tools\")\n", - " tool = tools[ev.name]\n", - "\n", - " ctx.write_event_to_stream(\n", - " Event(\n", - " msg=f\"Querying tool {tool.metadata.name} with query: {ev.query}\"\n", - " )\n", - " )\n", - "\n", - " result = await tool.acall(ev.query)\n", - "\n", - " ctx.write_event_to_stream(\n", - " Event(msg=f\"Tool {tool.metadata.name} returned: {result}\")\n", - " )\n", - "\n", - " return QueryPlanItemResult(query=ev.query, result=str(result))\n", - "\n", - " @step\n", - " async def aggregate_results(\n", - " self, ctx: Context, ev: QueryPlanItemResult\n", - " ) -> ExecutedPlanEvent:\n", - " # We need to collect the results of the query plan items to aggregate them.\n", - " num_items = await ctx.store.get(\"num_items\")\n", - " results = ctx.collect_events(ev, [QueryPlanItemResult] * num_items)\n", - "\n", - " # collect_events returns None if not all events were found\n", - " # return and wait for the remaining events to come in.\n", - " if results is None:\n", - " return\n", - "\n", - " aggregated_result = \"\\n------\\n\".join(\n", - " [\n", - " f\"{i+1}. {result.query}: {result.result}\"\n", - " for i, result in enumerate(results)\n", - " ]\n", - " )\n", - " return ExecutedPlanEvent(result=aggregated_result)" + "from workflows import Workflow, Context, step\nfrom workflows.events import StopEvent, StartEvent\nfrom llama_index.core.prompts import PromptTemplate\nfrom llama_index.llms.openai import OpenAI\n\n\nclass QueryPlanningWorkflow(Workflow):\n llm = OpenAI(model=\"gpt-4o\")\n planning_prompt = PromptTemplate(\n \"Think step by step. Given an initial query, as well as information about the indexes you can query, return a plan for a RAG system.\\n\"\n \"The plan should be a list of QueryPlanItem objects, where each object contains a query.\\n\"\n \"The result of executing an entire plan should provide a result that is a substantial answer to the initial query, \"\n \"or enough information to form a new query plan.\\n\"\n \"Sources you can query: {context}\\n\"\n \"Initial query: {query}\\n\"\n \"Plan:\"\n )\n decision_prompt = PromptTemplate(\n \"Given the following information, return a final response that satisfies the original query, or return 'PLAN' if you need to continue planning.\\n\"\n \"Original query: {query}\\n\"\n \"Current results: {results}\\n\"\n )\n\n @step\n async def planning_step(\n self, ctx: Context, ev: StartEvent | ExecutedPlanEvent\n ) -> QueryPlanItem | StopEvent:\n if isinstance(ev, StartEvent):\n # Initially, we need to plan\n query = ev.get(\"query\")\n\n tools = ev.get(\"tools\")\n\n await ctx.store.set(\"tools\", {t.metadata.name: t for t in tools})\n await ctx.store.set(\"original_query\", query)\n\n context_str = \"\\n\".join(\n [\n f\"{i+1}. {tool.metadata.name}: {tool.metadata.description}\"\n for i, tool in enumerate(tools)\n ]\n )\n await ctx.store.set(\"context\", context_str)\n\n query_plan = await self.llm.astructured_predict(\n QueryPlan,\n self.planning_prompt,\n context=context_str,\n query=query,\n )\n\n ctx.write_event_to_stream(\n Event(msg=f\"Planning step: {query_plan}\")\n )\n\n num_items = len(query_plan.items)\n await ctx.store.set(\"num_items\", num_items)\n for item in query_plan.items:\n ctx.send_event(item)\n else:\n # If we've already gone through planning and executing, we need to decide\n # if we should continue planning or if we can stop and return a result.\n query = await ctx.store.get(\"original_query\")\n current_results_str = ev.result\n\n decision = await self.llm.apredict(\n self.decision_prompt,\n query=query,\n results=current_results_str,\n )\n\n # Simple string matching to see if we need to keep planning or if we can stop.\n if \"PLAN\" in decision:\n context_str = await ctx.store.get(\"context\")\n query_plan = await self.llm.astructured_predict(\n QueryPlan,\n self.planning_prompt,\n context=context_str,\n query=query,\n )\n\n ctx.write_event_to_stream(\n Event(msg=f\"Re-Planning step: {query_plan}\")\n )\n\n num_items = len(query_plan.items)\n await ctx.store.set(\"num_items\", num_items)\n for item in query_plan.items:\n ctx.send_event(item)\n else:\n return StopEvent(result=decision)\n\n @step(num_workers=4)\n async def execute_item(\n self, ctx: Context, ev: QueryPlanItem\n ) -> QueryPlanItemResult:\n tools = await ctx.store.get(\"tools\")\n tool = tools[ev.name]\n\n ctx.write_event_to_stream(\n Event(\n msg=f\"Querying tool {tool.metadata.name} with query: {ev.query}\"\n )\n )\n\n result = await tool.acall(ev.query)\n\n ctx.write_event_to_stream(\n Event(msg=f\"Tool {tool.metadata.name} returned: {result}\")\n )\n\n return QueryPlanItemResult(query=ev.query, result=str(result))\n\n @step\n async def aggregate_results(\n self, ctx: Context, ev: QueryPlanItemResult\n ) -> ExecutedPlanEvent:\n # We need to collect the results of the query plan items to aggregate them.\n num_items = await ctx.store.get(\"num_items\")\n results = ctx.collect_events(ev, [QueryPlanItemResult] * num_items)\n\n # collect_events returns None if not all events were found\n # return and wait for the remaining events to come in.\n if results is None:\n return\n\n aggregated_result = \"\\n------\\n\".join(\n [\n f\"{i+1}. {result.query}: {result.result}\"\n for i, result in enumerate(results)\n ]\n )\n return ExecutedPlanEvent(result=aggregated_result)" ] }, { @@ -483,4 +328,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/rag.ipynb b/docs/examples/workflow/rag.ipynb index 3530d8567e..2a9cc47eda 100644 --- a/docs/examples/workflow/rag.ipynb +++ b/docs/examples/workflow/rag.ipynb @@ -133,20 +133,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "from llama_index.core.schema import NodeWithScore\n", - "\n", - "\n", - "class RetrieverEvent(Event):\n", - " \"\"\"Result of running retrieval\"\"\"\n", - "\n", - " nodes: list[NodeWithScore]\n", - "\n", - "\n", - "class RerankEvent(Event):\n", - " \"\"\"Result of running reranking on retrieved nodes\"\"\"\n", - "\n", - " nodes: list[NodeWithScore]" + "from workflows.events import Event\nfrom llama_index.core.schema import NodeWithScore\n\n\nclass RetrieverEvent(Event):\n \"\"\"Result of running retrieval\"\"\"\n\n nodes: list[NodeWithScore]\n\n\nclass RerankEvent(Event):\n \"\"\"Result of running reranking on retrieved nodes\"\"\"\n\n nodes: list[NodeWithScore]" ] }, { @@ -166,84 +153,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core import SimpleDirectoryReader, VectorStoreIndex\n", - "from llama_index.core.response_synthesizers import CompactAndRefine\n", - "from llama_index.core.postprocessor.llm_rerank import LLMRerank\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.embeddings.openai import OpenAIEmbedding\n", - "\n", - "\n", - "class RAGWorkflow(Workflow):\n", - " @step\n", - " async def ingest(self, ctx: Context, ev: StartEvent) -> StopEvent | None:\n", - " \"\"\"Entry point to ingest a document, triggered by a StartEvent with `dirname`.\"\"\"\n", - " dirname = ev.get(\"dirname\")\n", - " if not dirname:\n", - " return None\n", - "\n", - " documents = SimpleDirectoryReader(dirname).load_data()\n", - " index = VectorStoreIndex.from_documents(\n", - " documents=documents,\n", - " embed_model=OpenAIEmbedding(model_name=\"text-embedding-3-small\"),\n", - " )\n", - " return StopEvent(result=index)\n", - "\n", - " @step\n", - " async def retrieve(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> RetrieverEvent | None:\n", - " \"Entry point for RAG, triggered by a StartEvent with `query`.\"\n", - " query = ev.get(\"query\")\n", - " index = ev.get(\"index\")\n", - "\n", - " if not query:\n", - " return None\n", - "\n", - " print(f\"Query the database with: {query}\")\n", - "\n", - " # store the query in the global context\n", - " await ctx.store.set(\"query\", query)\n", - "\n", - " # get the index from the global context\n", - " if index is None:\n", - " print(\"Index is empty, load some documents before querying!\")\n", - " return None\n", - "\n", - " retriever = index.as_retriever(similarity_top_k=2)\n", - " nodes = await retriever.aretrieve(query)\n", - " print(f\"Retrieved {len(nodes)} nodes.\")\n", - " return RetrieverEvent(nodes=nodes)\n", - "\n", - " @step\n", - " async def rerank(self, ctx: Context, ev: RetrieverEvent) -> RerankEvent:\n", - " # Rerank the nodes\n", - " ranker = LLMRerank(\n", - " choice_batch_size=5, top_n=3, llm=OpenAI(model=\"gpt-4o-mini\")\n", - " )\n", - " print(await ctx.store.get(\"query\", default=None), flush=True)\n", - " new_nodes = ranker.postprocess_nodes(\n", - " ev.nodes, query_str=await ctx.store.get(\"query\", default=None)\n", - " )\n", - " print(f\"Reranked nodes to {len(new_nodes)}\")\n", - " return RerankEvent(nodes=new_nodes)\n", - "\n", - " @step\n", - " async def synthesize(self, ctx: Context, ev: RerankEvent) -> StopEvent:\n", - " \"\"\"Return a streaming response using reranked nodes.\"\"\"\n", - " llm = OpenAI(model=\"gpt-4o-mini\")\n", - " summarizer = CompactAndRefine(llm=llm, streaming=True, verbose=True)\n", - " query = await ctx.store.get(\"query\", default=None)\n", - "\n", - " response = await summarizer.asynthesize(query, nodes=ev.nodes)\n", - " return StopEvent(result=response)" + "from llama_index.core import SimpleDirectoryReader, VectorStoreIndex\nfrom llama_index.core.response_synthesizers import CompactAndRefine\nfrom llama_index.core.postprocessor.llm_rerank import LLMRerank\nfrom workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.embeddings.openai import OpenAIEmbedding\n\n\nclass RAGWorkflow(Workflow):\n @step\n async def ingest(self, ctx: Context, ev: StartEvent) -> StopEvent | None:\n \"\"\"Entry point to ingest a document, triggered by a StartEvent with `dirname`.\"\"\"\n dirname = ev.get(\"dirname\")\n if not dirname:\n return None\n\n documents = SimpleDirectoryReader(dirname).load_data()\n index = VectorStoreIndex.from_documents(\n documents=documents,\n embed_model=OpenAIEmbedding(model_name=\"text-embedding-3-small\"),\n )\n return StopEvent(result=index)\n\n @step\n async def retrieve(\n self, ctx: Context, ev: StartEvent\n ) -> RetrieverEvent | None:\n \"Entry point for RAG, triggered by a StartEvent with `query`.\"\n query = ev.get(\"query\")\n index = ev.get(\"index\")\n\n if not query:\n return None\n\n print(f\"Query the database with: {query}\")\n\n # store the query in the global context\n await ctx.store.set(\"query\", query)\n\n # get the index from the global context\n if index is None:\n print(\"Index is empty, load some documents before querying!\")\n return None\n\n retriever = index.as_retriever(similarity_top_k=2)\n nodes = await retriever.aretrieve(query)\n print(f\"Retrieved {len(nodes)} nodes.\")\n return RetrieverEvent(nodes=nodes)\n\n @step\n async def rerank(self, ctx: Context, ev: RetrieverEvent) -> RerankEvent:\n # Rerank the nodes\n ranker = LLMRerank(\n choice_batch_size=5, top_n=3, llm=OpenAI(model=\"gpt-4o-mini\")\n )\n print(await ctx.store.get(\"query\", default=None), flush=True)\n new_nodes = ranker.postprocess_nodes(\n ev.nodes, query_str=await ctx.store.get(\"query\", default=None)\n )\n print(f\"Reranked nodes to {len(new_nodes)}\")\n return RerankEvent(nodes=new_nodes)\n\n @step\n async def synthesize(self, ctx: Context, ev: RerankEvent) -> StopEvent:\n \"\"\"Return a streaming response using reranked nodes.\"\"\"\n llm = OpenAI(model=\"gpt-4o-mini\")\n summarizer = CompactAndRefine(llm=llm, streaming=True, verbose=True)\n query = await ctx.store.get(\"query\", default=None)\n\n response = await summarizer.asynthesize(query, nodes=ev.nodes)\n return StopEvent(result=response)" ] }, { @@ -326,4 +236,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/react_agent.ipynb b/docs/examples/workflow/react_agent.ipynb index 810261bbe1..f1946e8186 100644 --- a/docs/examples/workflow/react_agent.ipynb +++ b/docs/examples/workflow/react_agent.ipynb @@ -1,602 +1,597 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Workflow for a ReAct Agent\n", - "\n", - "This notebook walks through setting up a `Workflow` to construct a ReAct agent from (mostly) scratch.\n", - "\n", - "React calling agents work by prompting an LLM to either invoke tools/functions, or return a final response.\n", - "\n", - "Our workflow will be stateful with memory, and will be able to call the LLM to select tools and process incoming user messages." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install -U llama-index" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### [Optional] Set up observability with Llamatrace\n", - "\n", - "Set up tracing to visualize each step in the workflow." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install \"llama-index-core>=0.10.43\" \"openinference-instrumentation-llama-index>=2\" \"opentelemetry-proto>=1.12.0\" opentelemetry-exporter-otlp opentelemetry-sdk" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from opentelemetry.sdk import trace as trace_sdk\n", - "from opentelemetry.sdk.trace.export import SimpleSpanProcessor\n", - "from opentelemetry.exporter.otlp.proto.http.trace_exporter import (\n", - " OTLPSpanExporter as HTTPSpanExporter,\n", - ")\n", - "from openinference.instrumentation.llama_index import LlamaIndexInstrumentor\n", - "\n", - "\n", - "# Add Phoenix API Key for tracing\n", - "PHOENIX_API_KEY = \"\"\n", - "os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"api_key={PHOENIX_API_KEY}\"\n", - "\n", - "# Add Phoenix\n", - "span_phoenix_processor = SimpleSpanProcessor(\n", - " HTTPSpanExporter(endpoint=\"https://app.phoenix.arize.com/v1/traces\")\n", - ")\n", - "\n", - "# Add them to the tracer\n", - "tracer_provider = trace_sdk.TracerProvider()\n", - "tracer_provider.add_span_processor(span_processor=span_phoenix_processor)\n", - "\n", - "# Instrument the application\n", - "LlamaIndexInstrumentor().instrument(tracer_provider=tracer_provider)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since workflows are async first, this all runs fine in a notebook. If you were running in your own code, you would want to use `asyncio.run()` to start an async event loop if one isn't already running.\n", - "\n", - "```python\n", - "async def main():\n", - " \n", - "\n", - "if __name__ == \"__main__\":\n", - " import asyncio\n", - " asyncio.run(main())\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Designing the Workflow\n", - "\n", - "An agent consists of several steps\n", - "1. Handling the latest incoming user message, including adding to memory and preparing the chat history\n", - "2. Using the chat history and tools to construct a ReAct prompt\n", - "3. Calling the llm with the react prompt, and parsing out function/tool calls\n", - "4. If no tool calls, we can return\n", - "5. If there are tool calls, we need to execute them, and then loop back for a fresh ReAct prompt using the latest tool calls\n", - "\n", - "### The Workflow Events\n", - "\n", - "To handle these steps, we need to define a few events:\n", - "1. An event to handle new messages and prepare the chat history\n", - "2. An event to stream the LLM response\n", - "3. An event to prompt the LLM with the react prompt\n", - "4. An event to trigger tool calls, if any\n", - "5. An event to handle the results of tool calls, if any\n", - "\n", - "The other steps will use the built-in `StartEvent` and `StopEvent` events.\n", - "\n", - "In addition to events, we will also use the global context to store the current react reasoning!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.llms import ChatMessage\n", - "from llama_index.core.tools import ToolSelection, ToolOutput\n", - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class PrepEvent(Event):\n", - " pass\n", - "\n", - "\n", - "class InputEvent(Event):\n", - " input: list[ChatMessage]\n", - "\n", - "\n", - "class StreamEvent(Event):\n", - " delta: str\n", - "\n", - "\n", - "class ToolCallEvent(Event):\n", - " tool_calls: list[ToolSelection]\n", - "\n", - "\n", - "class FunctionOutputEvent(Event):\n", - " output: ToolOutput" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### The Workflow Itself\n", - "\n", - "With our events defined, we can construct our workflow and steps. \n", - "\n", - "Note that the workflow automatically validates itself using type annotations, so the type annotations on our steps are very helpful!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Any, List\n", - "\n", - "from llama_index.core.agent.react import ReActChatFormatter, ReActOutputParser\n", - "from llama_index.core.agent.react.types import (\n", - " ActionReasoningStep,\n", - " ObservationReasoningStep,\n", - ")\n", - "from llama_index.core.llms.llm import LLM\n", - "from llama_index.core.memory import ChatMemoryBuffer\n", - "from llama_index.core.tools.types import BaseTool\n", - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "class ReActAgent(Workflow):\n", - " def __init__(\n", - " self,\n", - " *args: Any,\n", - " llm: LLM | None = None,\n", - " tools: list[BaseTool] | None = None,\n", - " extra_context: str | None = None,\n", - " **kwargs: Any,\n", - " ) -> None:\n", - " super().__init__(*args, **kwargs)\n", - " self.tools = tools or []\n", - " self.llm = llm or OpenAI()\n", - " self.formatter = ReActChatFormatter.from_defaults(\n", - " context=extra_context or \"\"\n", - " )\n", - " self.output_parser = ReActOutputParser()\n", - "\n", - " @step\n", - " async def new_user_msg(self, ctx: Context, ev: StartEvent) -> PrepEvent:\n", - " # clear sources\n", - " await ctx.store.set(\"sources\", [])\n", - "\n", - " # init memory if needed\n", - " memory = await ctx.store.get(\"memory\", default=None)\n", - " if not memory:\n", - " memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\n", - "\n", - " # get user input\n", - " user_input = ev.input\n", - " user_msg = ChatMessage(role=\"user\", content=user_input)\n", - " memory.put(user_msg)\n", - "\n", - " # clear current reasoning\n", - " await ctx.store.set(\"current_reasoning\", [])\n", - "\n", - " # set memory\n", - " await ctx.store.set(\"memory\", memory)\n", - "\n", - " return PrepEvent()\n", - "\n", - " @step\n", - " async def prepare_chat_history(\n", - " self, ctx: Context, ev: PrepEvent\n", - " ) -> InputEvent:\n", - " # get chat history\n", - " memory = await ctx.store.get(\"memory\")\n", - " chat_history = memory.get()\n", - " current_reasoning = await ctx.store.get(\n", - " \"current_reasoning\", default=[]\n", - " )\n", - "\n", - " # format the prompt with react instructions\n", - " llm_input = self.formatter.format(\n", - " self.tools, chat_history, current_reasoning=current_reasoning\n", - " )\n", - " return InputEvent(input=llm_input)\n", - "\n", - " @step\n", - " async def handle_llm_input(\n", - " self, ctx: Context, ev: InputEvent\n", - " ) -> ToolCallEvent | StopEvent:\n", - " chat_history = ev.input\n", - " current_reasoning = await ctx.store.get(\n", - " \"current_reasoning\", default=[]\n", - " )\n", - " memory = await ctx.store.get(\"memory\")\n", - "\n", - " response_gen = await self.llm.astream_chat(chat_history)\n", - " async for response in response_gen:\n", - " ctx.write_event_to_stream(StreamEvent(delta=response.delta or \"\"))\n", - "\n", - " try:\n", - " reasoning_step = self.output_parser.parse(response.message.content)\n", - " current_reasoning.append(reasoning_step)\n", - "\n", - " if reasoning_step.is_done:\n", - " memory.put(\n", - " ChatMessage(\n", - " role=\"assistant\", content=reasoning_step.response\n", - " )\n", - " )\n", - " await ctx.store.set(\"memory\", memory)\n", - " await ctx.store.set(\"current_reasoning\", current_reasoning)\n", - "\n", - " sources = await ctx.store.get(\"sources\", default=[])\n", - "\n", - " return StopEvent(\n", - " result={\n", - " \"response\": reasoning_step.response,\n", - " \"sources\": [sources],\n", - " \"reasoning\": current_reasoning,\n", - " }\n", - " )\n", - " elif isinstance(reasoning_step, ActionReasoningStep):\n", - " tool_name = reasoning_step.action\n", - " tool_args = reasoning_step.action_input\n", - " return ToolCallEvent(\n", - " tool_calls=[\n", - " ToolSelection(\n", - " tool_id=\"fake\",\n", - " tool_name=tool_name,\n", - " tool_kwargs=tool_args,\n", - " )\n", - " ]\n", - " )\n", - " except Exception as e:\n", - " current_reasoning.append(\n", - " ObservationReasoningStep(\n", - " observation=f\"There was an error in parsing my reasoning: {e}\"\n", - " )\n", - " )\n", - " await ctx.store.set(\"current_reasoning\", current_reasoning)\n", - "\n", - " # if no tool calls or final response, iterate again\n", - " return PrepEvent()\n", - "\n", - " @step\n", - " async def handle_tool_calls(\n", - " self, ctx: Context, ev: ToolCallEvent\n", - " ) -> PrepEvent:\n", - " tool_calls = ev.tool_calls\n", - " tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}\n", - " current_reasoning = await ctx.store.get(\n", - " \"current_reasoning\", default=[]\n", - " )\n", - " sources = await ctx.store.get(\"sources\", default=[])\n", - "\n", - " # call tools -- safely!\n", - " for tool_call in tool_calls:\n", - " tool = tools_by_name.get(tool_call.tool_name)\n", - " if not tool:\n", - " current_reasoning.append(\n", - " ObservationReasoningStep(\n", - " observation=f\"Tool {tool_call.tool_name} does not exist\"\n", - " )\n", - " )\n", - " continue\n", - "\n", - " try:\n", - " tool_output = tool(**tool_call.tool_kwargs)\n", - " sources.append(tool_output)\n", - " current_reasoning.append(\n", - " ObservationReasoningStep(observation=tool_output.content)\n", - " )\n", - " except Exception as e:\n", - " current_reasoning.append(\n", - " ObservationReasoningStep(\n", - " observation=f\"Error calling tool {tool.metadata.get_name()}: {e}\"\n", - " )\n", - " )\n", - "\n", - " # save new state in context\n", - " await ctx.store.set(\"sources\", sources)\n", - " await ctx.store.set(\"current_reasoning\", current_reasoning)\n", - "\n", - " # prep the next iteraiton\n", - " return PrepEvent()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And thats it! Let's explore the workflow we wrote a bit.\n", - "\n", - "`new_user_msg()`:\n", - "Adds the user message to memory, and clears the global context to keep track of a fresh string of reasoning.\n", - "\n", - "`prepare_chat_history()`:\n", - "Prepares the react prompt, using the chat history, tools, and current reasoning (if any)\n", - "\n", - "`handle_llm_input()`:\n", - "Prompts the LLM with our react prompt, and uses some utility functions to parse the output. If there are no tool calls, we can stop and emit a `StopEvent`. Otherwise, we emit a `ToolCallEvent` to handle tool calls. Lastly, if there are no tool calls, and no final response, we simply loop again.\n", - "\n", - "`handle_tool_calls()`:\n", - "Safely calls tools with error handling, adding the tool outputs to the current reasoning. Then, by emitting a `PrepEvent`, we loop around for another round of ReAct prompting and parsing." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run the Workflow!\n", - "\n", - "**NOTE:** With loops, we need to be mindful of runtime. Here, we set a timeout of 120s." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step new_user_msg\n", - "Step new_user_msg produced event PrepEvent\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n" - ] - } - ], - "source": [ - "from llama_index.core.tools import FunctionTool\n", - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "def add(x: int, y: int) -> int:\n", - " \"\"\"Useful function to add two numbers.\"\"\"\n", - " return x + y\n", - "\n", - "\n", - "def multiply(x: int, y: int) -> int:\n", - " \"\"\"Useful function to multiply two numbers.\"\"\"\n", - " return x * y\n", - "\n", - "\n", - "tools = [\n", - " FunctionTool.from_defaults(add),\n", - " FunctionTool.from_defaults(multiply),\n", - "]\n", - "\n", - "agent = ReActAgent(\n", - " llm=OpenAI(model=\"gpt-4o\"), tools=tools, timeout=120, verbose=True\n", - ")\n", - "\n", - "ret = await agent.run(input=\"Hello!\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Workflow for a ReAct Agent\n", + "\n", + "This notebook walks through setting up a `Workflow` to construct a ReAct agent from (mostly) scratch.\n", + "\n", + "React calling agents work by prompting an LLM to either invoke tools/functions, or return a final response.\n", + "\n", + "Our workflow will be stateful with memory, and will be able to call the LLM to select tools and process incoming user messages." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Hello! How can I assist you today?\n" - ] - } - ], - "source": [ - "print(ret[\"response\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "!pip install -U llama-index" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step new_user_msg\n", - "Step new_user_msg produced event PrepEvent\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event ToolCallEvent\n", - "Running step handle_tool_calls\n", - "Step handle_tool_calls produced event PrepEvent\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event ToolCallEvent\n", - "Running step handle_tool_calls\n", - "Step handle_tool_calls produced event PrepEvent\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n" - ] - } - ], - "source": [ - "ret = await agent.run(input=\"What is (2123 + 2321) * 312?\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "The result of (2123 + 2321) * 312 is 1,386,528.\n" - ] - } - ], - "source": [ - "print(ret[\"response\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Chat History\n", - "\n", - "By default, the workflow is creating a fresh `Context` for each run. This means that the chat history is not preserved between runs. However, we can pass our own `Context` to the workflow to preserve chat history." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### [Optional] Set up observability with Llamatrace\n", + "\n", + "Set up tracing to visualize each step in the workflow." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step new_user_msg\n", - "Step new_user_msg produced event PrepEvent\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n", - "Hello, Logan! How can I assist you today?\n", - "Running step new_user_msg\n", - "Step new_user_msg produced event PrepEvent\n", - "Running step prepare_chat_history\n", - "Step prepare_chat_history produced event InputEvent\n", - "Running step handle_llm_input\n", - "Step handle_llm_input produced event StopEvent\n", - "Your name is Logan.\n" - ] - } - ], - "source": [ - "from llama_index.core.workflow import Context\n", - "\n", - "ctx = Context(agent)\n", - "\n", - "ret = await agent.run(input=\"Hello! My name is Logan\", ctx=ctx)\n", - "print(ret[\"response\"])\n", - "\n", - "ret = await agent.run(input=\"What is my name?\", ctx=ctx)\n", - "print(ret[\"response\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Streaming\n", - "\n", - "We can also access the streaming response from the LLM, using the `handler` object returned from the `.run()` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "metadata": {}, + "source": [ + "!pip install \"llama-index-core>=0.10.43\" \"openinference-instrumentation-llama-index>=2\" \"opentelemetry-proto>=1.12.0\" opentelemetry-exporter-otlp opentelemetry-sdk" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from opentelemetry.sdk import trace as trace_sdk\n", + "from opentelemetry.sdk.trace.export import SimpleSpanProcessor\n", + "from opentelemetry.exporter.otlp.proto.http.trace_exporter import (\n", + " OTLPSpanExporter as HTTPSpanExporter,\n", + ")\n", + "from openinference.instrumentation.llama_index import LlamaIndexInstrumentor\n", + "\n", + "\n", + "# Add Phoenix API Key for tracing\n", + "PHOENIX_API_KEY = \"\"\n", + "os.environ[\"OTEL_EXPORTER_OTLP_HEADERS\"] = f\"api_key={PHOENIX_API_KEY}\"\n", + "\n", + "# Add Phoenix\n", + "span_phoenix_processor = SimpleSpanProcessor(\n", + " HTTPSpanExporter(endpoint=\"https://app.phoenix.arize.com/v1/traces\")\n", + ")\n", + "\n", + "# Add them to the tracer\n", + "tracer_provider = trace_sdk.TracerProvider()\n", + "tracer_provider.add_span_processor(span_processor=span_phoenix_processor)\n", + "\n", + "# Instrument the application\n", + "LlamaIndexInstrumentor().instrument(tracer_provider=tracer_provider)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since workflows are async first, this all runs fine in a notebook. If you were running in your own code, you would want to use `asyncio.run()` to start an async event loop if one isn't already running.\n", + "\n", + "```python\n", + "async def main():\n", + " \n", + "\n", + "if __name__ == \"__main__\":\n", + " import asyncio\n", + " asyncio.run(main())\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Designing the Workflow\n", + "\n", + "An agent consists of several steps\n", + "1. Handling the latest incoming user message, including adding to memory and preparing the chat history\n", + "2. Using the chat history and tools to construct a ReAct prompt\n", + "3. Calling the llm with the react prompt, and parsing out function/tool calls\n", + "4. If no tool calls, we can return\n", + "5. If there are tool calls, we need to execute them, and then loop back for a fresh ReAct prompt using the latest tool calls\n", + "\n", + "### The Workflow Events\n", + "\n", + "To handle these steps, we need to define a few events:\n", + "1. An event to handle new messages and prepare the chat history\n", + "2. An event to stream the LLM response\n", + "3. An event to prompt the LLM with the react prompt\n", + "4. An event to trigger tool calls, if any\n", + "5. An event to handle the results of tool calls, if any\n", + "\n", + "The other steps will use the built-in `StartEvent` and `StopEvent` events.\n", + "\n", + "In addition to events, we will also use the global context to store the current react reasoning!" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.llms import ChatMessage\n", + "from llama_index.core.tools import ToolSelection, ToolOutput\n", + "from workflows.events import Event\n", + "\n", + "\n", + "class PrepEvent(Event):\n", + " pass\n", + "\n", + "\n", + "class InputEvent(Event):\n", + " input: list[ChatMessage]\n", + "\n", + "\n", + "class StreamEvent(Event):\n", + " delta: str\n", + "\n", + "\n", + "class ToolCallEvent(Event):\n", + " tool_calls: list[ToolSelection]\n", + "\n", + "\n", + "class FunctionOutputEvent(Event):\n", + " output: ToolOutput" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Workflow Itself\n", + "\n", + "With our events defined, we can construct our workflow and steps. \n", + "\n", + "Note that the workflow automatically validates itself using type annotations, so the type annotations on our steps are very helpful!" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from typing import Any, List\n", + "\n", + "from llama_index.core.agent.react import ReActChatFormatter, ReActOutputParser\n", + "from llama_index.core.agent.react.types import (\n", + " ActionReasoningStep,\n", + " ObservationReasoningStep,\n", + ")\n", + "from llama_index.core.llms.llm import LLM\n", + "from llama_index.core.memory import ChatMemoryBuffer\n", + "from llama_index.core.tools.types import BaseTool\n", + "from workflows import Context, Workflow, step\n", + "from workflows.events import StartEvent, StopEvent\n", + "from llama_index.llms.openai import OpenAI\n", + "\n", + "\n", + "class ReActAgent(Workflow):\n", + " def __init__(\n", + " self,\n", + " *args: Any,\n", + " llm: LLM | None = None,\n", + " tools: list[BaseTool] | None = None,\n", + " extra_context: str | None = None,\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " super().__init__(*args, **kwargs)\n", + " self.tools = tools or []\n", + " self.llm = llm or OpenAI()\n", + " self.formatter = ReActChatFormatter.from_defaults(\n", + " context=extra_context or \"\"\n", + " )\n", + " self.output_parser = ReActOutputParser()\n", + "\n", + " @step\n", + " async def new_user_msg(self, ctx: Context, ev: StartEvent) -> PrepEvent:\n", + " # clear sources\n", + " await ctx.store.set(\"sources\", [])\n", + "\n", + " # init memory if needed\n", + " memory = await ctx.store.get(\"memory\", default=None)\n", + " if not memory:\n", + " memory = ChatMemoryBuffer.from_defaults(llm=self.llm)\n", + "\n", + " # get user input\n", + " user_input = ev.input\n", + " user_msg = ChatMessage(role=\"user\", content=user_input)\n", + " memory.put(user_msg)\n", + "\n", + " # clear current reasoning\n", + " await ctx.store.set(\"current_reasoning\", [])\n", + "\n", + " # set memory\n", + " await ctx.store.set(\"memory\", memory)\n", + "\n", + " return PrepEvent()\n", + "\n", + " @step\n", + " async def prepare_chat_history(\n", + " self, ctx: Context, ev: PrepEvent\n", + " ) -> InputEvent:\n", + " # get chat history\n", + " memory = await ctx.store.get(\"memory\")\n", + " chat_history = memory.get()\n", + " current_reasoning = await ctx.store.get(\n", + " \"current_reasoning\", default=[]\n", + " )\n", + "\n", + " # format the prompt with react instructions\n", + " llm_input = self.formatter.format(\n", + " self.tools, chat_history, current_reasoning=current_reasoning\n", + " )\n", + " return InputEvent(input=llm_input)\n", + "\n", + " @step\n", + " async def handle_llm_input(\n", + " self, ctx: Context, ev: InputEvent\n", + " ) -> ToolCallEvent | StopEvent:\n", + " chat_history = ev.input\n", + " current_reasoning = await ctx.store.get(\n", + " \"current_reasoning\", default=[]\n", + " )\n", + " memory = await ctx.store.get(\"memory\")\n", + "\n", + " response_gen = await self.llm.astream_chat(chat_history)\n", + " async for response in response_gen:\n", + " ctx.write_event_to_stream(StreamEvent(delta=response.delta or \"\"))\n", + "\n", + " try:\n", + " reasoning_step = self.output_parser.parse(response.message.content)\n", + " current_reasoning.append(reasoning_step)\n", + "\n", + " if reasoning_step.is_done:\n", + " memory.put(\n", + " ChatMessage(\n", + " role=\"assistant\", content=reasoning_step.response\n", + " )\n", + " )\n", + " await ctx.store.set(\"memory\", memory)\n", + " await ctx.store.set(\"current_reasoning\", current_reasoning)\n", + "\n", + " sources = await ctx.store.get(\"sources\", default=[])\n", + "\n", + " return StopEvent(\n", + " result={\n", + " \"response\": reasoning_step.response,\n", + " \"sources\": [sources],\n", + " \"reasoning\": current_reasoning,\n", + " }\n", + " )\n", + " elif isinstance(reasoning_step, ActionReasoningStep):\n", + " tool_name = reasoning_step.action\n", + " tool_args = reasoning_step.action_input\n", + " return ToolCallEvent(\n", + " tool_calls=[\n", + " ToolSelection(\n", + " tool_id=\"fake\",\n", + " tool_name=tool_name,\n", + " tool_kwargs=tool_args,\n", + " )\n", + " ]\n", + " )\n", + " except Exception as e:\n", + " current_reasoning.append(\n", + " ObservationReasoningStep(\n", + " observation=f\"There was an error in parsing my reasoning: {e}\"\n", + " )\n", + " )\n", + " await ctx.store.set(\"current_reasoning\", current_reasoning)\n", + "\n", + " # if no tool calls or final response, iterate again\n", + " return PrepEvent()\n", + "\n", + " @step\n", + " async def handle_tool_calls(\n", + " self, ctx: Context, ev: ToolCallEvent\n", + " ) -> PrepEvent:\n", + " tool_calls = ev.tool_calls\n", + " tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}\n", + " current_reasoning = await ctx.store.get(\n", + " \"current_reasoning\", default=[]\n", + " )\n", + " sources = await ctx.store.get(\"sources\", default=[])\n", + "\n", + " # call tools -- safely!\n", + " for tool_call in tool_calls:\n", + " tool = tools_by_name.get(tool_call.tool_name)\n", + " if not tool:\n", + " current_reasoning.append(\n", + " ObservationReasoningStep(\n", + " observation=f\"Tool {tool_call.tool_name} does not exist\"\n", + " )\n", + " )\n", + " continue\n", + "\n", + " try:\n", + " tool_output = tool(**tool_call.tool_kwargs)\n", + " sources.append(tool_output)\n", + " current_reasoning.append(\n", + " ObservationReasoningStep(observation=tool_output.content)\n", + " )\n", + " except Exception as e:\n", + " current_reasoning.append(\n", + " ObservationReasoningStep(\n", + " observation=f\"Error calling tool {tool.metadata.get_name()}: {e}\"\n", + " )\n", + " )\n", + "\n", + " # save new state in context\n", + " await ctx.store.set(\"sources\", sources)\n", + " await ctx.store.set(\"current_reasoning\", current_reasoning)\n", + "\n", + " # prep the next iteraiton\n", + " return PrepEvent()" + ], + "execution_count": null, + "outputs": [] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Thought: The current language of the user is: English. I cannot use a tool to help me answer the question.\n", - "Answer: Why don't scientists trust atoms? Because they make up everything!" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And thats it! Let's explore the workflow we wrote a bit.\n", + "\n", + "`new_user_msg()`:\n", + "Adds the user message to memory, and clears the global context to keep track of a fresh string of reasoning.\n", + "\n", + "`prepare_chat_history()`:\n", + "Prepares the react prompt, using the chat history, tools, and current reasoning (if any)\n", + "\n", + "`handle_llm_input()`:\n", + "Prompts the LLM with our react prompt, and uses some utility functions to parse the output. If there are no tool calls, we can stop and emit a `StopEvent`. Otherwise, we emit a `ToolCallEvent` to handle tool calls. Lastly, if there are no tool calls, and no final response, we simply loop again.\n", + "\n", + "`handle_tool_calls()`:\n", + "Safely calls tools with error handling, adding the tool outputs to the current reasoning. Then, by emitting a `PrepEvent`, we loop around for another round of ReAct prompting and parsing." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run the Workflow!\n", + "\n", + "**NOTE:** With loops, we need to be mindful of runtime. Here, we set a timeout of 120s." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.core.tools import FunctionTool\n", + "from llama_index.llms.openai import OpenAI\n", + "\n", + "\n", + "def add(x: int, y: int) -> int:\n", + " \"\"\"Useful function to add two numbers.\"\"\"\n", + " return x + y\n", + "\n", + "\n", + "def multiply(x: int, y: int) -> int:\n", + " \"\"\"Useful function to multiply two numbers.\"\"\"\n", + " return x * y\n", + "\n", + "\n", + "tools = [\n", + " FunctionTool.from_defaults(add),\n", + " FunctionTool.from_defaults(multiply),\n", + "]\n", + "\n", + "agent = ReActAgent(\n", + " llm=OpenAI(model=\"gpt-4o\"), tools=tools, timeout=120, verbose=True\n", + ")\n", + "\n", + "ret = await agent.run(input=\"Hello!\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step new_user_msg\n", + "Step new_user_msg produced event PrepEvent\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "print(ret[\"response\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello! How can I assist you today?\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "ret = await agent.run(input=\"What is (2123 + 2321) * 312?\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step new_user_msg\n", + "Step new_user_msg produced event PrepEvent\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event ToolCallEvent\n", + "Running step handle_tool_calls\n", + "Step handle_tool_calls produced event PrepEvent\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event ToolCallEvent\n", + "Running step handle_tool_calls\n", + "Step handle_tool_calls produced event PrepEvent\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "print(ret[\"response\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The result of (2123 + 2321) * 312 is 1,386,528.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chat History\n", + "\n", + "By default, the workflow is creating a fresh `Context` for each run. This means that the chat history is not preserved between runs. However, we can pass our own `Context` to the workflow to preserve chat history." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Context\n", + "\n", + "ctx = Context(agent)\n", + "\n", + "ret = await agent.run(input=\"Hello! My name is Logan\", ctx=ctx)\n", + "print(ret[\"response\"]) \n", + "\n", + "ret = await agent.run(input=\"What is my name?\", ctx=ctx)\n", + "print(ret[\"response\"])" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step new_user_msg\n", + "Step new_user_msg produced event PrepEvent\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n", + "Hello, Logan! How can I assist you today?\n", + "Running step new_user_msg\n", + "Step new_user_msg produced event PrepEvent\n", + "Running step prepare_chat_history\n", + "Step prepare_chat_history produced event InputEvent\n", + "Running step handle_llm_input\n", + "Step handle_llm_input produced event StopEvent\n", + "Your name is Logan.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Streaming\n", + "\n", + "We can also access the streaming response from the LLM, using the `handler` object returned from the `.run()` method." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "agent = ReActAgent(\n", + " llm=OpenAI(model=\"gpt-4o\"), tools=tools, timeout=120, verbose=False\n", + ")\n", + "\n", + "handler = agent.run(input=\"Hello! Tell me a joke.\")\n", + "\n", + "async for event in handler.stream_events():\n", + " if isinstance(event, StreamEvent):\n", + " print(event.delta, end=\"\", flush=True)\n", + "\n", + "response = await handler\n", + "# print(response)" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Thought: The current language of the user is: English. I cannot use a tool to help me answer the question.\n", + "Answer: Why don't scientists trust atoms? Because they make up everything!" + ] + } + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "llama-index-cDlKpkFt-py3.11", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } - ], - "source": [ - "agent = ReActAgent(\n", - " llm=OpenAI(model=\"gpt-4o\"), tools=tools, timeout=120, verbose=False\n", - ")\n", - "\n", - "handler = agent.run(input=\"Hello! Tell me a joke.\")\n", - "\n", - "async for event in handler.stream_events():\n", - " if isinstance(event, StreamEvent):\n", - " print(event.delta, end=\"\", flush=True)\n", - "\n", - "response = await handler\n", - "# print(response)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "llama-index-cDlKpkFt-py3.11", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/docs/examples/workflow/reflection.ipynb b/docs/examples/workflow/reflection.ipynb index a4cc666abd..110efef35b 100644 --- a/docs/examples/workflow/reflection.ipynb +++ b/docs/examples/workflow/reflection.ipynb @@ -63,18 +63,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class ExtractionDone(Event):\n", - " output: str\n", - " passage: str\n", - "\n", - "\n", - "class ValidationErrorEvent(Event):\n", - " error: str\n", - " wrong_output: str\n", - " passage: str" + "from workflows.events import Event\n\n\nclass ExtractionDone(Event):\n output: str\n passage: str\n\n\nclass ValidationErrorEvent(Event):\n error: str\n wrong_output: str\n passage: str" ] }, { @@ -122,95 +111,7 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n", - "\n", - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " Context,\n", - " step,\n", - ")\n", - "from llama_index.llms.ollama import Ollama\n", - "\n", - "EXTRACTION_PROMPT = \"\"\"\n", - "Context information is below:\n", - "---------------------\n", - "{passage}\n", - "---------------------\n", - "\n", - "Given the context information and not prior knowledge, create a JSON object from the information in the context.\n", - "The JSON object must follow the JSON schema:\n", - "{schema}\n", - "\n", - "\"\"\"\n", - "\n", - "REFLECTION_PROMPT = \"\"\"\n", - "You already created this output previously:\n", - "---------------------\n", - "{wrong_answer}\n", - "---------------------\n", - "\n", - "This caused the JSON decode error: {error}\n", - "\n", - "Try again, the response must contain only valid JSON code. Do not add any sentence before or after the JSON object.\n", - "Do not repeat the schema.\n", - "\"\"\"\n", - "\n", - "\n", - "class ReflectionWorkflow(Workflow):\n", - " max_retries: int = 3\n", - "\n", - " @step\n", - " async def extract(\n", - " self, ctx: Context, ev: StartEvent | ValidationErrorEvent\n", - " ) -> StopEvent | ExtractionDone:\n", - " current_retries = await ctx.store.get(\"retries\", default=0)\n", - " if current_retries >= self.max_retries:\n", - " return StopEvent(result=\"Max retries reached\")\n", - " else:\n", - " await ctx.store.set(\"retries\", current_retries + 1)\n", - "\n", - " if isinstance(ev, StartEvent):\n", - " passage = ev.get(\"passage\")\n", - " if not passage:\n", - " return StopEvent(result=\"Please provide some text in input\")\n", - " reflection_prompt = \"\"\n", - " elif isinstance(ev, ValidationErrorEvent):\n", - " passage = ev.passage\n", - " reflection_prompt = REFLECTION_PROMPT.format(\n", - " wrong_answer=ev.wrong_output, error=ev.error\n", - " )\n", - "\n", - " llm = Ollama(\n", - " model=\"llama3\",\n", - " request_timeout=30,\n", - " # Manually set the context window to limit memory usage\n", - " context_window=8000,\n", - " )\n", - " prompt = EXTRACTION_PROMPT.format(\n", - " passage=passage, schema=CarCollection.schema_json()\n", - " )\n", - " if reflection_prompt:\n", - " prompt += reflection_prompt\n", - "\n", - " output = await llm.acomplete(prompt)\n", - "\n", - " return ExtractionDone(output=str(output), passage=passage)\n", - "\n", - " @step\n", - " async def validate(\n", - " self, ev: ExtractionDone\n", - " ) -> StopEvent | ValidationErrorEvent:\n", - " try:\n", - " CarCollection.model_validate_json(ev.output)\n", - " except Exception as e:\n", - " print(\"Validation failed, retrying...\")\n", - " return ValidationErrorEvent(\n", - " error=str(e), wrong_output=ev.output, passage=ev.passage\n", - " )\n", - "\n", - " return StopEvent(result=ev.output)" + "import json\n\nfrom workflows import Workflow, Context, step\nfrom workflows.events import StartEvent, StopEvent\nfrom llama_index.llms.ollama import Ollama\n\nEXTRACTION_PROMPT = \"\"\"\nContext information is below:\n---------------------\n{passage}\n---------------------\n\nGiven the context information and not prior knowledge, create a JSON object from the information in the context.\nThe JSON object must follow the JSON schema:\n{schema}\n\n\"\"\"\n\nREFLECTION_PROMPT = \"\"\"\nYou already created this output previously:\n---------------------\n{wrong_answer}\n---------------------\n\nThis caused the JSON decode error: {error}\n\nTry again, the response must contain only valid JSON code. Do not add any sentence before or after the JSON object.\nDo not repeat the schema.\n\"\"\"\n\n\nclass ReflectionWorkflow(Workflow):\n max_retries: int = 3\n\n @step\n async def extract(\n self, ctx: Context, ev: StartEvent | ValidationErrorEvent\n ) -> StopEvent | ExtractionDone:\n current_retries = await ctx.store.get(\"retries\", default=0)\n if current_retries >= self.max_retries:\n return StopEvent(result=\"Max retries reached\")\n else:\n await ctx.store.set(\"retries\", current_retries + 1)\n\n if isinstance(ev, StartEvent):\n passage = ev.get(\"passage\")\n if not passage:\n return StopEvent(result=\"Please provide some text in input\")\n reflection_prompt = \"\"\n elif isinstance(ev, ValidationErrorEvent):\n passage = ev.passage\n reflection_prompt = REFLECTION_PROMPT.format(\n wrong_answer=ev.wrong_output, error=ev.error\n )\n\n llm = Ollama(\n model=\"llama3\",\n request_timeout=30,\n # Manually set the context window to limit memory usage\n context_window=8000,\n )\n prompt = EXTRACTION_PROMPT.format(\n passage=passage, schema=CarCollection.schema_json()\n )\n if reflection_prompt:\n prompt += reflection_prompt\n\n output = await llm.acomplete(prompt)\n\n return ExtractionDone(output=str(output), passage=passage)\n\n @step\n async def validate(\n self, ev: ExtractionDone\n ) -> StopEvent | ValidationErrorEvent:\n try:\n CarCollection.model_validate_json(ev.output)\n except Exception as e:\n print(\"Validation failed, retrying...\")\n return ValidationErrorEvent(\n error=str(e), wrong_output=ev.output, passage=ev.passage\n )\n\n return StopEvent(result=ev.output)" ] }, { @@ -305,4 +206,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/router_query_engine.ipynb b/docs/examples/workflow/router_query_engine.ipynb index 267869e95f..f9d318359a 100644 --- a/docs/examples/workflow/router_query_engine.ipynb +++ b/docs/examples/workflow/router_query_engine.ipynb @@ -69,23 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "from llama_index.core.base.base_selector import SelectorResult\n", - "from typing import Dict, List, Any\n", - "from llama_index.core.base.response.schema import RESPONSE_TYPE\n", - "\n", - "\n", - "class QueryEngineSelectionEvent(Event):\n", - " \"\"\"Result of selecting the query engine tools.\"\"\"\n", - "\n", - " selected_query_engines: SelectorResult\n", - "\n", - "\n", - "class SynthesizeEvent(Event):\n", - " \"\"\"Event for synthesizing the response from different query engines.\"\"\"\n", - "\n", - " result: List[RESPONSE_TYPE]\n", - " selected_query_engines: SelectorResult" + "from workflows.events import Event\nfrom llama_index.core.base.base_selector import SelectorResult\nfrom typing import Dict, List, Any\nfrom llama_index.core.base.response.schema import RESPONSE_TYPE\n\n\nclass QueryEngineSelectionEvent(Event):\n \"\"\"Result of selecting the query engine tools.\"\"\"\n\n selected_query_engines: SelectorResult\n\n\nclass SynthesizeEvent(Event):\n \"\"\"Event for synthesizing the response from different query engines.\"\"\"\n\n result: List[RESPONSE_TYPE]\n selected_query_engines: SelectorResult" ] }, { @@ -119,167 +103,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Context,\n", - " Workflow,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.core.selectors.utils import get_selector_from_llm\n", - "from llama_index.core.base.response.schema import (\n", - " PydanticResponse,\n", - " Response,\n", - " AsyncStreamingResponse,\n", - ")\n", - "from llama_index.core.bridge.pydantic import BaseModel\n", - "from llama_index.core.response_synthesizers import TreeSummarize\n", - "from llama_index.core.schema import QueryBundle\n", - "from llama_index.core import Settings\n", - "\n", - "from IPython.display import Markdown, display\n", - "import asyncio\n", - "\n", - "\n", - "class RouterQueryEngineWorkflow(Workflow):\n", - " @step\n", - " async def selector(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> QueryEngineSelectionEvent:\n", - " \"\"\"\n", - " Selects a single/ multiple query engines based on the query.\n", - " \"\"\"\n", - "\n", - " await ctx.store.set(\"query\", ev.get(\"query\"))\n", - " await ctx.store.set(\"llm\", ev.get(\"llm\"))\n", - " await ctx.store.set(\"query_engine_tools\", ev.get(\"query_engine_tools\"))\n", - " await ctx.store.set(\"summarizer\", ev.get(\"summarizer\"))\n", - "\n", - " llm = Settings.llm\n", - " select_multiple_query_engines = ev.get(\"select_multi\")\n", - " query = ev.get(\"query\")\n", - " query_engine_tools = ev.get(\"query_engine_tools\")\n", - "\n", - " selector = get_selector_from_llm(\n", - " llm, is_multi=select_multiple_query_engines\n", - " )\n", - "\n", - " query_engines_metadata = [\n", - " query_engine.metadata for query_engine in query_engine_tools\n", - " ]\n", - "\n", - " selected_query_engines = await selector.aselect(\n", - " query_engines_metadata, query\n", - " )\n", - "\n", - " return QueryEngineSelectionEvent(\n", - " selected_query_engines=selected_query_engines\n", - " )\n", - "\n", - " @step\n", - " async def generate_responses(\n", - " self, ctx: Context, ev: QueryEngineSelectionEvent\n", - " ) -> SynthesizeEvent:\n", - " \"\"\"Generate the responses from the selected query engines.\"\"\"\n", - "\n", - " query = await ctx.store.get(\"query\", default=None)\n", - " selected_query_engines = ev.selected_query_engines\n", - " query_engine_tools = await ctx.store.get(\"query_engine_tools\")\n", - "\n", - " query_engines = [engine.query_engine for engine in query_engine_tools]\n", - "\n", - " print(\n", - " f\"number of selected query engines: {len(selected_query_engines.selections)}\"\n", - " )\n", - "\n", - " if len(selected_query_engines.selections) > 1:\n", - " tasks = []\n", - " for selected_query_engine in selected_query_engines.selections:\n", - " print(\n", - " f\"Selected query engine: {selected_query_engine.index}: {selected_query_engine.reason}\"\n", - " )\n", - " query_engine = query_engines[selected_query_engine.index]\n", - " tasks.append(query_engine.aquery(query))\n", - "\n", - " response_generated = await asyncio.gather(*tasks)\n", - "\n", - " else:\n", - " query_engine = query_engines[\n", - " selected_query_engines.selections[0].index\n", - " ]\n", - "\n", - " print(\n", - " f\"Selected query engine: {selected_query_engines.ind}: {selected_query_engines.reason}\"\n", - " )\n", - "\n", - " response_generated = [await query_engine.aquery(query)]\n", - "\n", - " return SynthesizeEvent(\n", - " result=response_generated,\n", - " selected_query_engines=selected_query_engines,\n", - " )\n", - "\n", - " async def acombine_responses(\n", - " self,\n", - " summarizer: TreeSummarize,\n", - " responses: List[RESPONSE_TYPE],\n", - " query_bundle: QueryBundle,\n", - " ) -> RESPONSE_TYPE:\n", - " \"\"\"Async combine multiple response from sub-engines.\"\"\"\n", - "\n", - " print(\"Combining responses from multiple query engines.\")\n", - "\n", - " response_strs = []\n", - " source_nodes = []\n", - " for response in responses:\n", - " if isinstance(\n", - " response, (AsyncStreamingResponse, PydanticResponse)\n", - " ):\n", - " response_obj = await response.aget_response()\n", - " else:\n", - " response_obj = response\n", - " source_nodes.extend(response_obj.source_nodes)\n", - " response_strs.append(str(response))\n", - "\n", - " summary = await summarizer.aget_response(\n", - " query_bundle.query_str, response_strs\n", - " )\n", - "\n", - " if isinstance(summary, str):\n", - " return Response(response=summary, source_nodes=source_nodes)\n", - " elif isinstance(summary, BaseModel):\n", - " return PydanticResponse(\n", - " response=summary, source_nodes=source_nodes\n", - " )\n", - " else:\n", - " return AsyncStreamingResponse(\n", - " response_gen=summary, source_nodes=source_nodes\n", - " )\n", - "\n", - " @step\n", - " async def synthesize_responses(\n", - " self, ctx: Context, ev: SynthesizeEvent\n", - " ) -> StopEvent:\n", - " \"\"\"Synthesizes the responses from the generated responses.\"\"\"\n", - "\n", - " response_generated = ev.result\n", - " query = await ctx.store.get(\"query\", default=None)\n", - " summarizer = await ctx.store.get(\"summarizer\")\n", - " selected_query_engines = ev.selected_query_engines\n", - "\n", - " if len(response_generated) > 1:\n", - " response = await self.acombine_responses(\n", - " summarizer, response_generated, QueryBundle(query_str=query)\n", - " )\n", - " else:\n", - " response = response_generated[0]\n", - "\n", - " response.metadata = response.metadata or {}\n", - " response.metadata[\"selector_result\"] = selected_query_engines\n", - "\n", - " return StopEvent(result=response)" + "from workflows import Context, Workflow, step\nfrom workflows.events import StartEvent, StopEvent\n\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.core.selectors.utils import get_selector_from_llm\nfrom llama_index.core.base.response.schema import (\n PydanticResponse,\n Response,\n AsyncStreamingResponse,\n)\nfrom llama_index.core.bridge.pydantic import BaseModel\nfrom llama_index.core.response_synthesizers import TreeSummarize\nfrom llama_index.core.schema import QueryBundle\nfrom llama_index.core import Settings\n\nfrom IPython.display import Markdown, display\nimport asyncio\n\n\nclass RouterQueryEngineWorkflow(Workflow):\n @step\n async def selector(\n self, ctx: Context, ev: StartEvent\n ) -> QueryEngineSelectionEvent:\n \"\"\"\n Selects a single/ multiple query engines based on the query.\n \"\"\"\n\n await ctx.store.set(\"query\", ev.get(\"query\"))\n await ctx.store.set(\"llm\", ev.get(\"llm\"))\n await ctx.store.set(\"query_engine_tools\", ev.get(\"query_engine_tools\"))\n await ctx.store.set(\"summarizer\", ev.get(\"summarizer\"))\n\n llm = Settings.llm\n select_multiple_query_engines = ev.get(\"select_multi\")\n query = ev.get(\"query\")\n query_engine_tools = ev.get(\"query_engine_tools\")\n\n selector = get_selector_from_llm(\n llm, is_multi=select_multiple_query_engines\n )\n\n query_engines_metadata = [\n query_engine.metadata for query_engine in query_engine_tools\n ]\n\n selected_query_engines = await selector.aselect(\n query_engines_metadata, query\n )\n\n return QueryEngineSelectionEvent(\n selected_query_engines=selected_query_engines\n )\n\n @step\n async def generate_responses(\n self, ctx: Context, ev: QueryEngineSelectionEvent\n ) -> SynthesizeEvent:\n \"\"\"Generate the responses from the selected query engines.\"\"\"\n\n query = await ctx.store.get(\"query\", default=None)\n selected_query_engines = ev.selected_query_engines\n query_engine_tools = await ctx.store.get(\"query_engine_tools\")\n\n query_engines = [engine.query_engine for engine in query_engine_tools]\n\n print(\n f\"number of selected query engines: {len(selected_query_engines.selections)}\"\n )\n\n if len(selected_query_engines.selections) > 1:\n tasks = []\n for selected_query_engine in selected_query_engines.selections:\n print(\n f\"Selected query engine: {selected_query_engine.index}: {selected_query_engine.reason}\"\n )\n query_engine = query_engines[selected_query_engine.index]\n tasks.append(query_engine.aquery(query))\n\n response_generated = await asyncio.gather(*tasks)\n\n else:\n query_engine = query_engines[\n selected_query_engines.selections[0].index\n ]\n\n print(\n f\"Selected query engine: {selected_query_engines.ind}: {selected_query_engines.reason}\"\n )\n\n response_generated = [await query_engine.aquery(query)]\n\n return SynthesizeEvent(\n result=response_generated,\n selected_query_engines=selected_query_engines,\n )\n\n async def acombine_responses(\n self,\n summarizer: TreeSummarize,\n responses: List[RESPONSE_TYPE],\n query_bundle: QueryBundle,\n ) -> RESPONSE_TYPE:\n \"\"\"Async combine multiple response from sub-engines.\"\"\"\n\n print(\"Combining responses from multiple query engines.\")\n\n response_strs = []\n source_nodes = []\n for response in responses:\n if isinstance(\n response, (AsyncStreamingResponse, PydanticResponse)\n ):\n response_obj = await response.aget_response()\n else:\n response_obj = response\n source_nodes.extend(response_obj.source_nodes)\n response_strs.append(str(response))\n\n summary = await summarizer.aget_response(\n query_bundle.query_str, response_strs\n )\n\n if isinstance(summary, str):\n return Response(response=summary, source_nodes=source_nodes)\n elif isinstance(summary, BaseModel):\n return PydanticResponse(\n response=summary, source_nodes=source_nodes\n )\n else:\n return AsyncStreamingResponse(\n response_gen=summary, source_nodes=source_nodes\n )\n\n @step\n async def synthesize_responses(\n self, ctx: Context, ev: SynthesizeEvent\n ) -> StopEvent:\n \"\"\"Synthesizes the responses from the generated responses.\"\"\"\n\n response_generated = ev.result\n query = await ctx.store.get(\"query\", default=None)\n summarizer = await ctx.store.get(\"summarizer\")\n selected_query_engines = ev.selected_query_engines\n\n if len(response_generated) > 1:\n response = await self.acombine_responses(\n summarizer, response_generated, QueryBundle(query_str=query)\n )\n else:\n response = response_generated[0]\n\n response.metadata = response.metadata or {}\n response.metadata[\"selector_result\"] = selected_query_engines\n\n return StopEvent(result=response)" ] }, { @@ -710,4 +534,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/self_discover_workflow.ipynb b/docs/examples/workflow/self_discover_workflow.ipynb index 6ccb2622fa..65f2e1a4ba 100644 --- a/docs/examples/workflow/self_discover_workflow.ipynb +++ b/docs/examples/workflow/self_discover_workflow.ipynb @@ -168,28 +168,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import Event\n", - "\n", - "\n", - "class GetModulesEvent(Event):\n", - " \"\"\"Event to get modules.\"\"\"\n", - "\n", - " task: str\n", - " modules: str\n", - "\n", - "\n", - "class RefineModulesEvent(Event):\n", - " \"\"\"Event to refine modules.\"\"\"\n", - "\n", - " task: str\n", - " refined_modules: str\n", - "\n", - "\n", - "class ReasoningStructureEvent(Event):\n", - " \"\"\"Event to create reasoning structure.\"\"\"\n", - "\n", - " task: str\n", - " reasoning_structure: str" + "from workflows.events import Event\n\n\nclass GetModulesEvent(Event):\n \"\"\"Event to get modules.\"\"\"\n\n task: str\n modules: str\n\n\nclass RefineModulesEvent(Event):\n \"\"\"Event to refine modules.\"\"\"\n\n task: str\n refined_modules: str\n\n\nclass ReasoningStructureEvent(Event):\n \"\"\"Event to create reasoning structure.\"\"\"\n\n task: str\n reasoning_structure: str" ] }, { @@ -205,93 +184,7 @@ "metadata": {}, "outputs": [], "source": [ - "from llama_index.core.workflow import (\n", - " Workflow,\n", - " Context,\n", - " StartEvent,\n", - " StopEvent,\n", - " step,\n", - ")\n", - "from llama_index.core.llms import LLM\n", - "\n", - "\n", - "class SelfDiscoverWorkflow(Workflow):\n", - " \"\"\"Self discover workflow.\"\"\"\n", - "\n", - " @step\n", - " async def get_modules(\n", - " self, ctx: Context, ev: StartEvent\n", - " ) -> GetModulesEvent:\n", - " \"\"\"Get modules step.\"\"\"\n", - " # get input data, store llm into ctx\n", - " task = ev.get(\"task\")\n", - " llm: LLM = ev.get(\"llm\")\n", - "\n", - " if task is None or llm is None:\n", - " raise ValueError(\"'task' and 'llm' arguments are required.\")\n", - "\n", - " await ctx.store.set(\"llm\", llm)\n", - "\n", - " # format prompt and get result from LLM\n", - " prompt = SELECT_PRMOPT_TEMPLATE.format(\n", - " task=task, reasoning_modules=_REASONING_MODULES\n", - " )\n", - " result = llm.complete(prompt)\n", - "\n", - " return GetModulesEvent(task=task, modules=str(result))\n", - "\n", - " @step\n", - " async def refine_modules(\n", - " self, ctx: Context, ev: GetModulesEvent\n", - " ) -> RefineModulesEvent:\n", - " \"\"\"Refine modules step.\"\"\"\n", - " task = ev.task\n", - " modules = ev.modules\n", - " llm: LLM = await ctx.store.get(\"llm\")\n", - "\n", - " # format prompt and get result\n", - " prompt = ADAPT_PROMPT_TEMPLATE.format(\n", - " task=task, selected_modules=modules\n", - " )\n", - " result = llm.complete(prompt)\n", - "\n", - " return RefineModulesEvent(task=task, refined_modules=str(result))\n", - "\n", - " @step\n", - " async def create_reasoning_structure(\n", - " self, ctx: Context, ev: RefineModulesEvent\n", - " ) -> ReasoningStructureEvent:\n", - " \"\"\"Create reasoning structures step.\"\"\"\n", - " task = ev.task\n", - " refined_modules = ev.refined_modules\n", - " llm: LLM = await ctx.store.get(\"llm\")\n", - "\n", - " # format prompt, get result\n", - " prompt = IMPLEMENT_PROMPT_TEMPLATE.format(\n", - " task=task, adapted_modules=refined_modules\n", - " )\n", - " result = llm.complete(prompt)\n", - "\n", - " return ReasoningStructureEvent(\n", - " task=task, reasoning_structure=str(result)\n", - " )\n", - "\n", - " @step\n", - " async def get_final_result(\n", - " self, ctx: Context, ev: ReasoningStructureEvent\n", - " ) -> StopEvent:\n", - " \"\"\"Gets final result from reasoning structure event.\"\"\"\n", - " task = ev.task\n", - " reasoning_structure = ev.reasoning_structure\n", - " llm: LLM = await ctx.store.get(\"llm\")\n", - "\n", - " # format prompt, get res\n", - " prompt = REASONING_PROMPT_TEMPLATE.format(\n", - " task=task, reasoning_structure=reasoning_structure\n", - " )\n", - " result = llm.complete(prompt)\n", - "\n", - " return StopEvent(result=result)" + "from workflows import Workflow, Context, step\nfrom workflows.events import StartEvent, StopEvent\nfrom llama_index.core.llms import LLM\n\n\nclass SelfDiscoverWorkflow(Workflow):\n \"\"\"Self discover workflow.\"\"\"\n\n @step\n async def get_modules(\n self, ctx: Context, ev: StartEvent\n ) -> GetModulesEvent:\n \"\"\"Get modules step.\"\"\"\n # get input data, store llm into ctx\n task = ev.get(\"task\")\n llm: LLM = ev.get(\"llm\")\n\n if task is None or llm is None:\n raise ValueError(\"'task' and 'llm' arguments are required.\")\n\n await ctx.store.set(\"llm\", llm)\n\n # format prompt and get result from LLM\n prompt = SELECT_PRMOPT_TEMPLATE.format(\n task=task, reasoning_modules=_REASONING_MODULES\n )\n result = llm.complete(prompt)\n\n return GetModulesEvent(task=task, modules=str(result))\n\n @step\n async def refine_modules(\n self, ctx: Context, ev: GetModulesEvent\n ) -> RefineModulesEvent:\n \"\"\"Refine modules step.\"\"\"\n task = ev.task\n modules = ev.modules\n llm: LLM = await ctx.store.get(\"llm\")\n\n # format prompt and get result\n prompt = ADAPT_PROMPT_TEMPLATE.format(\n task=task, selected_modules=modules\n )\n result = llm.complete(prompt)\n\n return RefineModulesEvent(task=task, refined_modules=str(result))\n\n @step\n async def create_reasoning_structure(\n self, ctx: Context, ev: RefineModulesEvent\n ) -> ReasoningStructureEvent:\n \"\"\"Create reasoning structures step.\"\"\"\n task = ev.task\n refined_modules = ev.refined_modules\n llm: LLM = await ctx.store.get(\"llm\")\n\n # format prompt, get result\n prompt = IMPLEMENT_PROMPT_TEMPLATE.format(\n task=task, adapted_modules=refined_modules\n )\n result = llm.complete(prompt)\n\n return ReasoningStructureEvent(\n task=task, reasoning_structure=str(result)\n )\n\n @step\n async def get_final_result(\n self, ctx: Context, ev: ReasoningStructureEvent\n ) -> StopEvent:\n \"\"\"Gets final result from reasoning structure event.\"\"\"\n task = ev.task\n reasoning_structure = ev.reasoning_structure\n llm: LLM = await ctx.store.get(\"llm\")\n\n # format prompt, get res\n prompt = REASONING_PROMPT_TEMPLATE.format(\n task=task, reasoning_structure=reasoning_structure\n )\n result = llm.complete(prompt)\n\n return StopEvent(result=result)" ] }, { @@ -401,4 +294,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/sub_question_query_engine.ipynb b/docs/examples/workflow/sub_question_query_engine.ipynb index 670b6d43bc..f45ea54165 100644 --- a/docs/examples/workflow/sub_question_query_engine.ipynb +++ b/docs/examples/workflow/sub_question_query_engine.ipynb @@ -41,25 +41,7 @@ "metadata": {}, "outputs": [], "source": [ - "import os, json\n", - "from llama_index.core import (\n", - " SimpleDirectoryReader,\n", - " VectorStoreIndex,\n", - " StorageContext,\n", - " load_index_from_storage,\n", - ")\n", - "from llama_index.core.tools import QueryEngineTool, ToolMetadata\n", - "from llama_index.core.workflow import (\n", - " step,\n", - " Context,\n", - " Workflow,\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - ")\n", - "from llama_index.core.agent import ReActAgent\n", - "from llama_index.llms.openai import OpenAI\n", - "from llama_index.utils.workflow import draw_all_possible_flows" + "import os, json\nfrom llama_index.core import (\n SimpleDirectoryReader,\n VectorStoreIndex,\n StorageContext,\n load_index_from_storage,\n)\nfrom llama_index.core.tools import QueryEngineTool, ToolMetadata\nfrom workflows import step, Context, Workflow\nfrom workflows.events import Event, StartEvent, StopEvent\nfrom llama_index.core.agent import ReActAgent\nfrom llama_index.llms.openai import OpenAI\nfrom llama_index.utils.workflow import draw_all_possible_flows" ] }, { @@ -644,4 +626,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/docs/examples/workflow/workflows_cookbook.ipynb b/docs/examples/workflow/workflows_cookbook.ipynb index 16bd299436..5f22bb2323 100644 --- a/docs/examples/workflow/workflows_cookbook.ipynb +++ b/docs/examples/workflow/workflows_cookbook.ipynb @@ -1,567 +1,560 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Workflows cookbook: walking through all features of Workflows\n", - "\n", - "First, we install our dependencies. Core contains most of what we need; OpenAI is to handle LLM access and utils-workflow provides the visualization capabilities we'll use later on." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install --upgrade llama-index-core llama-index-llms-openai llama-index-utils-workflow" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then we bring in the deps we just installed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from llama_index.core.workflow import (\n", - " Event,\n", - " StartEvent,\n", - " StopEvent,\n", - " Workflow,\n", - " step,\n", - " Context,\n", - ")\n", - "import random\n", - "from llama_index.core.workflow import draw_all_possible_flows\n", - "from llama_index.utils.workflow import draw_most_recent_execution\n", - "from llama_index.llms.openai import OpenAI" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Set up our OpenAI key, so we can do actual LLM things." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Workflow basics\n", - "\n", - "Let's start with the basic possible workflow: it just starts, does one thing, and stops. There's no reason to have a real workflow if your task is this simple, but we're just demonstrating how they work." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "LlamaIndex, formerly known as GPT Index, is a data framework designed to facilitate the connection between large language models (LLMs) and external data sources. It provides tools to index various data types, such as documents, databases, and APIs, enabling LLMs to interact with and retrieve information from these sources more effectively. The framework supports the creation of indices that can be queried by LLMs, enhancing their ability to access and utilize external data in a structured manner. This capability is particularly useful for applications requiring the integration of LLMs with specific datasets or knowledge bases.\n" - ] - } - ], - "source": [ - "from llama_index.llms.openai import OpenAI\n", - "\n", - "\n", - "class OpenAIGenerator(Workflow):\n", - " @step\n", - " async def generate(self, ev: StartEvent) -> StopEvent:\n", - " llm = OpenAI(model=\"gpt-4o\")\n", - " response = await llm.acomplete(ev.query)\n", - " return StopEvent(result=str(response))\n", - "\n", - "\n", - "w = OpenAIGenerator(timeout=10, verbose=False)\n", - "result = await w.run(query=\"What's LlamaIndex?\")\n", - "print(result)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "One of the neat things about Workflows is that we can use pyvis to visualize them. Let's see what that looks like for this very simple flow." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "draw_all_possible_flows(OpenAIGenerator, filename=\"trivial_workflow.html\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Screenshot 2024-08-05 at 11.59.03 AM.png](data:image/png;base64,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0mw9r68O/QeyLuGKNs766w22fmXXSVGJq3044cQPns6OMGtIHx6e0nUencmaSouUEkhHI7WgyiEmxjIg71ppTdJ5sihJTGn2I3WwdHOxuqybad8uxLOlB0bCSoGsVRnkotzymLamrFgpguq0cj9okDILB60YIwNbk5KE9bdkhWYWFFm/YCuVGBemDHXpCgPIrYTELoshAZTjM2kuXVPBWPOroXiycJqi1/54uxfP9iPiHJgAE2ACXYOAWjzZMciyz6zTx3la9vY4dybABJjAzUOArFHZlGvMMr15SFj+TtnytDxjLoEJMAEmwATsjABPT2JnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxP4P8DRL/CIZEgMZUAAAAASUVORK5CYII=)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Not a lot to see here, yet! The start event goes to generate() and then straight to StopEvent.\n", - "\n", - "## Loops and branches\n", - "\n", - "Let's go to a more interesting example, demonstrating our ability to loop:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class FailedEvent(Event):\n", - " error: str\n", - "\n", - "\n", - "class QueryEvent(Event):\n", - " query: str\n", - "\n", - "\n", - "class LoopExampleFlow(Workflow):\n", - " @step\n", - " async def answer_query(\n", - " self, ev: StartEvent | QueryEvent\n", - " ) -> FailedEvent | StopEvent:\n", - " query = ev.query\n", - " # try to answer the query\n", - " random_number = random.randint(0, 1)\n", - " if random_number == 0:\n", - " return FailedEvent(error=\"Failed to answer the query.\")\n", - " else:\n", - " return StopEvent(result=\"The answer to your query\")\n", - "\n", - " @step\n", - " async def improve_query(self, ev: FailedEvent) -> QueryEvent | StopEvent:\n", - " # improve the query or decide it can't be fixed\n", - " random_number = random.randint(0, 1)\n", - " if random_number == 0:\n", - " return QueryEvent(query=\"Here's a better query.\")\n", - " else:\n", - " return StopEvent(result=\"Your query can't be fixed.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We're using random numbers to simulate LLM actions here so that we can get reliably interesting behavior.\n", - "\n", - "answer_query() accepts a start event. It can then do 2 things:\n", - "* it can answer the query and emit a StopEvent, which returns the result\n", - "* it can decide the query was bad and emit a FailedEvent\n", - "\n", - "improve_query() accepts a FailedEvent. It can also do 2 things:\n", - "* it can decide the query can't be improved and emit a StopEvent, which returns failure\n", - "* it can present a better query and emit a QueryEvent, which creates a loop back to answer_query()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also visualize this more complicated workflow:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "loop_workflow.html\n" - ] - } - ], - "source": [ - "draw_all_possible_flows(LoopExampleFlow, filename=\"loop_workflow.html\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Screenshot 2024-08-05 at 11.36.05 AM.png](data:image/png;base64,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)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We've set `verbose=True` here so we can see exactly what events were triggered. You can see it conveniently demonstrates looping and then answering." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step answer_query\n", - "Step answer_query produced event FailedEvent\n", - "Running step improve_query\n", - "Step improve_query produced event StopEvent\n", - "Your query can't be fixed.\n" - ] - } - ], - "source": [ - "l = LoopExampleFlow(timeout=10, verbose=True)\n", - "result = await l.run(query=\"What's LlamaIndex?\")\n", - "print(result)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Maintaining state between events\n", - "\n", - "There is a global state which allows you to keep arbitrary data or functions around for use by all event handlers." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class GlobalExampleFlow(Workflow):\n", - " @step\n", - " async def setup(self, ctx: Context, ev: StartEvent) -> QueryEvent:\n", - " # load our data here\n", - " await ctx.store.set(\"some_database\", [\"value1\", \"value2\", \"value3\"])\n", - "\n", - " return QueryEvent(query=ev.query)\n", - "\n", - " @step\n", - " async def query(self, ctx: Context, ev: QueryEvent) -> StopEvent:\n", - " # use our data with our query\n", - " data = await ctx.store.get(\"some_database\")\n", - "\n", - " result = f\"The answer to your query is {data[1]}\"\n", - " return StopEvent(result=result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step setup\n", - "Step setup produced event QueryEvent\n", - "Running step query\n", - "Step query produced event StopEvent\n", - "The answer to your query is value2\n" - ] - } - ], - "source": [ - "g = GlobalExampleFlow(timeout=10, verbose=True)\n", - "result = await g.run(query=\"What's LlamaIndex?\")\n", - "print(result)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Of course, this flow is essentially still linear. A more realistic example would be if your start event could either be a query or a data population event, and you needed to wait. Let's set that up to see what it looks like:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class WaitExampleFlow(Workflow):\n", - " @step\n", - " async def setup(self, ctx: Context, ev: StartEvent) -> StopEvent:\n", - " if hasattr(ev, \"data\"):\n", - " await ctx.store.set(\"data\", ev.data)\n", - "\n", - " return StopEvent(result=None)\n", - "\n", - " @step\n", - " async def query(self, ctx: Context, ev: StartEvent) -> StopEvent:\n", - " if hasattr(ev, \"query\"):\n", - " # do we have any data?\n", - " if hasattr(self, \"data\"):\n", - " data = await ctx.store.get(\"data\")\n", - " return StopEvent(result=f\"Got the data {data}\")\n", - " else:\n", - " # there's non data yet\n", - " return None\n", - " else:\n", - " # this isn't a query\n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running step query\n", - "Step query produced no event\n", - "Running step setup\n", - "Step setup produced event StopEvent\n", - "No you can't\n", - "---\n", - "Running step query\n", - "Step query produced no event\n", - "Running step setup\n", - "Step setup produced event StopEvent\n", - "---\n", - "Running step query\n", - "Step query produced event StopEvent\n", - "Running step setup\n", - "Step setup produced event StopEvent\n", - "Got the data Yes you can\n" - ] - } - ], - "source": [ - "w = WaitExampleFlow(verbose=True)\n", - "result = await w.run(query=\"Can I kick it?\")\n", - "if result is None:\n", - " print(\"No you can't\")\n", - "print(\"---\")\n", - "result = await w.run(data=\"Yes you can\")\n", - "print(\"---\")\n", - "result = await w.run(query=\"Can I kick it?\")\n", - "print(result)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's visualize how this flow works:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "wait_workflow.html\n" - ] - } - ], - "source": [ - "draw_all_possible_flows(WaitExampleFlow, filename=\"wait_workflow.html\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Screenshot 2024-08-05 at 1.37.23 PM.png](data:image/png;base64,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)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Waiting for one or more events\n", - "\n", - "Because waiting for events is such a common pattern, the context object has a convenience function, `collect_events()`. It will capture events and store them, returning `None` until all the events it requires have been collected. Those events will be attached to the output of `collect_events` in the order that they were specified. Let's see this in action:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class InputEvent(Event):\n", - " input: str\n", - "\n", - "\n", - "class SetupEvent(Event):\n", - " error: bool\n", - "\n", - "\n", - "class QueryEvent(Event):\n", - " query: str\n", - "\n", - "\n", - "class CollectExampleFlow(Workflow):\n", - " @step\n", - " async def setup(self, ctx: Context, ev: StartEvent) -> SetupEvent:\n", - " # generically start everything up\n", - " if not hasattr(self, \"setup\") or not self.setup:\n", - " self.setup = True\n", - " print(\"I got set up\")\n", - " return SetupEvent(error=False)\n", - "\n", - " @step\n", - " async def collect_input(self, ev: StartEvent) -> InputEvent:\n", - " if hasattr(ev, \"input\"):\n", - " # perhaps validate the input\n", - " print(\"I got some input\")\n", - " return InputEvent(input=ev.input)\n", - "\n", - " @step\n", - " async def parse_query(self, ev: StartEvent) -> QueryEvent:\n", - " if hasattr(ev, \"query\"):\n", - " # parse the query in some way\n", - " print(\"I got a query\")\n", - " return QueryEvent(query=ev.query)\n", - "\n", - " @step\n", - " async def run_query(\n", - " self, ctx: Context, ev: InputEvent | SetupEvent | QueryEvent\n", - " ) -> StopEvent | None:\n", - " ready = ctx.collect_events(ev, [QueryEvent, InputEvent, SetupEvent])\n", - " if ready is None:\n", - " print(\"Not enough events yet\")\n", - " return None\n", - "\n", - " # run the query\n", - " print(\"Now I have all the events\")\n", - " print(ready)\n", - "\n", - " result = f\"Ran query '{ready[0].query}' on input '{ready[1].input}'\"\n", - " return StopEvent(result=result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "I got some input\n", - "I got a query\n", - "Not enough events yet\n", - "Not enough events yet\n", - "Now I have all the events\n", - "[QueryEvent(query=\"Here's my question\"), InputEvent(input=\"Here's some input\"), SetupEvent(error=False)]\n", - "Ran query 'Here's my question' on input 'Here's some input'\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Workflows cookbook: walking through all features of Workflows\n", + "\n", + "First, we install our dependencies. Core contains most of what we need; OpenAI is to handle LLM access and utils-workflow provides the visualization capabilities we'll use later on." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "!pip install --upgrade llama-index-core llama-index-llms-openai llama-index-utils-workflow" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we bring in the deps we just installed" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from workflows import Workflow, step, Context\n", + "from workflows.events import Event, StartEvent, StopEvent\n", + "import random\n", + "from llama_index.utils.workflow import draw_all_possible_flows, draw_most_recent_execution\n", + "from llama_index.llms.openai import OpenAI" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set up our OpenAI key, so we can do actual LLM things." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = \"sk-proj-...\"" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Workflow basics\n", + "\n", + "Let's start with the basic possible workflow: it just starts, does one thing, and stops. There's no reason to have a real workflow if your task is this simple, but we're just demonstrating how they work." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from llama_index.llms.openai import OpenAI\n", + "\n", + "\n", + "class OpenAIGenerator(Workflow):\n", + " @step\n", + " async def generate(self, ev: StartEvent) -> StopEvent:\n", + " llm = OpenAI(model=\"gpt-4o\")\n", + " response = await llm.acomplete(ev.query)\n", + " return StopEvent(result=str(response))\n", + "\n", + "\n", + "w = OpenAIGenerator(timeout=10, verbose=False)\n", + "result = await w.run(query=\"What's LlamaIndex?\")\n", + "print(result)" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LlamaIndex, formerly known as GPT Index, is a data framework designed to facilitate the connection between large language models (LLMs) and external data sources. It provides tools to index various data types, such as documents, databases, and APIs, enabling LLMs to interact with and retrieve information from these sources more effectively. The framework supports the creation of indices that can be queried by LLMs, enhancing their ability to access and utilize external data in a structured manner. This capability is particularly useful for applications requiring the integration of LLMs with specific datasets or knowledge bases.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One of the neat things about Workflows is that we can use pyvis to visualize them. Let's see what that looks like for this very simple flow." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "draw_all_possible_flows(OpenAIGenerator, filename=\"trivial_workflow.html\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot 2024-08-05 at 11.59.03 AM.png](data:image/png;base64,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0mw9r68O/QeyLuGKNs766w22fmXXSVGJq3044cQPns6OMGtIHx6e0nUencmaSouUEkhHI7WgyiEmxjIg71ppTdJ5sihJTGn2I3WwdHOxuqybad8uxLOlB0bCSoGsVRnkotzymLamrFgpguq0cj9okDILB60YIwNbk5KE9bdkhWYWFFm/YCuVGBemDHXpCgPIrYTELoshAZTjM2kuXVPBWPOroXiycJqi1/54uxfP9iPiHJgAE2ACXYOAWjzZMciyz6zTx3la9vY4dybABJjAzUOArFHZlGvMMr15SFj+TtnytDxjLoEJMAEmwATsjABPT2JnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxPgMXT8oy5BCbABJgAE7AzAiyedvZA+XaYABNgAkzA8gRYPC3PmEtgAkyACTABOyPA4mlnD5RvhwkwASbABCxP4P8DRL/CIZEgMZUAAAAASUVORK5CYII=)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Not a lot to see here, yet! The start event goes to generate() and then straight to StopEvent.\n", + "\n", + "## Loops and branches\n", + "\n", + "Let's go to a more interesting example, demonstrating our ability to loop:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "class FailedEvent(Event):\n", + " error: str\n", + "\n", + "\n", + "class QueryEvent(Event):\n", + " query: str\n", + "\n", + "\n", + "class LoopExampleFlow(Workflow):\n", + " @step\n", + " async def answer_query(\n", + " self, ev: StartEvent | QueryEvent\n", + " ) -> FailedEvent | StopEvent:\n", + " query = ev.query\n", + " # try to answer the query\n", + " random_number = random.randint(0, 1)\n", + " if random_number == 0:\n", + " return FailedEvent(error=\"Failed to answer the query.\")\n", + " else:\n", + " return StopEvent(result=\"The answer to your query\")\n", + "\n", + " @step\n", + " async def improve_query(self, ev: FailedEvent) -> QueryEvent | StopEvent:\n", + " # improve the query or decide it can't be fixed\n", + " random_number = random.randint(0, 1)\n", + " if random_number == 0:\n", + " return QueryEvent(query=\"Here's a better query.\")\n", + " else:\n", + " return StopEvent(result=\"Your query can't be fixed.\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We're using random numbers to simulate LLM actions here so that we can get reliably interesting behavior.\n", + "\n", + "answer_query() accepts a start event. It can then do 2 things:\n", + "* it can answer the query and emit a StopEvent, which returns the result\n", + "* it can decide the query was bad and emit a FailedEvent\n", + "\n", + "improve_query() accepts a FailedEvent. It can also do 2 things:\n", + "* it can decide the query can't be improved and emit a StopEvent, which returns failure\n", + "* it can present a better query and emit a QueryEvent, which creates a loop back to answer_query()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also visualize this more complicated workflow:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "draw_all_possible_flows(LoopExampleFlow, filename=\"loop_workflow.html\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "loop_workflow.html\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot 2024-08-05 at 11.36.05 AM.png](data:image/png;base64,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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We've set `verbose=True` here so we can see exactly what events were triggered. You can see it conveniently demonstrates looping and then answering." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "l = LoopExampleFlow(timeout=10, verbose=True)\n", + "result = await l.run(query=\"What's LlamaIndex?\")\n", + "print(result)" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step answer_query\n", + "Step answer_query produced event FailedEvent\n", + "Running step improve_query\n", + "Step improve_query produced event StopEvent\n", + "Your query can't be fixed.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Maintaining state between events\n", + "\n", + "There is a global state which allows you to keep arbitrary data or functions around for use by all event handlers." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "class GlobalExampleFlow(Workflow):\n", + " @step\n", + " async def setup(self, ctx: Context, ev: StartEvent) -> QueryEvent:\n", + " # load our data here\n", + " await ctx.store.set(\"some_database\", [\"value1\", \"value2\", \"value3\"])\n", + "\n", + " return QueryEvent(query=ev.query)\n", + "\n", + " @step\n", + " async def query(self, ctx: Context, ev: QueryEvent) -> StopEvent:\n", + " # use our data with our query\n", + " data = await ctx.store.get(\"some_database\")\n", + "\n", + " result = f\"The answer to your query is {data[1]}\"\n", + " return StopEvent(result=result)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "g = GlobalExampleFlow(timeout=10, verbose=True)\n", + "result = await g.run(query=\"What's LlamaIndex?\")\n", + "print(result)" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step setup\n", + "Step setup produced event QueryEvent\n", + "Running step query\n", + "Step query produced event StopEvent\n", + "The answer to your query is value2\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, this flow is essentially still linear. A more realistic example would be if your start event could either be a query or a data population event, and you needed to wait. Let's set that up to see what it looks like:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "class WaitExampleFlow(Workflow):\n", + " @step\n", + " async def setup(self, ctx: Context, ev: StartEvent) -> StopEvent:\n", + " if hasattr(ev, \"data\"):\n", + " await ctx.store.set(\"data\", ev.data)\n", + "\n", + " return StopEvent(result=None)\n", + "\n", + " @step\n", + " async def query(self, ctx: Context, ev: StartEvent) -> StopEvent:\n", + " if hasattr(ev, \"query\"):\n", + " # do we have any data?\n", + " if hasattr(self, \"data\"):\n", + " data = await ctx.store.get(\"data\")\n", + " return StopEvent(result=f\"Got the data {data}\")\n", + " else:\n", + " # there's non data yet\n", + " return None\n", + " else:\n", + " # this isn't a query\n", + " return None" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "w = WaitExampleFlow(verbose=True)\n", + "result = await w.run(query=\"Can I kick it?\")\n", + "if result is None:\n", + " print(\"No you can't\")\n", + "print(\"---\")\n", + "result = await w.run(data=\"Yes you can\")\n", + "print(\"---\")\n", + "result = await w.run(query=\"Can I kick it?\")\n", + "print(result)" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running step query\n", + "Step query produced no event\n", + "Running step setup\n", + "Step setup produced event StopEvent\n", + "No you can't\n", + "---\n", + "Running step query\n", + "Step query produced no event\n", + "Running step setup\n", + "Step setup produced event StopEvent\n", + "---\n", + "Running step query\n", + "Step query produced event StopEvent\n", + "Running step setup\n", + "Step setup produced event StopEvent\n", + "Got the data Yes you can\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's visualize how this flow works:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "draw_all_possible_flows(WaitExampleFlow, filename=\"wait_workflow.html\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "wait_workflow.html\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot 2024-08-05 at 1.37.23 PM.png](data:image/png;base64,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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Waiting for one or more events\n", + "\n", + "Because waiting for events is such a common pattern, the context object has a convenience function, `collect_events()`. It will capture events and store them, returning `None` until all the events it requires have been collected. Those events will be attached to the output of `collect_events` in the order that they were specified. Let's see this in action:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "class InputEvent(Event):\n", + " input: str\n", + "\n", + "\n", + "class SetupEvent(Event):\n", + " error: bool\n", + "\n", + "\n", + "class QueryEvent(Event):\n", + " query: str\n", + "\n", + "\n", + "class CollectExampleFlow(Workflow):\n", + " @step\n", + " async def setup(self, ctx: Context, ev: StartEvent) -> SetupEvent:\n", + " # generically start everything up\n", + " if not hasattr(self, \"setup\") or not self.setup:\n", + " self.setup = True\n", + " print(\"I got set up\")\n", + " return SetupEvent(error=False)\n", + "\n", + " @step\n", + " async def collect_input(self, ev: StartEvent) -> InputEvent:\n", + " if hasattr(ev, \"input\"):\n", + " # perhaps validate the input\n", + " print(\"I got some input\")\n", + " return InputEvent(input=ev.input)\n", + "\n", + " @step\n", + " async def parse_query(self, ev: StartEvent) -> QueryEvent:\n", + " if hasattr(ev, \"query\"):\n", + " # parse the query in some way\n", + " print(\"I got a query\")\n", + " return QueryEvent(query=ev.query)\n", + "\n", + " @step\n", + " async def run_query(\n", + " self, ctx: Context, ev: InputEvent | SetupEvent | QueryEvent\n", + " ) -> StopEvent | None:\n", + " ready = ctx.collect_events(ev, [QueryEvent, InputEvent, SetupEvent])\n", + " if ready is None:\n", + " print(\"Not enough events yet\")\n", + " return None\n", + "\n", + " # run the query\n", + " print(\"Now I have all the events\")\n", + " print(ready)\n", + "\n", + " result = f\"Ran query '{ready[0].query}' on input '{ready[1].input}'\"\n", + " return StopEvent(result=result)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "c = CollectExampleFlow()\n", + "result = await c.run(input=\"Here's some input\", query=\"Here's my question\")\n", + "print(result)" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "I got some input\n", + "I got a query\n", + "Not enough events yet\n", + "Not enough events yet\n", + "Now I have all the events\n", + "[QueryEvent(query=\"Here's my question\"), InputEvent(input=\"Here's some input\"), SetupEvent(error=False)]\n", + "Ran query 'Here's my question' on input 'Here's some input'\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see each of the events getting triggered as well as the collection event repeatedly returning `None` until enough events have arrived. Let's see what this looks like in a flow diagram:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "draw_all_possible_flows(CollectExampleFlow, \"collect_workflow.html\")" + ], + "execution_count": null, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "collect_workflow.html\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot 2024-08-05 at 2.27.46 PM.png](data:image/png;base64,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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This concludes our tour of creating, running and visualizing workflows! Check out the [docs](https://docs.llamaindex.ai/en/stable/module_guides/workflow/) and [examples](https://docs.llamaindex.ai/en/stable/examples/workflow/function_calling_agent/) to learn more." + ] } - ], - "source": [ - "c = CollectExampleFlow()\n", - "result = await c.run(input=\"Here's some input\", query=\"Here's my question\")\n", - "print(result)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can see each of the events getting triggered as well as the collection event repeatedly returning `None` until enough events have arrived. Let's see what this looks like in a flow diagram:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "collect_workflow.html\n" - ] + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" } - ], - "source": [ - "draw_all_possible_flows(CollectExampleFlow, \"collect_workflow.html\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Screenshot 2024-08-05 at 2.27.46 PM.png](data:image/png;base64,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)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This concludes our tour of creating, running and visualizing workflows! Check out the [docs](https://docs.llamaindex.ai/en/stable/module_guides/workflow/) and [examples](https://docs.llamaindex.ai/en/stable/examples/workflow/function_calling_agent/) to learn more." - ] - } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/src/content/docs/framework/getting_started/starter_example.mdx b/docs/src/content/docs/framework/getting_started/starter_example.mdx index bd83351559..367f8c0436 100644 --- a/docs/src/content/docs/framework/getting_started/starter_example.mdx +++ b/docs/src/content/docs/framework/getting_started/starter_example.mdx @@ -97,7 +97,7 @@ The `AgentWorkflow` is also able to remember previous messages. This is containe If the `Context` is passed in, the agent will use it to continue the conversation. ```python -from llama_index.core.workflow import Context +from workflows import Context # create context ctx = Context(agent) diff --git a/docs/src/content/docs/framework/getting_started/starter_example_local.mdx b/docs/src/content/docs/framework/getting_started/starter_example_local.mdx index 95cedac522..a9024fd812 100644 --- a/docs/src/content/docs/framework/getting_started/starter_example_local.mdx +++ b/docs/src/content/docs/framework/getting_started/starter_example_local.mdx @@ -102,7 +102,7 @@ The `AgentWorkflow` is also able to remember previous messages. This is containe If the `Context` is passed in, the agent will use it to continue the conversation. ```python -from llama_index.core.workflow import Context +from workflows import Context # create context ctx = Context(agent) diff --git a/docs/src/content/docs/framework/module_guides/deploying/agents/memory.mdx b/docs/src/content/docs/framework/module_guides/deploying/agents/memory.mdx index 40c959696f..6e4ba84bcd 100644 --- a/docs/src/content/docs/framework/module_guides/deploying/agents/memory.mdx +++ b/docs/src/content/docs/framework/module_guides/deploying/agents/memory.mdx @@ -63,7 +63,7 @@ response = await agent.run( You can get the latest memory from an agent by grabbing it from the agent context: ```python -from llama_index.core.workflow import Context +from workflows import Context ctx = Context(agent) @@ -277,7 +277,7 @@ In comparison, the `Memory` object is a simpler object, holding only `ChatMessag In most practical cases, you will end up using both. If you aren't customizing the memory, then serializing the `Context` object will be sufficient. ```python -from llama_index.core.workflow import Context +from workflows import Context ctx = Context(workflow) diff --git a/docs/src/content/docs/framework/understanding/agent/human_in_the_loop.md b/docs/src/content/docs/framework/understanding/agent/human_in_the_loop.md index fd959b76d5..4c7bb3e71c 100644 --- a/docs/src/content/docs/framework/understanding/agent/human_in_the_loop.md +++ b/docs/src/content/docs/framework/understanding/agent/human_in_the_loop.md @@ -15,10 +15,7 @@ To get a human in the loop, we'll get our tool to emit an event that isn't recei We have built-in `InputRequiredEvent` and `HumanResponseEvent` events to use for this purpose. If you want to capture different forms of human input, you can subclass these events to match your own preferences. Let's import them: ```python -from llama_index.core.workflow import ( - InputRequiredEvent, - HumanResponseEvent, -) +from workflows.events import InputRequiredEvent, HumanResponseEvent ``` Next we'll create a tool that performs a hypothetical dangerous task. There are a couple of new things happening here: @@ -29,7 +26,7 @@ Next we'll create a tool that performs a hypothetical dangerous task. There are - The `requirements` argument is used to specify that we want to wait for a HumanResponseEvent with a specific `user_name`. ```python -from llama_index.core.workflow import Context +from workflows import Context async def dangerous_task(ctx: Context) -> str: diff --git a/docs/src/content/docs/framework/understanding/agent/multi_agent.md b/docs/src/content/docs/framework/understanding/agent/multi_agent.md index 647407abc5..85a7903ec5 100644 --- a/docs/src/content/docs/framework/understanding/agent/multi_agent.md +++ b/docs/src/content/docs/framework/understanding/agent/multi_agent.md @@ -92,7 +92,7 @@ You can see the full example in the [agents_as_tools notebook](/python/examples/ ```python import re from llama_index.core.agent.workflow import FunctionAgent -from llama_index.core.workflow import Context +from workflows import Context # assume research_agent / write_agent / review_agent defined as before # except we really only need the `search_web` tool at a minimum @@ -199,14 +199,8 @@ from pydantic import BaseModel, Field from typing import Any, Optional from llama_index.core.llms import ChatMessage -from llama_index.core.workflow import ( - Context, - Event, - StartEvent, - StopEvent, - Workflow, - step, -) +from workflows import Context, Workflow, step +from workflows.events import Event, StartEvent, StopEvent # Assume we created helper functions to call the agents diff --git a/docs/src/content/docs/framework/understanding/agent/state.md b/docs/src/content/docs/framework/understanding/agent/state.md index 9246c41f17..f895c89b29 100644 --- a/docs/src/content/docs/framework/understanding/agent/state.md +++ b/docs/src/content/docs/framework/understanding/agent/state.md @@ -9,7 +9,7 @@ By default, the `AgentWorkflow` is stateless between runs. This means that the a To maintain state, we need to keep track of the previous state. In LlamaIndex, Workflows have a `Context` class that can be used to maintain state within and between runs. Since the AgentWorkflow is just a pre-built Workflow, we can also use it now. ```python -from llama_index.core.workflow import Context +from workflows import Context ``` To maintain state between runs, we'll create a new Context called ctx. We pass in our workflow to properly configure this Context object for the workflow that will use it. @@ -50,12 +50,13 @@ The Context is serializable, so it can be saved to a database, file, etc. and lo The JsonSerializer is a simple serializer that uses `json.dumps` and `json.loads` to serialize and deserialize the context. -The JsonPickleSerializer is a serializer that uses pickle to serialize and deserialize the context. If you have objects in your context that are not serializable, you can use this serializer. +The PickleSerializer is a serializer that uses pickle to serialize and deserialize the context. If you have objects in your context that are not serializable, you can use this serializer. We bring in our serializers as any other import: ```python -from llama_index.core.workflow import JsonPickleSerializer, JsonSerializer +from workflows.context import PickleSerializer +from workflows.context.serializers import JsonSerializer ``` We can then serialize our context to a dictionary and save it to a file: diff --git a/docs/src/content/docs/framework/understanding/workflows/basic_flow.md b/docs/src/content/docs/framework/understanding/workflows/basic_flow.md index a833d88d4f..39e42cec84 100644 --- a/docs/src/content/docs/framework/understanding/workflows/basic_flow.md +++ b/docs/src/content/docs/framework/understanding/workflows/basic_flow.md @@ -23,12 +23,8 @@ pip install llama-index-utils-workflow The minimal dependencies for a workflow are: ```python -from llama_index.core.workflow import ( - StartEvent, - StopEvent, - Workflow, - step, -) +from workflows import Workflow, step +from workflows.events import StartEvent, StopEvent ``` ## Single-step workflow @@ -117,13 +113,8 @@ Multiple steps are created by defining custom events that can be emitted by step We bring in our imports as before, plus a new import for `Event`: ```python -from llama_index.core.workflow import ( - StartEvent, - StopEvent, - Workflow, - step, - Event, -) +from workflows import Workflow, step +from workflows.events import StartEvent, StopEvent, Event from llama_index.utils.workflow import draw_all_possible_flows ``` diff --git a/docs/src/content/docs/framework/understanding/workflows/observability.md b/docs/src/content/docs/framework/understanding/workflows/observability.md index 52f4fa331f..6224234f6e 100644 --- a/docs/src/content/docs/framework/understanding/workflows/observability.md +++ b/docs/src/content/docs/framework/understanding/workflows/observability.md @@ -121,24 +121,7 @@ Note that instead of passing the class name you are passing the handler object r Full workflow executions may end up taking a lot of time, and its often the case that only a few steps at a time need to be debugged and observed. To help with speed up Workflow development cycles, the `WorkflowCheckpointer` object wraps a `Workflow` and creates and stores `Checkpoint`'s upon every step completion of a run. These checkpoints can be viewed, inspected and chosen as the starting point for future run's. -```python -from llama_index.core.workflow.checkpointer import WorkflowCheckpointer - -w = ConcurrentFlow() -w_ckptr = WorkflowCheckpointer(workflow=w) - -# run the workflow via w_ckptr to get checkpoints -handler = w_cptr.run() -await handler - -# view checkpoints of the last run -w_ckptr.checkpoints[handler.run_id] - -# run from a previous ckpt -ckpt = w_ckptr.checkpoints[handler.run_id][0] -handler = w_ckptr.run_from(checkpoint=ckpt) -await handler -``` +Note: The WorkflowCheckpointer utility has been removed (deprecated). See the CHANGELOG for details. ## Third party tools diff --git a/docs/src/content/docs/framework/understanding/workflows/resources.md b/docs/src/content/docs/framework/understanding/workflows/resources.md index 96f4f5e4f3..201d37926c 100644 --- a/docs/src/content/docs/framework/understanding/workflows/resources.md +++ b/docs/src/content/docs/framework/understanding/workflows/resources.md @@ -17,14 +17,9 @@ resources can lead to unexpected behaviour, let's see it in detail. First of all, to use resources within our code, we need to import `Resource` from the `resource` submodule: ```python -from llama_index.core.workflow.resource import Resource -from llama_index.core.workflow import ( - Event, - step, - StartEvent, - StopEvent, - Workflow, -) +from workflows.resource import Resource +from workflows import Workflow, step +from workflows.events import Event, StartEvent, StopEvent ``` `Resource` wraps a function or callable that must return an object of the same type as the one in the resource diff --git a/docs/src/content/docs/framework/understanding/workflows/state.md b/docs/src/content/docs/framework/understanding/workflows/state.md index fcaae826a8..5841e4f606 100644 --- a/docs/src/content/docs/framework/understanding/workflows/state.md +++ b/docs/src/content/docs/framework/understanding/workflows/state.md @@ -11,14 +11,8 @@ To avoid this pitfall, we have a `Context` object available to every step in the We need one new import, the `Context` type: ```python -from llama_index.core.workflow import ( - StartEvent, - StopEvent, - Workflow, - step, - Event, - Context, -) +from workflows import Workflow, step, Context +from workflows.events import StartEvent, StopEvent, Event ``` Now we define a `start` event that checks if data has been loaded into the context. If not, it returns a `SetupEvent` which triggers `setup` that loads the data and loops back to `start`. @@ -97,13 +91,8 @@ Here's a quick example of how you can leverage workflows + pydantic to take adva from pydantic import BaseModel, Field, field_validator, field_serializer from typing import Union -from llama_index.core.workflow import ( - Context, - Workflow, - StartEvent, - StopEvent, - step, -) +from workflows import Context, Workflow, step +from workflows.events import StartEvent, StopEvent # This is a random object that we want to use in our state diff --git a/docs/src/content/docs/framework/understanding/workflows/stream.mdx b/docs/src/content/docs/framework/understanding/workflows/stream.mdx index 4e1e4c4f1c..8df3062678 100644 --- a/docs/src/content/docs/framework/understanding/workflows/stream.mdx +++ b/docs/src/content/docs/framework/understanding/workflows/stream.mdx @@ -9,14 +9,8 @@ Workflows can be complex -- they are designed to handle complex, branching, conc To get this done, let's bring in all the deps we need: ```python -from llama_index.core.workflow import ( - StartEvent, - StopEvent, - Workflow, - step, - Event, - Context, -) +from workflows import Workflow, step, Context +from workflows.events import StartEvent, StopEvent, Event import asyncio from llama_index.llms.openai import OpenAI from llama_index.utils.workflow import draw_all_possible_flows diff --git a/docs/src/content/docs/framework/understanding/workflows/subclass.md b/docs/src/content/docs/framework/understanding/workflows/subclass.md index 571b54c9af..553fd9dcde 100644 --- a/docs/src/content/docs/framework/understanding/workflows/subclass.md +++ b/docs/src/content/docs/framework/understanding/workflows/subclass.md @@ -11,14 +11,8 @@ The first is subclassing: workflows are just regular Python classes, which means Here's our base workflow: ```python -from llama_index.core.workflow import ( - StartEvent, - StopEvent, - Workflow, - step, - Event, - Context, -) +from workflows import Workflow, step, Context +from workflows.events import StartEvent, StopEvent, Event class Step2Event(Event):