Skip to content

Instantly share code, notes, and snippets.

@mjrusso
Created March 8, 2024 19:53
Show Gist options
  • Save mjrusso/c74803ed7ed49d42f9aefe77b6a62c52 to your computer and use it in GitHub Desktop.
Save mjrusso/c74803ed7ed49d42f9aefe77b6a62c52 to your computer and use it in GitHub Desktop.
Livebook demonstrating the use of Instructor with LangChain

Instructor+LangChain Demo

Mix.install(
  [
    {:instructor, "~> 0.0.5"},
    {:langchain, "~> 0.1.0"},
    {:kino, "~> 0.12.0"}
  ],
  config: [
    instructor: [
      adapter: Instructor.Adapters.OpenAI,
      openai: [api_key: System.fetch_env!("LB_OPENAI_API_KEY")]
    ],
    langchain: [
      openai_key: System.fetch_env!("LB_OPENAI_API_KEY")
    ]
  ],
  consolidate_protocols: false
)

Schema Definitions

These Ecto schemas are used by Instructor.

defmodule Recipe do
  use Ecto.Schema
  use Instructor.Validator

  @doc """
  A recipe.

  Recipes have a name, ingredients (name and quantity), and a list of 
  steps to perform (using the indicated ingredients).
  """
  @derive {Jason.Encoder, only: [:name, :ingredients, :steps]}
  @primary_key false
  embedded_schema do
    field(:name, :string)

    embeds_many :ingredients, Ingredient, primary_key: false do
      @derive Jason.Encoder

      field(:name, :string)
      field(:quantity, :integer)
    end

    embeds_many :steps, Step, primary_key: false do
      @derive Jason.Encoder

      field(:description, :string)
    end
  end
end
{:module, Recipe, <<70, 79, 82, 49, 0, 0, 18, ...>>,
 [__schema__: 1, __schema__: 1, __schema__: 1, __schema__: 1, __schema__: 2, __schema__: 2, ...]}
defmodule ShoppingList do
  use Ecto.Schema
  use Instructor.Validator

  @doc """
  A shopping list.

  Recipes have a name, ingredients (name and quantity), and a list of 
  steps to perform (using the indicated ingredients).
  """
  @derive Jason.Encoder
  @primary_key false
  embedded_schema do
    embeds_many :ingredients, Ingredient, primary_key: false do
      @derive Jason.Encoder

      field(:name, :string)
      field(:quantity, :integer)
    end
  end
end
{:module, ShoppingList, <<70, 79, 82, 49, 0, 0, 16, ...>>,
 [__schema__: 1, __schema__: 1, __schema__: 1, __schema__: 1, __schema__: 2, __schema__: 2, ...]}

Chain

Build a chain with LangChain. Note that some of the functions in the chain use Instructor to coerce the LLM to return structured output, as per the Ecto schemas defined above.

alias LangChain.Function
alias LangChain.FunctionParam
alias LangChain.Message
alias LangChain.ChatModels.ChatOpenAI
alias LangChain.Chains.LLMChain

functions = [
  Function.new!(%{
    name: "get_recipe_from_cookbook",
    parameters: [
      FunctionParam.new!(%{name: "recipe_name", type: :string, required: true})
    ],
    function: fn %{"recipe_name" => recipe_name}, _context ->
      {:ok, %{ingredients: ingredients, steps: steps}} =
        Instructor.chat_completion(
          model: "gpt-3.5-turbo",
          response_model: Recipe,
          messages: [
            %{role: "system", content: "You are an expert baker."},
            %{role: "user", content: "Prepare a recipe for: #{recipe_name}"}
          ]
        )

      Jason.encode!(%{ingredients: ingredients, steps: steps})
    end
  }),
  Function.new!(%{
    name: "check_whats_in_the_fridge",
    description: "Look inside the fridge and return what you find",
    function: fn _params, _context ->
      ~s<3 sticks of butter, 2 dozen eggs, a spoilt carton of milk>
    end
  }),
  Function.new!(%{
    name: "check_whats_in_the_pantry",
    description: "Look inside the pantry and return what you find",
    function: fn _params, _context ->
      ~s<a lot of flour, a bag full of sugar, some vanilla extract, and half a box of baking powder>
    end
  }),
  Function.new!(%{
    name: "generate_shopping_list",
    description:
      "Compare the ingredients you already have at home, with the ingredients that the recipe calls for, and generates a shopping list with what's missing.",
    parameters: [
      FunctionParam.new!(%{name: "what_I_have_at_home", type: :string, required: true}),
      FunctionParam.new!(%{name: "what_the_recipe_calls_for", type: :string, required: true})
    ],
    function: fn %{
                   "what_I_have_at_home" => have,
                   "what_the_recipe_calls_for" => need
                 },
                 _context ->
      {:ok, %{ingredients: ingredients}} =
        Instructor.chat_completion(
          model: "gpt-4",
          response_model: ShoppingList,
          messages: [
            %{
              role: "user",
              content: ~s<I have the following ingredients at home: 

                          #{have}

                          The recipe I'm preparing calls for the following ingredients:

                          #{need}

                          Prepare a shopping list.
                          Only include items that the recipe calls for that I don't already have.
                         >
            }
          ]
        )

      Jason.encode!(%{ingredients: ingredients})
    end
  })
]

messages = [
  Message.new_system!(~s<You are a helpful assistant.>),
  Message.new_user!(~s<
    I'd love to bake a yummy treat.
    
    Please get me a relevant recipe (ingredients and steps) from my cookbook.
    
    Pick the type of recipe on your own without asking.

    If I'm missing any ingredients, let me know exactly what I need to grab at the grocery store.
    >)
]

chat_model = ChatOpenAI.new!(%{model: "gpt-4", temperature: 1, stream: false})

context = %{}

{:ok, updated_chain, response} =
  %{llm: chat_model, custom_context: context, verbose: true}
  |> LLMChain.new!()
  |> LLMChain.add_messages(messages)
  |> LLMChain.add_functions(functions)
  |> LLMChain.run(while_needs_response: true)

IO.puts(response.content)
response.content
LLM: %LangChain.ChatModels.ChatOpenAI{
  endpoint: "https://api.openai.com/v1/chat/completions",
  model: "gpt-4",
  api_key: nil,
  temperature: 1.0,
  frequency_penalty: 0.0,
  receive_timeout: 60000,
  seed: nil,
  n: 1,
  json_response: false,
  stream: false,
  max_tokens: nil
}
MESSAGES: [
  %LangChain.Message{
    content: "You are a helpful assistant.",
    index: nil,
    status: :complete,
    role: :system,
    function_name: nil,
    arguments: nil
  },
  %LangChain.Message{
    content: "\n    I'd love to bake a yummy treat.\n    \n    Please get me a relevant recipe (ingredients and steps) from my cookbook.\n    \n    Pick the type of recipe on your own without asking.\n\n    If I'm missing any ingredients, let me know exactly what I need to grab at the grocery store.\n    ",
    index: nil,
    status: :complete,
    role: :user,
    function_name: nil,
    arguments: nil
  }
]
FUNCTIONS: [
  %LangChain.Function{
    name: "get_recipe_from_cookbook",
    description: nil,
    display_text: nil,
    function: #Function<41.105768164/2 in :erl_eval.expr/6>,
    parameters_schema: nil,
    parameters: [
      %LangChain.FunctionParam{
        name: "recipe_name",
        type: :string,
        item_type: nil,
        enum: [],
        description: nil,
        required: true,
        object_properties: []
      }
    ]
  },
  %LangChain.Function{
    name: "check_whats_in_the_fridge",
    description: "Look inside the fridge and return what you find",
    display_text: nil,
    function: #Function<41.105768164/2 in :erl_eval.expr/6>,
    parameters_schema: nil,
    parameters: []
  },
  %LangChain.Function{
    name: "check_whats_in_the_pantry",
    description: "Look inside the pantry and return what you find",
    display_text: nil,
    function: #Function<41.105768164/2 in :erl_eval.expr/6>,
    parameters_schema: nil,
    parameters: []
  },
  %LangChain.Function{
    name: "generate_shopping_list",
    description: "Compare the ingredients you already have at home, with the ingredients that the recipe calls for, and generates a shopping list with what's missing.",
    display_text: nil,
    function: #Function<41.105768164/2 in :erl_eval.expr/6>,
    parameters_schema: nil,
    parameters: [
      %LangChain.FunctionParam{
        name: "what_I_have_at_home",
        type: :string,
        item_type: nil,
        enum: [],
        description: nil,
        required: true,
        object_properties: []
      },
      %LangChain.FunctionParam{
        name: "what_the_recipe_calls_for",
        type: :string,
        item_type: nil,
        enum: [],
        description: nil,
        required: true,
        object_properties: []
      }
    ]
  }
]
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  function_name: "get_recipe_from_cookbook",
  arguments: %{"recipe_name" => "Chocolate Chip Cookies"}
}
EXECUTING FUNCTION: "get_recipe_from_cookbook"

14:39:44.490 [debug] Executing function "get_recipe_from_cookbook"
FUNCTION RESULT: "{\"ingredients\":[{\"name\":\"butter\",\"quantity\":1},{\"name\":\"sugar\",\"quantity\":1},{\"name\":\"brown sugar\",\"quantity\":1},{\"name\":\"vanilla extract\",\"quantity\":1},{\"name\":\"eggs\",\"quantity\":2},{\"name\":\"flour\",\"quantity\":3},{\"name\":\"baking soda\",\"quantity\":1},{\"name\":\"salt\",\"quantity\":1},{\"name\":\"chocolate chips\",\"quantity\":2}],\"steps\":[{\"description\":\"Preheat the oven to 350°F (175°C).\"},{\"description\":\"In a bowl, cream together the butter, sugar, brown sugar, and vanilla extract.\"},{\"description\":\"Beat in the eggs one at a time.\"},{\"description\":\"Combine flour, baking soda, and salt; gradually add to the butter mixture.\"},{\"description\":\"Stir in the chocolate chips.\"},{\"description\":\"Drop rounded spoonfuls of dough onto ungreased baking sheets.\"},{\"description\":\"Bake for 10 to 12 minutes in the preheated oven, or until the edges are nicely browned.\"},{\"description\":\"Allow cookies to cool on baking sheet for 5 minutes before transferring to a wire rack to cool completely.\"}]}"
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  function_name: "check_whats_in_the_fridge",
  arguments: %{}
}
EXECUTING FUNCTION: "check_whats_in_the_fridge"

14:39:48.899 [debug] Executing function "check_whats_in_the_fridge"
FUNCTION RESULT: "3 sticks of butter, 2 dozen eggs, a spoilt carton of milk"
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  function_name: "check_whats_in_the_pantry",
  arguments: %{}
}
EXECUTING FUNCTION: "check_whats_in_the_pantry"

14:39:49.695 [debug] Executing function "check_whats_in_the_pantry"
FUNCTION RESULT: "a lot of flour, a bag full of sugar, some vanilla extract, and half a box of baking powder"
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: nil,
  index: 0,
  status: :complete,
  role: :assistant,
  function_name: "generate_shopping_list",
  arguments: %{
    "what_I_have_at_home" => "butter, eggs, flour, sugar, bag full of sugar, vanilla extract",
    "what_the_recipe_calls_for" => "butter, sugar, brown sugar, vanilla extract, eggs, flour, baking soda, salt, chocolate chips"
  }
}
EXECUTING FUNCTION: "generate_shopping_list"

14:39:52.429 [debug] Executing function "generate_shopping_list"
FUNCTION RESULT: "{\"ingredients\":[{\"name\":\"brown sugar\",\"quantity\":1},{\"name\":\"baking soda\",\"quantity\":1},{\"name\":\"salt\",\"quantity\":1},{\"name\":\"chocolate chips\",\"quantity\":1}]}"
SINGLE MESSAGE RESPONSE: %LangChain.Message{
  content: "Here is a wonderful recipe for Chocolate Chip Cookies from your cookbook:\n\nIngredients:\n- 1 stick of butter\n- 1 cup of sugar\n- 1 cup of brown sugar\n- 1 tsp of vanilla extract\n- 2 eggs\n- 3 cups of flour\n- 1 tsp of baking soda\n- 1 tsp of salt\n- 2 cups of chocolate chips\n\nSteps:\n1. Preheat the oven to 350°F (175°C).\n2. In a bowl, cream together the butter, sugar, brown sugar, and vanilla extract.\n3. Beat in the eggs one at a time.\n4. Combine flour, baking soda, and salt; gradually add to the butter mixture.\n5. Stir in the chocolate chips.\n6. Drop rounded spoonfuls of dough onto ungreased baking sheets.\n7. Bake for 10 to 12 minutes in the preheated oven, or until the edges are nicely browned.\n8. Allow cookies to cool on baking sheet for 5 minutes before transferring to a wire rack to cool completely.\n\nFrom the inventory, you're missing the following ingredients, so you need to buy these from the grocery store:\n- 1 cup of brown sugar\n- 1 tsp of baking soda\n- 1 tsp of salt\n- 2 cups of chocolate chips\n\nHappy baking!",
  index: 0,
  status: :complete,
  role: :assistant,
  function_name: nil,
  arguments: nil
}
Here is a wonderful recipe for Chocolate Chip Cookies from your cookbook:

Ingredients:
- 1 stick of butter
- 1 cup of sugar
- 1 cup of brown sugar
- 1 tsp of vanilla extract
- 2 eggs
- 3 cups of flour
- 1 tsp of baking soda
- 1 tsp of salt
- 2 cups of chocolate chips

Steps:
1. Preheat the oven to 350°F (175°C).
2. In a bowl, cream together the butter, sugar, brown sugar, and vanilla extract.
3. Beat in the eggs one at a time.
4. Combine flour, baking soda, and salt; gradually add to the butter mixture.
5. Stir in the chocolate chips.
6. Drop rounded spoonfuls of dough onto ungreased baking sheets.
7. Bake for 10 to 12 minutes in the preheated oven, or until the edges are nicely browned.
8. Allow cookies to cool on baking sheet for 5 minutes before transferring to a wire rack to cool completely.

From the inventory, you're missing the following ingredients, so you need to buy these from the grocery store:
- 1 cup of brown sugar
- 1 tsp of baking soda
- 1 tsp of salt
- 2 cups of chocolate chips

Happy baking!
"Here is a wonderful recipe for Chocolate Chip Cookies from your cookbook:\n\nIngredients:\n- 1 stick of butter\n- 1 cup of sugar\n- 1 cup of brown sugar\n- 1 tsp of vanilla extract\n- 2 eggs\n- 3 cups of flour\n- 1 tsp of baking soda\n- 1 tsp of salt\n- 2 cups of chocolate chips\n\nSteps:\n1. Preheat the oven to 350°F (175°C).\n2. In a bowl, cream together the butter, sugar, brown sugar, and vanilla extract.\n3. Beat in the eggs one at a time.\n4. Combine flour, baking soda, and salt; gradually add to the butter mixture.\n5. Stir in the chocolate chips.\n6. Drop rounded spoonfuls of dough onto ungreased baking sheets.\n7. Bake for 10 to 12 minutes in the preheated oven, or until the edges are nicely browned.\n8. Allow cookies to cool on baking sheet for 5 minutes before transferring to a wire rack to cool completely.\n\nFrom the inventory, you're missing the following ingredients, so you need to buy these from the grocery store:\n- 1 cup of brown sugar\n- 1 tsp of baking soda\n- 1 tsp of salt\n- 2 cups of chocolate chips\n\nHappy baking!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment