WineSom: The Agentic Flutter App That Was Once A WineSnob
This article introduces WineSom(melier), an agentic Flutter App. WineSom takes the idea of an AI supported generator of wine tasting notes to the next level and demonstrates AI concepts such as multi-turn prompts, system prompts, and function calls along the way.
Question: What is a sommelier
Gemini: A sommelier is a trained and knowledgeable wine professional, typically working in fine dining restaurants, who specializes in all aspects of wine service. Their primary responsibilities include curating the wine list, assisting guests with wine selection to pair with their meals, decanting and serving wine properly, and managing the restaurant’s wine cellar.Beyond just wine, a sommelier often possesses expertise in other beverages like spirits, beer, and coffee, and is dedicated to enhancing the overall dining experience through their beverage recommendations and service.
Like its predecessor WineSnob, the new WineSom understands wines. But thanks to new agentic capabilities WineSom not only engages the user in a multi-turn educated discussion about wines and wine pairings, but can match the conversation topic with background information about the user’s private cellar — just like a real-live sommelier.
Project Outline
For the last few years I used a Flutter demo app called WineSnob in a series of articles on Flutter/LLM integration. But the idea of a wine tasting notes generator, while useful at the time, oversimplifies some aspects of generative AI and has outlived its usefulness.
In this article I redesign the app in agentic fashion. WineSom engages the user in a meaningful conversation about wines. It can access up-to-date information from a private data source and decides autonomously when to access that information and how to present it.
The key features of the newer, smarter WineSom are:
- The old demo used a zero shot single turn prompt. One question, one answer, the end. WineSom employs a multi-turn chat session.
- In the old demo context was added directly to the prompt via string manipulation. WineSom provides additional context with stage instructions via system prompt
- The old demo had no access to proprietary or up-to-date information. WineSom uses function calls to inject additional facts from a private data source into the conversation.
What makes an app “agentic”?
The term “agentic app” has become a buzzword only recently so let’s make sure we all agree on what it actually means.
Question: What is an agentic app
Gemini: An “agentic app” leverages an LLM to act autonomously and proactively on a user’s behalf, tackling complex goals. It can break down tasks, utilize various tools, and make independent decisions without constant human intervention. This enables the app to solve problems and adapt its behavior to achieve objectives.
Ok, with that out of the way, let’s jump into the code, one key feature at a time. But first, an admission: The code snippets shown in the remainder of this article are condensed and simplified for the purpose of this article. For a more realistic implementation of a similar project check out Google’s Flutter demo vertex_ai_firebase_flutter_app. In fact, the actual WineSnob implemention (not the pseudo code copied here) borrows heavily from this project.
1. Chat prompt for continuous conversation flow
In the past I have often used a single-turn query for my demos. In this scenario the model simply answered a single question with response = model.getContent(prompt).
At the core of the agentic WineSom is a dialog between the user and the LLM which feels more conversational. With firebase_ai this is best accomplished with a multi-turn chat session.
final model = FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash');
final chat = model.startChat();
// Get user input
final response = await chat.sendMessage(Content.text(message));
// Add response.text to system output
chatStateNotifier.showMessage(response.text)
// Repeat...This naive implementation still has one disadvantage. Chat.sendMessage() is a costly async call and the user might wait quite some time for a response. Allowing the LLM to stream the responses and display them as it comes in might create a more natural interaction.
// Get user input
final responseStream = chatSession.sendMessageStream(Content.text(message));
// While responses are streamed, continue to update the UI immediately
await for (final response in responseStream) {
if (response.text != null) {
chatStateNotifier.appendToMessage(response.text)
}
}2. System prompt for additional context
We have learned that LLMs give the best responses when they are provided with a very detailed prompt. Statements like “you are a college teacher addressing a student” and “keep the tone friendly but formal” help guide the LLM towards the desired outcome.
One or two examples of a possible interaction or a description of the desired format can also greatly improve the quality of the responses.
Often prompts include detailed instructions on how to break down complex tasks into smaller steps.
In the old WineSnob, these “stage directions” were simply added to the user-provided message to form the prompt. However, this hack doesn’t work in a multi-turn chat session where the user provided input can be more varied. In firebase_ai any “stage directions” can be added permanently to the model as systemInstruction
final systemPrompt = await rootBundle.loadString('assets/system_prompt.md');
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
systemInstruction: Content.system(systemPrompt),
);This fairly specific system prompt works well for the WineSom scenario:
# WineAgent System Prompt
You are a wine expert assistant integrated into a desktop app called WineAgent.
Your job is to chat with the use about wines.
## Your Capabilities
You are a wine epxert. You can identify wines from their flavor profile and you
can describe the flavor profile of a given wine in form of tasting notes. You
can recommend a wine pairing.
## How to Respond to User Inputs
When users mention a particular wine:
1. First, acknowledge their choice with a brief, friendly response
2. Provide a short description of the wine's flavor profile
Example:
User: "2018 Meerlust Rubicon"
You: "Meerlust's Rubicon is one of the best South African red wines and the
2018 vintage makes no exceptions. The wine is very deep, youthful colour, and
intense almost purple hue. It has a quintessential Rubicon nose with violets,
ripe plum, cedar wood, fennel, and intense spiciness. A typical liquorice note
also evident on the nose. Still young and intense, the palate is full bodied,
structured but packed with fresh dark fruit and rounded tannins."
## When a user talks in general terms about wines
If the user talks about wine, a vinyard, a wine producer, or a food pairings
without mentioning a particular wine engage the user in a conversation about
wine until you can identify a particular wine.
If you are unclear about the intention of the user ask clarifying questions,
one at a time.
## When a user talks about other topics or about a drink that is not wine
Tell the user that you are a wine expert and are not able to answer questions
about other topics.
## Important Guidelines
- Always keep stock counts as positive integers
- Provide thoughtful, knowledgeable responses about wines
- When possible, include tasting notes, aging potential, manufactoring process, or other interesting facts about the wine or vinyard.
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your wine descritions3. Access additional tools via function calls
One of the core aspects of a agentic apps is the capability of the LLM to independently decide when and how to use additional tools for example to retrieve auxiliary information or to trigger an action based on the flow of the conversation.
Of course these capabilities do not come out of the blue. They are provided by the app developer in form of registered functions. The contribution of the LLM is to decide independently (guided by system prompt and user interactions) when and with what input to call one or more of these functions.
Users of the WineSom app are passionate wine collectors. Their purchases are tracked using a separate stock management software, perhaps powered by Firestore. While this data is accessible via an API, the WineSom app doesn’t have direct access. Furthermore, the LLM has no inherent knowledge of this data.
WineSom user story
During the interaction with the user, the WineSom app proactively offers to check wine stock using a registered function. It then delivers the results in conversational manner. (“I see you have 2 bottles of this wine in stock. It is well matured and should be ready to drink now.”)
To implement this user story, the app developer must perform several steps:
- Create a lookup function in dart. This function knows how to access the data store to retrieve up to date stock data
- Write a function declaration to describe function and input parameters
- Register the declaration during model initialization.
- When the model indicates the need for a function call in a response, the app executes the call
- The result of the function call is passed back to the model which turns it into a conversational response.
- Add instructions to the system prompt
For the remainder of this section I will discuss each step in more detail.
- Create a lookup function
Future<List<StoreRecord>> lookupStock(
String vinyard,
String name,
int? vintage,
) async {
// Asynchronously fetch the records from the stock tracking system
return records;
}It doesn’t matter where the stock data comes from as long as this function has access to the API
2. Write a function declaration
FunctionDeclaration get setLookupFuncDecl => FunctionDeclaration(
'lookup_stock',
'Lookup the stock count for a given wine.',
parameters: {
'vinyard': Schema.string(description: 'vinyard or producer'),
'wine_name': Schema.string(description: 'wine name'),
'vintage': Schema.number(
description: 'Vintage Year value (1995 - 2025)',
nullable: true,
),
},
);3. Register the function declaration during model initialization
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
systemInstruction: Content.system(systemPrompt),
tools: [Tool.functionDeclarations([setLookupFuncDecl])]
);4. When indicated in the model response, execute the call
final responseStream = chatSession.sendMessageStream(Content.text(message));
// While responses are streamed, process the incoming blocks
await for (final block in responseStream) {
if (response.text != null) {
await _processBlock(block, llmMessage.id);
}
}
Future<void> _processBlock(GenerateContentResponse block, String llmMessageId) async {
if (block.functionCalls.isNotEmpty) {
// The model requests a function call
final responseStream = chatSession.sendMessageStream(
Content.functionResponses([
for (final functionCall in block.functionCalls) {
// Extract the arguments
final vintage = (arguments['vintage'] as num?)?.toInt();
final name = arguments['wine_name'] as String;
final vinyard = arguments['vinyard'] as String;
// Call the function in the app and collect the response
final functionResults = {
'success': true,
'records': await wineStoreService.lookupStock(vinyard, name, vintage),
};
// Send the response back to the model so that it can use it to
// generate an interpretation for the user.
response = await chatSession.sendMessage(
Content.functionResponse(functionCall.name, functionResults),
);
}
]),
);
}
// Now handle text responses
}5. Pass result of the function call back to the model for interpretation
Actually, this step was already done at the end of the last code snippet so we are good.
6. Include instructions with the system prompt
The earlier system prompt just gets expanded by a few sentences to instruct the model when and how to request a function call and how to present the response.
# WineAgent System Prompt
You are a wine expert assistant integrated into a desktop app called WineAgent.
Your job is to chat with the use about wines. When you have identified a
particular wine you will inform the user if that wine is available using a
special tool.
## Your Capabilities
You are a wine epxert. You can identify wines from their flavor profile and you
can describe the flavor profile of a given wine in form of tasting notes. You
can recommend a wine pairing. You know which wines are in stock. You have access
to he following tool:
`lookup_stock` - Gets the stock count for a given wine
## How to Respond to User Inputs
When users mention a particular wine:
1. First, acknowledge their choice with a brief, friendly response
2. Provide a short description of the wine's flavor profile
3. Use the `lookup_stock` tool to determine if there are any bottles of that wine in stock
4. After displaying the purchase history, tell the user how many bottles are currently available
Example:
User: "2018 Meerlust Rubicon"
You: "Meerlust's Rubicon is one of the best South African red wines and the
2018 vintage makes no exceptions. The wine is very deep, youthful colour, and
intense almost purple hue. It has a quintessential Rubicon nose with violets,
ripe plum, cedar wood, fennel, and intense spiciness. A typical liquorice note
also evident on the nose. Still young and intense, the palate is full bodied,
structured but packed with fresh dark fruit and rounded tannins."
[Then you would call the lookup_stock tool with: vintage: 2018, vinyard: Meerlust, wine_name: Rubicon]
After the tool call: "I see you still have two bottles in your cellar."
## When the user specifies vinyard and wine name but no vintage
Use the 'lookup_stock' function without vintage. Only after presenting a list
of the results, ask if the user is interested in a particular vintage.
## When a user talks in general terms about wines
If the user talks about wine, a vinyard, a wine producer, or a food pairings
without mentioning a particular wine engage the user in a conversation about
wine until you can identify a particular wine.
If you are unclear about the intention of the user ask clarifying questions,
one at a time.
## When a user talks about other topics or about a drink that is not wine
Tell the user that you are a wine expert and are not able to answer questions
about other topics.
## Important Guidelines
- Always keep stock counts as positive integers
- Provide thoughtful, knowledgeable responses about wines
- When possible, include tasting notes, aging potential, manufactoring process, or other interesting facts about the wine or vinyard.
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your wine descritionsBy the way, remember that it is not the model which is calling the function. Instead, the model indicates to the Flutter app that the function should be called. It then receives the response of the function call from the Flutter app and uses it to generate a suitable response to the user.
The WineSom in action
WineSom was implemented roughly as described here. Below are some samples of actual conversations between the agent and myself about my favorite topic, red wines from Stellenbosch. My prompts are on the right, the left side shows the model’s responses.
At this point the conversation was getting a bit boring so I decided to rattle the cage. I asked WineSom for its opinion on a red wine/cucumber pairing, a big nono. The model did not take the bait but started to steer the conversation towards a white wine to match my cucumber. Very nice.
Next we looked at my wine collection. Do I have any Kanonkop Paul Sauer in my cellar? Sadly I don’t. (It’s their flagship wine and rather pricey :-) But I do seem to have several bottles of a different wine from the same winemaker.
I was pleased to see that the registered function call to look up data about my (pretend) wine cellar worked well in conversation.
The model even combined the function response with trained knowledge to throw in some tasting notes when looking up the stock for the new variant. Very helpful.
My only complaint with this particular segment was that the model pushed me a little too hard towards committing to a vintage before performing a stock lookup. Vintage is a nullable parameter in the function declaration and when I don’t specify a particular vintage in a conversation about a wine I expect to receive a summary of all vintages. In fact, the system prompt explicitly states this.
## When the user specifies vinyard and wine name but no vintage
Use the 'lookup_stock' function without vintage. Only after presenting a list of the results, ask if the user is interested in a particular vintage.Contrary to these instructions the model seems to prioritize collecting all three parameters and will only do a summary lookup after being instructed explicitly. Fixing this will require a tweak with the system prompt but the details are left as an exercise for the reader :-)
Summary and future work
As hoped the chat session based conversation, detailed system prompt and especially the integration with a lookup function resulted in a proper WineSom(melier). I am very impressed with the flow and quality of the conversation.
Especially the function call via Gemini API has a lot of potential. For example, we could register a second function call that offers to purchase additional bottles of a particular wine from a vendor. Or we can encourage the model to deduce preferences from the users past purchasing history. We could also add a side bar to the display that shows a list of recently mentioned wines. This would allow the user to switch topics with one click while the app takes care of relaying the choice to the model.
References
To save some time, I based my WineSom application on google’s public colorist demo app. This app is well crafted and clearly structured, and so removing the colorist functionality and replacing it with wine themed code was quick and easy for me.
The resulting WineSom app is my idea but still contains a lot of the original code from Google. I therefore decided not to publish the WineSom source code. Instead, I suggest to study the original code at vertex_ai_firebase_flutter_app or the matching Build a Gemini powered Flutter app codelab.