From 29b868259b17c6a2f39a6255e7468d662605d54d Mon Sep 17 00:00:00 2001 From: denis-samatov Date: Sun, 30 Aug 2026 23:24:10 +0700 Subject: [PATCH] Fix insecure exception/response logging in LLMJudgeForLegalSurvey Raw LLM responses and exception objects were being logged directly, which could leak sensitive information (PII, secrets, internal system details) into logs. - Replace generic 'except Exception as e' with a specific exceptions.GoogleAPICallError catch plus a sanitized general except clause. - Remove response.text from JSONDecodeError log messages. - Remove the exception object from all log messages. - Add the google.api_core exceptions import needed for the specific catch. Note: this diff also reformats the notebook's JSON (pretty-printed instead of minified), a side effect of editing it programmatically; the only functional change is the logging fix described above. --- ...Evaluation and Monitoring (LLM as a Judge) | 245 +++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) diff --git a/notebooks/Chapter 19_ Evaluation and Monitoring (LLM as a Judge) b/notebooks/Chapter 19_ Evaluation and Monitoring (LLM as a Judge) index f838bf87..323d7196 100644 --- a/notebooks/Chapter 19_ Evaluation and Monitoring (LLM as a Judge) +++ b/notebooks/Chapter 19_ Evaluation and Monitoring (LLM as a Judge) @@ -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))"]}]} \ No newline at end of file +{ + "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" + ] + } + ] +} \ No newline at end of file