Typed Results
Validate task outputs with structured result types.
In addition to providing discrete goals for your agents, ControlFlow tasks are designed to translate between the unstructured, conversational world of your AI agents and the structured, programmatic world of your application. The primary mechanism for this translation is the task’s result, which should be a well-defined, validated output that can be used by other tasks or components in your workflow.
ControlFlow allows you to specify the expected structure of a task’s result using the result_type
parameter. This ensures that the result conforms to a specific data schema, making it easier to work with and reducing the risk of errors in downstream tasks.
In addition to the basic benefits of type safety and data integrity, result types also serve as a form of documentation for your agents, indicating exactly what kind of data they should expect to produce.
Default Results are Strings
By default, the result_type
of a task is a string, meaning the agent can return any value that satisfies the task’s objective.
import controlflow as cf
task = cf.Task('Say hello in three languages')
print(task.run())
In the above example, you may get a result like "Hello; Hola; Bonjour"
, or even something more complex like `“‘Hello!\n\nIn three languages, “Hello” is expressed as follows:\n\n1. English: Hello\n2. Spanish: Hola\n3. French: Bonjour’“.
While this flexibility can be useful in some cases, especially if this task’s result is only being consumed by another ControlFlow task, it can also lead to ambiguity and errors if the agent produces unexpected output.
Returning Basic Types
For simple task results, you can use any of Python’s built-in types.
Here’s the above example, specifying that the result should be a list of strings. This guides the agent to give the result you probably expected (three strings, each representing a greeting in a different language):
import controlflow as cf
task = cf.Task('Say hello in three languages', result_type=list[str])
result = task.run()
print(result)
assert isinstance(result, list)
assert len(result) == 3
If your result is a number, you can specify the result_type
as int
or float
:
import controlflow as cf
task = cf.Task("Generate a random number", result_type=int)
result = task.run()
print(result)
assert isinstance(result, int)
You can even use bool
for tasks whose result is a simple true/false value:
import controlflow as cf
task = cf.Task("Evaluate the statement: the earth is flat", result_type=bool)
result = task.run()
assert result is False
Constrained Choices
Sometimes you want to limit the possible results to a specific set of values. You can do this by specifying a list of allowed values for the result type:
import controlflow as cf
task = cf.Task(
"Is this a book or a movie?",
result_type=["book", "movie"],
context=dict(title="Game of Thrones"),
)
result = task.run()
assert result == "book"
Pydantic Models
For complex, structured results, you can use a Pydantic model as the result_type
. Pydantic models provide a powerful way to define data schemas and validate input data.
import controlflow as cf
from pydantic import BaseModel, Field
class ResearchReport(BaseModel):
title: str
summary: str
key_findings: list[str] = Field(min_items=3, max_items=10)
references: list[str]
task = cf.Task(
"Generate a research report on quantum computing",
result_type=ResearchReport
)
report = task.run()
print(f"Report title: {report.title}")
print(f"Number of key findings: {len(report.key_findings)}")
Advanced Validation
Pydantic allows for advanced validation using custom validators:
from pydantic import BaseModel, field_validator
class SentimentAnalysis(BaseModel):
text: str
sentiment: float
@field_validator('sentiment')
def check_sentiment_range(cls, v):
if not -1 <= v <= 1:
raise ValueError('Sentiment must be between -1 and 1')
return v
task = cf.Task(
"Analyze sentiment of given text",
result_type=SentimentAnalysis,
context=dict(text="I love ControlFlow!"),
)
analysis = task.run()
print(f"Sentiment: {analysis.sentiment}")