kiln_ai.synthetic_user.role_swap
Role-swap eval-frame conversation roles into LLM-frame roles.
Chat models are trained to generate assistant-labeled responses, so we
flip the eval-frame labels before the call: the LLM's "assistant" turn IS
the synthetic user.
Eval frame: LLM frame (post-swap):
user = synthetic user assistant = synthetic user
assistant = target agent user = target agent
The driver keeps only user and assistant turns upstream, so a system or tool turn reaching here is an internal invariant violation — fail loud rather than silently drop.
1"""Role-swap eval-frame conversation roles into LLM-frame roles. 2 3Chat models are trained to generate `assistant`-labeled responses, so we 4flip the eval-frame labels before the call: the LLM's "assistant" turn IS 5the synthetic user. 6 7 Eval frame: LLM frame (post-swap): 8 user = synthetic user assistant = synthetic user 9 assistant = target agent user = target agent 10 11The driver keeps only user and assistant turns upstream, so a system or tool 12turn reaching here is an internal invariant violation — fail loud rather 13than silently drop. 14""" 15 16from kiln_ai.utils.open_ai_types import ( 17 ChatCompletionAssistantMessageParamWrapper, 18 ChatCompletionMessageParam, 19 ChatCompletionUserMessageParam, 20) 21 22 23def role_swap( 24 conversation: list[ChatCompletionMessageParam], 25) -> list[ChatCompletionMessageParam]: 26 """Flip eval-frame user/assistant labels into LLM-frame labels. 27 28 Only `user` and `assistant` are handled. The driver is expected to 29 have filtered out other roles before calling this. 30 """ 31 result: list[ChatCompletionMessageParam] = [] 32 for msg in conversation: 33 role = msg["role"] 34 if role not in ("user", "assistant"): 35 raise ValueError( 36 f"role_swap received unsupported role {role!r}; " 37 "the driver should have filtered it" 38 ) 39 # The TypedDict union allows non-string content for multimodal / 40 # tool turns, but the synthetic user only ever sees plain text from 41 # the target. Narrowing here lets us assign into the swapped wrapper 42 # type without a cast. 43 content = msg["content"] 44 if not isinstance(content, str): 45 raise ValueError( 46 f"role_swap requires string content for role {role!r}; " 47 f"got {type(content).__name__}" 48 ) 49 if role == "user": 50 assistant_msg: ChatCompletionAssistantMessageParamWrapper = { 51 "role": "assistant", 52 "content": content, 53 } 54 result.append(assistant_msg) 55 else: # role == "assistant" 56 user_msg: ChatCompletionUserMessageParam = { 57 "role": "user", 58 "content": content, 59 } 60 result.append(user_msg) 61 return result
def
role_swap( conversation: list[typing.Union[openai.types.chat.chat_completion_developer_message_param.ChatCompletionDeveloperMessageParam, openai.types.chat.chat_completion_system_message_param.ChatCompletionSystemMessageParam, openai.types.chat.chat_completion_user_message_param.ChatCompletionUserMessageParam, kiln_ai.utils.open_ai_types.ChatCompletionAssistantMessageParamWrapper, kiln_ai.utils.open_ai_types.ChatCompletionToolMessageParamWrapper, openai.types.chat.chat_completion_function_message_param.ChatCompletionFunctionMessageParam]]) -> list[typing.Union[openai.types.chat.chat_completion_developer_message_param.ChatCompletionDeveloperMessageParam, openai.types.chat.chat_completion_system_message_param.ChatCompletionSystemMessageParam, openai.types.chat.chat_completion_user_message_param.ChatCompletionUserMessageParam, kiln_ai.utils.open_ai_types.ChatCompletionAssistantMessageParamWrapper, kiln_ai.utils.open_ai_types.ChatCompletionToolMessageParamWrapper, openai.types.chat.chat_completion_function_message_param.ChatCompletionFunctionMessageParam]]:
24def role_swap( 25 conversation: list[ChatCompletionMessageParam], 26) -> list[ChatCompletionMessageParam]: 27 """Flip eval-frame user/assistant labels into LLM-frame labels. 28 29 Only `user` and `assistant` are handled. The driver is expected to 30 have filtered out other roles before calling this. 31 """ 32 result: list[ChatCompletionMessageParam] = [] 33 for msg in conversation: 34 role = msg["role"] 35 if role not in ("user", "assistant"): 36 raise ValueError( 37 f"role_swap received unsupported role {role!r}; " 38 "the driver should have filtered it" 39 ) 40 # The TypedDict union allows non-string content for multimodal / 41 # tool turns, but the synthetic user only ever sees plain text from 42 # the target. Narrowing here lets us assign into the swapped wrapper 43 # type without a cast. 44 content = msg["content"] 45 if not isinstance(content, str): 46 raise ValueError( 47 f"role_swap requires string content for role {role!r}; " 48 f"got {type(content).__name__}" 49 ) 50 if role == "user": 51 assistant_msg: ChatCompletionAssistantMessageParamWrapper = { 52 "role": "assistant", 53 "content": content, 54 } 55 result.append(assistant_msg) 56 else: # role == "assistant" 57 user_msg: ChatCompletionUserMessageParam = { 58 "role": "user", 59 "content": content, 60 } 61 result.append(user_msg) 62 return result
Flip eval-frame user/assistant labels into LLM-frame labels.
Only user and assistant are handled. The driver is expected to
have filtered out other roles before calling this.