You will be able to
- Describe what a text generation app is and what its code is responsible for
- Set model parameters such as temperature and maximum output length on purpose
- Build prompts from templates filled with validated user input
- Request JSON output, parse it and reject replies that do not fit
- Handle timeouts, rate limits and server errors without crashing
From prompt to program
A text generation app takes some input, wraps it in a prompt your code controls, sends it to a model and does something useful with the reply. Summarisers, quiz makers, rewriting tools and email drafters all follow this shape.
Unlike a command-line tool with fixed commands, the input can be natural language in any phrasing. That flexibility is the appeal, and it is also the risk: your code has to cope with replies that are long, short, oddly formatted or occasionally nonsense.
The work splits cleanly. The model writes text; your code owns the template, checks the input, sets the parameters, validates the output and decides what the user sees when something fails. Most of the reliability of an AI app lives in that ordinary code.
You can build all of this with the plain openai SDK. Frameworks such as LangChain or LlamaIndex add abstractions on top, which help in bigger projects but hide the mechanics. Learn the direct calls first; the frameworks will make more sense afterwards.
Parameters that matter
Every request names a model and a list of messages. Beyond those, a few parameters shape the reply, and setting them deliberately is part of designing the feature.
Pick values per feature and write down why. A quiz maker wants enough variety that repeated runs give fresh questions, so a moderate temperature suits it, while a feature that marks answers should run cold. The length cap should fit the largest valid reply with a margin, not the smallest.
Some providers accept extra options such as n for several alternative replies in one call or seed for more repeatable sampling. They are not universal, so keep the core of your app working without them and treat them as optional improvements.
temperature: low for predictable, factual output; higher for variety.max_tokens: a cap on the length of the reply, which also caps its cost.stop: one or more strings that end generation when the model writes them.timeoutandmax_retrieson the client: how long to wait and how often the SDK retries.
Reading the reply properly
Check response.choices[0].finish_reason when output looks wrong. A value of stop means the model ended normally; length means it hit your token cap mid-answer. A truncated JSON object is not a model bug, it is a limit set too low.
Parameter support varies. Some reasoning models reject temperature or expect max_completion_tokens instead of max_tokens, and some providers ignore options they do not implement. When a request fails on a parameter, read the error message; it usually names the argument it dislikes.
Record the token usage of each call while you develop. It tells you what a feature costs per use and warns you early if a template or a history has grown much longer than you meant.
Treat empty content as a real possibility. A reply can come back with no text, for example after a safety filter steps in, so write content or "" before calling string methods, as the example does, and decide what the user should see in that case.
Templates with variables
Keep the fixed part of a prompt in a template and insert user input into placeholders. Python's str.format works well for this, but literal braces in the template, such as a JSON example, must be doubled as {{ and }} so they are not treated as placeholders.
Validate input before it reaches the template. Trim whitespace, cap the length, and restrict values that should come from a fixed list, such as the number of questions or a difficulty level. The model should never receive a request for ten thousand questions because a user typed an extra zero.
Place user text where it is clearly data, inside quotes or a labelled block, and keep your instructions outside it. That will not stop every attack, but it is the first defence against users who type instructions instead of a topic, a problem lesson 14 covers in depth.
Templates make behaviour testable. Because the wording is fixed, you can run the same template against your test set whenever you change the model or the parameters and compare results directly.
Structured output and failures
When code consumes the answer, ask for JSON with exactly the keys you need and show their shape. Many providers also offer a JSON mode, set with response_format={"type": "json_object"}, and some support a strict JSON Schema option. Support differs between services, so test it on yours.
JSON mode guarantees valid syntax at best, not correct content. Always parse with json.loads and check what your code depends on: required keys, list lengths, value ranges. Models also like to wrap JSON in Markdown fences, so strip those before parsing.
When validation fails, retry once with a short note explaining the problem, then give up cleanly with a message the user understands. Endless retries waste tokens and hide a prompt that needs fixing.
Network failures are a separate matter. The SDK already retries connection errors, rate limits and server errors with backoff; you control how many times with max_retries. Do not retry authentication or bad-request errors, because they will fail the same way every time; catch them and show a clear message instead.
Set a timeout that matches the feature. A quiz can wait a minute, but an autocomplete suggestion that arrives after ten seconds is useless, and a short timeout with a polite fallback gives a better experience than a spinner that never stops.
Example
quiz_maker.py: JSON quiz with validation and retry
# quiz_maker.py - a quiz as JSON, validated, with one retry on bad outputimport json, os, sysfrom dotenv import load_dotenvfrom openai import OpenAI load_dotenv()client = OpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"], max_retries=3, timeout=60) # the SDK retries rate limits and server errors itselfTEMPLATE = ('Write {count} multiple-choice questions about "{topic}" for a beginner. Reply with JSON only: ' '{{"questions": [{{"q": "...", "options": ["...", "...", "...", "..."], "answer": 0}}]}}')FENCE = "`" * 3 def make_quiz(topic: str, count: int = 3) -> list: messages = [{"role": "user", "content": TEMPLATE.format(count=count, topic=topic)}] for _ in range(2): response = client.chat.completions.create( model=os.environ["LLM_MODEL"], messages=messages, temperature=0.4, max_tokens=1000) text = (response.choices[0].message.content or "").strip() text = text.removeprefix(FENCE + "json").removeprefix(FENCE).removesuffix(FENCE) try: questions = json.loads(text)["questions"] if questions and all(len(q["options"]) == 4 and q["answer"] in range(4) for q in questions): return questions except (json.JSONDecodeError, KeyError, TypeError): pass messages += [{"role": "assistant", "content": text}, {"role": "user", "content": "That was not valid. Reply with the JSON object only, same shape."}] raise ValueError("The model did not return a valid quiz after two attempts.")for i, q in enumerate(make_quiz(" ".join(sys.argv[1:]) or "photosynthesis"), 1): print(f"{i}. {q['q']}\n " + " | ".join(q["options"]) + f" (answer {q['answer'] + 1})")Run python quiz_maker.py photosynthesis. Doubled braces in TEMPLATE survive str.format as single braces. If your provider supports JSON mode, add response_format={"type": "json_object"} to the call and compare how often the first attempt passes.
Practice
Give Study Buddy a quiz command
- Save
quiz_maker.pyin your project and run it on three topics from your notes. - Set
max_tokens=60, printresponse.choices[0].finish_reasoninside the loop, run again and watch the reply get cut off and rejected; then restore the limit. - Run the script five times with and five times without JSON mode (if your provider has it) and count how often the first attempt passes validation.
- Wrap the final loop in
tryand catchopenai.APIConnectionError,openai.AuthenticationErrorandValueError, printing a friendly one-line message for each; test by setting a wrong base URL. - Read an optional question count from the command line, cap it between 1 and 10, and pass it to
make_quiz.
Done when: python quiz_maker.py photosynthesis prints numbered questions with four options each, and with a broken base URL the script prints your friendly message instead of a traceback.
Common mistakes
- Cutting the reply apart with string slicing instead of a JSON parser plus checks.
- Retrying every error, including a wrong key or a malformed request that can never succeed.
- Ignoring
finish_reason, so truncated output is mistaken for a bad model. - Concatenating raw user input into the instruction part of the prompt.
Check yourself
Pick an answer to see whether it is right and why.
From the founder
Break the script on purpose in three ways: a tiny token limit, a wrong address and bad JSON. Only once you have seen each failure and chosen what the user sees is the feature ready for real people.