Skip to content

Structured Output from Google ADK agents

One of the essential points of communicating and steering an Angular client through a ‘Human in the Loop’ (HITL) interface is having the agent deliver a structured output. In the case of this project, this means a JSON object. 

This turned out to me more problematic than initially thought. In many examples, try to use the prompt wording to decide how the output should look. For example, Three agents were set up a coordinator, a get parameters agent and a computation agent. When the status was set to ‘use’, the get parameters agent should be called and it should, without further processing, give a JSON object:

{status: “GetParameters”, task: “Operation”}

In the end, after a journey through several types of prompts and parameters, a final version was found that gave the desired result. Possibly the useful lesson here (and not necessarily true to the use of agents) is that the sub-agent being worked on was a ‘final’ agent and nothing should happen after. It should just yield the desired JSON result and not go further. The agent structure/philosphy is used to get to this agent in the first place. But to ensure that nothing should happen after, two build commands are critical:

.disallowTransferToParent(true)
.disallowTransferToPeers(true)

Attempts within the prompt to say that this is a final agent were not successful.

The final agent that worked is as follows:

📘
agent.ts
	private static LlmAgent setupparameters = LlmAgent.builder()
			.name("Setup Parameters Agent")
			.model(Constants.MODEL)
			.description("The input is a task keyword. The output is the same task keyword and the status 'GetParameters' ")
			.instruction(
					"You are a terminal response generator. " +
						    "1. Identify the task field of the input JSON object, for example 'do operation'" +
						    "2. Set 'task' field of the output to the input task field (in the example, 'do operation'" +
						    "3. Set 'status' to 'GetParameters'. " 
					)
			.disallowTransferToParent(true)
		    .disallowTransferToPeers(true)
			.build();

Initial experiment

The following is the starting point of trying to find the right definition. The following is the agent code that served as the starting point:

🌐
filename.html
private static LlmAgent setupparameters = LlmAgent.builder()
			.name("Setup Parameters Agent")
			.model(Constants.MODEL)
			.description("The input is a task keyword. The output is the same task keyword and the status 'GetParameters' ")
			.instruction(
					"Critical: Respond with the JSON object." +
                     "{status: "GetParameters", task: 'Operation'}" +
                      "do no further processing." 
					)
			.build();

With the prompt being: 

Critical: Respond with the JSON object.
{status: “GetParameters”, task: “Operation”}
do no further processing.

Answers, with function calls from the other agents would give answers such as (the answer differs on different runs):

{“userId”: “user3”, “sessionId”: “session21”, “status”: “new”, “task”: “addition with status new”, “result”: 5}

or 

{“status”: “use”, “result”: 12.0}

These indicate that processing did not stop and the other agents were called.

Critical: Respond with the JSON object

the fields are: ‘status’ which is ‘GetParameters’ 

and ‘task’ which is the same as the input task keyword.

do no further processing.

Sometimes the expected answer would come,

{“status”: “GetParameters”, “task”: “addition with status new”}

But on other calls: 

I’m sorry, I cannot fulfill this request. The available tools lack the desired functionality.

📘
setupparameters.ts
	private static LlmAgent setupparameters = LlmAgent.builder()
			.name("Setup Parameters Agent")
			.model(Constants.MODEL)
			.description("The input is a task keyword. The output is the same task keyword and the status 'GetParameters' ")
			.instruction(
	                 "Critical: Respond with the  JSON object with status as 'GetParameters' "
	                 + "and the task field should be the input task field" + 
					"do no further processing.")
			.disallowTransferToParent(true)
		    .disallowTransferToPeers(true)
			.outputSchema(Schema.builder()
				    .type("OBJECT")
				    .properties(Map.of(
				        "status", Schema.builder().type("STRING").build(),
				        "task", Schema.builder().type("STRING").build(),
				        "result", Schema.builder().type("STRING").build()
				    ))
				    .required(List.of("status", "task"))
				    .build() // <--- ADD THIS CALL
				)
			.build();

Prompt engineering

first attempt:

This seems to solve the JSON output problem, but the task is a task that is to be performed by the agent:

{
“status”:”GetParameters”,
“task”:”For context:[Coordinator] `transfer_to_agent` tool returned result: {}”

Second Attempt

“You are a terminal response generator. ” +
“1. Look at the conversation history for the original user task (e.g., ‘addition’). ” +
“2. Ignore any system messages about ‘transfer_to_agent’ or ‘tool results’. ” +
“3. Set the ‘task’ field in your JSON output to ONLY the name of the operation. ” +
“4. Set ‘status’ to ‘GetParameters’. ” +
“5. Do not include any technical context or metadata in the output.”

This got rid of some instructions, but still the result was 

{“status”:”GetParameters”,”task”:”Setup Parameters Agent”}

third attempt

“You are a terminal response generator. ” +
“1. Look at the conversation history for the original user task (e.g., ‘addition’). ” +
“2. Ignore any system messages about ‘transfer_to_agent’ or ‘tool results’. ” +
“3. Set the ‘task’ field in your JSON output the same as in input task field. ” +
“4. Set ‘status’ to ‘GetParameters’. ” +
“5. Do not include any technical context or metadata in the output.”

This took the ‘addition’ as the default:

{“status”: “GetParameters”, “task”: “addition”}

fourth attempt

“You are a terminal response generator. ” +
“1. Look at the conversation history for the original user task (the task field in the input JSON object ” +
“2. Ignore any system messages about ‘transfer_to_agent’ or ‘tool results’. ” +
“3. Set the ‘task’ field in your JSON output the same as in input task field. ” +
“4. Set ‘status’ to ‘GetParameters’. ” +
“5. Do not include any technical context or metadata in the output.”

not much better…. still didn’t get the task parameter

{“status”:”GetParameters”,”task”:”Set Parameters”}

 

Working attempt

In this attempt, more explicit instructions about the task field were given. It seemed to help to give an example. 

“You are a terminal response generator. ” +
“1. Identify the task field of the input JSON object, for example ‘do operation'” +
“2. Ignore any system messages about ‘transfer_to_agent’ or ‘tool results’. ” +
“3. Set ‘task’ field of the output to the input task field (in the example, ‘do operation'” +
“4. Set ‘status’ to ‘GetParameters’. ” +
“5. Do not include any technical context or metadata in the output.”

This gave the proper reply:

{“status”:”GetParameters”,”task”:”subtract two numbers”}

Triming down the instruction to:

“You are a terminal response generator. ” +
“1. Identify the task field of the input JSON object, for example ‘do operation'” +
“2. Set ‘task’ field of the output to the input task field (in the example, ‘do operation'” +
“3. Set ‘status’ to ‘GetParameters’. ”

seems to also work. The not continuing further may be attributed to the commands in the build:

.disallowTransferToParent(true)
.disallowTransferToPeers(true)

 

Is a schema needed`

Now, trimming down the agent with the minimal prompt and taking away the schema definition: 

private static LlmAgent setupparameters = LlmAgent.builder()
.name(“Setup Parameters Agent”)
.model(Constants.MODEL)
.description(“The input is a task keyword. The output is the same task keyword and the status ‘GetParameters’ “)
.instruction(
“You are a terminal response generator. ” +
“1. Identify the task field of the input JSON object, for example ‘do operation'” +
“2. Set ‘task’ field of the output to the input task field (in the example, ‘do operation'” +
“3. Set ‘status’ to ‘GetParameters’. ”
)
.disallowTransferToParent(true)
.disallowTransferToPeers(true)
.build();

Actually also seems to work.

What seems to be critical, since this is supposed to give the final answer, are the parameters:

.disallowTransferToParent(true)
.disallowTransferToPeers(true)

Without these, further processing occurs and the output gets scrambled.

Leave a Reply

Your email address will not be published. Required fields are marked *

en_USEnglish