VITON13School

Generative AI for BeginnersLesson 7 of 21

Building a text generation app

A prompt in a chat window is an experiment; a prompt inside a program is a product. In this lesson Study Buddy gets its first real feature, a quiz maker that returns structured data and copes when the model or the network misbehaves.

Course by , Founder of VITON13 SCHOOL
  • Part 2: Build
  • 50 min

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.
  • timeout and max_retries on 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

  1. Save quiz_maker.py in your project and run it on three topics from your notes.
  2. Set max_tokens=60, print response.choices[0].finish_reason inside the loop, run again and watch the reply get cut off and rejected; then restore the limit.
  3. 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.
  4. Wrap the final loop in try and catch openai.APIConnectionError, openai.AuthenticationError and ValueError, printing a friendly one-line message for each; test by setting a wrong base URL.
  5. 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.

A reply ends with finish_reason set to length. What happened?

Right.length signals that generation stopped because of the token cap, so the text is likely incomplete.

Not quite.The right answer is “The reply hit the maximum token limit before it was finished”. length signals that generation stopped because of the token cap, so the text is likely incomplete.

Which errors are worth retrying?

Right.Rate limits and server hiccups are temporary and often succeed on a later attempt, while key and request errors repeat until you fix them.

Not quite.The right answer is “Rate limits and temporary server errors”. Rate limits and server hiccups are temporary and often succeed on a later attempt, while key and request errors repeat until you fix them.

Why validate the parsed JSON even when JSON mode is on?

Right.A reply can be perfectly valid JSON yet miss a key, use the wrong type or have three options instead of four.

Not quite.The right answer is “Valid syntax does not guarantee the keys and values your app needs”. A reply can be perfectly valid JSON yet miss a key, use the wrong type or have three options instead of four.

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.
Vitalii Tarasov, Founder of VITON13 SCHOOL

Course by Vitalii Tarasov, VITON13 SCHOOL. Draws in part on Microsoft's Generative AI for Beginners, MIT licence, Copyright (c) Microsoft Corporation. microsoft/generative-ai-for-beginners