From 418dcc5fd5aa6cdc0529563e01da0078a18a56d5 Mon Sep 17 00:00:00 2001 From: Charles Frye Date: Thu, 31 Oct 2024 18:38:07 -0700 Subject: [PATCH] Bring back codelangchain (#944) * move to sandboxes, get running again * rework as a Python agent * Refactor to focus on sandboxes * text updates * minor text fixes * fix sample commands, rename, text fixes for langserve --------- Co-authored-by: Peyton Walters --- .../langchains/codelangchain/agent.py | 63 ------ 06_gpu_and_ml/langchains/codelangchain/app.py | 61 ----- .../langchains/codelangchain/common.py | 56 ----- .../langchains/codelangchain/sandbox.py | 33 --- .../codelangchain/README.md | 23 +- .../codelangchain/__init__.py | 0 13_sandboxes/codelangchain/agent.py | 210 +++++++++++++++++ 13_sandboxes/codelangchain/langserve.py | 86 +++++++ 13_sandboxes/codelangchain/src/__init__.py | 0 13_sandboxes/codelangchain/src/common.py | 40 ++++ .../codelangchain/src}/edges.py | 11 +- .../codelangchain/src}/nodes.py | 213 +++++++++++++----- .../codelangchain/src}/retrieval.py | 10 +- 13 files changed, 514 insertions(+), 292 deletions(-) delete mode 100644 06_gpu_and_ml/langchains/codelangchain/agent.py delete mode 100644 06_gpu_and_ml/langchains/codelangchain/app.py delete mode 100644 06_gpu_and_ml/langchains/codelangchain/common.py delete mode 100644 06_gpu_and_ml/langchains/codelangchain/sandbox.py rename {06_gpu_and_ml/langchains => 13_sandboxes}/codelangchain/README.md (72%) rename {06_gpu_and_ml/langchains => 13_sandboxes}/codelangchain/__init__.py (100%) create mode 100644 13_sandboxes/codelangchain/agent.py create mode 100644 13_sandboxes/codelangchain/langserve.py create mode 100644 13_sandboxes/codelangchain/src/__init__.py create mode 100644 13_sandboxes/codelangchain/src/common.py rename {06_gpu_and_ml/langchains/codelangchain => 13_sandboxes/codelangchain/src}/edges.py (89%) rename {06_gpu_and_ml/langchains/codelangchain => 13_sandboxes/codelangchain/src}/nodes.py (62%) rename {06_gpu_and_ml/langchains/codelangchain => 13_sandboxes/codelangchain/src}/retrieval.py (82%) diff --git a/06_gpu_and_ml/langchains/codelangchain/agent.py b/06_gpu_and_ml/langchains/codelangchain/agent.py deleted file mode 100644 index 24e7776df..000000000 --- a/06_gpu_and_ml/langchains/codelangchain/agent.py +++ /dev/null @@ -1,63 +0,0 @@ -"""This module defines our agent and attaches it to the Modal App. - -Our agent is defined as a graph: a collection of nodes and edges, -where nodes represent actions and edges represent transitions between actions. - -The meat of the logic is therefore in the edges and nodes modules. - -We have a very simple "context-stuffing" retrieval approach in the retrieval module. -Replace this with something that retrieves your documentation and adjust the prompts accordingly. - -You can test the agent from the command line with `modal run agent.py --question` followed by your query""" - -import edges -import nodes -import retrieval -from common import app - - -@app.local_entrypoint() -def main(question: str = "How do I build a RAG pipeline?", debug: bool = False): - """Sends a question to the LCEL code generation agent. - - Switch to debug mode for shorter context and smaller model.""" - if debug: - if question == "How do I build a RAG pipeline?": - question = "gm king, how are you?" - print(go.remote(question, debug=debug)["keys"]["response"]) - - -@app.function() -def go(question: str = "How do I build a RAG pipeline?", debug: bool = False): - """Compiles the LCEL code generation agent graph and runs it, returning the result.""" - graph = construct_graph(debug=debug) - runnable = graph.compile() - result = runnable.invoke( - {"keys": {"question": question, "iterations": 0}}, - config={"recursion_limit": 50}, - ) - - return result - - -def construct_graph(debug=False): - from common import GraphState - from langgraph.graph import StateGraph - - context = retrieval.retrieve_docs(debug=debug) - - graph = StateGraph(GraphState) - - # attach our nodes to the graph - graph_nodes = nodes.Nodes(context, debug=debug) - for key, value in graph_nodes.node_map.items(): - graph.add_node(key, value) - - # construct the graph by adding edges - graph = edges.enrich(graph) - - # set the starting and ending nodes of the graph - graph.set_entry_point(key="generate") - graph.set_finish_point(key="finish") - - return graph diff --git a/06_gpu_and_ml/langchains/codelangchain/app.py b/06_gpu_and_ml/langchains/codelangchain/app.py deleted file mode 100644 index 347859805..000000000 --- a/06_gpu_and_ml/langchains/codelangchain/app.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Application serving logic for the CodeLangChain agent.""" - -import agent -import modal -from agent import app, nodes -from fastapi import FastAPI, responses -from fastapi.middleware.cors import CORSMiddleware - -# create a FastAPI app -web_app = FastAPI( - title="CodeLangChain Server", - version="1.0", - description="Answers questions about LangChain Expression Language (LCEL).", -) - - -# set all CORS enabled origins -web_app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], -) - - -# host it on Modal -@app.function(keep_warm=1) -@modal.asgi_app() -def serve(): - from langchain_core.runnables import RunnableLambda - from langserve import add_routes - - def inp(question: str) -> dict: - return {"keys": {"question": question, "iterations": 0}} - - def out(state: dict) -> str: - if "keys" in state: - return state["keys"]["response"] - elif "generate" in state: - return nodes.extract_response(state["generate"]) - else: - return str(state) - - graph = agent.construct_graph(debug=False).compile() - - chain = RunnableLambda(inp) | graph | RunnableLambda(out) - - add_routes( - web_app, - chain, - path="/codelangchain", - ) - - # redirect the root to the interactive playground - @web_app.get("/") - def redirect(): - return responses.RedirectResponse(url="/codelangchain/playground") - - return web_app diff --git a/06_gpu_and_ml/langchains/codelangchain/common.py b/06_gpu_and_ml/langchains/codelangchain/common.py deleted file mode 100644 index fc9219be3..000000000 --- a/06_gpu_and_ml/langchains/codelangchain/common.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Shared information: image definitions and common utilities.""" - -import os -from typing import Any, Dict, TypedDict - -import modal - -image = modal.Image.debian_slim(python_version="3.11").pip_install( - "fastapi[standard]==0.115.4", - "pydantic==2.9.2", - "starlette==0.41.2", - "beautifulsoup4~=4.12.3", - "langchain==0.1.11", - "langgraph==0.0.26", - "langchain_community==0.0.27", - "langchain-openai==0.0.8", - "langserve[all]==0.0.46", -) - -agent_image = image.pip_install( - "chromadb==0.4.24", - "langchainhub==0.1.15", - "faiss-cpu~=1.8.0", - "tiktoken==0.6.0", -) - -app = modal.App( - "code-langchain", - image=image, - secrets=[ - modal.Secret.from_name("my-openai-secret"), - modal.Secret.from_name("my-langsmith-secret"), - ], -) - - -class GraphState(TypedDict): - """ - Represents the state of our graph. - - Attributes: - keys: A dictionary where each key is a string. - """ - - keys: Dict[str, Any] - - -os.environ["LANGCHAIN_PROJECT"] = "codelangchain" - -COLOR = { - "HEADER": "\033[95m", - "BLUE": "\033[94m", - "GREEN": "\033[92m", - "RED": "\033[91m", - "ENDC": "\033[0m", -} diff --git a/06_gpu_and_ml/langchains/codelangchain/sandbox.py b/06_gpu_and_ml/langchains/codelangchain/sandbox.py deleted file mode 100644 index a00f20c4b..000000000 --- a/06_gpu_and_ml/langchains/codelangchain/sandbox.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Defines the logic for running agent code in a sandbox.""" - -import modal -from common import COLOR, agent_image, app - - -def run(code: str): - print( - f"{COLOR['HEADER']}📦: Running in sandbox{COLOR['ENDC']}", - f"{COLOR['GREEN']}{code}{COLOR['ENDC']}", - sep="\n", - ) - sb = app.spawn_sandbox( - "python", - "-c", - code, - image=agent_image, - timeout=60 * 10, # 10 minutes - secrets=[ - modal.Secret.from_name( - "my-openai-secret" - ) # could be a different secret! - ], - ) - - sb.wait() - - if sb.returncode != 0: - print( - f"{COLOR['HEADER']}📦: Failed with exitcode {sb.returncode}{COLOR['ENDC']}" - ) - - return sb diff --git a/06_gpu_and_ml/langchains/codelangchain/README.md b/13_sandboxes/codelangchain/README.md similarity index 72% rename from 06_gpu_and_ml/langchains/codelangchain/README.md rename to 13_sandboxes/codelangchain/README.md index e05cf89f0..e614f995d 100644 --- a/06_gpu_and_ml/langchains/codelangchain/README.md +++ b/13_sandboxes/codelangchain/README.md @@ -3,10 +3,8 @@ This example deploys a "code agent": a language model that can write and execute code in a flexible control flow aimed at completing a task or goal. -The agent is designed to help write programs in the LangChain Expression -Language (LCEL). And, naturally, it is implemented in LangChain, using the -LangGraph library to structure the agent and the LangServe framework to turn it -into a FastAPI app. +It is implemented in LangChain, using the LangGraph library to structure the agent +and the LangServe framework to turn it into a FastAPI app. We use Modal to turn that app into a web endpoint. We also use Modal to "sandbox" the agent's code execution, so that it can't accidentally (or when @@ -21,19 +19,19 @@ the project's context and implementation. Check it out if you're curious! To run this app, you need to `pip install modal` and then create the following [secrets](https://modal.com/docs/guide/secrets): -- `my-openai-secret` with an OpenAI API key, so that we can query OpenAI's +- `openai-secret` with an OpenAI API key, so that we can query OpenAI's models to power the agent, - and `my-langsmith-secret` with a LangSmith API key, so that we can monitor the agent's behavior with LangSmith. Head to the -[secret creation dashboard](https://modal.com/charles-modal-labs/secrets/create) +[secret creation dashboard](https://modal.com/secrets/) and follow the instructions for each secret type. Then, you can deploy the app with: ```bash -modal deploy app.py +modal deploy codelangchain.py ``` Navigate to the URL that appears in the output and you'll be dropped into an @@ -44,23 +42,20 @@ You can also navigate to the `/docs` path to see OpenAPI/Swagger docs, for everything you'd need to see how to incorporate the agent into your downstream applications via API requests. -When developing the app, use `modal serve app.py` to get a hot-reloading server. +When developing the app, use `modal serve codelangchain.py` to get a hot-reloading server. ## Repo structure -The web application is defined in `app.py`. +The web application is defined in `codelangchain.py`. It wraps the `agent.py` module, which contains the LangChain agent's definition. To test the agent in isolation, run `modal run agent.py` in the terminal and -provide a `--question` about LCEL as input. +provide a `--question` about Python programming as input. Because the agent is a graph, it is defined by specifying nodes and edges, which are found in `nodes.py` and `edges.py`, respectively. -The logic for spinning up a `modal.Sandbox` to contain the agent's actions is in -`sandbox.py`. - -The retrieval logic is very simple: all of the data from the LCEL docs is +The retrieval logic is very simple: all of the data from the relevant docs is retrieved and put at the beginning of the language model's prompt. You can find it in `retrieval.py`. diff --git a/06_gpu_and_ml/langchains/codelangchain/__init__.py b/13_sandboxes/codelangchain/__init__.py similarity index 100% rename from 06_gpu_and_ml/langchains/codelangchain/__init__.py rename to 13_sandboxes/codelangchain/__init__.py diff --git a/13_sandboxes/codelangchain/agent.py b/13_sandboxes/codelangchain/agent.py new file mode 100644 index 000000000..8dd35cc9b --- /dev/null +++ b/13_sandboxes/codelangchain/agent.py @@ -0,0 +1,210 @@ +# --- +# cmd: ["modal", "run", "13_sandboxes.codelangchain.agent", "--question", "Use gpt2 and transformers to generate text"] +# tags: ["featured", "use-case-sandboxed-code-execution"] +# pytest: false +# --- + +# # Build a coding agent with Modal Sandboxes and LangGraph + +# This example demonstrates how to build an LLM coding "agent" that can generate and execute Python code, using +# documentation from the web to inform its approach. + +# Naturally, we use the agent to generate code that runs language models. + +# The agent is built with [LangGraph](https://github.com/langchain-ai/langgraph), a library for building +# directed graphs of computation popular with AI agent developers, +# and uses models from the OpenAI API. + +# ## Setup + +import modal + +from .src import edges, nodes, retrieval +from .src.common import COLOR, PYTHON_VERSION, image + +# You will need two [Modal Secrets](https://modal.com/docs/guide/secrets) to run this example: +# one to access the OpenAI API and another to access the LangSmith API for logging the agent's behavior. + +# To create them, head to the [Secrets dashboard](https://modal.com/secrets), select "Create new secret", +# and use the provided templates for OpenAI and LangSmith. + +app = modal.App( + "example-code-langchain", + image=image, + secrets=[ + modal.Secret.from_name("openai-secret"), + modal.Secret.from_name("my-langsmith-secret"), + ], +) + +# ## Creating a Sandbox + +# We execute the agent's code in a Modal [Sandbox](https://modal.com/docs/guide/sandbox), which allows us to +# run arbitrary code in a safe environment. In this example, we will use the [`transformers`](https://huggingface.co/docs/transformers/index) +# library to generate text with a pre-trained model. Let's create a Sandbox with the necessary dependencies. + + +def create_sandbox(app) -> modal.Sandbox: + # Change this image (and the retrieval logic in the retrieval module) + # if you want the agent to give coding advice on other libraries! + agent_image = modal.Image.debian_slim( + python_version=PYTHON_VERSION + ).pip_install( + "torch==2.5.0", + "transformers==4.46.0", + ) + + return modal.Sandbox.create( + image=agent_image, + timeout=60 * 10, # 10 minutes + app=app, + # Modal sandboxes support GPUs! + gpu="T4", + # you can also pass secrets here -- note that the main app's secrets are not shared + ) + + +# We also need a way to run our code in the sandbox. For this, we'll write a simple wrapper +# around the Modal Sandox `exec` method. We use `exec` because it allows us to run code without spinning up a +# new container. And we can reuse the same container for multiple runs, preserving state. + + +def run(code: str, sb: modal.Sandbox) -> tuple[str, str]: + print( + f"{COLOR['HEADER']}📦: Running in sandbox{COLOR['ENDC']}", + f"{COLOR['GREEN']}{code}{COLOR['ENDC']}", + sep="\n", + ) + + exc = sb.exec("python", "-c", code) + exc.wait() + + stdout = exc.stdout.read() + stderr = exc.stderr.read() + + if exc.returncode != 0: + print( + f"{COLOR['HEADER']}📦: Failed with exitcode {sb.returncode}{COLOR['ENDC']}" + ) + + return stdout, stderr + + +# ## Constructing the agent's graph + +# Now that we have the sandbox to execute code in, we can construct our agent's graph. Our graph is +# defined in the `edges` and `nodes` modules +# [associated with this example](https://github.com/modal-labs/modal-examples/tree/main/13_sandboxes/codelangchain). +# Nodes are actions that change the state. Edges are transitions between nodes. + +# The idea is simple: we start at the node `generate`, which invokes the LLM to generate code based off documentation. +# The generated code is executed (in the sandbox) as part of an edge called `check_code_execution` +# and then the outputs are passed to the LLM for evaluation (the `evaluate_execution` node). +# If the LLM determines that the code has executed correctly -- which might mean that the code raised an exception! -- +# we pass along the `decide_to_finish` edge and finish. + + +def construct_graph(sandbox: modal.Sandbox, debug: bool = False): + from langgraph.graph import StateGraph + + from .src.common import GraphState + + # Crawl the transformers documentation to inform our code generation + context = retrieval.retrieve_docs(debug=debug) + + graph = StateGraph(GraphState) + + # Attach our nodes to the graph + graph_nodes = nodes.Nodes(context, sandbox, run, debug=debug) + for key, value in graph_nodes.node_map.items(): + graph.add_node(key, value) + + # Construct the graph by adding edges + graph = edges.enrich(graph) + + # Set the starting and ending nodes of the graph + graph.set_entry_point(key="generate") + graph.set_finish_point(key="finish") + + return graph + + +# We now set up the graph and compile it. See the `src` module for details +# on the content of the graph and the nodes we've defined. + +DEFAULT_QUESTION = "How do I generate Python code using a pre-trained model from the transformers library?" + + +@app.function() +def go( + question: str = DEFAULT_QUESTION, + debug: bool = False, +): + """Compiles the Python code generation agent graph and runs it, returning the result.""" + sb = create_sandbox(app) + + graph = construct_graph(sb, debug=debug) + runnable = graph.compile() + result = runnable.invoke( + {"keys": {"question": question, "iterations": 0}}, + config={"recursion_limit": 50}, + ) + + sb.terminate() + + return result["keys"]["response"] + + +# ## Running the Graph + +# Now let's call the agent from the command line! + +# We define a `local_entrypoint` that runs locally and triggers execution on Modal. + +# You can invoke it by executing following command from a folder that contains the `codelangchain` directory +# [from our examples repo](https://github.com/modal-labs/modal-examples/tree/main/13_sandboxes/codelangchain): + +# ```bash +# modal run codelangchain.agent --question "How do I run a pre-trained model from the transformers library?" +# ``` + + +@app.local_entrypoint() +def main( + question: str = DEFAULT_QUESTION, + debug: bool = False, +): + """Sends a question to the Python code generation agent. + + Switch to debug mode for shorter context and smaller model.""" + if debug: + if question == DEFAULT_QUESTION: + question = "hi there, how are you?" + + print(go.remote(question, debug=debug)) + + +# If things are working properly, you should see output like the following: + +# ```bash +# $ modal run agent.py --question "generate some cool output with transformers" +# ---DECISION: FINISH--- +# ---FINISHING--- +# To generate some cool output using transformers, we can use a pre-trained language model from the Hugging Face Transformers library. In this example, we'll use the GPT-2 model to generate text based on a given prompt. The GPT-2 model is a popular choice for text generation tasks due to its ability to produce coherent and contextually relevant text. We'll use the pipeline API from the Transformers library, which simplifies the process of using pre-trained models for various tasks, including text generation. +# +# from transformers import pipeline +# # Initialize the text generation pipeline with the GPT-2 model +# generator = pipeline('text-generation', model='gpt2') +# +# # Define a prompt for the model to generate text from +# prompt = "Once upon a time in a land far, far away" +# +# # Generate text using the model +# output = generator(prompt, max_length=50, num_return_sequences=1) +# +# # Print the generated text +# print(output[0]['generated_text']) +# +# Result of code execution: +# Once upon a time in a land far, far away, and still inhabited even after all the human race, there would be one God: a perfect universal God who has always been and will ever be worshipped. All His acts and deeds are immutable, +# ``` diff --git a/13_sandboxes/codelangchain/langserve.py b/13_sandboxes/codelangchain/langserve.py new file mode 100644 index 000000000..dcbe0cff8 --- /dev/null +++ b/13_sandboxes/codelangchain/langserve.py @@ -0,0 +1,86 @@ +# --- +# pytest: false +# cmd: ["modal", "serve", "13_sandboxes.codelangchain.langserve"] +# --- + +# # Deploy LangChain and LangGraph applications with LangServe + +# This code demonstrates how to deploy a +# [LangServe](https://python.langchain.com/docs/langserve/) application on Modal. +# LangServe makes it easy to wrap LangChain and LangGraph applications in a FastAPI server, +# and Modal makes it easy to deploy FastAPI servers. + +# The LangGraph application that it serves is from our [sandboxed LLM coding agent example](https://modal.com/docs/examples/agent). + +# You can find the code for the agent and several other code files associated with this example in the +# [`codelangchain` directory of our examples repo](https://github.com/modal-labs/modal-examples/tree/main/13_sandboxes/codelangchain). + +import modal + +from .agent import construct_graph, create_sandbox +from .src.common import image + +app = modal.App("example-langserve") + +image = image.pip_install("langserve[all]==0.3.0") + + +@app.function( + image=image, + secrets=[ # see the agent.py file for more information on Secrets + modal.Secret.from_name("openai-secret"), + modal.Secret.from_name("my-langsmith-secret"), + ], +) +@modal.asgi_app() +def serve(): + from fastapi import FastAPI, responses + from fastapi.middleware.cors import CORSMiddleware + from langchain_core.runnables import RunnableLambda + from langserve import add_routes + + # create a FastAPI app + web_app = FastAPI( + title="CodeLangChain Server", + version="1.0", + description="Writes code and checks if it runs.", + ) + + # set all CORS enabled origins + web_app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"], + ) + + def inp(question: str) -> dict: + return {"keys": {"question": question, "iterations": 0}} + + def out(state: dict) -> str: + if "finish" in state: + return state["finish"]["keys"]["response"] + elif len(state) > 0 and "finish" in state[-1]: + return state[-1]["finish"]["keys"]["response"] + else: + return str(state) + + graph = construct_graph(create_sandbox(app), debug=False).compile() + + chain = RunnableLambda(inp) | graph | RunnableLambda(out) + + add_routes( + web_app, + chain, + path="/codelangchain", + ) + + # redirect the root to the interactive playground + @web_app.get("/") + def redirect(): + return responses.RedirectResponse(url="/codelangchain/playground") + + # return the FastAPI app and Modal will deploy it for us + return web_app diff --git a/13_sandboxes/codelangchain/src/__init__.py b/13_sandboxes/codelangchain/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/13_sandboxes/codelangchain/src/common.py b/13_sandboxes/codelangchain/src/common.py new file mode 100644 index 000000000..3d9038a62 --- /dev/null +++ b/13_sandboxes/codelangchain/src/common.py @@ -0,0 +1,40 @@ +"""Shared information: image definitions and common utilities.""" + +import os +from typing import Any, Dict, TypedDict + +import modal + +PYTHON_VERSION = "3.11" + +image = modal.Image.debian_slim(python_version=PYTHON_VERSION).pip_install( + "beautifulsoup4~=4.12.3", + "langchain==0.3.4", + "langchain-core==0.3.12", + "langgraph==0.2.39", + "langchain-community==0.3.3", + "langchain-openai==0.2.3", +) + + +class GraphState(TypedDict): + """ + Represents the state of our graph. + + Attributes: + keys: A dictionary where each key is a string. + """ + + keys: Dict[str, Any] + + +os.environ["LANGCHAIN_PROJECT"] = "codelangchain" +os.environ["LANGCHAIN_TRACING"] = "true" + +COLOR = { + "HEADER": "\033[95m", + "BLUE": "\033[94m", + "GREEN": "\033[92m", + "RED": "\033[91m", + "ENDC": "\033[0m", +} diff --git a/06_gpu_and_ml/langchains/codelangchain/edges.py b/13_sandboxes/codelangchain/src/edges.py similarity index 89% rename from 06_gpu_and_ml/langchains/codelangchain/edges.py rename to 13_sandboxes/codelangchain/src/edges.py index 584db9f17..5784cc062 100644 --- a/06_gpu_and_ml/langchains/codelangchain/edges.py +++ b/13_sandboxes/codelangchain/src/edges.py @@ -2,7 +2,7 @@ from typing import Callable -from common import GraphState +from .common import GraphState EXPECTED_NODES = [ "generate", @@ -27,8 +27,9 @@ def enrich(graph): "generate": "generate", }, ) + graph.add_edge("check_code_execution", "evaluate_execution") graph.add_conditional_edges( - "check_code_execution", + "evaluate_execution", EDGE_MAP["decide_to_finish"], { "finish": "finish", @@ -75,12 +76,12 @@ def decide_to_finish(state: GraphState) -> str: str: Next node to call """ - print("---DECIDE TO TEST CODE EXECUTION---") + print("---DECIDE TO FINISH---") state_dict = state["keys"] - error = state_dict["error"] + evaluation = state_dict["evaluation"] iter = state_dict["iterations"] - if error == "None" or iter >= 3: + if evaluation.decision == "finish" or iter >= 3: print("---DECISION: FINISH---") return "finish" else: diff --git a/06_gpu_and_ml/langchains/codelangchain/nodes.py b/13_sandboxes/codelangchain/src/nodes.py similarity index 62% rename from 06_gpu_and_ml/langchains/codelangchain/nodes.py rename to 13_sandboxes/codelangchain/src/nodes.py index c3128e14e..0e3c3d31a 100644 --- a/06_gpu_and_ml/langchains/codelangchain/nodes.py +++ b/13_sandboxes/codelangchain/src/nodes.py @@ -1,34 +1,47 @@ import sys +from enum import Enum from operator import itemgetter +from typing import Callable -import sandbox -from common import GraphState, agent_image, image +import modal + +from .common import GraphState, image with image.imports(): from langchain.output_parsers.openai_tools import PydanticToolsParser from langchain.prompts import PromptTemplate - from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.utils.function_calling import convert_to_openai_tool from langchain_openai import ChatOpenAI + from pydantic import BaseModel, Field class Nodes: - def __init__(self, context: str, debug: bool = False): + def __init__( + self, + context: str, + sb: modal.Sandbox, + run: Callable[[str, modal.Sandbox], tuple[str, str]], + debug: bool = False, + ): self.context = context self.debug = debug self.model = ( - "gpt-4-0125-preview" if not self.debug else "gpt-3.5-turbo-0125" + "gpt-4o-2024-08-06" if not self.debug else "gpt-4o-mini-2024-07-18" ) self.node_map = { "generate": self.generate, "check_code_imports": self.check_code_imports, "check_code_execution": self.check_code_execution, + "evaluate_execution": self.evaluate_execution, # New node "finish": self.finish, } + self.sb = sb + self.run = run + def generate(self, state: GraphState) -> GraphState: """ - Generate a code solution based on LCEL docs and the input question + Generate a code solution based on docs and the input question with optional feedback from code execution tests Args: @@ -44,7 +57,7 @@ def generate(self, state: GraphState) -> GraphState: iter = state_dict["iterations"] ## Data model - class code(BaseModel): + class Code(BaseModel): """Code output""" prefix: str = Field( @@ -59,39 +72,37 @@ class code(BaseModel): llm = ChatOpenAI(temperature=0, model=self.model, streaming=True) # Tool - code_tool_oai = convert_to_openai_tool(code) + code_tool_oai = convert_to_openai_tool(Code) # LLM with tool and enforce invocation llm_with_tool = llm.bind( tools=[code_tool_oai], - tool_choice={"type": "function", "function": {"name": "code"}}, + tool_choice={"type": "function", "function": {"name": "Code"}}, ) # Parser - parser_tool = PydanticToolsParser(tools=[code]) + parser_tool = PydanticToolsParser(tools=[Code]) ## Prompt - template = ( - """You are a coding assistant with expertise in LCEL, LangChain expression language. \n - You are able to execute Python code in a sandbox environment that was constructed by chaining together the following two Dockerfile commands: \n - """ - + f"{image.dockerfile_commands()}" - + "\n" - + f"{agent_image.dockerfile_commands()}" - + """ - You are tasked with responding to the following user question: {question} - Your response will be shown to the user. - Here is a full set of LCEL documentation: - \n ------- \n - {context} - \n ------- \n - Answer the user question based on the above provided documentation. \n - Ensure any code you provide can be executed with all required imports and variables defined. \n - Structure your answer as a description of the code solution, \n - then a list of the imports, and then finally list the functioning code block. \n - Here is the user question again: \n --- --- --- \n {question} - """ - ) + template = """ +You are a coding assistant with expertise in Python. +You are able to execute Python code in a sandbox environment. +You are tasked with responding to the following user question: {question} +Your response will be shown to the user. +Here is a full set of documentation: + +------- +{context} +------- + +Answer the user question based on the above provided documentation. +Ensure any code you provide can be executed with all required imports and variables defined. +Structure your answer as a description of the code solution, +then a list of the imports, and then finally list the functioning code block. +Here is the user question again: + +--- --- --- +{question}""" ## Generation if "error" in state_dict: @@ -101,13 +112,21 @@ class code(BaseModel): code_solution = state_dict["generation"] # Update prompt - addendum = """ \n --- --- --- \n You previously tried to solve this problem. \n Here is your solution: - \n --- --- --- \n {generation} \n --- --- --- \n Here is the resulting error from code - execution: \n --- --- --- \n {error} \n --- --- --- \n Please re-try to answer this. - Structure your answer with a description of the code solution. \n Then list the imports. - And finally list the functioning code block. Structure your answer with a description of - the code solution. \n Then list the imports. And finally list the functioning code block. - \n Here is the user question: \n --- --- --- \n {question}""" + addendum = """You previously tried to solve this problem. Here is your solution: + +{generation} + +Here is the resulting error from code execution: + +{error} + +Please re-try to answer this. Structure your answer with a description of the code solution. +Then list the imports. And finally list the functioning code block. Structure your answer with a description of +the code solution. Then list the imports. And finally list the functioning code block. + +Here is the user question: + +{question}""" template = template + addendum # Prompt @@ -188,8 +207,7 @@ def check_code_imports(self, state: GraphState) -> GraphState: iter = state_dict["iterations"] # Attempt to execute the imports - sb = sandbox.run(imports) - output, error = sb.stdout.read(), sb.stderr.read() + output, error = self.run(imports, self.sb) if error: print("---CODE IMPORT CHECK: FAILED---") # Catch any error during execution (e.g., ImportError, SyntaxError) @@ -197,14 +215,15 @@ def check_code_imports(self, state: GraphState) -> GraphState: print(f"Error: {error}", file=sys.stderr) if "error" in state_dict: error_prev_runs = state_dict["error"] - error = ( - error_prev_runs - + "\n --- Most recent run output and error --- \n" - " ------ output ------ \n" - + output - + "\n ------ error ------ \n" - + error - ) + error = f""" +{error_prev_runs} + +--- Most recent run output and error --- +------ output ------ +{output} +------ error ------ +{error} +""" else: print("---CODE IMPORT CHECK: SUCCESS---") # No errors occurred @@ -235,14 +254,12 @@ def check_code_execution(self, state: GraphState) -> GraphState: state_dict = state["keys"] question = state_dict["question"] code_solution = state_dict["generation"] - prefix = code_solution[0].prefix imports = code_solution[0].imports code = code_solution[0].code code_block = imports + "\n" + code iter = state_dict["iterations"] - sb = sandbox.run(code_block) - output, error = sb.stdout.read(), sb.stderr.read() + output, error = self.run(code_block, self.sb) if error: print("---CODE BLOCK CHECK: FAILED---") error = f"Execution error: {error}" @@ -267,10 +284,84 @@ def check_code_execution(self, state: GraphState) -> GraphState: "generation": code_solution, "question": question, "error": error, - "prefix": prefix, - "imports": imports, + "output": output, "iterations": iter, - "code": code, + } + } + + def evaluate_execution(self, state: GraphState) -> GraphState: + """ + Evaluate the code execution results and determine whether to finish or retry. + + Args: + state (dict): The current graph state + + Returns: + state (dict): Updated state with decision to finish or retry + """ + print("---EVALUATING EXECUTION---") + + state_dict = state["keys"] + output = state_dict["output"] + error = state_dict["error"] + + code_solution = state_dict["generation"][0] + code = code_solution.code + + class Decision(str, Enum): + FINISH = "finish" + RETRY = "retry" + + class ExecutionEvaluation(BaseModel): + """Evaluation of code execution""" + + decision: Decision = Field( + description="Decision to finish or retry" + ) + explanation: str = Field(description="Explanation for the decision") + + llm = ChatOpenAI(temperature=0, model=self.model) + evaluation_tool = convert_to_openai_tool(ExecutionEvaluation) + llm_with_tool = llm.bind( + tools=[evaluation_tool], + tool_choice={ + "type": "function", + "function": {"name": "ExecutionEvaluation"}, + }, + ) + parser_tool = PydanticToolsParser(tools=[ExecutionEvaluation]) + + template = """ +You are an expert code evaluator. Analyze the following code execution results and determine if the execution was successful. + +Code: +{code} + +Output: +{output} + +Error: +{error} + +Decide whether to finish (if the execution was successful) or retry (if there were errors or unexpected results). +Provide a brief explanation for your decision. + """.strip() + + prompt = PromptTemplate( + template=template, + input_variables=["code", "output", "error"], + ) + + chain = prompt | llm_with_tool | parser_tool + + evaluation = chain.invoke( + {"code": code, "output": output, "error": error} + ) + + return { + "keys": { + **state_dict, + "evaluation": evaluation[0], } } @@ -286,6 +377,8 @@ def finish(self, state: GraphState) -> dict: response = extract_response(state) + self.sb.terminate() + return {"keys": {"response": response}} @@ -302,8 +395,18 @@ def extract_response(state: GraphState) -> str: state_dict = state["keys"] code_solution = state_dict["generation"][0] + prefix = code_solution.prefix imports = code_solution.imports code = code_solution.code - return "\n".join([prefix, imports, code]) + code_output = state_dict["output"] + + return f"""{prefix} + +{imports} +{code} + +Result of code execution: +{code_output} +""" diff --git a/06_gpu_and_ml/langchains/codelangchain/retrieval.py b/13_sandboxes/codelangchain/src/retrieval.py similarity index 82% rename from 06_gpu_and_ml/langchains/codelangchain/retrieval.py rename to 13_sandboxes/codelangchain/src/retrieval.py index 35eb041b4..574b4c1cf 100644 --- a/06_gpu_and_ml/langchains/codelangchain/retrieval.py +++ b/13_sandboxes/codelangchain/src/retrieval.py @@ -1,11 +1,11 @@ """Just as a constant function is _technically_ a polynomial, so too is injecting the same information every time _technically_ RAG.""" -from common import COLOR +from .common import COLOR -lcel_docs_url = "https://python.langchain.com/docs/expression_language/" +docs_url = "https://huggingface.co/docs/transformers/index" -def retrieve_docs(url: str = lcel_docs_url, debug=False): +def retrieve_docs(url: str = docs_url, debug=False): from bs4 import BeautifulSoup as Soup from langchain_community.document_loaders.recursive_url_loader import ( RecursiveUrlLoader, @@ -15,8 +15,8 @@ def retrieve_docs(url: str = lcel_docs_url, debug=False): f"{COLOR['HEADER']}📜: Retrieving documents from {url}{COLOR['ENDC']}" ) loader = RecursiveUrlLoader( - url=lcel_docs_url, - max_depth=20 // (int(debug) + 1), # retrieve fewer docs in debug mode + url=docs_url, + max_depth=2 // (int(debug) + 1), # retrieve fewer docs in debug mode extractor=lambda x: Soup(x, "html.parser").text, ) docs = loader.load()