Magentic
Seamlessly integrate LLMs as Python functions
Install / Use
/learn @jackmpcollins/MagenticREADME
magentic
Seamlessly integrate Large Language Models into Python code. Use the @prompt and @chatprompt decorators to create functions that return structured output from an LLM. Combine LLM queries and tool use with traditional Python code to build complex agentic systems.
Features
- Structured Outputs using pydantic models and built-in python types.
- Streaming of structured outputs and function calls, to use them while being generated.
- LLM-Assisted Retries to improve LLM adherence to complex output schemas.
- Observability using OpenTelemetry, with native Pydantic Logfire integration.
- Type Annotations to work nicely with linters and IDEs.
- Configuration options for multiple LLM providers including OpenAI, Anthropic, and Ollama.
- Many more features: Chat Prompting, Parallel Function Calling, Vision, Formatting, Asyncio...
Installation
pip install magentic
or using uv
uv add magentic
Configure your OpenAI API key by setting the OPENAI_API_KEY environment variable. To configure a different LLM provider see Configuration for more.
Usage
@prompt
The @prompt decorator allows you to define a template for a Large Language Model (LLM) prompt as a Python function. When this function is called, the arguments are inserted into the template, then this prompt is sent to an LLM which generates the function output.
from magentic import prompt
@prompt('Add more "dude"ness to: {phrase}')
def dudeify(phrase: str) -> str: ... # No function body as this is never executed
dudeify("Hello, how are you?")
# "Hey, dude! What's up? How's it going, my man?"
The @prompt decorator will respect the return type annotation of the decorated function. This can be any type supported by pydantic including a pydantic model.
from magentic import prompt
from pydantic import BaseModel
class Superhero(BaseModel):
name: str
age: int
power: str
enemies: list[str]
@prompt("Create a Superhero named {name}.")
def create_superhero(name: str) -> Superhero: ...
create_superhero("Garden Man")
# Superhero(name='Garden Man', age=30, power='Control over plants', enemies=['Pollution Man', 'Concrete Woman'])
See Structured Outputs for more.
@chatprompt
The @chatprompt decorator works just like @prompt but allows you to pass chat messages as a template rather than a single text prompt. This can be used to provide a system message or for few-shot prompting where you provide example responses to guide the model's output. Format fields denoted by curly braces {example} will be filled in all messages (except FunctionResultMessage).
from magentic import chatprompt, AssistantMessage, SystemMessage, UserMessage
from pydantic import BaseModel
class Quote(BaseModel):
quote: str
character: str
@chatprompt(
SystemMessage("You are a movie buff."),
UserMessage("What is your favorite quote from Harry Potter?"),
AssistantMessage(
Quote(
quote="It does not do to dwell on dreams and forget to live.",
character="Albus Dumbledore",
)
),
UserMessage("What is your favorite quote from {movie}?"),
)
def get_movie_quote(movie: str) -> Quote: ...
get_movie_quote("Iron Man")
# Quote(quote='I am Iron Man.', character='Tony Stark')
See Chat Prompting for more.
FunctionCall
An LLM can also decide to call functions. In this case the @prompt-decorated function returns a FunctionCall object which can be called to execute the function using the arguments provided by the LLM.
from typing import Literal
from magentic import prompt, FunctionCall
def search_twitter(query: str, category: Literal["latest", "people"]) -> str:
"""Searches Twitter for a query."""
print(f"Searching Twitter for {query!r} in category {category!r}")
return "<twitter results>"
def search_youtube(query: str, channel: str = "all") -> str:
"""Searches YouTube for a query."""
print(f"Searching YouTube for {query!r} in channel {channel!r}")
return "<youtube results>"
@prompt(
"Use the appropriate search function to answer: {question}",
functions=[search_twitter, search_youtube],
)
def perform_search(question: str) -> FunctionCall[str]: ...
output = perform_search("What is the latest news on LLMs?")
print(output)
# > FunctionCall(<function search_twitter at 0x10c367d00>, 'LLMs', 'latest')
output()
# > Searching Twitter for 'Large Language Models news' in category 'latest'
# '<twitter results>'
See Function Calling for more.
@prompt_chain
Sometimes the LLM requires making one or more function calls to generate a final answer. The @prompt_chain decorator will resolve FunctionCall objects automatically and pass the output back to the LLM to continue until the final answer is reached.
In the following example, when describe_weather is called the LLM first calls the get_current_weather function, then uses the result of this to formulate its final answer which gets returned.
from magentic import prompt_chain
def get_current_weather(location, unit="fahrenheit"):
"""Get the current weather in a given location"""
# Pretend to query an API
return {"temperature": "72", "forecast": ["sunny", "windy"]}
@prompt_chain(
"What's the weather like in {city}?",
functions=[get_current_weather],
)
def describe_weather(city: str) -> str: ...
describe_weather("Boston")
# 'The current weather in Boston is 72°F and it is sunny and windy.'
LLM-powered functions created using @prompt, @chatprompt and @prompt_chain can be supplied as functions to other @prompt/@prompt_chain decorators, just like regular python functions. This enables increasingly complex LLM-powered functionality, while allowing individual components to be tested and improved in isolation.
Streaming
The StreamedStr (and AsyncStreamedStr) class can be used to stream the output of the LLM. This allows you to process the text while it is being generated, rather than receiving the whole output at once.
from magentic import prompt, StreamedStr
@prompt("Tell me about {country}")
def describe_country(country: str) -> StreamedStr: ...
# Print the chunks while they are being received
for chunk in describe_country("Brazil"):
print(chunk, end="")
# 'Brazil, officially known as the Federative Republic of Brazil, is ...'
Multiple StreamedStr can be created at the same time to stream LLM outputs concurrently. In the below example, generating the description for multiple countries takes approximately the same amount of time as for a single country.
from time import time
countries = ["Australia", "Brazil", "Chile"]
# Generate the descriptions one at a time
start_time = time()
for country in countries:
# Converting `StreamedStr` to `str` blocks until the LLM output is fully generated
description = str(describe_country(country))
print(f"{time() - start_time:.2f}s : {country} - {len(description)} chars")
# 22.72s : Australia - 2130 chars
# 41.63s : Brazil - 1884 chars
# 74.31s : Chile - 2968 chars
# Generate the descriptions concurrently by creating the StreamedStrs at the same time
start_time = time()
streamed_strs = [describe_country(country) for country in countries]
for country, streamed_str in zip(countries, streamed_strs):
description = str(streamed_str)
print(f"{time() - start_time:.2f}s : {country} - {len(description)} chars")
# 22.79s : Australia - 2147 chars
# 23.64s : Brazil - 2202 chars
# 24.67s : Chile - 2186 chars
Object Streaming
Structured outputs can also be streamed from the LLM by using the return type annotation Iterable (or AsyncIterable). This allows each item to be processed while the next one is being generated.
from collections.abc import Iterable
from time import time
from magentic import prompt
from pydantic import BaseModel
class Superhero(BaseModel):
name: str
age: int
power: str
enemies: list[str]
@prompt("Create a Superhero team named {name}.")
def create_superhero_team(name: str) -> Iterable[Superhero]: ...
start_time = time()
for hero in create_superhero_team("The Food Dudes"):
print(f"{time() - start_time:.2f}s : {hero}")
# 2.23s : name='Pizza Man' age=30 power='Can shoot pizza slices from his hands' enemies=['The Hungry Horde', 'The Junk Food Gang']
# 4.03s : name='Captain Carrot' age=35 power='Super strength and agility from eating carrots' enemies=['The Sugar Squad', 'The Greasy Gang']
# 6.05s : name='Ice Cream Girl' age=25 power='Can create ice cream out of thin air' enemies=['The Hot Sauce Squad', 'The Healthy Eaters']
See Streaming for more.
Asyncio
Asynchronous functions / coroutines can be used to concurrently query the LLM. This can greatly increase the overall speed of generation, and also allow other asynchronous code to run while waiting on LLM output. In the below example, the LLM generates a description for each US president while it is waiting on the next one in the list. Measuring the characters generated per second shows that th
Related Skills
openhue
339.1kControl Philips Hue lights and scenes via the OpenHue CLI.
sag
339.1kElevenLabs text-to-speech with mac-style say UX.
weather
339.1kGet current weather and forecasts via wttr.in or Open-Meteo
tweakcc
1.5kCustomize Claude Code's system prompts, create custom toolsets, input pattern highlighters, themes/thinking verbs/spinners, customize input box & user message styling, support AGENTS.md, unlock private/unreleased features, and much more. Supports both native/npm installs on all platforms.
