97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
355 lines
7.6 KiB
Markdown
355 lines
7.6 KiB
Markdown
---
|
|
title: Philosophy
|
|
description: The principles behind Instructor - why simple beats complex every time.
|
|
---
|
|
|
|
# Philosophy
|
|
|
|
Great tools make hard things easy without making easy things hard. That's Instructor.
|
|
|
|
## Start with what developers know
|
|
|
|
Most AI frameworks invent their own abstractions. We don't.
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
|
|
# What you already know (Pydantic)
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
# What Instructor adds
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
_user = client.create(
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
) # That's it
|
|
```
|
|
|
|
If you know Pydantic, you know Instructor. No new concepts, no new syntax, no 200-page manual.
|
|
|
|
## Your escape hatch is always there
|
|
|
|
The worst frameworks are roach motels - easy to get in, impossible to get out. Instructor is different:
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
# With Instructor
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
_result = client.create(
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
)
|
|
|
|
# Want to go back to raw API? Just remove response_model:
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
_result = client.create(messages=[{"role": "user", "content": "Say hello"}])
|
|
|
|
# Or use the provider directly:
|
|
from openai import OpenAI
|
|
|
|
_raw_client = OpenAI() # Back to vanilla
|
|
```
|
|
|
|
We patch, we don't wrap. Your code, your control.
|
|
|
|
## Show, don't hide
|
|
|
|
Bad frameworks hide complexity. Good tools help you understand it.
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
# See exactly what Instructor sends
|
|
instructor.logfire.configure() # Full observability
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
result = client.create(
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
)
|
|
|
|
# Access raw responses
|
|
_raw_response = result._raw_response # See what the LLM actually returned
|
|
```
|
|
|
|
When something goes wrong (and it will), you can see exactly what happened.
|
|
|
|
## Composition beats configuration
|
|
|
|
No YAML files. No decorators. No magic. Just functions.
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
class Company(BaseModel):
|
|
name: str
|
|
industry: str
|
|
|
|
|
|
class Analysis(BaseModel):
|
|
user: User
|
|
company: Company
|
|
|
|
|
|
# Build complex systems with simple functions
|
|
def extract_user(text: str) -> User:
|
|
return client.create(
|
|
response_model=User, messages=[{"role": "user", "content": text}]
|
|
)
|
|
|
|
|
|
def extract_company(text: str) -> Company:
|
|
return client.create(
|
|
response_model=Company, messages=[{"role": "user", "content": text}]
|
|
)
|
|
|
|
|
|
def analyze_email(email: str) -> Analysis:
|
|
user = extract_user(email)
|
|
company = extract_company(email)
|
|
return Analysis(user=user, company=company)
|
|
|
|
|
|
# Compose however makes sense for YOUR application
|
|
_analysis = analyze_email("Please introduce Jane from Acme.")
|
|
```
|
|
|
|
## Start simple, grow naturally
|
|
|
|
The best code is code that grows with your needs:
|
|
|
|
```python
|
|
import instructor
|
|
from instructor import Partial
|
|
from pydantic import BaseModel, field_validator
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
# Day 1: Just get it working
|
|
_user = client.create(
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
)
|
|
|
|
|
|
# Day 7: Add validation
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
@field_validator("age")
|
|
def check_age(cls, value: int) -> int:
|
|
if value < 0 or value > 150:
|
|
raise ValueError("Invalid age")
|
|
return value
|
|
|
|
|
|
# Day 14: Add retries for production
|
|
_user = client.create(
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
max_retries=3,
|
|
)
|
|
|
|
|
|
# Day 30: Add streaming for better UX
|
|
def update_ui(_partial: Partial[User]) -> None:
|
|
pass
|
|
|
|
|
|
for partial in client.create(
|
|
response_model=Partial[User],
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
stream=True,
|
|
):
|
|
update_ui(partial)
|
|
```
|
|
|
|
Each addition is one line. No refactoring. No migration guide.
|
|
|
|
## What we intentionally DON'T do
|
|
|
|
### No prompt engineering
|
|
|
|
We don't write prompts for you. You know your domain better than we do.
|
|
|
|
```python
|
|
# We DON'T do this:
|
|
# @instructor.prompt("Extract the user information carefully")
|
|
# def extract_user(text: str):
|
|
# ...
|
|
|
|
|
|
# You write your own prompts:
|
|
text = "Jane is 33"
|
|
_messages = [
|
|
{"role": "system", "content": "You are a precise data extractor"},
|
|
{"role": "user", "content": f"Extract user from: {text}"},
|
|
]
|
|
```
|
|
|
|
### No new abstractions
|
|
|
|
We don't invent concepts like "Agents", "Chains", or "Tools". Those are your domain concepts.
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
# We DON'T do this:
|
|
# class UserExtractionAgent(instructor.Agent):
|
|
# tools = [instructor.WebSearch(), instructor.Calculator()]
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
def search_web(query: str) -> str:
|
|
return f"Results for {query}"
|
|
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
|
|
# You build what makes sense:
|
|
def extract_user_with_search(query: str) -> User:
|
|
# Your logic, your way
|
|
search_results = search_web(query)
|
|
return client.create(
|
|
response_model=User, messages=[{"role": "user", "content": search_results}]
|
|
)
|
|
|
|
|
|
_user = extract_user_with_search("Find Jane")
|
|
```
|
|
|
|
### No framework lock-in
|
|
|
|
Your code should work with or without us:
|
|
|
|
```python
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
|
|
# This is just a Pydantic model
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
# This is just a function
|
|
def process_user(user: User) -> dict:
|
|
return {"name": user.name.upper(), "adult": user.age >= 18}
|
|
|
|
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
# Instructor just connects them to LLMs
|
|
user = client.create(
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "Jane is 33"}],
|
|
)
|
|
|
|
_result = process_user(user) # Works with or without Instructor
|
|
```
|
|
|
|
## The result
|
|
|
|
By following these principles, we get:
|
|
|
|
- **Tiny API surface**: Learn it in minutes, not days
|
|
- **Zero vendor lock-in**: Switch providers or remove Instructor anytime
|
|
- **Debuggable**: When things break, you can see why
|
|
- **Composable**: Build complex systems from simple parts
|
|
- **Pythonic**: If it feels natural in Python, it feels natural in Instructor
|
|
|
|
## In practice
|
|
|
|
Here's what building with Instructor actually looks like:
|
|
|
|
```python
|
|
from enum import Enum
|
|
from typing import List
|
|
|
|
import instructor
|
|
from pydantic import BaseModel
|
|
|
|
|
|
# Your domain models (not ours)
|
|
class Priority(str, Enum):
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
|
|
|
|
class Ticket(BaseModel):
|
|
title: str
|
|
description: str
|
|
priority: Priority
|
|
estimated_hours: float
|
|
|
|
|
|
# Your business logic (not ours)
|
|
def prioritize_tickets(tickets: List[Ticket]) -> List[Ticket]:
|
|
return sorted(tickets, key=lambda t: (t.priority.value, -t.estimated_hours))
|
|
|
|
|
|
# Connect to LLM (one line)
|
|
client = instructor.from_provider("openai/gpt-4.1-mini")
|
|
|
|
# Extract structured data (simple function call)
|
|
tickets = client.create(
|
|
response_model=List[Ticket],
|
|
messages=[{"role": "user", "content": "Parse these support tickets: ..."}],
|
|
)
|
|
|
|
# Use your business logic
|
|
_prioritized = prioritize_tickets(tickets)
|
|
```
|
|
|
|
No framework. No abstractions. Just Python.
|
|
|
|
## The philosophy in one sentence
|
|
|
|
**Make structured LLM outputs as easy as defining a Pydantic model.**
|
|
|
|
Everything else follows from that.
|