Samplers and Questions¶
A Sampler asks focused questions about existing data. Every ask() returns a
Sample, with typed answers, the evaluated inputs, and model/usage information.
Install pip install chatsnack (0.9.0 or later) and set TYPESAFE_API_KEY in your
environment or local .env. TypeSafe support is included. The key is needed when
you evaluate a Sampler; authoring and saving definitions require no credentials.
Something crunchy¶
from chatsnack import Sampler
sample = Sampler(data="buttered popcorn").ask("Is this crunchy?")
if sample.answer.yes:
print("Crunch!")
sample.answer is always sample.answers[0], even in a batch. Likewise,
sample.question is sample.questions[0]. These are the same objects, not copies.
Use await sampler.ask_a(...) for async code. Both methods always return a Sample.
A reusable Sampler keeps its synchronous provider connection ready for the next call:
judge = Sampler(questions=["Is this crunchy?"])
first = judge.ask(data="popcorn")
second = judge.ask(data="a potato chip")
judge.close()
Call close() when a long-running synchronous application shuts down. ask_a()
uses a scoped async client and closes it after each evaluation, so it does not keep
an event-loop-bound connection on the Sampler. One-call endpoint, credential,
timeout, and retry overrides stay isolated from the reusable synchronous connection.
Keep a useful question¶
from chatsnack import Question, Sampler
crunchy = Question(
name="crunchy",
question="Is this crunchy?",
yes="It makes a crisp cracking sound when bitten.",
)
print(crunchy.yaml)
crunchy.save()
review = Sampler(name="SnackCheck", data="{snack}", questions=["{question.crunchy}"])
print(review.yaml)
review.save()
The saved Sampler is small:
Load it and supply a filling when you need it:
review = Sampler(name="SnackCheck")
review.load()
sample = review.ask(snack="popcorn")
print(sample.answer.choice, sample.answer.score)
Construction stays in memory. Questions and Samplers save under questions/ and
samplers/ in the chatsnack data directory. Existing CHATSNACK_BASE_DIR and
snapclass stash configuration apply. An anonymous definition needs a name or an
explicit file path before saving. Saved Sampler names cannot contain {, },
!, or :, because those characters have special meaning to the filling formatter.
Pass a Question object to embed its definition. Use "{question.crunchy}" for a
live connection to the saved asset. Saving preserves that choice; a Question's
name or save history never turns an embedded value into a reference.
A few questions together¶
category = Question(
name="category",
question="What kind of food is this?",
choices={"snack": "A small bite between meals", "meal": "A full meal"},
)
sweetness = Question(
name="sweetness",
question="How sweet is this?",
levels=["Not sweet", "A little sweet", "Very sweet"],
)
sample = review.ask(questions=["{question.crunchy}", category, sweetness], snack="popcorn")
print(sample.answers["category"].choice)
print(sample.answers["sweetness"].score)
Named access remains stable when you reorder questions. All collections retain
authored order; unnamed questions are available by position. Names must be
nonempty strings without the filling metacharacters ., [, ], {, }, !,
or :, and unique within a batch. Empty batches are rejected.
Every answer has choice, score, probabilities, and confidence:
| Kind | Choice | Score |
|---|---|---|
| Yes/no | "yes" or "no" |
Probability of yes |
| Choice | Selected option key | Probability of that option |
| Score | Most probable authored level name | Provider's weighted position across levels |
Yes/no answers also have Boolean .yes and .no. Confidence is the binary
probability margin for yes/no, and the provider's reported confidence for Choice
and Score. Scores have different meanings across question kinds.
Compose into a Chat¶
Keep the decision connection in the authored prompt:
from chatsnack import Chat
description = Chat(
"Describe {snack} in one sentence. "
"Crunchy: {sampler.SnackCheck.crunchy.choice}."
)
print(description.ask(snack="popcorn"))
You can also read .score and .confidence in result fillings. Several fields
from the same Sampler share one evaluation during that prompt expansion. A later
call evaluates again. Saved result fillings always name their answer explicitly.
Keep what was evaluated¶
The replay contains the resolved data and questions, with the returned model version. Later edits to Question assets won't change it. Evaluating again may produce different answers.
To ask about the previous result itself, use it as new data:
followup = Sampler(data=sample).ask("Do these answers describe a sweet snack?")
print(followup.answer.yes)
Call-time data= overrides the stored data without changing it. Data supplied
that way, and values injected into fillings, remain literal: braces inside a
document do not become new template expressions.
See the advanced Sampler reference for execution settings, request inspection, and the public filling resolver.