Last code lesson. Five week-4 + earlier patterns into one script:
ask_json(prompt) wraps the LLM call (L24).label and confidence keys (L11-L13).import json
from pydantic_ai import Agent
FINAL_USAGE = {"total": 0}
def ask_json(prompt, max_attempts=2):
base = prompt + '\n\nReply with ONLY valid JSON. No prose around it.'
p = base
for _ in range(max_attempts):
result = Agent(model).run_sync(p)
FINAL_USAGE["total"] += result.usage().total_tokens
text = result.output.strip().strip("`").strip()
try:
return json.loads(text)
except json.JSONDecodeError as e:
p = base + f'\nThe previous response was not valid JSON ({e}). Try again — JSON only.'
raise ValueError("could not parse JSON after retries")
items = ["I love this!", "Total disaster.", "It was fine."]
results = []
for item in items:
parsed = ask_json(
f'Classify the sentiment of "{item}" as positive, negative, or neutral. '
f'Reply with JSON: {{"label": "<one of the three>", "confidence": <0-1 float>}}'
)
results.append({"input": item, **parsed})
print(f'{parsed["label"]} ({parsed["confidence"]:.2f}): {item}')
print(f'\ntotal tokens: {FINAL_USAGE["total"]} items: {len(results)}')Every line uses something from earlier in the track.
That's the point. No new tools. Just composition. This is what "writing AI scripts" looks like in practice — small primitives, deliberately stacked. Once each piece is reflexive, building a new script feels like assembly, not engineering.
No new technique. Composition of week-4 + earlier patterns into one shape that's representative of real LLM scripts.
def ask_json(prompt, max_attempts=2):
base = prompt + '\n\nReply with ONLY valid JSON. No prose around it.'→ Helper (L24). System-level instruction baked in once.
p = base
for _ in range(max_attempts):
result = Agent(model).run_sync(p)
FINAL_USAGE["total"] += result.usage().total_tokens→ Cost tracking (L27) inside the helper — every call site gets accumulation for free.
text = result.output.strip().strip("`").strip()
try:
return json.loads(text)
except json.JSONDecodeError as e:
p = base + f'\nThe previous response was not valid JSON ({e}). Try again — JSON only.'→ Retry on bad output (L25 + L12). Catch the parse error, tell the model what went wrong, retry.
raise ValueError("could not parse JSON after retries")→ Hard fail after cap. Don't loop forever; surface the bug.
for item in items:
parsed = ask_json(...)
results.append({"input": item, **parsed})→ Batch (L23). Loop over input, accumulate output.
print(f'\ntotal tokens: {FINAL_USAGE["total"]} items: {len(results)}')→ Diagnostic logging. Run summary after the batch.
Production LLM scripts that feel big are usually doing this. Small primitives stacked. Once each individual piece is reflexive — ask_json, retry-on-bad-output, batch, cost track — building a new script feels like assembly, not engineering.
ask_json.The shape stays the same. Add layers as the requirements need them.
Last code lesson. Five week-4 + earlier patterns into one script:
ask_json(prompt) wraps the LLM call (L24).label and confidence keys (L11-L13).import json
from pydantic_ai import Agent
FINAL_USAGE = {"total": 0}
def ask_json(prompt, max_attempts=2):
base = prompt + '\n\nReply with ONLY valid JSON. No prose around it.'
p = base
for _ in range(max_attempts):
result = Agent(model).run_sync(p)
FINAL_USAGE["total"] += result.usage().total_tokens
text = result.output.strip().strip("`").strip()
try:
return json.loads(text)
except json.JSONDecodeError as e:
p = base + f'\nThe previous response was not valid JSON ({e}). Try again — JSON only.'
raise ValueError("could not parse JSON after retries")
items = ["I love this!", "Total disaster.", "It was fine."]
results = []
for item in items:
parsed = ask_json(
f'Classify the sentiment of "{item}" as positive, negative, or neutral. '
f'Reply with JSON: {{"label": "<one of the three>", "confidence": <0-1 float>}}'
)
results.append({"input": item, **parsed})
print(f'{parsed["label"]} ({parsed["confidence"]:.2f}): {item}')
print(f'\ntotal tokens: {FINAL_USAGE["total"]} items: {len(results)}')Every line uses something from earlier in the track.
That's the point. No new tools. Just composition. This is what "writing AI scripts" looks like in practice — small primitives, deliberately stacked. Once each piece is reflexive, building a new script feels like assembly, not engineering.
No new technique. Composition of week-4 + earlier patterns into one shape that's representative of real LLM scripts.
def ask_json(prompt, max_attempts=2):
base = prompt + '\n\nReply with ONLY valid JSON. No prose around it.'→ Helper (L24). System-level instruction baked in once.
p = base
for _ in range(max_attempts):
result = Agent(model).run_sync(p)
FINAL_USAGE["total"] += result.usage().total_tokens→ Cost tracking (L27) inside the helper — every call site gets accumulation for free.
text = result.output.strip().strip("`").strip()
try:
return json.loads(text)
except json.JSONDecodeError as e:
p = base + f'\nThe previous response was not valid JSON ({e}). Try again — JSON only.'→ Retry on bad output (L25 + L12). Catch the parse error, tell the model what went wrong, retry.
raise ValueError("could not parse JSON after retries")→ Hard fail after cap. Don't loop forever; surface the bug.
for item in items:
parsed = ask_json(...)
results.append({"input": item, **parsed})→ Batch (L23). Loop over input, accumulate output.
print(f'\ntotal tokens: {FINAL_USAGE["total"]} items: {len(results)}')→ Diagnostic logging. Run summary after the batch.
Production LLM scripts that feel big are usually doing this. Small primitives stacked. Once each individual piece is reflexive — ask_json, retry-on-bad-output, batch, cost track — building a new script feels like assembly, not engineering.
ask_json.The shape stays the same. Add layers as the requirements need them.
Create a free account to get started. Paid plans unlock all tracks.