Note
Go to the end to download the full example code.
Structured Output
In this tutorial, we will be building a simple agent that outputs structured data in JSON dictionary format using the agentscope.parsers module.
from agentscope.models import ModelResponse
Defining the Parser
from agentscope.parsers import MarkdownJsonDictParser
parser = MarkdownJsonDictParser(
content_hint='{"thought": "What you thought", "speak": "What you speak to the user"}',
required_keys=["thought", "speak"],
)
The parser will generate a format instruction according to your input. You can use the format_instruction property to in your prompt to guide LLM to generate the desired output.
print(parser.format_instruction)
Respond a JSON dictionary in a markdown's fenced code block as follows:
```json
{"thought": "What you thought", "speak": "What you speak to the user"}
```
Parsing the Output
When receiving output from LLM, use parse method to extract the structured data. It takes an object of agentscope.models.ModelResponse as input, parses the value of the text field, and returns a parsed dictionary in the parsed field.
dummy_response = ModelResponse(
text="""```json
{
"thought": "I should greet the user",
"speak": "Hi! How can I help you?"
}
```""",
)
print(f"parsed field before parsing: {dummy_response.parsed}")
parsed_response = parser.parse(dummy_response)
print(f"parsed field after parsing: {parsed_response.parsed}")
print(type(parsed_response.parsed))
parsed field before parsing: None
parsed field after parsing: {'thought': 'I should greet the user', 'speak': 'Hi! How can I help you?'}
<class 'dict'>
Error Handling
If the LLM output does not match the expected format, the parser will raise an error with a detailed message. So developers can present the error message to LLM to guide it to correct the output.
error_response = ModelResponse(
text="""```json
{
"thought": "I should greet the user"
}
```""",
)
try:
parsed_response = parser.parse(error_response)
except Exception as e:
print(e)
RequiredFieldNotFoundError: Missing required field speak in the JSON dictionary object.
Advanced Usage
More Complex Content
Asking LLM to directly generate a JSON dictionary can be challenging, especially when the JSON content is complex (e.g. code snippets, nested structures). In this case, you can use more advanced parsers to guide LLM to generate the desired output. Here is an example of a more complex parser that handle code snippets.
from agentscope.parsers import RegexTaggedContentParser
parser = RegexTaggedContentParser(
format_instruction="""Response in the following format:
<thought>what you thought</thought>
<number>A random number here</number>
<code>your python code here</code>
""",
try_parse_json=True, # Try to parse the each value as a JSON object
required_keys=[
"thought",
"number",
"code",
], # Required keys in the parsed dictionary
)
print(parser.format_instruction)
Response in the following format:
<thought>what you thought</thought>
<number>A random number here</number>
<code>your python code here</code>
The RegexTaggedContentParser uses regular expressions to match the tagged content in the text and return the parsed dictionary.
Note
The parsed output of RegexTaggedContentParser is a dictionary, which means the required keys should be unique.
You can also change the regular expression pattern by settings the tagged_content_pattern parameter when initializing the parser.
import json
dummy_response = ModelResponse(
text="""<thought>Print the current date</thought>
<number>42</number>
<code>import datetime
print(datetime.datetime.now())
</code>
""",
)
parsed_response = parser.parse(dummy_response)
print("The type of parsed response: ", type(parsed_response.parsed))
print("The type of the number: ", type(parsed_response.parsed["number"]))
print(json.dumps(parsed_response.parsed, indent=4))
The type of parsed response: <class 'dict'>
The type of the number: <class 'int'>
{
"thought": "Print the current date",
"number": 42,
"code": "import datetime\nprint(datetime.datetime.now())\n"
}
Auto Post-Processing
Within the parsed dictionary, different keys may require different post-processing steps. For example, in a werewolf game, the LLM is playing the role of a seer, and the output should contain the following keys:
thought: The seer’s thoughts
speak: The seer’s speech
use_ability: A boolean value indicating whether the seer should use its ability
In this case, the thought and speak contents should be stored in the agent’s memory to ensure the consistency of the agent’s behavior. The speak content should be spoken out to the user. The use_ability key should be accessed outside the agent easily to determine the game flow.
AgentScope supports automatic post-processing of the parsed dictionary by providing the following parameters when initializing the parser.
keys_to_memory: key(s) that should be stored in the agent’s memory
keys_to_content: key(s) that should be spoken out
keys_to_metadata: key(s) that should be stored in the metadata field of the agent’s response message
Note
If a string is provided, the parser will extract the value of the given key from the parsed dictionary. If a list of strings is provided, a sub-dictionary will be created with the given keys.
Here is an example of using the MarkdownJsonDictParser to automatically post-process the parsed dictionary.
parser = MarkdownJsonDictParser(
content_hint='{"thought": "what you thought", "speak": "what you speak", "use_ability": "whether to use the ability"}',
keys_to_memory=["thought", "speak"],
keys_to_content="speak",
keys_to_metadata="use_ability",
)
dummy_response = ModelResponse(
text="""```json
{
"thought": "I should ...",
"speak": "I will not use my ability",
"use_ability": false
}```
""",
)
parsed_response = parser.parse(dummy_response)
print("The parsed response: ", parsed_response.parsed)
print("To memory", parser.to_memory(parsed_response.parsed))
print("To message content: ", parser.to_content(parsed_response.parsed))
print("To message metadata: ", parser.to_metadata(parsed_response.parsed))
The parsed response: {'thought': 'I should ...', 'speak': 'I will not use my ability', 'use_ability': False}
To memory {'thought': 'I should ...', 'speak': 'I will not use my ability'}
To message content: I will not use my ability
To message metadata: False
Here we show how to create an agent that can automatically post-process the parsed dictionary by the following core steps in the reply method.
Put the format instruction in prompt to guide LLM to generate the desired output
Parse the LLM response
Post-process the parsed dictionary by using the to_memory, to_content, and to_metadata methods
Tip
By changing different parsers, the agent can adapt to different scenarios and generate structured output in various formats.
from agentscope.models import DashScopeChatWrapper
from agentscope.agents import AgentBase
from agentscope.message import Msg
class Agent(AgentBase):
def __init__(self):
self.name = "Alice"
super().__init__(name=self.name)
self.sys_prompt = f"You're a helpful assistant named {self.name}."
self.model = DashScopeChatWrapper(
config_name="_",
model_name="qwen-max",
)
self.parser = MarkdownJsonDictParser(
content_hint='{"thought": "what you thought", "speak": "what you speak", "use_ability": "whether to use the ability"}',
keys_to_memory=["thought", "speak"],
keys_to_content="speak",
keys_to_metadata="use_ability",
)
self.memory.add(Msg("system", self.sys_prompt, "system"))
def reply(self, msg):
self.memory.add(msg)
prompt = self.model.format(
self.memory.get_memory(),
# Instruct the model to respond in the required format
Msg("system", self.parser.format_instruction, "system"),
)
response = self.model(prompt)
parsed_response = self.parser.parse(response)
self.memory.add(
Msg(
name=self.name,
content=self.parser.to_memory(parsed_response.parsed),
role="assistant",
),
)
return Msg(
name=self.name,
content=self.parser.to_content(parsed_response.parsed),
role="assistant",
metadata=self.parser.to_metadata(parsed_response.parsed),
)
Total running time of the script: (0 minutes 0.004 seconds)