Skip to content

Commit d47d352

Browse files
committed
More MCP goodness
1 parent 5e8b401 commit d47d352

19 files changed

Lines changed: 2167 additions & 13 deletions

examples/graphics/source/examples/AI.h

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class AiDemo : public yup::Component
4343
modelLabel.setText ("Model", yup::dontSendNotification);
4444
addAndMakeVisible (modelLabel);
4545

46-
modelEditor.setText ("llama3.2", yup::dontSendNotification);
46+
modelEditor.setText ("gemma4", yup::dontSendNotification);
4747
modelEditor.setMultiLine (false);
4848
addAndMakeVisible (modelEditor);
4949

@@ -68,6 +68,10 @@ class AiDemo : public yup::Component
6868
};
6969
addAndMakeVisible (askButton);
7070

71+
toolsToggle.setButtonText ("Tools");
72+
toolsToggle.setToggleState (true, yup::dontSendNotification);
73+
addAndMakeVisible (toolsToggle);
74+
7175
statusLabel.setText ("Ollama can call set_background_color for this page.", yup::dontSendNotification);
7276
addAndMakeVisible (statusLabel);
7377

@@ -116,6 +120,8 @@ class AiDemo : public yup::Component
116120
auto actionRow = area.removeFromTop (34);
117121
askButton.setBounds (actionRow.removeFromLeft (96));
118122
actionRow.removeFromLeft (12);
123+
toolsToggle.setBounds (actionRow.removeFromLeft (86));
124+
actionRow.removeFromLeft (12);
119125
statusLabel.setBounds (actionRow);
120126

121127
area.removeFromTop (18);
@@ -138,12 +144,13 @@ class AiDemo : public yup::Component
138144
class OllamaRequestThread final : public yup::Thread
139145
{
140146
public:
141-
OllamaRequestThread (AiDemo& ownerToUse, yup::String modelToUse, yup::String baseUrlToUse, yup::String promptToUse)
147+
OllamaRequestThread (AiDemo& ownerToUse, yup::String modelToUse, yup::String baseUrlToUse, yup::String promptToUse, bool useToolsToUse)
142148
: Thread ("OllamaRequest")
143149
, owner (ownerToUse)
144150
, model (std::move (modelToUse))
145151
, baseUrl (std::move (baseUrlToUse))
146152
, prompt (std::move (promptToUse))
153+
, useTools (useToolsToUse)
147154
, ownerReference (&ownerToUse)
148155
{
149156
}
@@ -159,25 +166,32 @@ class AiDemo : public yup::Component
159166
yup::LLMHttpClient client (std::move (options));
160167

161168
yup::LLMClient::Request request;
162-
request.systemPrompt = "You are a concise assistant inside a YUP example app. "
163-
"If the user asks to change the page background, call set_background_color with a CSS color name, #RRGGBB value, rgb(...), or hsl(...). "
164-
"After a tool result, briefly tell the user what changed.";
165169
request.messages.push_back (yup::LLMMessage::user (prompt));
166170
request.temperature = 0.2f;
167171

168172
yup::LLMToolRegistry toolRegistry;
169-
owner.registerTools (toolRegistry, ownerReference);
170-
request.tools = toolRegistry.getAllTools();
171-
request.toolChoice = "auto";
173+
if (useTools)
174+
{
175+
request.systemPrompt = "You are a concise assistant inside a YUP example app. "
176+
"If the user asks to change the page background, call set_background_color with a CSS color name, #RRGGBB value, rgb(...), or hsl(...). "
177+
"After a tool result, briefly tell the user what changed.";
178+
179+
owner.registerTools (toolRegistry, ownerReference);
180+
request.tools = toolRegistry.getAllTools();
181+
request.toolChoice = "auto";
182+
}
172183

173184
auto response = client.runToolLoop (request, toolRegistry);
174185

175186
yup::String responseText;
176-
if (! response.choices.empty())
187+
if (response.failed() && response.errorMessage.has_value())
188+
responseText = "Ollama error: " + *response.errorMessage;
189+
else if (! response.choices.empty())
177190
responseText = response.choices.front().message.content.trim();
178191

179192
if (responseText.isEmpty())
180-
responseText = "No response was returned. Check that Ollama is running, the model is pulled, and the base URL is reachable.";
193+
responseText = useTools ? "No response was returned. Check that Ollama is running, the model is pulled, the base URL is reachable, and the model supports tool calls."
194+
: "No response was returned. Check that Ollama is running, the model is pulled, and the base URL is reachable.";
181195

182196
if (threadShouldExit())
183197
return;
@@ -199,6 +213,7 @@ class AiDemo : public yup::Component
199213
yup::String model;
200214
yup::String baseUrl;
201215
yup::String prompt;
216+
bool useTools;
202217
yup::WeakReference<yup::Component> ownerReference;
203218
};
204219

@@ -215,6 +230,7 @@ class AiDemo : public yup::Component
215230
const auto model = modelEditor.getText().trim();
216231
const auto baseUrl = baseUrlEditor.getText().trim();
217232
const auto prompt = promptEditor.getText().trim();
233+
const auto useTools = toolsToggle.getToggleState();
218234

219235
if (model.isEmpty() || baseUrl.isEmpty() || prompt.isEmpty())
220236
{
@@ -226,7 +242,7 @@ class AiDemo : public yup::Component
226242
statusLabel.setText ("Waiting for Ollama...", yup::dontSendNotification);
227243
responseEditor.setText ("", yup::dontSendNotification);
228244

229-
requestThread = std::make_unique<OllamaRequestThread> (*this, model, baseUrl, prompt);
245+
requestThread = std::make_unique<OllamaRequestThread> (*this, model, baseUrl, prompt, useTools);
230246
if (! requestThread->startThread (yup::Thread::Priority::background))
231247
{
232248
requestThread.reset();
@@ -306,6 +322,7 @@ class AiDemo : public yup::Component
306322
yup::TextEditor promptEditor { "promptEditor" };
307323
yup::TextEditor responseEditor { "responseEditor" };
308324
yup::TextButton askButton { "askButton" };
325+
yup::ToggleButton toolsToggle { "toolsToggle" };
309326

310327
yup::Font titleFont;
311328
std::optional<yup::Color> backgroundColor;

modules/yup_ai/llm/yup_LLMHttpClient.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ struct LLMHttpClient::Pimpl
123123

124124
if (stream != nullptr && statusCode >= 200 && statusCode < 300)
125125
{
126+
LLMResponse accumulatedResponse;
127+
126128
while (! stream->isExhausted())
127129
{
128130
auto line = stream->readNextLine().trim();
@@ -136,7 +138,9 @@ struct LLMHttpClient::Pimpl
136138

137139
auto parsed = JSON::parse (payload);
138140
auto chunk = LLMResponse::fromStreamChunk (parsed);
139-
onChunk (chunk);
141+
142+
accumulatedResponse.appendStreamChunk (chunk);
143+
onChunk (accumulatedResponse);
140144

141145
if (chunk.failed())
142146
return false;

modules/yup_ai/llm/yup_LLMMessage.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ std::optional<LLMToolCall> LLMToolCall::fromVar (const var& value)
7272
return std::nullopt;
7373

7474
LLMToolCall result;
75+
result.index = static_cast<int> (value["index"]);
7576
result.id = value["id"].toString();
7677

7778
if (auto* functionObject = value["function"].getDynamicObject())
@@ -85,7 +86,11 @@ std::optional<LLMToolCall> LLMToolCall::fromVar (const var& value)
8586
result.arguments = parseArguments (value["arguments"]);
8687
}
8788

88-
if (result.name.isEmpty())
89+
const auto hasArguments = ! result.arguments.isVoid()
90+
&& ! result.arguments.isUndefined()
91+
&& result.arguments.toString().isNotEmpty();
92+
93+
if (result.name.isEmpty() && result.id.isEmpty() && ! hasArguments)
8994
return std::nullopt;
9095

9196
return result;

modules/yup_ai/llm/yup_LLMMessage.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace yup
3333
*/
3434
struct YUP_API LLMToolCall
3535
{
36+
int index = 0;
3637
String id;
3738
String name;
3839
var arguments;

modules/yup_ai/llm/yup_LLMResponse.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@ String getOpenAiErrorMessage (const var& json)
3737

3838
return {};
3939
}
40+
41+
var parseStreamArguments (const String& arguments)
42+
{
43+
auto parsed = JSON::parse (arguments);
44+
return parsed.isVoid() ? var (arguments) : parsed;
45+
}
46+
47+
String argumentsToStreamText (const var& arguments)
48+
{
49+
if (arguments.isObject() || arguments.isArray())
50+
return JSON::toString (arguments, true);
51+
52+
return arguments.toString();
53+
}
54+
55+
LLMResponse::Choice& findOrAppendChoice (std::vector<LLMResponse::Choice>& choices, const LLMResponse::Choice& chunkChoice)
56+
{
57+
for (auto& choice : choices)
58+
if (choice.index == chunkChoice.index)
59+
return choice;
60+
61+
choices.push_back ({});
62+
auto& choice = choices.back();
63+
choice.index = chunkChoice.index;
64+
choice.message.role = chunkChoice.message.role;
65+
return choice;
66+
}
4067
} // namespace
4168

4269
bool LLMResponse::hasToolCalls() const noexcept
@@ -64,6 +91,60 @@ std::vector<LLMToolCall> LLMResponse::getToolCalls() const
6491
return result;
6592
}
6693

94+
void LLMResponse::appendStreamChunk (const LLMResponse& chunk)
95+
{
96+
if (chunk.errorMessage.has_value())
97+
{
98+
errorMessage = chunk.errorMessage;
99+
return;
100+
}
101+
102+
if (model.isEmpty())
103+
model = chunk.model;
104+
105+
for (const auto& chunkChoice : chunk.choices)
106+
{
107+
auto& choice = findOrAppendChoice (choices, chunkChoice);
108+
109+
if (choice.message.role == LLMMessage::Role::assistant)
110+
choice.message.role = chunkChoice.message.role;
111+
112+
choice.message.content += chunkChoice.message.content;
113+
114+
if (chunkChoice.finishReason.has_value())
115+
choice.finishReason = chunkChoice.finishReason;
116+
117+
if (! chunkChoice.message.toolCalls.has_value())
118+
continue;
119+
120+
if (! choice.message.toolCalls.has_value())
121+
choice.message.toolCalls = std::vector<LLMToolCall>();
122+
123+
for (const auto& chunkToolCall : *chunkChoice.message.toolCalls)
124+
{
125+
const auto toolIndex = chunkToolCall.index;
126+
if (toolIndex < 0)
127+
continue;
128+
129+
if (toolIndex >= static_cast<int> (choice.message.toolCalls->size()))
130+
choice.message.toolCalls->resize (static_cast<size_t> (toolIndex + 1));
131+
132+
auto& toolCall = (*choice.message.toolCalls)[static_cast<size_t> (toolIndex)];
133+
toolCall.index = toolIndex;
134+
135+
if (chunkToolCall.id.isNotEmpty())
136+
toolCall.id = chunkToolCall.id;
137+
138+
if (chunkToolCall.name.isNotEmpty())
139+
toolCall.name = chunkToolCall.name;
140+
141+
const auto mergedArguments = argumentsToStreamText (toolCall.arguments) + argumentsToStreamText (chunkToolCall.arguments);
142+
if (mergedArguments.isNotEmpty())
143+
toolCall.arguments = parseStreamArguments (mergedArguments);
144+
}
145+
}
146+
}
147+
67148
LLMResponse LLMResponse::fromError (const String& message)
68149
{
69150
LLMResponse response;

modules/yup_ai/llm/yup_LLMResponse.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class YUP_API LLMResponse
5858
/** Returns all tool calls from all choices. */
5959
std::vector<LLMToolCall> getToolCalls() const;
6060

61+
/** Appends a streaming response chunk to this response, concatenating content and tool-call arguments by choice index. */
62+
void appendStreamChunk (const LLMResponse& chunk);
63+
6164
/** Creates an error response with a diagnostic message. */
6265
static LLMResponse fromError (const String& message);
6366

0 commit comments

Comments
 (0)