AWS Bedrock with .NET: Converse API with Tools
The Problem: You Can't Trust Plain Text
You ask the AI for movie details. It responds with a beautifully written paragraph. Now your code needs to extract the title, the year, the director, and the rating from that paragraph. So you write a regex. It works — until the model rephrases something slightly differently, and suddenly your parser breaks silently in production.
This is the fragility of relying on plain text responses from AI models. The output looks right to a human, but it is unpredictable for a machine. Prompt engineering can nudge the model toward a format, but it cannot enforce one.
What is the Converse API with Tools?
AWS Bedrock's Converse API can be supercharged with tools—structured interfaces that let the model call functions, enforce output schemas, or interact with external systems. This enables developers to build AI agents that not only chat, but also return structured data, trigger workflows, or integrate with business logic.
In this article, you'll learn how to use the Converse API with tools to guarantee structured, reliable responses from generative AI models. We'll walk through a real-world example: extracting detailed movie information as JSON using a tool schema.
When and Why to Use Tools with Converse API
- Structured Output: Enforce JSON schemas for reliable, machine-readable responses.
- Function Calling: Let the model trigger business logic, database queries, or API calls.
- Data Extraction: Extract entities, summaries, or analytics from unstructured text.
- Workflow Automation: Build agents that can take actions, not just answer questions.
Use tools when you need more than just a chat—you want the AI to work as part of your system.
How: Building a Movie Info Extractor with Converse Tools
1. Prerequisites
- AWS account with Bedrock access enabled.
- AWS credentials with permissions for Bedrock Converse API and tools.
- .NET 8 or later SDK installed.
- NuGet package: AWSSDK.BedrockRuntime
2. Service Implementation Example
This example is broken into smaller steps for clarity. Let's walk through each part of the service.
Step 1: Service Setup
Define the service class with a Bedrock client and the model ID. The client is initialised with the target AWS region.
using Amazon.BedrockRuntime;
using Amazon.BedrockRuntime.Model;
using Amazon.Runtime.Documents;
using System.Text.Json;
public class BedrockWithConverseToolsService
{
private readonly AmazonBedrockRuntimeClient _client;
private const string ModelId = "anthropic.claude-3-haiku-20240307-v1:0";
public BedrockWithConverseToolsService()
{
_client = new AmazonBedrockRuntimeClient(Amazon.RegionEndpoint.USEast1);
}
}
Step 2: Sending the Request
Build the user message with the movie query, attach the tool configuration, and send the request to Bedrock. After receiving the response, locate the tool use block that contains the structured output.
public async Task<string> GetMovieDetailsAsJson(string movieQuery)
{
// Build the tool configuration - this GUARANTEES the output structure
var toolConfig = BuildToolConfiguration();
var userMessage = new Message
{
Role = "user",
Content = new List<ContentBlock>
{
new ContentBlock
{
Text = $"Provide detailed information about the movie '{movieQuery}'."
}
}
};
var converseRequest = new ConverseRequest
{
ModelId = ModelId,
Messages = new List<Message> { userMessage },
ToolConfig = toolConfig,
System = new List<SystemContentBlock>
{
new SystemContentBlock { Text = "You are a movie information expert" }
}
};
var response = await _client.ConverseAsync(converseRequest);
if (response?.Output?.Message?.Content == null)
return JsonSerializer.Serialize(new { error = "Invalid response from Bedrock" });
// Find the tool use response - in v4 it's accessed via ToolUse property
var toolUseBlock = response.Output.Message.Content
.FirstOrDefault(c => c.ToolUse != null);
if (toolUseBlock?.ToolUse == null)
return JsonSerializer.Serialize(new { error = "Model did not use the expected tool" });
// Extract the structured data from the tool input
return ExtractMovieInfoAsJson(toolUseBlock.ToolUse.Input);
}
Step 3: Defining the Tool Schema
The tool schema is the key piece — it tells the model exactly what shape the output must take. We define a movies array with required fields for each entry. Setting ToolChoice to the specific tool name forces the model to always use it.
private ToolConfiguration BuildToolConfiguration()
{
// Define the JSON schema for the tool input - this is what the model MUST follow
// Root must be object type, with movies array as a property
var toolInputSchema = new
{
type = "object",
properties = new
{
movies = new
{
type = "array",
description = "Array of movie information objects",
items = new
{
type = "object",
properties = new
{
title = new { type = "string", description = "The movie title" },
year = new { type = "integer", description = "The release year" },
category = new { type = "string", description = "Category to which this title belongs to. Like a TV show or Movie" },
directors = new { type = "array", description = "Array of director names", items = new { type = "string", description = "A director name" } },
actors = new { type = "array", description = "Array of actor names", items = new { type = "string", description = "An actor name" } },
plot = new { type = "string", description = "Brief plot summary" },
genre = new { type = "string", description = "Movie genre" },
rating = new { type = "string", description = "IMDb rating or similar" }
},
required = new[] { "title", "year", "category", "directors", "actors", "plot", "genre", "rating" },
additionalProperties = false
}
}
},
required = new[] { "movies" },
additionalProperties = false
};
var tool = new Tool
{
ToolSpec = new ToolSpecification
{
Name = "return_movie_info",
Description = "Return structured movie information as an array to handle multiple matching movies",
InputSchema = new ToolInputSchema
{
Json = Document.FromObject(toolInputSchema)
}
}
};
return new ToolConfiguration
{
Tools = new List<Tool> { tool },
ToolChoice = new ToolChoice
{
Tool = new SpecificToolChoice { Name = "return_movie_info" }
}
};
}
Step 4: Extracting the Structured Output
Once the model responds, we parse the Document object from the tool input into a typed list of movies and serialise it as formatted JSON.
/// <summary>
/// Extracts movie info from the tool input Document and returns as formatted JSON.
/// The schema GUARANTEES this will be an object with a movies array.
/// </summary>
private string ExtractMovieInfoAsJson(Document toolInput)
{
try
{
var inputDict = toolInput.AsDictionary();
if (!inputDict.ContainsKey("movies"))
return JsonSerializer.Serialize(new { error = "Expected 'movies' property in response" });
var moviesDoc = inputDict["movies"];
if (!moviesDoc.IsList())
return JsonSerializer.Serialize(new { error = "Expected 'movies' to be an array" });
var movies = new List<object>();
foreach (var movieDoc in moviesDoc.AsList())
{
var movieDict = movieDoc.AsDictionary();
var directors = new List<string>();
if (movieDict.ContainsKey("directors") && movieDict["directors"].IsList())
directors = movieDict["directors"].AsList().Select(doc => doc.AsString()).ToList();
var actors = new List<string>();
if (movieDict.ContainsKey("actors") && movieDict["actors"].IsList())
actors = movieDict["actors"].AsList().Select(doc => doc.AsString()).ToList();
movies.Add(new
{
title = movieDict.ContainsKey("title") ? movieDict["title"].AsString() : "",
year = movieDict.ContainsKey("year") ? movieDict["year"].AsInt() : 0,
category = movieDict.ContainsKey("category") ? movieDict["category"].AsString() : "",
directors,
actors,
plot = movieDict.ContainsKey("plot") ? movieDict["plot"].AsString() : "",
genre = movieDict.ContainsKey("genre") ? movieDict["genre"].AsString() : "",
rating = movieDict.ContainsKey("rating") ? movieDict["rating"].AsString() : ""
});
}
return JsonSerializer.Serialize(movies, new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = $"Failed to extract movie info: {ex.Message}" });
}
}
3. How the Example Works
- Tool Schema: Defines exactly what fields the model must return for each movie.
- System Prompt: Sets the AI’s persona as a movie expert.
- Tool Use: The model is required to use the tool and return structured data.
- Extraction: The code parses the tool output and returns formatted JSON.
4. Example Use Case: Movie Info Extraction
Suppose a user wants to get details about "Inception". The service will return a structured JSON array with all the required fields, ready for use in your app or workflow.
var service = new BedrockWithConverseToolsService();
string movieJson = await service.GetMovieDetailsAsJson("Inception");
Console.WriteLine(movieJson);
Running the above produces the following structured JSON output:
=== Movie Information (Structured JSON) ===
[
{
"title": "Inception",
"year": 2010,
"category": "Movie",
"directors": [
"Christopher Nolan"
],
"actors": [
"Leonardo DiCaprio",
"Ellen Page",
"Joseph Gordon-Levitt",
"Tom Hardy"
],
"plot": "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea in the mind of a CEO.",
"genre": "Action, Adventure, Sci-Fi",
"rating": "8.2"
}
]
Summary
- What: Converse API with tools lets you build AI agents that return structured, reliable data—not just text.
- When: Use tools for data extraction, workflow automation, and any scenario where you need more than chat.
- How: Define tool schemas, enforce output structure, and integrate with your .NET apps using the AWS SDK.
For more details and the full code, see the GitHub example.
References & Further Reading
For reference, the code repository being discussed is available at github: https://github.com/ajaysskumar/ai-playground/blob/main/AwsBedrockExamples/Services/BedrockWithConverseToolsService.cs
Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com
