ALToolbox.llms
- ALToolbox.llms
- Any
- Dict
- List
- Mapping
- Optional
- Sequence
- Union
- Literal
- cast
- keyword
- os
- json
- base64
- re
- tiktoken
- OpenAI
- NotFoundError
- MarkItDown
- util
- get_config
- log
- define
- DAList
- DAObject
- DADict
- DAFile
- DAFileList
- DALazyTemplate
- get_config
- language_name
- space_to_underscore
- __all__
- always_reserved_names
- DEFAULT_MODEL_SETS
- MODEL_TYPE_FALLBACKS
- MODEL_FAMILY_PATTERNS
- _extract_model_id
- list_available_models
- get_available_models
- _normalize_model_sets
- get_first_available_model_set
- detect_model_family
- get_available_model_families
- get_first_small_model
- get_default_model
- IMAGE_TOKEN_ESTIMATES
- IMAGE_TOKEN_ESTIMATE_UNKNOWN
- _message_text
- _image_token_estimate
- _messages_image_token_estimate
- _moderation_input
- _as_image_url
- _attach_images
- _is_thinking_model
- chat_completion
- extract_fields_from_text
- extract_fields_from_file
- match_goals_from_text
- classify_text
- synthesize_user_responses
- define_fields_from_dict
- translate_text
- Goal
- GoalDict
- GoalQuestion
- GoalSatisfactionList
- GoalOrientedQuestion
- GoalOrientedQuestionList
- IntakeQuestion
- IntakeQuestionList
Any
Dict
List
Mapping
Optional
Sequence
Union
Literal
cast
keyword
os
json
base64
re
tiktoken
OpenAI
NotFoundError
MarkItDown
util
get_config
log
define
DAList
DAObject
DADict
DAFile
DAFileList
DALazyTemplate
get_config
language_name
space_to_underscore
__all__
always_reserved_names
DEFAULT_MODEL_SETS: Dict[str, List[List[str]]]
MODEL_TYPE_FALLBACKS
MODEL_FAMILY_PATTERNS: Dict[str, List[str]]
_extract_model_id
def _extract_model_id(model: Any) -> Optional[str]
Extract a model ID from OpenAI model objects or dictionaries.
list_available_models
def list_available_models(openai_client: Optional[OpenAI] = None) -> List[str]
Return model IDs available on the configured OpenAI-compatible provider.
Example
With the provider configured on the server, list model IDs:
Input (interview YAML)
code: |
available_models = list_available_models()
get_available_models
def get_available_models(
candidate_models: List[str],
openai_client: Optional[OpenAI] = None,
available_models: Optional[List[str]] = None) -> List[str]
Return candidate model names that exist on the configured provider.
Matching is case-insensitive, and results preserve the order from
candidate_models.
Example
Using illustrative provider IDs and a previously fetched list, the
value of matching_models preserves candidate order and provider casing:
Input (interview YAML)
code: |
matching_models = get_available_models(
["MODEL-A", "model-b"], available_models=["model-a", "model-c"]
)
Output
['model-a']
_normalize_model_sets
def _normalize_model_sets(model_sets: Any) -> List[List[str]]
Normalize input into a list of model-name lists.
get_first_available_model_set
def get_first_available_model_set(
preferred_model_sets: List[List[str]],
openai_client: Optional[OpenAI] = None,
require_full_set: bool = True,
return_partial_if_needed: bool = True,
fallback_to_first_small_model: bool = True) -> List[str]
Return the first usable model set from a prioritized list.
Arguments
preferred_model_sets- Ordered fallback list of model sets. Each inner list is a preferred set for a family or provider.openai_client- Optional client override.require_full_set- If True, a set is selected only when every model in that set is available; if False, any non-empty intersection is accepted.return_partial_if_needed- If True and no full set is found, returns the first non-empty subset encountered.fallback_to_first_small_model- If True and no subset is found, returns[get_first_small_model(...)]when available.
Example
With preferred_model_sets containing your ordered lists of provider
model IDs, select the first usable group:
Input (interview YAML)
code: |
selected_models = get_first_available_model_set(
preferred_model_sets, require_full_set=False
)
detect_model_family
def detect_model_family(model_name: str) -> str
Infer provider family from a model name.
Returns values like openai, google, anthropic, mistral,
qwen, deepseek, or meta when patterns match.
Example
Using an illustrative model ID, the value of model_family shows
the family recognized by the name pattern; this does not query availability:
Input (interview YAML)
code: |
model_family = detect_model_family("gpt-example")
Output
openai
get_available_model_families
def get_available_model_families(
openai_client: Optional[OpenAI] = None) -> Dict[str, List[str]]
Group available models by detected family.
Example
Group the configured provider’s available models by family:
Input (interview YAML)
code: |
model_families = get_available_model_families()
get_first_small_model
def get_first_small_model(
openai_client: Optional[OpenAI] = None,
keywords: Optional[List[str]] = None) -> Optional[str]
Get the first available small/lite model from an OpenAI-compatible endpoint.
Queries the models endpoint and returns the first model whose name contains any of the provided keywords. This is useful for finding cost-effective models for tasks that don't require full capability.
Common keywords for small models: "nano", "mini", "small", "lite", "haiku", "turbo", "fast"
Arguments
openai_clientOptional[OpenAI] - An OpenAI client object. If None, uses the global client.keywordsOptional[List[str]] - Keywords to filter model names. If None (recommended), defaults to ["nano", "mini", "small", "lite", "haiku", "turbo", "fast"]. Matching is case-insensitive.
Returns
str- The ID of the first matching small model, or None if no models match or no client is available.
Example
Select an available small model. The result may be None if none match:
Input (interview YAML)
code: |
small_model = get_first_small_model()
get_default_model
def get_default_model(model_type: str = "small",
openai_client: Optional[OpenAI] = None) -> str
Get the default model to use for LLM operations.
Checks the Docassemble configuration for a default model in this order:
- open ai -> default model
- openai default model
If no configuration is found, returns the first small model from the endpoint, falling back to a sensible hardcoded default if that's also unavailable.
This allows users to override model selection globally in their config:
or alternatively:
open ai:
default model: gpt-5-nano
openai default model: gpt-5-nano
Arguments
model_typestr - Type of model to retrieve, such as "small", "medium", or "large". Other values may be supported via configuration; unknown types fall back to the "small" model-type fallback.openai_clientOptional[OpenAI] - Client to use for model discovery. If omitted, uses the configured global client.
Returns
str- The model ID to use for LLM operations.
Example
Select a model using the server’s configuration and available models:
Input (interview YAML)
code: |
draft_model = get_default_model(model_type="small")
IMAGE_TOKEN_ESTIMATES
IMAGE_TOKEN_ESTIMATE_UNKNOWN
_message_text
def _message_text(message: Mapping[str, Any]) -> str
Return a message's text, whether its content is a string or parts.
_image_token_estimate
def _image_token_estimate(model: str, detail: Optional[str]) -> int
Return a conservative token upper bound for one image.
_messages_image_token_estimate
def _messages_image_token_estimate(messages: Sequence[Mapping[str, Any]],
model: str) -> int
Estimate all image parts, respecting each part's requested detail.
_moderation_input
def _moderation_input(
messages: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]
Convert chat messages into OpenAI's multimodal moderation input.
_as_image_url
def _as_image_url(image: Union[DAFile, str, bytes]) -> str
Normalise an image into something the chat endpoint will accept.
Arguments
-
image- The image to normalise. Accepted forms are:DAFile– a Docassemble file object; the raw bytes are read fromimage.path()and then processed as if they had been passed directly.bytes– raw image bytes; the media type is sniffed from the magic number (JPEG, PNG, GIF, or WEBP).str– either adata:URI or anhttp(s)://URL, both of which are passed through unchanged.
Returns
A string that the OpenAI chat endpoint will accept as an image URL:
either a data:<media-type>;base64,<payload> URI (for byte inputs)
or the original URL / data-URI string.
Raises
ValueError- If the bytes do not match a recognised image magic number, or if the string is not adata:URI or anhttp(s)://URL.
_attach_images
def _attach_images(messages: List[Dict[str, Any]],
images: Sequence[Union[DAFile, str, bytes]],
image_detail: Optional[str] = None) -> List[Dict[str, Any]]
Turn the last user message into a multimodal one carrying the images.
Callers keep writing ordinary string-content messages; only the message the images belong to changes shape, and only when images are supplied.
_is_thinking_model
def _is_thinking_model(model: str) -> bool
Whether a model only accepts the default temperature.
OpenAI's reasoning models reject any temperature but the default and take
reasoning_effort instead. That covers o1, o3, and every GPT model from
gpt-5 on (gpt-5, gpt-5.6-luna, gpt-6-luna, ...), so the GPT check reads the
major version rather than listing each one.
Arguments
modelstr - the model name, e.g. "gpt-6-luna".
Returns
bool- True if the model is a thinking model.
chat_completion
def chat_completion(
system_message: Optional[str] = None,
user_message: Optional[str] = None,
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
temperature: float = 0.5,
json_mode=False,
model: Optional[str] = None,
messages: Optional[List[Dict[str, str]]] = None,
skip_moderation: bool = True,
openai_base_url: Optional[str] = None,
max_output_tokens: Optional[int] = None,
max_input_tokens: Optional[int] = None,
reasoning_effort: Optional[Literal["minimal", "low", "medium",
"high"]] = None,
images: Optional[Sequence[Union[DAFile, str, bytes]]] = None,
image_detail: Optional[Literal["low", "high", "auto"]] = None
) -> Union[List[Any], Dict[str, Any], str]
A light wrapper on the OpenAI chat endpoint.
Includes support for token limits, minimal error handling, and moderation.
Arguments
-
system_messagestr - The role the chat engine should play -
user_messagestr - The message (data) from the user -
openai_clientOptional[OpenAI] - An OpenAI client object, optional. If omitted, will fall back to creating a new OpenAI client with the API key provided as an environment variable -
openai_apiOptional[str] - the API key for an OpenAI client, optional. If provided, a new OpenAI client will be created. -
temperaturefloat - The temperature to use for the GPT API -
json_modebool - Whether to use JSON mode for the GPT API. Requires the wordjsonin the system message, but will add if you omit it. -
modelstr - The model to use for the GPT API. If not provided, uses the configured default model or the first available small model. -
messagesOptional[List[Dict[str, str]]] - A list of messages to send to the chat engine. If provided, system_message and user_message will be ignored. -
skip_moderationbool - Whether to skip the OpenAI moderation step, which may save seconds but risks banning your account. Only enable when you have full control over the inputs. -
openai_base_urlOptional[str] - The base URL for the OpenAI API. Defaults to value provided in the configuration or "https://api.openai.com/v1/". -
max_output_tokensOptional[int] - The maximum number of tokens to return from the API. Defaults to 16380. -
max_input_tokensOptional[int] - The maximum number of tokens to send to the API. Defaults to 128000. reasoning_effort (Optional[Literal["minimal", "low", "medium", "high"]]) = None: The reasoning effort to use for thinking models. Defaults to value provided in the configuration or "low". -
imagesOptional[Sequence[Union[DAFile, str, bytes]]] - Images to send alongside the text. Each image may be:- a
DAFile– the bytes are read fromimage.path(); - raw
bytes– the media type is sniffed from the magic number (JPEG, PNG, GIF, WEBP); - a
str– either adata:URI or anhttp(s)://URL.
The last user message becomes multimodal; every other message is untouched. Sending pixels to a third party is a decision, so this is opt-in and never inferred.
- a
-
image_detailOptional[Literal["low", "high", "auto"]] - The detail level to request for each image. "low" is markedly cheaper and is enough to say what a logo or a seal is.
Returns
A string with the response from the API endpoint or JSON data if json_mode is True
Example
After gathering incident_description, generate text in a code block
and display draft_summary on a review screen. Generated wording varies:
Input (interview YAML)
code: |
draft_summary = chat_completion(
system_message="Summarize the supplied text in plain language. Do not add facts.",
user_message=incident_description,
)
extract_fields_from_text
def extract_fields_from_text(
text: str,
field_list: Dict[str, str],
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
temperature: float = 0,
model: Optional[str] = None,
reasoning_effort: Optional[Literal["minimal", "low", "medium",
"high"]] = "low"
) -> Dict[str, Any]
Extracts fields from text.
Arguments
textstr - The text to extract fields fromfield_listDict[str, str] - A list of fields to extract, with the key being the field name and the value being a description of the fieldopenai_clientOptional[OpenAI] - An OpenAI client object. Defaults to None.openai_apiOptional[str] - An OpenAI API key. Defaults to None.temperaturefloat - The temperature to use for the OpenAI API. Defaults to 0.modelstr - The model to use for the OpenAI API. If not provided, uses the configured default model or the first available small model.reasoning_effortOptional[Literal["minimal", "low", "medium", "high"]] - The reasoning effort to use for the LLM. Defaults to "low".
Returns
dict- A dictionary of fields extracted from the text
Example
With uploaded_text containing the text of a letter, extract values
for a review screen:
Input (interview YAML)
code: |
extracted_fields = extract_fields_from_text(
uploaded_text,
{"sender_name": "The sender’s full name", "letter_date": "The date on the letter"},
)
extract_fields_from_file
def extract_fields_from_file(
the_file: Union[DAFile, DAFileList],
field_list: Dict[str, str],
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
model: Optional[str] = None,
reasoning_effort: Optional[Literal["minimal", "low", "medium",
"high"]] = "low",
llm_hint: Optional[str] = "",
process_pdfs_with_ai: bool = True,
ocr_images_and_pdfs: bool = False,
ocr_use_google: Optional[bool] = False,
openai_base_url: Optional[str] = None) -> Dict[str, Any]
Extracts data (in the form of a list of expected fields) from a file using an LLM.
When the file is a PDF, relies on the OpenAI vision API to interpret the document. Note that this may increase cost, but will also improve accuracy.
If it is another file type that is convertible by Markitdown, it uses Markitdown to convert the file to text first.
Can be combined with define_fields_from_dict to populate Docassemble fields.
You can provide a hint to the LLM if it would help with data extraction. For example: "the document ID is usually found near the top right of the first page."
You should normally call this function in the background as it may take some time to run, especially when ocr_images_and_pdfs is True.
Arguments
the_fileUnion[DAFile, DAFileList] - The file to extract fields fromfield_listDict[str, str] - A list of fields to extract, with the key being the field name and the value being a description of the fieldopenai_clientOptional[OpenAI] - An OpenAI client object. Defaults to None.openai_apiOptional[str] - An OpenAI API key. Defaults to None.modelstr - The model to use for the OpenAI API. If not provided, uses the configured default model or the first available small model.reasoning_effortOptional[Literal["minimal", "low", "medium", "high"]] - The reasoning effort to use for the LLM. Defaults to "low".llm_hintOptional[str] - an optional hint to improve processing the text layer with the LLM.process_pdfs_with_aibool - Whether to process PDFs with the OpenAI API (True) or convert to text first (False). Defaults to True.ocr_images_and_pdfsbool - Whether to perform OCR on PDFs before processing with the OpenAI API. Defaults to False. May be useful if the PDF has a text layer that is incomplete.ocr_use_googleOptional[bool] - whether to use Google Vision API instead of local OCR. Only applies if ocr_images_and_pdfs is Trueopenai_base_urlOptional[str] - The base URL for the OpenAI API. Defaults to value provided in the configuration or "https://api.openai.com/v1/".
Returns
dict- A dictionary of fields extracted from the file
Example
With uploaded_letter gathered using datatype: file, extract
values for the user to review:
Input (interview YAML)
code: |
extracted_fields = extract_fields_from_file(
uploaded_letter,
{"sender_name": "The sender’s full name", "letter_date": "The date on the letter"},
)
match_goals_from_text
def match_goals_from_text(question: str,
user_response: str,
goals: Dict[str, str],
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
temperature: float = 0,
model: Optional[str] = None) -> Dict[str, Any]
Reads a user's message and determines whether it meets a set of goals, with the help of an LLM.
Arguments
questionstr - The question that was asked to the useruser_responsestr - The user's response to the questiongoalsDict[str,str] - A list of goals to extract, with the key being the goal name and the value being a description of the goalopenai_clientOptional[OpenAI] - An OpenAI client object. Defaults to None.openai_apiOptional[str] - An OpenAI API key. Defaults to None.temperaturefloat - The temperature to use for the OpenAI API. Defaults to 0.modelstr - The model to use for the OpenAI API. If not provided, uses the configured default model or the first available small model.
Returns
A dictionary of fields extracted from the text
Check whether a gathered narrative supplies the requested details:
Input (interview YAML)
code: |
matched_goals = match_goals_from_text(
question="Describe what happened",
user_response=incident_description,
goals={"date": "Includes when it happened", "event": "Describes what happened"},
)
Example
Check whether a gathered narrative supplies the requested details:
Input (interview YAML)
code: |
matched_goals = match_goals_from_text(
question="Describe what happened",
user_response=incident_description,
goals={"date": "Includes when it happened", "event": "Describes what happened"},
)
classify_text
def classify_text(text: str,
choices: Dict[str, str],
default_response: str = "null",
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
temperature: float = 0,
model: Optional[str] = None) -> str
Given a text, classify it into one of the provided choices with the assistance of a large language model.
Arguments
textstr - The text to classifychoicesDict[str,str] - A list of choices to classify the text into, with the key being the choice name and the value being a description of the choicedefault_responsestr - The default response to return if the text cannot be classified. Defaults to "null".openai_clientOptional[OpenAI] - An OpenAI client object, optional. If omitted, will fall back to creating a new OpenAI client with the API key provided as an environment variableopenai_apiOptional[str] - the API key for an OpenAI client, optional. If provided, a new OpenAI client will be created.temperaturefloat - The temperature to use for GPT. Defaults to 0.modelstr - The model to use for the GPT API. If not provided, uses the configured default model or the first available small model.
Returns
The classification of the text.
Classify a gathered narrative into one of the supplied categories:
Input (interview YAML)
code: |
issue_category = classify_text(
incident_description,
choices={"housing": "Problems with a home or landlord", "employment": "Problems at work"},
default_response="other",
)
Example
Classify a gathered narrative into one of the supplied categories:
Input (interview YAML)
code: |
issue_category = classify_text(
incident_description,
choices={"housing": "Problems with a home or landlord", "employment": "Problems at work"},
default_response="other",
)
synthesize_user_responses
def synthesize_user_responses(messages: List[Dict[str, str]],
custom_instructions: Optional[str] = "",
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
temperature: float = 0,
model: Optional[str] = None,
user_language: str = "en") -> str
Given a first draft and a series of follow-up questions and answers, use an LLM to synthesize the user's responses into a single, coherent reply.
Arguments
messagesList[Dict[str, str]] - A list of questions from the LLM and responses from the usercustom_instructionsstr - Custom instructions for the LLM to follow in constructing the synthesized responseopenai_clientOptional[OpenAI] - An OpenAI client object, optional. If omitted, will fall back to creating a new OpenAI client with the API key provided as an environment variableopenai_apiOptional[str] - the API key for an OpenAI client, optional. If provided, a new OpenAI client will be created.temperaturefloat - The temperature to use for GPT. Defaults to 0.modelstr - The model to use for the GPT API. If not provided, uses the configured default model or the first available small model.user_languagestr - The language of the user that the response should be written in, in addition to English. Uses ISO 639-1 two-letter codes or ISO 639-3 three-letter codes. Defaults to "en".
Returns
A synthesized response from the user.
Example
Combine a gathered narrative and follow-up answer for a review screen:
Input (interview YAML)
code: |
draft_summary = synthesize_user_responses([
{"role": "assistant", "content": "Describe what happened"},
{"role": "user", "content": incident_description},
{"role": "assistant", "content": "When did it happen?"},
{"role": "user", "content": incident_date_description},
])
define_fields_from_dict
def define_fields_from_dict(field_dict: Dict[str, Any],
fields_to_ignore: Optional[List] = None) -> None
Assign values from a dictionary to corresponding Docassemble interview fields.
Docassemble and built-in keywords are never defined by this function. If fields_to_ignore is provided, those fields will also be ignored.
Arguments
field_dictDict[str, Any] - A dictionary of fields to define, with the key being the field name and the value presumably taken from the output of extract_fields_from_text.fields_to_ignoreOptional[List] - A list of fields to ignore. Defaults to None. Should be used to ensure safety when defining fields from untrusted sources. E.g., ["user_is_logged_in"]
Example
With users[0] already defined, assign reviewed field values to the
first person in the interview:
Input (interview YAML)
code: |
define_fields_from_dict(
{"users[0].name.first": "Alex", "users[0].name.last": "Rivera"},
)
translate_text
def translate_text(text: str,
input_language: str = "",
output_language: str = "en",
include_original: bool = False,
custom_instructions: Optional[str] = "",
openai_client: Optional[OpenAI] = None,
openai_api: Optional[str] = None,
temperature: float = 0,
model: Optional[str] = None) -> str
Given some text, translate it into another language.
Arguments
textstr - The text to translate.input_languagestr - Optional source language code (ISO 639-1 or ISO 639-3). If blank, the model will infer it.output_languagestr - Target language code (ISO 639-1 or ISO 639-3). Defaults to "en".include_originalbool - If True, includes the original text after the translation. If False, returns only the translated text. Defaults to False.custom_instructionsstr - Optional extra instructions for translation style/constraints.openai_clientOptional[OpenAI] - An OpenAI client object, optional.openai_apiOptional[str] - An OpenAI API key, optional. If provided, a new OpenAI client will be created.temperaturefloat - The temperature to use for GPT. Defaults to 0.modelstr - The model to use for the GPT API. If not provided, uses the configured default model or the first available small model.
Returns
The translated text.
Example
Translate gathered notice_text in a code block, then display
translated_notice. The generated wording may vary:
Input (interview YAML)
code: |
translated_notice = translate_text(
notice_text, input_language="en", output_language="es"
)
Goal Objects
class Goal(DAObject)
A class to represent a goal.
Attributes
-
namestr - The name of the goal -
descriptionstr - A description of the goal -
satisfiedbool - Whether the goal is satisfiedAfter gathering this value:
Input (Mako)
${ follow_up.goal_dict["goal_0"].description }
Example
A goal in a GoalSatisfactionList’s goal_dict. After gathering this value:
Input (Mako)
${ follow_up.goal_dict["goal_0"].description }
response_satisfies_me_or_follow_up
def response_satisfies_me_or_follow_up(
messages: List[Dict[str, str]],
openai_client: Optional[OpenAI] = None,
model: Optional[str] = None,
system_message: Optional[str] = None,
llm_assumed_role: Optional[str] = "teacher",
user_assumed_role: Optional[str] = "student") -> str
Returns the text of the next question to ask the user or the string "satisfied" if the user's response satisfies the goal.
Arguments
messagesList[Dict[str, str]] - The messages to checkopenai_clientOptional[OpenAI] - An OpenAI client object. Defaults to None.modelstr - The model to use for the OpenAI API. If not provided, uses the configured default model or the first available small model.system_messageOptional[str] - The system message to use for the OpenAI API. Defaults to None.llm_assumed_roleOptional[str] - The role for the LLM to assume. Defaults to "teacher".user_assumed_roleOptional[str] - The role for the user to assume. Defaults to "student".
Returns
The text of the next question to ask the user or the string "satisfied"
Example
With a GoalSatisfactionList and conversation containing dictionaries
with role and content, request a follow-up for one goal:
Input (interview YAML)
code: |
next_question = follow_up.goal_dict["goal_0"].response_satisfies_me_or_follow_up(messages=conversation)
get_next_question
def get_next_question(thread_so_far: List[Dict[str, str]],
openai_client: Optional[OpenAI] = None,
model: Optional[str] = None) -> str
Returns the text of the next question to ask the user.
Arguments
thread_so_farList[Dict[str, str]] - The thread of the conversation so faropenai_clientOptional[OpenAI] - An OpenAI client object. Defaults to None.modelstr - The model to use for the OpenAI API. If not provided, uses the configured default model or the first available small model.
Returns
The text of the next question to ask the user.
Example
With a GoalSatisfactionList and conversation containing dictionaries
with role and content, request a follow-up for one goal:
Input (interview YAML)
code: |
next_question = follow_up.goal_dict["goal_0"].get_next_question(thread_so_far=conversation)
__str__
def __str__()
GoalDict Objects
class GoalDict(DADict)
A class to represent a DADict of Goals.
After gathering this value:
Input (Mako)
${ follow_up.goal_dict.satisfied() }
Example
A dictionary of goals created by a GoalSatisfactionList. After gathering this value:
Input (Mako)
${ follow_up.goal_dict.satisfied() }
init
def init(*pargs, **kwargs)
satisfied
def satisfied() -> bool
Returns True if all goals are satisfied, False otherwise.
Returns
True if all goals are satisfied, False otherwise.
With a configured GoalSatisfactionList:
Input (interview YAML)
code: |
all_goals_satisfied = follow_up.goal_dict.satisfied()
Example
With a configured GoalSatisfactionList:
Input (interview YAML)
code: |
all_goals_satisfied = follow_up.goal_dict.satisfied()
GoalQuestion Objects
class GoalQuestion(DAObject)
A class to represent a question about a goal.
Attributes
-
goalGoal - The goal the question is about -
questionstr - The question to ask the user -
responsestr - The user's response to the questionAfter gathering this value:
Input (Mako)
${ follow_up[0].response }
Example
An entry created by a GoalSatisfactionList. After gathering this value:
Input (Mako)
${ follow_up[0].response }
complete
@property
def complete()
Returns True if the goal, question, and response attributes are present.
Example
Docassemble uses this property during list gathering. Accessing it triggers any missing question and response attributes:
Input (interview YAML)
code: |
first_response_complete = follow_up[0].complete
GoalSatisfactionList Objects
class GoalSatisfactionList(DAList)
A class to help ask the user questions until all goals are satisfied.
Uses an LLM to prompt the user with follow-up questions if the initial response isn't complete. By default, the number of follow-up questions is limited to 10.
This can consume a lot of tokens, as each follow-up has a chance to send the whole conversation thread to the LLM.
By default, this will use the OpenAI API key defined in the global configuration under this path:
You can specify the path to an alternative configuration by setting the openai_configuration_path attribute.
This object does NOT accept the key as a direct parameter, as that will be leaked in the user's answers.
open ai:
key: sk-...
Attributes
-
goalsList[Goal] - The goals in the list, provided as a dictionary -
goal_listGoalList - The list of Goals -
question_limitint - The maximum number of follow-up questions to ask the user -
question_per_goal_limitint - The maximum number of follow-up questions to ask the user per goal -
initial_draftstr - The initial draft of the user's response -
initial_questionstr - The original question posed in the interviewWith an LLM provider configured on the server:
Input (interview YAML)
include:
- docassemble.ALToolbox:goal_satisfy.yml
question: |
Describe the incident
fields:
- Description: follow_up.initial_draft
datatype: area
objects:
- follow_up: GoalSatisfactionList.using(goals=["Describe what happened", "Explain when it happened"], initial_question="Describe the incident")
code: |
follow_up.gather()
draft_summary = follow_up.synthesize_draft_response()
init
def init(*pargs, **kwargs)
mark_satisfied_goals
def mark_satisfied_goals() -> None
Marks goals as satisfied if the user's response satisfies the goal. This should be used as soon as the user gives their initial reply.
Example
After gathering follow_up.initial_draft, check the initial response
before starting the follow-up questions:
Input (interview YAML)
code: |
follow_up.mark_satisfied_goals()
keep_going
def keep_going() -> bool
Returns True if there is at least one unsatisfied goal and if the number of follow-up questions asked is less than the question limit, False otherwise.
Returns
True if there is at least one unsatisfied goal and if the number of follow-up questions asked is less than the question limit, False otherwise.
Example
With follow_up configured as a GoalSatisfactionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
can_ask_another_question = follow_up.keep_going()
need_more_questions
def need_more_questions() -> bool
Returns True if there is at least one unsatisfied goal, False otherwise.
Also has the side effect of checking the user's most recent response to see if it satisfies the goal and updating the next question to be asked.
Returns
True if there is at least one unsatisfied goal, False otherwise.
Example
With follow_up configured as a GoalSatisfactionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
follow_up.there_is_another = follow_up.need_more_questions()
satisfied
def satisfied() -> bool
Returns True if all goals are satisfied, False otherwise.
Returns
True if all goals are satisfied, False otherwise.
Example
With follow_up configured as a GoalSatisfactionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
response_is_complete = follow_up.satisfied()
_get_next_unsatisfied_goal
def _get_next_unsatisfied_goal() -> Optional[Goal]
Returns the next unsatisfied goal.
get_next_goal_and_question
def get_next_goal_and_question() -> tuple
Returns the next unsatisfied goal, along with a follow-up question to ask the user, if relevant.
Returns
A tuple of (Goal, str) where the first item is the next unsatisfied goal and the second item is the next question to ask the user, if relevant. If the user's response to the last question satisfied the goal, returns (None, None).
Example
With follow_up configured as a GoalSatisfactionList, assign the
next gathered list entry’s goal and question:
Input (interview YAML)
code: |
follow_up[i].goal, follow_up[i].question = follow_up.get_next_goal_and_question()
_get_related_thread
def _get_related_thread(goal: Goal) -> List[Dict[str, str]]
Returns a list of messages (with corresponding role) related to the given goal.
This is appropriate to pass to the OpenAI ChatCompletion APIs.
Arguments
goalGoal - The goal to get the related thread for
Returns
A list of messages (with corresponding role) related to the given goal.
synthesize_draft_response
def synthesize_draft_response() -> str
Returns a draft response that synthesizes the user's responses to the questions.
Returns
A draft response that synthesizes the user's responses to the questions.
Example
With follow_up configured as a GoalSatisfactionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
draft_summary = follow_up.synthesize_draft_response()
provide_feedback
def provide_feedback(
feedback_prompt: str = "") -> Union[List[Any], Dict[str, Any], str]
Returns feedback to the user based on the goals they satisfied.
Arguments
feedback_promptstr - The prompt to use for the feedback. Defaults to "".
Returns
Feedback to the user based on the goals they satisfied.
Example
With follow_up configured as a GoalSatisfactionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
draft_feedback = follow_up.provide_feedback(feedback_prompt="Explain which details are still missing.")
GoalOrientedQuestion Objects
class GoalOrientedQuestion(DAObject)
A class to represent a question in a goal-oriented questionnaire.
Attributes
-
questionstr or dict - The question to ask the user (text or field structure) -
responsestr or dict - The user's response to the question (text or field values)After gathering this value:
Input (Mako)
${ follow_up[0].response }
Example
An entry created by a GoalOrientedQuestionList. After gathering this value:
Input (Mako)
${ follow_up[0].response }
complete
@property
def complete()
Returns True if the question and response attributes are present.
Example
Docassemble uses this property during list gathering. Accessing it triggers any missing question and response attributes:
Input (interview YAML)
code: |
first_response_complete = follow_up[0].complete
response_as_text
def response_as_text() -> str
Returns the response in a readable text format for the LLM.
Combines both structured responses from response_dict and the open-ended response. Uses original labels from the question for better context. Handles checkboxes specially by using .true_values() to show only checked items.
Returns
A formatted string representation of all responses.
After gathering the first entry in a GoalOrientedQuestionList:
Input (interview YAML)
code: |
response_text = follow_up[0].response_as_text()
Example
After gathering the first entry in a GoalOrientedQuestionList:
Input (interview YAML)
code: |
response_text = follow_up[0].response_as_text()
build_field_list
def build_field_list() -> List[Dict[str, Any]]
Build a field list from this question object for use in docassemble fields.
Returns
A list of field dictionaries suitable for use in a fields: code: block
Example
With a GoalOrientedQuestionList whose next entry’s question has
already been generated:
Input (interview YAML)
question: |
Please tell us more
fields:
- code: follow_up[i].build_field_list()
GoalOrientedQuestionList Objects
class GoalOrientedQuestionList(DAList)
A class to help ask the user follow-up questions until their response satisfies a single rubric.
Unlike GoalSatisfactionList which tracks multiple individual goals, this class focuses on a single rubric that describes what constitutes a complete response. The AI will continue asking follow-up questions until the response satisfies the rubric or the question limit is reached.
This can consume a lot of tokens, as each follow-up has a chance to send the whole conversation thread to the LLM.
By default, this will use the OpenAI API key defined in the global configuration under this path:
You can specify the path to an alternative configuration by setting the openai_configuration_path attribute.
This object does NOT accept the key as a direct parameter, as that will be leaked in the user's answers.
open ai:
key: sk-...
Attributes
-
rubricstr - The rubric that describes what constitutes a complete response -
question_limitint - The maximum number of follow-up questions to ask the user. Defaults to 6. -
initial_draftstr, optional - The initial draft of the user's response (for open-ended initial questions) -
initial_draft_dictDADict, optional - Dictionary of structured responses (for structured initial questions) -
initial_draft_responsestr, optional - Open-ended response for structured initial questions -
initial_questionstr - The original question posed in the interview -
use_structured_initial_questionbool - If True, generate structured fields for initial question. Defaults to False. -
modelstr - The model to use for the OpenAI API. Defaults to "gpt-5-nano". -
llm_assumed_rolestr - The role for the LLM to assume. Defaults to "legal aid intake worker". -
user_assumed_rolestr - The role for the user to assume. Defaults to "applicant for legal help". -
user_languagestr - The language of the user that the questions should be written in, in addition to English. Uses ISO 639-1 two-letter codes or ISO 639-3 three-letter codes. Defaults to "en". -
skip_moderationbool - If True, skips moderation checks when generating structured fields. Defaults to True. -
reasoning_effortOptional[Literal["minimal", "low", "medium", "high"]] - The level of reasoning effort to use when generating responses. Defaults to "low"; use "minimal" for increased speed.With an LLM provider configured on the server:
Input (interview YAML)
include:
- docassemble.ALToolbox:goal_oriented_question.yml
question: |
Describe the incident
fields:
- Description: follow_up.initial_draft
datatype: area
objects:
- follow_up: GoalOrientedQuestionList.using(rubric="The response explains what happened and when.", initial_question="Describe the incident")
code: |
follow_up.gather()
draft_summary = follow_up.synthesize_draft_response()
reasoning_effort: Optional[Literal["minimal", "low", "medium", "high"]]
init
def init(*pargs, **kwargs)
bilingual_rule
def bilingual_rule() -> str
Return prompt instructions for the question list’s configured language.
Example
With follow_up.user_language set, get the language instructions for
a custom prompt used with this GoalOrientedQuestionList:
Input (interview YAML)
code: |
language_instructions = follow_up.bilingual_rule()
generate_initial_question_fields
def generate_initial_question_fields() -> Dict[str, Any]
Generate structured fields for the initial question using the LLM.
This allows the initial question to use structured fields (radio, checkboxes, etc.) instead of requiring an open-ended narrative response.
Returns
A dict with the structure for the initial question fields.
Example
With follow_up configured as a GoalOrientedQuestionList and
use_structured_initial_question=True, generate the initial question:
Input (interview YAML)
code: |
follow_up.initial_question_structure = follow_up.generate_initial_question_fields()
build_initial_field_list
def build_initial_field_list() -> List[Dict[str, Any]]
Build field list for the initial question when using structured format.
Returns
A list of field dictionaries suitable for use in a fields: code: block
GoalOrientedQuestionList:
Input (interview YAML)
question: |
Tell us about the incident
fields:
- code: follow_up.build_initial_field_list()
Example
After generating follow_up.initial_question_structure for a
GoalOrientedQuestionList:
Input (interview YAML)
question: |
Tell us about the incident
fields:
- code: follow_up.build_initial_field_list()
initial_response_as_text
def initial_response_as_text() -> str
Returns the initial response in text format, handling both string and dict formats.
Handles checkboxes specially by using .true_values() to show only checked items.
Returns
A formatted string representation of the initial response.
After gathering the initial response in a GoalOrientedQuestionList:
Input (interview YAML)
code: |
initial_response_text = follow_up.initial_response_as_text()
Example
After gathering the initial response in a GoalOrientedQuestionList:
Input (interview YAML)
code: |
initial_response_text = follow_up.initial_response_as_text()
keep_going
def keep_going() -> bool
Returns True if the response is not yet complete and the question limit hasn't been reached.
Returns
True if more questions can be asked, False otherwise.
Example
With follow_up configured as a GoalOrientedQuestionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
can_ask_another_question = follow_up.keep_going()
need_more_questions
def need_more_questions() -> bool
Returns True if the user needs to answer more questions, False otherwise.
Also has the side effect of checking the user's most recent response to see if it satisfies the rubric and updating the next question to be asked.
Returns
True if more questions are needed, False otherwise.
Example
With follow_up configured as a GoalOrientedQuestionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
follow_up.there_is_another = follow_up.need_more_questions()
satisfied
def satisfied() -> bool
Returns True if the rubric is satisfied, False otherwise.
Returns
True if the rubric is satisfied, False otherwise.
Example
With follow_up configured as a GoalOrientedQuestionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
response_is_complete = follow_up.satisfied()
_check_satisfaction
def _check_satisfaction() -> Union[str, Dict[str, Any]]
Checks if the user's responses satisfy the rubric.
Returns
Either the string "satisfied" if the rubric is satisfied, or a dict with generated fields for the next question.
get_next_question
def get_next_question() -> Optional[Union[str, Dict[str, Any]]]
Returns the text or field structure of the next question to ask the user.
Returns
The text/fields of the next question, or None if no more questions are needed.
Example
With follow_up configured as a GoalOrientedQuestionList, assign the
next gathered list entry’s question:
Input (interview YAML)
code: |
follow_up[i].question = follow_up.get_next_question()
_get_thread
def _get_thread() -> List[Dict[str, str]]
Returns a list of messages (with corresponding role) for the conversation thread.
This is appropriate to pass to the OpenAI ChatCompletion APIs.
Returns
A list of messages (with corresponding role) for the conversation.
synthesize_draft_response
def synthesize_draft_response() -> str
Returns a draft response that synthesizes the user's responses to the questions.
Returns
A draft response that synthesizes the user's responses to the questions.
Example
With follow_up configured as a GoalOrientedQuestionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
draft_summary = follow_up.synthesize_draft_response()
provide_feedback
def provide_feedback(
feedback_prompt: str = "") -> Union[List[Any], Dict[str, Any], str]
Returns feedback to the user based on how well they satisfied the rubric.
Arguments
feedback_promptstr - The prompt to use for the feedback. Defaults to "".
Returns
Feedback to the user based on how well they satisfied the rubric.
Example
With follow_up configured as a GoalOrientedQuestionList (see the class
example), use in the interview’s follow-up flow:
Input (interview YAML)
code: |
draft_feedback = follow_up.provide_feedback(feedback_prompt="Explain which details are still missing.")
IntakeQuestion Objects
class IntakeQuestion(DAObject)
A class to represent a question in an LLM-assisted intake questionnaire.
Attributes
-
questionstr - The question to ask the user -
responsestr - The user's response to the questionAfter gathering this value:
Input (Mako)
${ intake_screener[0].response }
Example
An entry created by an IntakeQuestionList. After gathering this value:
Input (Mako)
${ intake_screener[0].response }
complete
@property
def complete()
Returns True if the question and response attributes are present.
Example
Docassemble uses this property during list gathering. Accessing it triggers any missing question and response attributes:
Input (interview YAML)
code: |
first_response_complete = intake_screener[0].complete
IntakeQuestionList Objects
class IntakeQuestionList(DAList)
Class to help create an LLM-assisted intake questionnaire.
The LLM will be provided a free-form set of in/out criteria (like that provided to a phone intake worker), an initial draft question from the user, and then guide the user through a series of follow-up questions to gather only enough information to determine if the user meets the criteria.
In/out criteria are often pretty short, so we do not make or support embeddings at the moment.
Attributes
criteriaDict[str, str] - A dictionary of criteria to match, indexed by problem typeproblem_type_descriptionsDict[str, str] - A dictionary of descriptions of the problem typesproblem_typestr - The type of problem to match. E.g., a unit/department inside the law firminitial_problem_descriptionstr - The initial description of the problem from the userinitial_questionstr - The original question posed in the interviewquestion_limitint - The maximum number of follow-up questions to ask the user. Defaults to 10.modelstr - The model to use for the GPT API. Defaults to gpt-4.1.max_output_tokensint - The maximum number of tokens to return from the API. Defaults to 4096llm_rolestr - The role the LLM should play. Allows you to customize the script the LLM uses to guide the user. We have provided a default script that should work for most intake questionnaires.llm_user_qualifies_promptstr - The prompt to use to determine if the user qualifies. We have provided a default prompt.out_of_questionsbool - Whether the user has run out of questions to answerqualifiesbool - Whether the user qualifies based on the criteria
Example
With an LLM provider configured, replace the sample screening criteria with the criteria used by your organization:
Input (interview YAML)
include:
- docassemble.ALToolbox:intake.yml
mandatory: True
code: |
intake_screener.gather()
intake_screener.intake_results
init
def init(*pargs, **kwargs)
_classify_problem_type
def _classify_problem_type()
Classifies the problem type based on the user's initial description.
_keep_going
def _keep_going()
Returns True if the user can and needs to answer more questions, False otherwise.
It respects the limit defined by self.question_limit.
As a side effect, checks if the user has run out of questions to answer and updates the next question to be asked to be a closing message instead of a follow-up.
need_more_questions
def need_more_questions() -> bool
Returns True if the user needs to answer more questions, False otherwise.
Also has the side effect of checking the user's most recent response to see if it satisfies the criteria and updating both the next question to be asked and the current qualification status.
Returns
True if the user needs to answer more questions, False otherwise.
Example
With intake_screener configured as an IntakeQuestionList, use in
the gathering flow. This also updates qualifies and next_question:
Input (interview YAML)
code: |
intake_screener.there_is_another = intake_screener.need_more_questions()
_current_qualification_status
def _current_qualification_status()
Returns a dictionary with the user's current qualification status
_get_thread
def _get_thread()
Returns a list of messages (with corresponding role) related to the given goal.
_ran_out_of_questions_message
def _ran_out_of_questions_message()
Returns a message to display when the user has run out of questions to answer.