Generating structured reasoning in LLM outputs using Pydantic and Instructor

Category: LLMs
Topic: Instructor LLMs Pydantic

Published by Nicole on Nov 04, 2024 • 3 min read.

In this post, we explore how to use OpenAI and Nebius APIs to build an interactive response model with structured outputs. The whole code can be found in this Google Colab Notebook. You'll learn the following:

Configure API keys for OpenAI and Nebius using .env files. In Colab, you can set this up directly in the terminal by running:

cat <<EOL > .env
OPENAI_API_KEY="your_API_key"
NEBIUS_API_KEY="your_API_key"
EOL

Next, we create a custom print_wrapper function that formats text, enabling markdown-style formatting for better readability and wrapping long lines. This function provides a cleaner output structure, which is useful when working with detailed responses and reasoning.

from textwrap import TextWrapper
import re

chars = 70

def print_wrapper(print_func):
    """Wraps text to specified width and handles markdown-style and escaped newlines."""
    def function_wrapper(text):
        if not isinstance(text, str):
            text = str(text)

        # Handle **bold** formatting
        text = re.sub(r'\*\*(.*?)\*\*', r'\033[1m\1\033[0m', text)  # ANSI for bold

        # Handle escaped newlines
        text = text.replace("\\n", "\n")
        text = text.replace("\\", "\n")

        # Wrap text
        wrapper = TextWrapper(width=chars)
        wrapped_text = "\n".join([wrapper.fill(line) for line in text.split("\n")])

        return print_func(wrapped_text)

    return function_wrapper

# Override the built-in print function with the wrapped version
print = print_wrapper(print)

# Pretty print function using the wrapped print
def pretty_print_response(response_obj):
    # Title for response
    print("Response:\n" + "-" * chars)
    print(response_obj.response)
    print("\n" + "=" * chars)

    # Title for thought
    print("Thought Process:\n" + "-" * chars)
    print(response_obj.thought)
    print("\n" + "-" * chars)

Using pydantic, we define a structured Response model, containing both an answer and a "Thought Process" section, providing transparent, step-by-step reasoning behind each answer.

class Response(BaseModel):
    response: str
    thought: str = Field(
        description="Provide and answer in a structered step-by-step reasoning."
    )

The run_model function allows for flexible interactions with either OpenAI or Nebius models by specifying the provider and model name. This makes it simple to test multiple models and configurations for your questions.

system_prompt = """You are an assistant that answers questions with clear reasoning. 
For each question, provide both a response and a detailed reasoning process that 
explains how you reached the answer."""

question = "What is the impact of reinforcement learning in robotics?"


def run_model(system_prompt: str, question:str, response_model: BaseModel, model_name:str, provider:str):
    if provider == "openai":
        llm = instructor.from_openai(OpenAI())

    elif provider == "nebius":
        llm = instructor.from_openai(OpenAI(
            base_url="https://api.studio.nebius.ai/v1/",
            api_key=os.environ.get("NEBIUS_API_KEY"),
        ))
    response = llm.chat.completions.create(
    model=model_name,
    response_model=Response,
    messages=[
        {"role": "system", "content": system_prompt},
        {
            "role": "user",
            "content": question,
        },
    ],
    temperature=0.1,
    top_p=0.8,
    frequency_penalty=0.6,
    )
    return response

The pretty_print_response function displays responses with distinct sections for the main answer and the thought process, ensuring that complex answers remain easy to follow.

%%time
response = run_model(system_prompt, question, Response, "meta-llama/Meta-Llama-3.1-405B-Instruct", "nebius")
pretty_print_response(response)

Test different models by specifying different model names in run_model, like "meta-llama/Meta-Llama-3.1-405B-Instruct" or "gpt-4o" to see how they differ in handling the prompt.

%%time
response = run_model(system_prompt, question, Response, "gpt-4o", "openai")
pretty_print_response(response)