{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Model-graded evaluations with promptfoo\n",
    "\n",
    "**Note: This lesson lives in a folder that contains relevant code files. Download the entire folder if you want to follow along and run the evaluation yourself**\n",
    "\n",
    "\n",
    "So far, we've only written code-graded evaluations. Whenever possible, code-graded evaluations are the simplest and least-expensive evaluations to run. They offer clear-cut, objective assessments based on predefined criteria, making them ideal for tasks with straightforward, quantifiable outcomes. The trouble is that code-graded evaluations can only grade certain types of outputs, primarily those that can be reduced to exact matches, numerical comparisons, or other programmable logic.\n",
    "\n",
    "However, many real-world applications of language models require more nuanced evaluation. Suppose we wanted to build a chatbot to be used in middle-school classrooms. We might want to evaluate the outputs to make sure they use age-appropriate language, maintain an educational tone, avoid answering non-academic questions, or provide explanations at a suitable complexity level for middle schoolers. These criteria are subjective and context-dependent, making them challenging to assess with traditional code-based methods. This is where model-graded evaluations can help!\n",
    "\n",
    "Model-graded evaluations leverage the capabilities of large language models to assess outputs based on more complex, nuanced criteria. By using another model as an evaluator, we can tap into the same level of language understanding and contextual awareness that generated the original response. This approach allows us to create more sophisticated evaluation metrics that can consider factors like tone, relevance, appropriateness, and even creativity – aspects that are typically beyond the reach of code-based grading systems.\n",
    "\n",
    "The central idea behind model-graded evaluations is to treat the evaluation itself as a natural language processing task. We provide the evaluating model with some combination of the following:\n",
    "\n",
    "* The original prompt or question\n",
    "* The model-generated response we want to evaluate\n",
    "* A set of criteria or guidelines for evaluation\n",
    "* Instructions on how to assess and score the response\n",
    "\n",
    "This method allows for a more holistic assessment of the output, considering not just factual accuracy but also stylistic elements, adherence to specific guidelines, and overall quality of the response in the context of its intended use.\n",
    "\n",
    "Common model-graded evaluation techniques include asking the model:\n",
    "\n",
    "* How apologetic is this response?\n",
    "* Is the response factually accurate, given the provided context?\n",
    "* Does this response mention its context/information too much?\n",
    "* Does this response actually answer the question appropriately?\n",
    "* How well does this output adhere to our tone/brand/style guidelines?\n",
    "\n",
    "In this lesson we'll write our own simple model-graded evaluation using promptfoo.\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Mdel-graded evals with promptfoo\n",
    "\n",
    "As with most things in promptfoo, there are multiple valid approaches to writing model-graded evaluations.  In this lesson we'll see the simplest pattern: utilizing built-in assertions.  In the next lesson, we'll see how to write our own custom model-graded assertion functions.\n",
    "\n",
    "To start, we'll use a built-in assertion called `llm-rubric`, which is promptfoo's general-purpose grader for \"LLM as a judge\" evaluations.  Using it is as simple as adding the following to your `promptfooconfig.yaml` file:\n",
    "\n",
    "```yaml\n",
    "assert:\n",
    "  - type: llm-rubric\n",
    "    # The model we want to use as the grader\n",
    "    provider: anthropic:messages:claude-3-opus-20240229\n",
    "    # Specify the criteria for grading the LLM output:\n",
    "    value: Is not apologetic\n",
    "```\n",
    "The above assertion will use Claude 3 Opus to grade the output based on whether or not the response is apologetic.\n",
    "\n",
    "Let's try using `llm-rubric` in our own evaluation!\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Writing our own evaluation\n",
    "In this lesson, we'll focus on evaluating prompts for an academic assistant for middle school children.  We're building a chatbot that should answer questions relevant to school subjects, but should avoid answering unrelated questions.  We'll start with a simple prompt like this one:\n",
    "\n",
    "```\n",
    "You are an interactive tutor assistant for middle school children.\n",
    "Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "Only answer questions related to middle school academics.\n",
    "This is the student question: {{question}}\n",
    "```\n",
    "\n",
    "We'll write a model-graded evaluation to make sure the model refuses to answer questions like \"How can I get my parents to buy me a phone?\" or \"What are some fun free video games?\"  We could theoretically try to write a code-graded evaluation that uses regular expressions to look for key refusal phrases like \"I'm sorry\", \"I can't help\", etc. but this is a very brittle approach.  Using a model to grade the response provides much better results."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "--- \n",
    "\n",
    "## Initializing promptfoo\n",
    "\n",
    "As always, the first step is to initialize promptfoo.  In a new folder, run the following command:\n",
    "\n",
    "```bash\n",
    "npx promptfoo@latest init\n",
    "```\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Basic configuration\n",
    "\n",
    "To keep things extra simple, we won't bother putting our prompts in a separate file, as we've done before.  We'll write the prompts and tests directly inside the `promptfooconfig.yaml` file. Delete any existing contents of the file and replace it with this: \n",
    "\n",
    "```yaml\n",
    "description: \"School Assistant Eval\"\n",
    "\n",
    "prompts:\n",
    "  - >- \n",
    "    You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    This is the student question {{question}}\n",
    "\n",
    "providers: [anthropic:messages:claude-3-5-sonnet-20240620]\n",
    "  # - anthropic:messages:claude-3-haiku-20240307\n",
    "\n",
    "defaultTest:\n",
    "  assert:\n",
    "    - type: llm-rubric\n",
    "      provider: anthropic:messages:claude-3-opus-20240229\n",
    "      value: 'Refuses to answer the question and instead redirects to academic topics'\n",
    "\n",
    "tests:\n",
    "  - vars:\n",
    "      question: What's the best free mobile video game?\n",
    "  - vars:\n",
    "      question: How can I convince my parents to let me stay up later?\n",
    "  - vars:\n",
    "      question: What are some fun theme ideas for a sleepover?\n",
    "  - vars:\n",
    "      question: What are some good prank ideas that won't get me in trouble?\n",
    "  - vars:\n",
    "      question: Who are some famous left footed soccer players?\n",
    "```\n",
    "\n",
    "The above configuration includes the following:\n",
    "\n",
    "* `prompts` - contains our initial basic prompt we want to evaluate.  Remember that we often put our prompts in a Python file, but we have the option to directly inline them in the config file.\n",
    "* `providers` - we tell promptfoo we want to run our prompts through Claude 3.5 Sonnet \n",
    "* `defaultTest` - on every test that promptfoo runs, we want to grade it using `llm-rubric` to ensure that the output refuses to answer the question and instead steers the discussion back to academic topics.  In this case we're telling promptfoo to use Claude 3 Opus to perform the grading.\n",
    "* `tests` - the set of tests we want to run.  We often specify these in a CSV file, we but can also write tests inline in the YAML file.  These tests all contain questions that we hope the model will refuse to answer.\n"
   ]
  },
  {
   "attachments": {
    "eval1.png": {
     "image/png": "[encoded data, 551920 characters]"
    },
    "soccer_players.png": {
     "image/png": "[encoded data, 625632 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The next step is to run our evaluation with:\n",
    "\n",
    "```bash\n",
    "npx promptfoo@latest eval\n",
    "```\n",
    "\n",
    "This is the output we generated the first time we ran this evaluation:\n",
    "\n",
    "![eval1.png](attachment:eval1.png)\n",
    "\n",
    "Our prompt is working with most of our evaluation data set (though this is a very small dataset), but it looks like the model is happy to answer questions about soccer players.  The following screenshot from the promptfoo web view showcases the model's response as well as the grader-model's grading logic: \n",
    "\n",
    "![soccer_players.png](attachment:soccer_players.png)\n",
    "\n",
    "Let's try adding a second more detailed prompt and see if we can get the model to stick to academic subjects."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## A Second prompt\n",
    "Let's add a second prompt that includes some guidelines about exactly which topics the model should discuss:\n",
    "\n",
    ">  You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    Acceptable topics you should respond to include: math, reading, science, foreign languages, social students, and art.\n",
    "    Refuse to answer questions not related to the above topics in an academic context.\n",
    "    This is the student question: {{question}}\n",
    "\n",
    "Update the `promptfooconfig.yaml` file to look like this: \n",
    "\n",
    "```yaml\n",
    "description: \"School Assistant Eval\"\n",
    "\n",
    "prompts:\n",
    "  - >- \n",
    "    You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    This is the student question: {{question}}\n",
    "  - >- \n",
    "    You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    Acceptable topics you should respond to include: math, reading, science, foreign languages, social students, and art.\n",
    "    Refuse to answer questions not related to the above topics in an academic context.\n",
    "    This is the student question: {{question}}\n",
    "\n",
    "\n",
    "providers: [anthropic:messages:claude-3-5-sonnet-20240620]\n",
    "\n",
    "defaultTest:\n",
    "  assert:\n",
    "    - type: llm-rubric\n",
    "      provider: anthropic:messages:claude-3-opus-20240229\n",
    "      value: 'Refuses to answer the question and instead redirects to academic topics'\n",
    "\n",
    "tests:\n",
    "  - vars:\n",
    "      question: What's the best free mobile video game?\n",
    "  - vars:\n",
    "      question: How can I convince my parents to let me stay up later?\n",
    "  - vars:\n",
    "      question: What are some fun theme ideas for a sleepover?\n",
    "  - vars:\n",
    "      question: What are some good prank ideas that won't get me in trouble?\n",
    "  - vars:\n",
    "      question: Who are some famous left footed soccer players?\n",
    "```\n",
    "\n",
    "We now have two prompts we're evaluating! Let's run the evaluation again with: \n",
    "\n",
    "```bash\n",
    "npx promptfoo@latest eval\n",
    "```\n",
    "\n"
   ]
  },
  {
   "attachments": {
    "eval2.png": {
     "image/png": "[encoded data, 882968 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is the result we got: \n",
    "\n",
    "![eval2.png](attachment:eval2.png)\n",
    "\n",
    "It looks like the changes to our second prompt made a difference! Please note that this evaluation dataset is too small to be useful in the real world.\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Grading for apologies \n",
    "\n",
    "In looking closely at the model outputs, we notice that most of them begin with apologies like \"I'm sorry,\" or \"I apologize.\"  This is not an ideal experience for our users, so we've decided to try and improve on this!  We want to evaluate a third prompt: \n",
    "\n",
    "> You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    Acceptable topics you should respond to include: math, reading, science, foreign languages, social students, and art.\n",
    "    Refuse to answer questions not related to the above topics in an academic context.\n",
    "    Do not apologize or use an apologetic tone when refusing.  Instead, gently nudge students to focus on school-related topics.\n",
    "    This is the student question: {{question}}\n",
    "\n",
    "The above prompt specifically tells the model to avoid apologizing and instead focus on gently nudging students to focus on academic topics.\n",
    "\n",
    "Next, let's add a second `llm-rubric` assertion to test whether the model's output is apologetic.  Update `promptfooconfig.yaml` to look like this: \n",
    "\n",
    "```yaml\n",
    "description: \"School Assistant Eval\"\n",
    "\n",
    "prompts:\n",
    "  - >- \n",
    "    You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    This is the student question: {{question}}\n",
    "  - >- \n",
    "    You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    Acceptable topics you should respond to include: math, reading, science, foreign languages, social students, and art.\n",
    "    Refuse to answer questions not related to the above topics in an academic context.\n",
    "    This is the student question: {{question}}\n",
    "  - >- \n",
    "    You are an interactive tutor assistant for middle school children.\n",
    "    Students will ask you a question and your job is to respond with explanations that are understandable to a middle school audience.\n",
    "    Only answer questions related to middle school academics.\n",
    "    Acceptable topics you should respond to include: math, reading, science, foreign languages, social students, and art.\n",
    "    Refuse to answer questions not related to the above topics in an academic context.\n",
    "    Do not apologize or use an apologetic tone when refusing.  Instead, gently nudge students to focus on school-related topics.\n",
    "    This is the student question: {{question}}\n",
    "\n",
    "\n",
    "providers: [anthropic:messages:claude-3-5-sonnet-20240620]\n",
    "\n",
    "defaultTest:\n",
    "  assert:\n",
    "    - type: llm-rubric\n",
    "      provider: anthropic:messages:claude-3-opus-20240229\n",
    "      value: 'Refuses to answer the question and instead redirects to academic topics'\n",
    "    - type: llm-rubric\n",
    "      provider: anthropic:messages:claude-3-opus-20240229\n",
    "      value: 'Is not apologetic'\n",
    "\n",
    "tests:\n",
    "  - vars:\n",
    "      question: What's the best free mobile video game?\n",
    "  - vars:\n",
    "      question: How can I convince my parents to let me stay up later?\n",
    "  - vars:\n",
    "      question: What are some fun theme ideas for a sleepover?\n",
    "  - vars:\n",
    "      question: What are some good prank ideas that won't get me in trouble?\n",
    "  - vars:\n",
    "      question: Who are some famous left footed soccer players?\n",
    "```\n",
    "\n",
    "We now have three prompts that we're testing.  For each of the test cases, we're use a model to grade two separate aspects:\n",
    "* The model should refuse to answer the question\n",
    "* The model should not be apologetic\n",
    "\n",
    "Let's run the evaluation: \n",
    "\n",
    "```bash\n",
    "npx promptfoo@latest eval\n",
    "```\n"
   ]
  },
  {
   "attachments": {
    "eval3.png": {
     "image/png": "[encoded data, 1440532 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "These are the results we got: \n",
    "\n",
    "![eval3.png](attachment:eval3.png)\n",
    "\n",
    "As expected, the first two prompts fail on the apologies assertion, but the third prompt seems to work!"
   ]
  },
  {
   "attachments": {
    "web_view.png": {
     "image/png": "[encoded data, 1092584 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's launch the web view with: \n",
    "\n",
    "```bash\n",
    "npx promptfoo@latest view\n",
    "```\n",
    "\n",
    "![web_view.png](attachment:web_view.png)"
   ]
  },
  {
   "attachments": {
    "details1.png": {
     "image/png": "[encoded data, 243252 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Remember that we can click on the magnifying glass icon to see more details about each model output and corresponding assertion grades.  Let's take a closer look at the second entry in the first row:\n",
    "\n",
    "![details1.png](attachment:details1.png)\n",
    "\n",
    "We can see that the output passes the original model-graded assertion and does in fact refuse to answer the off-topic question.  We can also see that the output failed the second assertion we added because \"The response begins with 'I'm sorry', which is an apologetic phrase.\""
   ]
  },
  {
   "attachments": {
    "details2.png": {
     "image/png": "[encoded data, 267940 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's zoom in on the third entry in the first row: \n",
    "\n",
    "![details2.png](attachment:details2.png)\n",
    "\n",
    "This output passed both assertions!\n",
    "\n",
    "**Please remember that this dataset is far too small for a realistic evaluation.**\n",
    "\n",
    "Promptfoo's built-in model-graded assertions are very useful, but there are situations we might need more control over the exact model-graded metrics and process.  In this next lesson we'll take a look at defining our own custom model-grader functions!"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
