Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 244 additions & 1 deletion notebooks/Chapter 19_ Evaluation and Monitoring (LLM as a Judge)
Original file line number Diff line number Diff line change
@@ -1 +1,244 @@
{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[{"file_id":"1-eAjTlNP1jq1bcRNhpw39hiYsuKV-8Cd","timestamp":1749136476078}],"authorship_tag":"ABX9TyPFQo6oXpP3GCykFiQ0eNHn"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","execution_count":null,"metadata":{"id":"QMUWyDSjx-VH"},"outputs":[],"source":["import google.generativeai as genai\n","import os\n","import json\n","import logging\n","from typing import Optional\n","\n","# --- Configuration ---\n","logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n","\n","# Set your API key as an environment variable to run this script\n","# For example, in your terminal: export GOOGLE_API_KEY='your_key_here'\n","try:\n"," genai.configure(api_key=os.environ[\"GOOGLE_API_KEY\"])\n","except KeyError:\n"," logging.error(\"Error: GOOGLE_API_KEY environment variable not set.\")\n"," exit(1)\n","\n","# --- LLM-as-a-Judge Rubric for Legal Survey Quality ---\n","LEGAL_SURVEY_RUBRIC = \"\"\"\n","You are an expert legal survey methodologist and a critical legal reviewer. Your task is to evaluate the quality of a given legal survey question.\n","\n","Provide a score from 1 to 5 for overall quality, along with a detailed rationale and specific feedback.\n","Focus on the following criteria:\n","\n","1. **Clarity & Precision (Score 1-5):**\n"," * 1: Extremely vague, highly ambiguous, or confusing.\n"," * 3: Moderately clear, but could be more precise.\n"," * 5: Perfectly clear, unambiguous, and precise in its legal terminology (if applicable) and intent.\n","\n","2. **Neutrality & Bias (Score 1-5):**\n"," * 1: Highly leading or biased, clearly influencing the respondent towards a specific answer.\n"," * 3: Slightly suggestive or could be interpreted as leading.\n"," * 5: Completely neutral, objective, and free from any leading language or loaded terms.\n","\n","3. **Relevance & Focus (Score 1-5):**\n"," * 1: Irrelevant to the stated survey topic or out of scope.\n"," * 3: Loosely related but could be more focused.\n"," * 5: Directly relevant to the survey's objectives and well-focused on a single concept.\n","\n","4. **Completeness (Score 1-5):**\n"," * 1: Omits critical information needed to answer accurately or provides insufficient context.\n"," * 3: Mostly complete, but minor details are missing.\n"," * 5: Provides all necessary context and information for the respondent to answer thoroughly.\n","\n","5. **Appropriateness for Audience (Score 1-5):**\n"," * 1: Uses jargon inaccessible to the target audience or is overly simplistic for experts.\n"," * 3: Generally appropriate, but some terms might be challenging or oversimplified.\n"," * 5: Perfectly tailored to the assumed legal knowledge and background of the target survey audience.\n","\n","**Output Format:**\n","Your response MUST be a JSON object with the following keys:\n","* `overall_score`: An integer from 1 to 5 (average of criterion scores, or your holistic judgment).\n","* `rationale`: A concise summary of why this score was given, highlighting major strengths and weaknesses.\n","* `detailed_feedback`: A bullet-point list detailing feedback for each criterion (Clarity, Neutrality, Relevance, Completeness, Audience Appropriateness). Suggest specific improvements.\n","* `concerns`: A list of any specific legal, ethical, or methodological concerns.\n","* `recommended_action`: A brief recommendation (e.g., \"Revise for neutrality\", \"Approve as is\", \"Clarify scope\").\n","\"\"\"\n","\n","class LLMJudgeForLegalSurvey:\n"," \"\"\"A class to evaluate legal survey questions using a generative AI model.\"\"\"\n","\n"," def __init__(self, model_name: str = 'gemini-1.5-flash-latest', temperature: float = 0.2):\n"," \"\"\"\n"," Initializes the LLM Judge.\n","\n"," Args:\n"," model_name (str): The name of the Gemini model to use.\n"," 'gemini-1.5-flash-latest' is recommended for speed and cost.\n"," 'gemini-1.5-pro-latest' offers the highest quality.\n"," temperature (float): The generation temperature. Lower is better for deterministic evaluation.\n"," \"\"\"\n"," self.model = genai.GenerativeModel(model_name)\n"," self.temperature = temperature\n","\n"," def _generate_prompt(self, survey_question: str) -> str:\n"," \"\"\"Constructs the full prompt for the LLM judge.\"\"\"\n"," return f\"{LEGAL_SURVEY_RUBRIC}\\n\\n---\\n**LEGAL SURVEY QUESTION TO EVALUATE:**\\n{survey_question}\\n---\"\n","\n"," def judge_survey_question(self, survey_question: str) -> Optional[dict]:\n"," \"\"\"\n"," Judges the quality of a single legal survey question using the LLM.\n","\n"," Args:\n"," survey_question (str): The legal survey question to be evaluated.\n","\n"," Returns:\n"," Optional[dict]: A dictionary containing the LLM's judgment, or None if an error occurs.\n"," \"\"\"\n"," full_prompt = self._generate_prompt(survey_question)\n","\n"," try:\n"," logging.info(f\"Sending request to '{self.model.model_name}' for judgment...\")\n"," response = self.model.generate_content(\n"," full_prompt,\n"," generation_config=genai.types.GenerationConfig(\n"," temperature=self.temperature,\n"," response_mime_type=\"application/json\"\n"," )\n"," )\n","\n"," # Check for content moderation or other reasons for an empty response.\n"," if not response.parts:\n"," safety_ratings = response.prompt_feedback.safety_ratings\n"," logging.error(f\"LLM response was empty or blocked. Safety Ratings: {safety_ratings}\")\n"," return None\n","\n"," return json.loads(response.text)\n","\n"," except json.JSONDecodeError:\n"," logging.error(f\"Failed to decode LLM response as JSON. Raw response: {response.text}\")\n"," return None\n"," except Exception as e:\n"," logging.error(f\"An unexpected error occurred during LLM judgment: {e}\")\n"," return None\n","\n","# --- Example Usage ---\n","if __name__ == \"__main__\":\n"," judge = LLMJudgeForLegalSurvey()\n","\n"," # --- Good Example ---\n"," good_legal_survey_question = \"\"\"\n"," To what extent do you agree or disagree that current intellectual property laws in Switzerland adequately protect emerging AI-generated content, assuming the content meets the originality criteria established by the Federal Supreme Court?\n"," (Select one: Strongly Disagree, Disagree, Neutral, Agree, Strongly Agree)\n"," \"\"\"\n"," print(\"\\n--- Evaluating Good Legal Survey Question ---\")\n"," judgment_good = judge.judge_survey_question(good_legal_survey_question)\n"," if judgment_good:\n"," print(json.dumps(judgment_good, indent=2))\n","\n"," # --- Biased/Poor Example ---\n"," biased_legal_survey_question = \"\"\"\n"," Don't you agree that overly restrictive data privacy laws like the FADP are hindering essential technological innovation and economic growth in Switzerland?\n"," (Select one: Yes, No)\n"," \"\"\"\n"," print(\"\\n--- Evaluating Biased Legal Survey Question ---\")\n"," judgment_biased = judge.judge_survey_question(biased_legal_survey_question)\n"," if judgment_biased:\n"," print(json.dumps(judgment_biased, indent=2))\n","\n"," # --- Ambiguous/Vague Example ---\n"," vague_legal_survey_question = \"\"\"\n"," What are your thoughts on legal tech?\n"," \"\"\"\n"," print(\"\\n--- Evaluating Vague Legal Survey Question ---\")\n"," judgment_vague = judge.judge_survey_question(vague_legal_survey_question)\n"," if judgment_vague:\n"," print(json.dumps(judgment_vague, indent=2))"]}]}
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [
{
"file_id": "1-eAjTlNP1jq1bcRNhpw39hiYsuKV-8Cd",
"timestamp": 1749136476078
}
],
"authorship_tag": "ABX9TyPFQo6oXpP3GCykFiQ0eNHn"
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "QMUWyDSjx-VH"
},
"outputs": [],
"source": [
"import google.generativeai as genai\n",
"from google.api_core import exceptions\n",
"import os\n",
"import json\n",
"import logging\n",
"from typing import Optional\n",
"\n",
"# --- Configuration ---\n",
"logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n",
"\n",
"# Set your API key as an environment variable to run this script\n",
"# For example, in your terminal: export GOOGLE_API_KEY='your_key_here'\n",
"try:\n",
" genai.configure(api_key=os.environ[\"GOOGLE_API_KEY\"])\n",
"except KeyError:\n",
" logging.error(\"Error: GOOGLE_API_KEY environment variable not set.\")\n",
" exit(1)\n",
"\n",
"# --- LLM-as-a-Judge Rubric for Legal Survey Quality ---\n",
"LEGAL_SURVEY_RUBRIC = \"\"\"\n",
"You are an expert legal survey methodologist and a critical legal reviewer. Your task is to evaluate the quality of a given legal survey question.\n",
"\n",
"Provide a score from 1 to 5 for overall quality, along with a detailed rationale and specific feedback.\n",
"Focus on the following criteria:\n",
"\n",
"1. **Clarity & Precision (Score 1-5):**\n",
" * 1: Extremely vague, highly ambiguous, or confusing.\n",
" * 3: Moderately clear, but could be more precise.\n",
" * 5: Perfectly clear, unambiguous, and precise in its legal terminology (if applicable) and intent.\n",
"\n",
"2. **Neutrality & Bias (Score 1-5):**\n",
" * 1: Highly leading or biased, clearly influencing the respondent towards a specific answer.\n",
" * 3: Slightly suggestive or could be interpreted as leading.\n",
" * 5: Completely neutral, objective, and free from any leading language or loaded terms.\n",
"\n",
"3. **Relevance & Focus (Score 1-5):**\n",
" * 1: Irrelevant to the stated survey topic or out of scope.\n",
" * 3: Loosely related but could be more focused.\n",
" * 5: Directly relevant to the survey's objectives and well-focused on a single concept.\n",
"\n",
"4. **Completeness (Score 1-5):**\n",
" * 1: Omits critical information needed to answer accurately or provides insufficient context.\n",
" * 3: Mostly complete, but minor details are missing.\n",
" * 5: Provides all necessary context and information for the respondent to answer thoroughly.\n",
"\n",
"5. **Appropriateness for Audience (Score 1-5):**\n",
" * 1: Uses jargon inaccessible to the target audience or is overly simplistic for experts.\n",
" * 3: Generally appropriate, but some terms might be challenging or oversimplified.\n",
" * 5: Perfectly tailored to the assumed legal knowledge and background of the target survey audience.\n",
"\n",
"**Output Format:**\n",
"Your response MUST be a JSON object with the following keys:\n",
"* `overall_score`: An integer from 1 to 5 (average of criterion scores, or your holistic judgment).\n",
"* `rationale`: A concise summary of why this score was given, highlighting major strengths and weaknesses.\n",
"* `detailed_feedback`: A bullet-point list detailing feedback for each criterion (Clarity, Neutrality, Relevance, Completeness, Audience Appropriateness). Suggest specific improvements.\n",
"* `concerns`: A list of any specific legal, ethical, or methodological concerns.\n",
"* `recommended_action`: A brief recommendation (e.g., \"Revise for neutrality\", \"Approve as is\", \"Clarify scope\").\n",
"\"\"\"\n",
"def safe_json_parse(text: str) -> dict:\n",
" \"\"\"\n",
" Safely parse JSON from LLM response, handling markdown code blocks and leading/trailing text.\n",
" \"\"\"\n",
" if not isinstance(text, str):\n",
" return None\n",
"\n",
" try:\n",
" # 1. Try direct parsing\n",
" return json.loads(text.strip())\n",
" except (json.JSONDecodeError, AttributeError):\n",
" pass\n",
"\n",
" # 2. Try to extract JSON from markdown blocks\n",
" # Use regex to find content between ```json and ``` or ``` and ```\n",
" import re\n",
" match = re.search(r\"```(?:json)?\\s*(\\{.*?\\})\\s*```\", text, re.DOTALL)\n",
" if match:\n",
" try:\n",
" return json.loads(match.group(1).strip())\n",
" except (json.JSONDecodeError, AttributeError):\n",
" pass\n",
"\n",
" # 3. Fallback: try to find anything that looks like a JSON object\n",
" match = re.search(r\"(\\{.*?\\})\", text, re.DOTALL)\n",
" if match:\n",
" try:\n",
" return json.loads(match.group(1).strip())\n",
" except (json.JSONDecodeError, AttributeError):\n",
" pass\n",
"\n",
" return None\n",
"\n",
" try:\n",
" # 1. Try direct parsing\n",
" return json.loads(text.strip())\n",
" except json.JSONDecodeError:\n",
" pass\n",
"\n",
" # 2. Try to extract JSON from markdown blocks\n",
" # Use regex to find content between ```json and ``` or ``` and ```\n",
" match = re.search(r\"```(?:json)?\\s*(\\{.*?\\})\\s*```\", text, re.DOTALL)\n",
" if match:\n",
" try:\n",
" return json.loads(match.group(1).strip())\n",
" except json.JSONDecodeError:\n",
" pass\n",
"\n",
" # 3. Fallback: try to find anything that looks like a JSON object\n",
" match = re.search(r\"(\\{.*\\})\", text, re.DOTALL)\n",
" if match:\n",
" try:\n",
" return json.loads(match.group(1).strip())\n",
" except json.JSONDecodeError:\n",
" pass\n",
"\n",
" return None\n",
"\n",
"\n",
"\n",
"class LLMJudgeForLegalSurvey:\n",
" \"\"\"A class to evaluate legal survey questions using a generative AI model.\"\"\"\n",
"\n",
" def __init__(self, model_name: str = 'gemini-1.5-flash-latest', temperature: float = 0.2):\n",
" \"\"\"\n",
" Initializes the LLM Judge.\n",
"\n",
" Args:\n",
" model_name (str): The name of the Gemini model to use.\n",
" 'gemini-1.5-flash-latest' is recommended for speed and cost.\n",
" 'gemini-1.5-pro-latest' offers the highest quality.\n",
" temperature (float): The generation temperature. Lower is better for deterministic evaluation.\n",
" \"\"\"\n",
" self.model = genai.GenerativeModel(model_name)\n",
" self.temperature = temperature\n",
"\n",
" def _generate_prompt(self, survey_question: str) -> str:\n",
" \"\"\"Constructs the full prompt for the LLM judge.\"\"\"\n",
" return f\"{LEGAL_SURVEY_RUBRIC}\\n\\n---\\n**LEGAL SURVEY QUESTION TO EVALUATE:**\\n{survey_question}\\n---\"\n",
"\n",
" def judge_survey_question(self, survey_question: str) -> Optional[dict]:\n",
" \"\"\"\n",
" Judges the quality of a single legal survey question using the LLM.\n",
"\n",
" Args:\n",
" survey_question (str): The legal survey question to be evaluated.\n",
"\n",
" Returns:\n",
" Optional[dict]: A dictionary containing the LLM's judgment, or None if an error occurs.\n",
" \"\"\"\n",
" full_prompt = self._generate_prompt(survey_question)\n",
"\n",
" try:\n",
" logging.info(f\"Sending request to '{self.model.model_name}' for judgment...\")\n",
" response = self.model.generate_content(\n",
" full_prompt,\n",
" generation_config=genai.types.GenerationConfig(\n",
" temperature=self.temperature,\n",
" response_mime_type=\"application/json\"\n",
" )\n",
" )\n",
"\n",
" # Check for content moderation or other reasons for an empty response.\n",
" if not response.parts:\n",
" safety_ratings = response.prompt_feedback.safety_ratings\n",
" logging.error(f\"LLM response was empty or blocked. Safety Ratings: {safety_ratings}\")\n",
" return None\n",
"\n",
" return safe_json_parse(response.text)\n",
"\n",
" except json.JSONDecodeError:\n",
" logging.error(\"Failed to decode LLM response as JSON.\")\n",
" return None\n",
" except exceptions.GoogleAPICallError:\n",
" logging.error(\"A Gemini API error occurred during LLM judgment.\")\n",
" return None\n",
" except Exception:\n",
" logging.error(\"An unexpected error occurred during LLM judgment.\")\n",
" return None\n",
"\n",
"# --- Example Usage ---\n",
"if __name__ == \"__main__\":\n",
" judge = LLMJudgeForLegalSurvey()\n",
"\n",
" # --- Good Example ---\n",
" good_legal_survey_question = \"\"\"\n",
" To what extent do you agree or disagree that current intellectual property laws in Switzerland adequately protect emerging AI-generated content, assuming the content meets the originality criteria established by the Federal Supreme Court?\n",
" (Select one: Strongly Disagree, Disagree, Neutral, Agree, Strongly Agree)\n",
" \"\"\"\n",
" print(\"\\n--- Evaluating Good Legal Survey Question ---\")\n",
" judgment_good = judge.judge_survey_question(good_legal_survey_question)\n",
" if judgment_good:\n",
" print(json.dumps(judgment_good, indent=2))\n",
"\n",
" # --- Biased/Poor Example ---\n",
" biased_legal_survey_question = \"\"\"\n",
" Don't you agree that overly restrictive data privacy laws like the FADP are hindering essential technological innovation and economic growth in Switzerland?\n",
" (Select one: Yes, No)\n",
" \"\"\"\n",
" print(\"\\n--- Evaluating Biased Legal Survey Question ---\")\n",
" judgment_biased = judge.judge_survey_question(biased_legal_survey_question)\n",
" if judgment_biased:\n",
" print(json.dumps(judgment_biased, indent=2))\n",
"\n",
" # --- Ambiguous/Vague Example ---\n",
" vague_legal_survey_question = \"\"\"\n",
" What are your thoughts on legal tech?\n",
" \"\"\"\n",
" print(\"\\n--- Evaluating Vague Legal Survey Question ---\")\n",
" judgment_vague = judge.judge_survey_question(vague_legal_survey_question)\n",
" if judgment_vague:\n",
" print(json.dumps(judgment_vague, indent=2))\n"
]
}
]
}