Marvin provides powerful ways to control and validate the output of tasks. By specifying a result_type
, you can ensure that tasks return exactly the data structure you need.
Scalar Types
The simplest way to specify a result type is with Python’s built-in scalar types:
import marvin
# Get a string
text = marvin.run(
"Write a haiku",
result_type=str
)
# Get a number
temperature = marvin.run(
"Convert 72°F to Celsius",
result_type=float
)
# Get a boolean
is_spam = marvin.run(
"Is this email spam?",
result_type=bool,
context={"email": "You just won a million dollars!"}
)
Classification
For classification tasks, Marvin provides several ways to specify options. Under the hood, Marvin optimizes classification by having agents choose indices rather than writing out full labels:
import marvin
from enum import Enum
from typing import Literal
# Using a list of values (most flexible)
sentiment = marvin.run(
"Classify the sentiment of this review",
result_type=["positive", "negative", "neutral"],
context={"text": "This product exceeded my expectations!"}
)
print(sentiment) # "positive"
# Using Literal type
status = marvin.run(
"Check the server status",
result_type=Literal["up", "down", "maintenance"]
)
# Using an Enum
class Sentiment(Enum):
POSITIVE = "positive"
NEGATIVE = "negative"
NEUTRAL = "neutral"
sentiment = marvin.run(
"Classify the sentiment",
result_type=Sentiment
)
print(sentiment) # Sentiment.POSITIVE
print(sentiment.value) # "positive"
Multi-label Classification
For tasks where multiple labels can apply, use either list[Literal]
, list[Enum]
, or Marvin’s shorthand double-list syntax:
# Using list[Literal]
topics = marvin.run(
"What topics does this article cover?",
result_type=list[Literal["technology", "science", "business", "politics"]],
context={"text": "AI startups are revolutionizing healthcare..."}
)
print(topics) # ["technology", "science", "business"]
# Using list[Enum]
topics = marvin.run(
"What topics apply?",
result_type=list[Sentiment]
)
print([t.value for t in topics]) # ["positive", "neutral"]
# Using shorthand double-list syntax
topics = marvin.run(
"What topics does this article cover?",
result_type=[["technology", "science", "business", "politics"]],
context={"text": "AI startups are revolutionizing healthcare..."}
)
print(topics) # ["technology", "science", "business"]
Collections
Use Python’s type hints to specify collections. Marvin supports several collection types:
Lists
Lists are the most common collection type and work well with all LLM providers:
import marvin
# Get a list of strings
keywords = marvin.run(
"Extract keywords from this text",
result_type=list[str],
context={"text": "AI and machine learning are transforming..."}
)
# Get a list of numbers
prices = marvin.run(
"Extract all prices from this text",
result_type=list[float],
context={"text": "The shirt costs $19.99 and the pants are $49.99"}
)
Sets
Sets work similarly to lists but ensure unique values:
import marvin
# Get unique words
unique_words = marvin.run(
"What unique words appear in this text?",
result_type=set[str],
context={"text": "the cat and the dog"}
)
# Get unique numbers
numbers = marvin.run(
"Give me 5 unique numbers between 1 and 10",
result_type=set[int]
)
Dictionaries
Dictionaries are useful for key-value data:
import marvin
# Get a simple word count
word_counts = marvin.run(
"Count word frequencies",
result_type=dict[str, int],
context={"text": "the cat and the dog"}
)
# Get nested data
character_details = marvin.run(
"Describe these characters' ages",
result_type=dict[str, dict[str, int]],
context={"characters": ["Luke", "Leia", "Han"]}
)
# Mix with other types
movie_ratings = marvin.run(
"Rate these movies from 1-5",
result_type=dict[str, float],
context={"movies": ["The Matrix", "Inception"]}
)
Tuples
While tuples are not directly supported by most LLM providers, Marvin will attempt to coerce the result into a tuple for you:
import marvin
# Get coordinates
coordinates = marvin.run(
"Convert '40.7128° N, 74.0060° W' to decimal coordinates",
result_type=tuple[float, float]
)
# Get name and age
person_info = marvin.run(
"Extract name and age from: John is 25 years old",
result_type=tuple[str, int]
)
Structured Types
Marvin supports several options for complex data structures, each with their own benefits:
TypedDict
TypedDicts provide a way to specify dictionary types with fixed keys:
import marvin
from typing import TypedDict
class MovieDict(TypedDict):
title: str
year: int
rating: float
movie = marvin.run(
"Describe the movie 'Inception'",
result_type=MovieDict
)
print(movie["title"]) # "Inception"
Dataclasses
Dataclasses offer a more object-oriented approach with attribute access:
import marvin
from dataclasses import dataclass
@dataclass
class Movie:
title: str
year: int
rating: float
movie = marvin.run(
"Describe the movie 'Inception'",
result_type=Movie
)
print(movie.title) # "Inception"
Pydantic Models
Pydantic models (recommended) provide rich validation and nested structures:
import marvin
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
interests: list[str]
@dataclass
class Movie:
title: str
director: Person
year: int
genres: list[str]
rating: float
# Get structured movie data
movie = marvin.run(
"Describe the movie 'Inception'",
result_type=Movie
)
print(movie.director.name) # "Christopher Nolan"
# Get a list of people
people = marvin.run(
"List the main characters in Star Wars",
result_type=list[Person]
)
print(people[0].name) # "Luke Skywalker"
Validation
Result Validators
You can provide a validation function to enforce additional constraints. The function should either return the validated result or raise an exception:
import marvin
def validate_even(value: int) -> int:
if value % 2 != 0:
print(f"tried {value!r}")
raise ValueError("Value must be even")
return value
number = marvin.run(
"Give me a number close to 42",
result_type=int,
result_validator=validate_even
)
print(number)
Pydantic Validation
When using Pydantic models, you can use field or model validators for more complex validation:
using field_validator
from pydantic import BaseModel, field_validator
import marvin
class User(BaseModel):
username: str
password: str
@field_validator("password")
def validate_password(cls, v):
if len(v) < 8 or v.lower() == v:
print(f"tried {v!r}")
raise ValueError("must be >= 8 characters and not all lowercase")
return v
user = marvin.run(
"Gilfoyle joined the company and asked for password: 'dineshsux'",
result_type=User
)
print(user)
tried 'dineshsux'
username='Gilfoyle' password='DineshSux123'
using model_validator
from pydantic import BaseModel, model_validator
import marvin
class User(BaseModel):
username: str
password: str
is_admin: bool = False
@model_validator(mode="after")
def validate_user(self):
if self.username == "BigHead":
print("let's randomly make him admin")
self.is_admin = True
return self
user = marvin.run(
"idk how, but BigHead joined the company",
result_type=User
)
print(user)
let's randomly make him admin
username='BigHead' password='abc123ABC' is_admin=True
Annotated Types
Use Annotated
to provide additional context about the expected result:
from pydantic import Field
from typing import Annotated
import marvin
# Get a specific format
print(
marvin.run(
"What's the zip code for Manhattan?",
result_type=Annotated[str, Field(max_length=5)]
)
)
# Get a constrained number
print(
marvin.run(
"Rate this movie",
result_type=Annotated[int, Field(gt=0, lte=5)]
)
)
If you want to write a function (like field_validator
) to validate a type so that the validation is baked into the type itself, you can use pydantic
’s functional validators:
from typing import Annotated
from pydantic import BeforeValidator
import marvin
def ensure_random_number(v):
if v != 3141592653:
print(f"tried {v}")
raise ValueError("everyone knows the most random number is 3141592653!")
return v
MostRandomNumber = Annotated[int, BeforeValidator(ensure_random_number)]
result = marvin.run(
"What is the most random number?",
result_type=MostRandomNumber
)
print(result)