Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 56 additions & 25 deletions core/src/org/labkey/core/mpc/McpServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Supplier;

Expand Down Expand Up @@ -314,6 +316,7 @@ public ChatClient getChat(HttpSession session, String agentName, Supplier<String
.maxMessages(100)
.chatMemoryRepository(chatMemoryRepository)
.build();

MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.conversationId(conversationId)
.build();
Expand All @@ -335,46 +338,73 @@ public ChatClient getChat(HttpSession session, String agentName, Supplier<String
@Override
public MessageResponse sendMessage(ChatClient chatSession, String message)
{
var callResponse = chatSession
.prompt(message)
.toolContext(McpContext.get().getToolContext().getContext())
.call();
StringBuilder sb = new StringBuilder();
for (Generation result : callResponse.chatResponse().getResults())
try
{
var output = result.getOutput();
if (ASSISTANT == output.getMessageType())
var callResponse = chatSession
.prompt(message)
.toolContext(McpContext.get().getToolContext().getContext())
.call();
StringBuilder sb = new StringBuilder();
for (Generation result : callResponse.chatResponse().getResults())
{
sb.append(output.getText());
sb.append("\n\n");
var output = result.getOutput();
if (ASSISTANT == output.getMessageType())
{
sb.append(output.getText());
sb.append("\n\n");
}
}
String md = sb.toString().strip();
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
return new MessageResponse("text/markdown", md, html);
}
catch (java.util.NoSuchElementException x)
{
// Spring AI GoogleGenAiChatModel bug: empty candidates cause NoSuchElementException
// https://github.com/spring-projects/spring-ai/issues/4556
LOG.warn("Empty response from chat model (likely a filtered or empty candidate)", x);
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."));
}
String md = sb.toString().strip();
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
return new MessageResponse("text/markdown", md, html);
}

@Override
public List<MessageResponse> sendMessageEx(ChatClient chatSession, String message)
{
if (isBlank(message))
return List.of();
var callResponse = chatSession
.prompt(message)
.toolContext(McpContext.get().getToolContext().getContext())
.call();
List<MessageResponse> ret = new ArrayList<>();
for (Generation result : callResponse.chatResponse().getResults())
try
{
var output = result.getOutput();
if (ASSISTANT == output.getMessageType())
var callResponse = chatSession
.prompt(message)
.toolContext(McpContext.get().getToolContext().getContext())
.call();
List<MessageResponse> ret = new ArrayList<>();
for (Generation result : callResponse.chatResponse().getResults())
{
String md = output.getText();
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
ret.add(new MessageResponse("text/markdown", md, html));
var output = result.getOutput();
if (ASSISTANT == output.getMessageType())
{
String md = output.getText();
HtmlString html = HtmlString.unsafe(MarkdownService.get().toHtml(md));
ret.add(new MessageResponse("text/markdown", md, html));
}
}
return ret;
}
catch (NoSuchElementException x)
{
// Spring AI GoogleGenAiChatModel bug: empty candidates cause NoSuchElementException
// https://github.com/spring-projects/spring-ai/issues/4556
LOG.warn("Empty response from chat model (likely a filtered or empty candidate)", x);
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.")));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into this early today. In my case, I resubmitted the exact same question and got a good response. So maybe advice should be, "Please try resubmitting your question." Then again, I didn't look under the covers to see what was happening.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased. I have a separate local hack that I didn't commit that tries to filter out these empty messages. I'm not sure it's working as intended so holding off on pushing that.

}
catch (ConcurrentModificationException x)
{
// This can happen when the vector store is still loading, typically a problem shortly after startup
// Should do better synchronization or state checking
LOG.warn("Vector store not ready", x);
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.")));
}
return ret;
}


Expand Down Expand Up @@ -464,6 +494,7 @@ public String getModel()
// gemini-2.5-flash
// gemini-2.5-pro
// gemini-3-flash-preview
// gemini-3-pro-preview
}

@Override
Expand Down
41 changes: 30 additions & 11 deletions query/src/org/labkey/query/controllers/LabKeySql.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,16 @@ A `PIVOT` query helps you summarize and re-visualize data by transforming rows i
PIVOT new_column_name BY pivoting_column IN ('value1', 'value2')
```
Note that pivot column names are case-sensitive. You may need to use `LOWER()` or `UPPER()` in your query to work around this issue.
* **Pivoting by Two Columns:**
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.
```sql
SELECT
Run.SampleCondition || ' ' || PeakLabel AS ConditionPeak,
AVG(Data.PercTimeCorrArea) AS AvgPercTimeCorrArea
FROM Data
GROUP BY Run.SampleCondition || ' ' || PeakLabel
PIVOT AvgPercTimeCorrArea BY ConditionPeak
```
* **Pivoting by Two Columns:**
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.
```sql
SELECT
Run.SampleCondition || ' ' || PeakLabel AS ConditionPeak,
AVG(Data.PercTimeCorrArea) AS AvgPercTimeCorrArea
FROM Data
GROUP BY Run.SampleCondition || ' ' || PeakLabel
PIVOT AvgPercTimeCorrArea BY ConditionPeak
```

-----

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

-----

### **7. Available Methods**
### **7. Container Filters**

In addition to targeting a container by its path, LabKey SQL supports container filters to alter the scope
of a query. Annotate tables in the FROM clause with an optional container filter. Syntax:

SELECT * FROM Issues [ContainerFilter='CurrentAndSubfolders'] alias

Possible values include:
- AllFolders
- AllInProject
- AllInProjectPlusShared
- Current
- CurrentAndFirstChildren
- CurrentAndParents
- CurrentAndSubfolders
- CurrentAndSubfoldersPlusShared
- CurrentPlusProject
- CurrentPlusProjectAndShared.

### **8. Available Methods**

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

Expand Down