Created
December 12, 2025 11:35
-
-
Save thorwhalen/58c995e3e0b9c412d43901908456661d to your computer and use it in GitHub Desktop.
Constrained AI answers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import oa | |
| from typing import Union | |
| def constrained_answer( | |
| prompt: str, | |
| valid_answers: Union[list[str], list[int], list[float], type, tuple[float, float]], | |
| model="gpt-4o-mini", | |
| *, | |
| n: int = 1 | |
| ): | |
| """ | |
| Get an answer from the LLM constrained to a set of valid answers or types. | |
| Uses prompt_json_function to ensure the LLM returns a valid response based on constraints. | |
| Args: | |
| prompt: The question or prompt to ask the LLM | |
| valid_answers: Can be: | |
| - list[str]: List of valid string options | |
| - list[int]: List of valid integer options | |
| - list[float]: List of valid float options | |
| - bool: Constrains answer to True or False | |
| - int: Any integer | |
| - float: Any number | |
| - tuple[float, float]: Numerical range (min, max) inclusive | |
| model: The model to use for the LLM (default: "gpt-4o-mini") | |
| n: Number of times to call the LLM (default: 1) | |
| Returns: | |
| One of the valid answers, respecting the type constraint | |
| Examples: | |
| >>> # String options | |
| >>> answer = constrained_answer( | |
| ... "Is Python compiled or interpreted?", | |
| ... ["compiled", "interpreted", "both"] | |
| ... ) | |
| >>> answer in ["compiled", "interpreted", "both"] | |
| True | |
| >>> # Boolean | |
| >>> answer = constrained_answer( | |
| ... "Is Python dynamically typed?", | |
| ... bool | |
| ... ) | |
| >>> isinstance(answer, bool) | |
| True | |
| >>> # Integer options | |
| >>> answer = constrained_answer( | |
| ... "How many wheels does a car have?", | |
| ... [2, 3, 4, 6, 8] | |
| ... ) | |
| >>> answer in [2, 3, 4, 6, 8] | |
| True | |
| >>> # Numerical range | |
| >>> answer = constrained_answer( | |
| ... "What is a reasonable hourly rate for a senior Python developer? (USD)", | |
| ... (50.0, 300.0) | |
| ... ) | |
| >>> 50.0 <= answer <= 300.0 | |
| True | |
| """ | |
| if n != 1: | |
| from functools import partial | |
| f = partial(constrained_answer, prompt, valid_answers, model, n=1) | |
| return [f() for _ in range(n)] | |
| # Determine the type and constraints | |
| if isinstance(valid_answers, list): | |
| # List of specific options | |
| if not valid_answers: | |
| raise ValueError("valid_answers list cannot be empty") | |
| first_item = valid_answers[0] | |
| if isinstance(first_item, str): | |
| json_type = "string" | |
| enum_values = valid_answers | |
| elif isinstance(first_item, int): | |
| json_type = "integer" | |
| enum_values = valid_answers | |
| elif isinstance(first_item, float): | |
| json_type = "number" | |
| enum_values = valid_answers | |
| else: | |
| raise ValueError(f"Unsupported list item type: {type(first_item)}") | |
| json_schema = { | |
| "name": "constrained_answer_schema", | |
| "schema": { | |
| "type": "object", | |
| "properties": { | |
| "answer": { | |
| "type": json_type, | |
| "enum": enum_values | |
| } | |
| }, | |
| "required": ["answer"] | |
| } | |
| } | |
| valid_answers_list = "\n".join(f"- {answer}" for answer in valid_answers) | |
| template = """ | |
| {prompt} | |
| You must respond with EXACTLY one of these options: | |
| {valid_answers_list} | |
| Choose the most appropriate answer. | |
| """ | |
| elif valid_answers is bool: | |
| # Boolean constraint | |
| json_schema = { | |
| "name": "constrained_answer_schema", | |
| "schema": { | |
| "type": "object", | |
| "properties": { | |
| "answer": { | |
| "type": "boolean" | |
| } | |
| }, | |
| "required": ["answer"] | |
| } | |
| } | |
| template = """ | |
| {prompt} | |
| You must respond with either True or False. | |
| """ | |
| valid_answers_list = "" | |
| elif valid_answers is int: | |
| # Any integer | |
| json_schema = { | |
| "name": "constrained_answer_schema", | |
| "schema": { | |
| "type": "object", | |
| "properties": { | |
| "answer": { | |
| "type": "integer" | |
| } | |
| }, | |
| "required": ["answer"] | |
| } | |
| } | |
| template = """ | |
| {prompt} | |
| You must respond with an integer number. | |
| """ | |
| valid_answers_list = "" | |
| elif valid_answers is float: | |
| # Any number | |
| json_schema = { | |
| "name": "constrained_answer_schema", | |
| "schema": { | |
| "type": "object", | |
| "properties": { | |
| "answer": { | |
| "type": "number" | |
| } | |
| }, | |
| "required": ["answer"] | |
| } | |
| } | |
| template = """ | |
| {prompt} | |
| You must respond with a number. | |
| """ | |
| valid_answers_list = "" | |
| elif isinstance(valid_answers, tuple) and len(valid_answers) == 2: | |
| # Numerical range | |
| min_val, max_val = valid_answers | |
| if not isinstance(min_val, (int, float)) or not isinstance(max_val, (int, float)): | |
| raise ValueError("Range tuple must contain two numbers") | |
| json_schema = { | |
| "name": "constrained_answer_schema", | |
| "schema": { | |
| "type": "object", | |
| "properties": { | |
| "answer": { | |
| "type": "number", | |
| "minimum": min_val, | |
| "maximum": max_val | |
| } | |
| }, | |
| "required": ["answer"] | |
| } | |
| } | |
| template = """ | |
| {prompt} | |
| You must respond with a number between {min_val} and {max_val} (inclusive). | |
| """ | |
| valid_answers_list = "" | |
| else: | |
| raise ValueError(f"Unsupported valid_answers type: {type(valid_answers)}") | |
| # Create the prompt function with JSON schema constraint | |
| ask_constrained = oa.prompt_json_function( | |
| template, | |
| json_schema=json_schema, | |
| name="ask_constrained", | |
| model=model | |
| ) | |
| # Call the function with appropriate kwargs | |
| if isinstance(valid_answers, tuple) and len(valid_answers) == 2: | |
| result = ask_constrained( | |
| prompt=prompt, | |
| min_val=valid_answers[0], | |
| max_val=valid_answers[1] | |
| ) | |
| elif isinstance(valid_answers, list): | |
| result = ask_constrained(prompt=prompt, valid_answers_list=valid_answers_list) | |
| else: | |
| result = ask_constrained(prompt=prompt) | |
| return result["answer"] |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Try it out and not the biases and variety of answers when you modify the question, model etc.
Just testing binary answers below: