Structured output with Instructor - Writer AI Studio

Documentation Index

Fetch the complete documentation index at: /llms.txt

Use this file to discover all available pages before exploring further.

This tutorial will guide you through using Writer with Instructor, a Python library that makes it easy to get structured data like JSON from LLMs.

Prerequisites

Getting started

To get started with Instructor, you’ll need to install the library and set up your environment.

  1. Obtain an API key
    First, make sure that you’ve signed up for a Writer AI Studio account and obtained an API key. You can follow the Quickstart to obtain an API key.

  2. Install instructor
    Once you’ve done so, install instructor with Writer support by running:

    pip install instructor[writer]
    
  3. Set the WRITER_API_KEY environment variable
    Make sure to set the WRITER_API_KEY environment variable with your Writer API key or pass it as an argument to the Writer constructor.

Basic usage

Following is a simple example of how to use instructor with Writer:

import instructor
from writerai import Writer
from pydantic import BaseModel

# Initialize Writer client
client = instructor.from_writer(Writer(api_key="your API key"))

class User(BaseModel):
    name: str
    age: int

# Extract structured data
user = client.chat.completions.create(
    model="palmyra-x5",
    messages=[{"role": "user", "content": "Extract: John is 30 years old"}],
    response_model=User,
)

print(user)
#> name='John' age=30

This code creates a simple data model with two fields: name and age. It then uses the instructor.from_writer function to create a client object that uses the Writer API to extract structured data from a text.

Instructor modes

Instructor supports two modes for structured output with Writer:

Both modes use the Writer chat completion endpoint to generate structured outputs. The key difference lies in how the model returns the data: either as a raw JSON object or via a structured tool call.

JSON mode

JSON mode uses Writer’s native JSON schema support. When you use this mode:

For more details on structured outputs and JSON schema usage, see our structured output documentation.

Tools mode

Tools mode uses Writer’s tool calling capabilities. When you use this mode:

For more details on tool calling, see our tool calling documentation.

Specifying the mode

You can specify which mode to use when initializing the Instructor client:

import instructor
from writerai import Writer

# Use JSON mode
client = instructor.from_writer(
    Writer(api_key="your API key"),
    mode=instructor.Mode.WRITER_JSON
)

# Use Tools mode
client = instructor.from_writer(
    Writer(api_key="your API key"),
    mode=instructor.Mode.WRITER_TOOLS
)

If you don’t specify a mode, Instructor will use WRITER_TOOLS by default.

Instructor vs. Writer SDK

When working with structured outputs, you can choose between two main approaches: using Instructor, a high-level framework built on top of LLM providers, or using the Writer SDK directly, which offers low-level access to the Writer LLMs. Each option comes with its own strengths and trade-offs, depending on your goals and level of control needed.

Feature Instructor Writer SDK
Retry mechanism Built-in retry logic with error handling and prompt editing Manual implementation required
Schema handling Auto-converts a wide range of Python types into JSON Schema via Pydantic Manual schema conversion required
Validation & reliability Enhanced outcomes through Pydantic validation + retries Same results possible with manual implementation
Prompt optimization Built-in prompt minimization and optimization Manual prompt engineering required
Model access Limited to supported models (currently V4 + Tool Calls) Full access to latest models (V4, V5) and all output modes
Update speed Requires updates/contributions for new features Immediate support for latest Palmyra features

For more on Instructor’s advanced retry and error handling capabilities, see the official Instructor retrying guide.

Building a data repair tool with Instructor and Writer

You can also use Instructor to do advanced data extraction and repair. In this example, you’ll build a Python application that extracts structured data from text, CSV, and PDF files using Instructor and Writer. This application will:

The finished code for this tutorial is available in the API tutorials GitHub repository.

Setting up the project

  1. Create a new project
    First, create a new project and set up Poetry for dependency management:
   mkdir instructor-and-writer-tutorial
   cd instructor-and-writer-tutorial
   poetry init -y
  1. Add dependencies
    Add the required dependencies to your project:
   poetry add instructor writer-sdk python-dotenv pydantic
  1. Set up your environment variables
    Create a .env file in your project root and add your Writer API key:
   WRITER_API_KEY=your_api_key_here
  1. Create main.py file and add imports
    Create a main.py file and add the following imports:
   import asyncio
   import csv
   import json
   import os
   from typing import Annotated, Type, Iterable, List

import instructor
   from dotenv import load_dotenv
   from pydantic import BaseModel, AfterValidator, Field
   from writerai import Writer, AsyncWriter

load_dotenv()

Here’s what each import is used for:

  1. Setting up Writer client
    Initialize the Writer client for both synchronous and asynchronous operations:
   writer_client = Writer()
   async_writer_client = AsyncWriter()

Defining the data model

In order for Instructor to extract structured output, you need to define a data model using Pydantic. To define the data model, create a UserExtract class to represent the data you want to extract:

class UserExtract(BaseModel):
    @staticmethod
    def first_last_name_validator(v):
        if v[0] != v[0].upper() or v[1:] != v[1:].lower() or not v.isalpha():
            raise ValueError("Name must contain only letters and start with uppercase letter")
        return v

first_name: Annotated[str, AfterValidator(first_last_name_validator)] = Field(
        ..., description="The name of the user"
    )
    last_name: Annotated[str, AfterValidator(first_last_name_validator)] = Field(
        ..., description="The surname of the user"
    )
    email: str

This data model defines the fields that you want to extract from the files. The first_name and last_name fields are validated to ensure they start with an uppercase letter and contain only letters. In this example, the email field is a simple string field, though you could also use a Pydantic field to validate the email format.

Parsing the files

With the data model defined, you can now implement file parsing. This involves creating functions to open the files and extract the text.

  1. Create a function to handle file processing
    Implement the main file handler function that orchestrates the entire process:
   async def handle_file(file_path: str, response_model: Type[BaseModel], output_path: str = None) -> None:
       extension = os.path.splitext(file_path)[1]
       name = os.path.splitext(os.path.basename(file_path))[0]

file_text = await fetch_file_text(file_path, name, extension)
       repaired_entities = await repair_data(file_text, response_model)

print(f"Number of entities extracted from {name}{extension}: {len(repaired_entities)}")
       return generate_csv(repaired_entities, response_model, output_path)

This function handles the file processing logic, including file type validation, text extraction, data repair, and CSV generation.

  1. Create a function to read the files
    Next, create a function to read the files based on the given path and extension:
   async def fetch_file_text(file_path: str, name: str, extension: str) -> str:
       allowed_extensions = [".txt", ".csv", ".pdf"]
       if extension not in allowed_extensions:
           raise ValueError(f"File extension {extension} is not allowed. Only {', '.join(allowed_extensions)}")

print(f"Reading {name}{extension} content...")
       with open(file_path, 'rb') as file:
           file_contents = file.read()

return await parse_file(file_contents, name, extension)
  1. Extract the file content
    Next, create a function to extract the text from the files. For text files, this function simply reads the file contents. For PDFs, the function uploads the PDF using Writer’s file upload endpoint, parses the text using PDF parsing tool, and then deletes the file from Writer’s servers using the file delete endpoint:
   async def parse_file(file_bytes_content: bytes, name: str, extension: str) -> str:
       file_text = ""

if extension == ".pdf":
           print(f"Uploading {name}{extension} content to writer servers...")
           file = await async_writer_client.files.upload(
               content=file_bytes_content,
               content_disposition=f"attachment; filename={name + extension}",
               content_type="application/octet-stream",
           )

print(f"Converting {name}{extension} content from PDF to text...")
           file_text = await async_writer_client.tools.parse_pdf(
               file_id=file.id,
               format="text",
           )

print(f"Deleting {name}{extension} from writer servers...")
           await async_writer_client.files.delete(file.id)
       else:
           print(f"Converting {name}{extension} content...")
           file_text = file_bytes_content.decode("utf-8")

return file_text

Extracting and repairing the data

With the file content extracted, you can now implement data extraction and repair using Instructor and Writer.

  1. Create a function to repair the data
    Create a function to extract and repair data using Instructor:
   async def repair_data(file_text: str, response_model: Type[BaseModel]) -> List[BaseModel]:
       instructor_client = instructor.from_writer(client=async_writer_client)

if not issubclass(response_model, BaseModel):
           raise ValueError("Response model must be subclass of pydantic BaseModel")

print("Extracting data featuring Instructor tools...")
       return await instructor_client.chat.completions.create(
           model="palmyra-x5",
           response_model=Iterable[response_model],
           max_retries=5,
           messages=[\
               {"role": "user", "content": f"Extract entities from {file_text}"},\
           ],
       )
  1. Implementing CSV generation
    Add a function to save the extracted data to CSV:
   def generate_csv(entities: List[BaseModel], response_model: Type[BaseModel], output_path: str = None) -> None:
       fieldnames = list(response_model.model_json_schema()["properties"].keys())
       file_path = f"{response_model.__name__}.csv"

if output_path:
           file_path = output_path + file_path
           os.makedirs(os.path.dirname(file_path), exist_ok=True)

with open(file_path, "w") as file:
           dict_writer = csv.DictWriter(file, fieldnames=fieldnames)
           dict_writer.writeheader()
           for entity in entities:
               dict_writer.writerow(json.loads(response_model(**entity.model_dump()).model_dump_json()))

Creating the main handler

Finally, implement the main function to process multiple files concurrently:

async def main():
    data = [\
        ("example_data/ExampleFileTextFormat.txt", UserExtract, None),\
        ("example_data/ExampleFilePDFFormat.pdf", UserExtract, "out/"),\
    ]
    tasks = []

for row in data:
        tasks.append(handle_file(row[0], row[1], row[2]))

await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())

In this example, the input paths are hardcoded, but you could modify the application to accept input paths from the command line or a web interface, or read from a directory or database.

Testing the application

Your data repair tool is now ready to use. To test it, follow these steps:

  1. Create an example_data directory
    Create an example_data directory and add some test files:
    • A text file with user information
    • A PDF file with user information

You can use the example data provided in the GitHub repository for this tutorial. If you provide your own, be sure to update the main.py file to point to the new files.

  1. Run the application
    Run the application:

    poetry run python main.py
    

    The application will process both files concurrently and generate CSV files containing the extracted user information.

Conclusion

You’ve now seen basic and advanced usage of Writer with Instructor. To learn more about Instructor, check out the Instructor documentation. Structured output is a powerful feature that can help you build more accurate and reliable applications, especially combined with tool calling.