Skip to content

Commit d89d5cf

Browse files
Work around some race conditions in MCP prototype (#7397)
1 parent 3417286 commit d89d5cf

2 files changed

Lines changed: 86 additions & 36 deletions

File tree

core/src/org/labkey/core/mpc/McpServiceImpl.java

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@
6565
import java.time.Duration;
6666
import java.util.ArrayList;
6767
import java.util.Arrays;
68+
import java.util.ConcurrentModificationException;
6869
import java.util.List;
70+
import java.util.NoSuchElementException;
6971
import java.util.Objects;
7072
import java.util.function.Supplier;
7173

@@ -314,6 +316,7 @@ public ChatClient getChat(HttpSession session, String agentName, Supplier<String
314316
.maxMessages(100)
315317
.chatMemoryRepository(chatMemoryRepository)
316318
.build();
319+
317320
MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
318321
.conversationId(conversationId)
319322
.build();
@@ -335,46 +338,73 @@ public ChatClient getChat(HttpSession session, String agentName, Supplier<String
335338
@Override
336339
public MessageResponse sendMessage(ChatClient chatSession, String message)
337340
{
338-
var callResponse = chatSession
339-
.prompt(message)
340-
.toolContext(McpContext.get().getToolContext().getContext())
341-
.call();
342-
StringBuilder sb = new StringBuilder();
343-
for (Generation result : callResponse.chatResponse().getResults())
341+
try
344342
{
345-
var output = result.getOutput();
346-
if (ASSISTANT == output.getMessageType())
343+
var callResponse = chatSession
344+
.prompt(message)
345+
.toolContext(McpContext.get().getToolContext().getContext())
346+
.call();
347+
StringBuilder sb = new StringBuilder();
348+
for (Generation result : callResponse.chatResponse().getResults())
347349
{
348-
sb.append(output.getText());
349-
sb.append("\n\n");
350+
var output = result.getOutput();
351+
if (ASSISTANT == output.getMessageType())
352+
{
353+
sb.append(output.getText());
354+
sb.append("\n\n");
355+
}
350356
}
357+
String md = sb.toString().strip();
358+
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
359+
return new MessageResponse("text/markdown", md, html);
360+
}
361+
catch (java.util.NoSuchElementException x)
362+
{
363+
// Spring AI GoogleGenAiChatModel bug: empty candidates cause NoSuchElementException
364+
// https://github.com/spring-projects/spring-ai/issues/4556
365+
LOG.warn("Empty response from chat model (likely a filtered or empty candidate)", x);
366+
return new MessageResponse("text/plain", "The model returned an empty response. Please try resubmitting and the problem continues, rephrase your question/prompt.", HtmlString.of("The model returned an empty response. Please try rephrasing your question."));
351367
}
352-
String md = sb.toString().strip();
353-
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
354-
return new MessageResponse("text/markdown", md, html);
355368
}
356369

357370
@Override
358371
public List<MessageResponse> sendMessageEx(ChatClient chatSession, String message)
359372
{
360373
if (isBlank(message))
361374
return List.of();
362-
var callResponse = chatSession
363-
.prompt(message)
364-
.toolContext(McpContext.get().getToolContext().getContext())
365-
.call();
366-
List<MessageResponse> ret = new ArrayList<>();
367-
for (Generation result : callResponse.chatResponse().getResults())
375+
try
368376
{
369-
var output = result.getOutput();
370-
if (ASSISTANT == output.getMessageType())
377+
var callResponse = chatSession
378+
.prompt(message)
379+
.toolContext(McpContext.get().getToolContext().getContext())
380+
.call();
381+
List<MessageResponse> ret = new ArrayList<>();
382+
for (Generation result : callResponse.chatResponse().getResults())
371383
{
372-
String md = output.getText();
373-
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
374-
ret.add(new MessageResponse("text/markdown", md, html));
384+
var output = result.getOutput();
385+
if (ASSISTANT == output.getMessageType())
386+
{
387+
String md = output.getText();
388+
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
389+
ret.add(new MessageResponse("text/markdown", md, html));
390+
}
375391
}
392+
return ret;
393+
}
394+
catch (NoSuchElementException x)
395+
{
396+
// Spring AI GoogleGenAiChatModel bug: empty candidates cause NoSuchElementException
397+
// https://github.com/spring-projects/spring-ai/issues/4556
398+
LOG.warn("Empty response from chat model (likely a filtered or empty candidate)", x);
399+
return List.of(new MessageResponse("text/plain", "The model returned an empty response. Please try rephrasing your question.", HtmlString.of("The model returned an empty response. Please try rephrasing your question.")));
400+
}
401+
catch (ConcurrentModificationException x)
402+
{
403+
// This can happen when the vector store is still loading, typically a problem shortly after startup
404+
// Should do better synchronization or state checking
405+
LOG.warn("Vector store not ready", x);
406+
return List.of(new MessageResponse("text/plain", "Vector store likely not ready yet. Try again.", HtmlString.of("Vector store likely not ready yet. Try again.")));
376407
}
377-
return ret;
378408
}
379409

380410

@@ -464,6 +494,7 @@ public String getModel()
464494
// gemini-2.5-flash
465495
// gemini-2.5-pro
466496
// gemini-3-flash-preview
497+
// gemini-3-pro-preview
467498
}
468499

469500
@Override

query/src/org/labkey/query/controllers/LabKeySql.md

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,16 @@ A `PIVOT` query helps you summarize and re-visualize data by transforming rows i
5757
PIVOT new_column_name BY pivoting_column IN ('value1', 'value2')
5858
```
5959
Note that pivot column names are case-sensitive. You may need to use `LOWER()` or `UPPER()` in your query to work around this issue.
60-
* **Pivoting by Two Columns:**
61-
Two levels of `PIVOT` are not directly supported. However, you can achieve a similar result by concatenating the two values together and pivoting on that "calculated" column.
62-
```sql
63-
SELECT
64-
Run.SampleCondition || ' ' || PeakLabel AS ConditionPeak,
65-
AVG(Data.PercTimeCorrArea) AS AvgPercTimeCorrArea
66-
FROM Data
67-
GROUP BY Run.SampleCondition || ' ' || PeakLabel
68-
PIVOT AvgPercTimeCorrArea BY ConditionPeak
69-
```
60+
* **Pivoting by Two Columns:**
61+
Two levels of `PIVOT` are not directly supported. However, you can achieve a similar result by concatenating the two values together and pivoting on that "calculated" column.
62+
```sql
63+
SELECT
64+
Run.SampleCondition || ' ' || PeakLabel AS ConditionPeak,
65+
AVG(Data.PercTimeCorrArea) AS AvgPercTimeCorrArea
66+
FROM Data
67+
GROUP BY Run.SampleCondition || ' ' || PeakLabel
68+
PIVOT AvgPercTimeCorrArea BY ConditionPeak
69+
```
7070

7171
-----
7272

@@ -130,7 +130,26 @@ LabKey SQL allows you to directly annotate your SQL statements to override how c
130130

131131
-----
132132

133-
### **7. Available Methods**
133+
### **7. Container Filters**
134+
135+
In addition to targeting a container by its path, LabKey SQL supports container filters to alter the scope
136+
of a query. Annotate tables in the FROM clause with an optional container filter. Syntax:
137+
138+
SELECT * FROM Issues [ContainerFilter='CurrentAndSubfolders'] alias
139+
140+
Possible values include:
141+
- AllFolders
142+
- AllInProject
143+
- AllInProjectPlusShared
144+
- Current
145+
- CurrentAndFirstChildren
146+
- CurrentAndParents
147+
- CurrentAndSubfolders
148+
- CurrentAndSubfoldersPlusShared
149+
- CurrentPlusProject
150+
- CurrentPlusProjectAndShared.
151+
152+
### **8. Available Methods**
134153

135154
Here is a summary of the available functions and methods in LabKey SQL.
136155

0 commit comments

Comments
 (0)