๐Ÿ’ป Exercise: Exercise: Normalise an LLM response

๐Ÿ“ Instructions

Different LLM providers return different JSON shapes. Write normalize_llm_response(raw) that maps an OpenAI-style raw response dict to one normalised dict with three keys: 'text' (the assistant message content as a string, or '' if it is null), 'tool_calls' (a list of {'name', 'args'} where args is parsed from the tool call's JSON arguments string), and 'stop_reason' (the choice's finish_reason). The raw shape is raw['choices'][0]: use ['message'] for content and tool_calls, and ['finish_reason'] for the stop reason. Standard library only.

๐Ÿงช Initial Code / Tests

๐Ÿ“„ evaluate.py
from unittest import TestCase
from exercise import normalize_llm_response


class Evaluate(TestCase):
    def test_extracts_text(self):
        raw = {'choices': [{'message': {'content': 'hello'}, 'finish_reason': 'stop'}]}
        self.assertEqual(normalize_llm_response(raw)['text'], 'hello')

    def test_null_content_becomes_empty_string(self):
        raw = {'choices': [{'message': {'content': None}, 'finish_reason': 'stop'}]}
        self.assertEqual(normalize_llm_response(raw)['text'], '')

    def test_maps_stop_reason(self):
        raw = {'choices': [{'message': {'content': 'x'}, 'finish_reason': 'length'}]}
        self.assertEqual(normalize_llm_response(raw)['stop_reason'], 'length')

    def test_parses_tool_call_arguments_to_dict(self):
        raw = {'choices': [{'message': {'content': None, 'tool_calls': [
            {'function': {'name': 'add', 'arguments': '{"a": 1, "b": 2}'}}]},
            'finish_reason': 'tool_calls'}]}
        out = normalize_llm_response(raw)
        self.assertEqual(out['tool_calls'], [{'name': 'add', 'args': {'a': 1, 'b': 2}}])

โœ… Solutions

๐Ÿ“„ exercise.py
import json


def normalize_llm_response(raw):
    choice = raw['choices'][0]
    msg = choice.get('message', {})
    tool_calls = []
    for tc in msg.get('tool_calls') or []:
        fn = tc.get('function', {})
        args = fn.get('arguments') or '{}'
        try:
            args = json.loads(args)
        except (json.JSONDecodeError, TypeError):
            args = {}
        tool_calls.append({'name': fn.get('name', ''), 'args': args})
    return {
        'text': msg.get('content') or '',
        'tool_calls': tool_calls,
        'stop_reason': choice.get('finish_reason'),
    }