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
2 changes: 2 additions & 0 deletions api/src/org/labkey/api/ApiModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
import org.labkey.api.view.ViewServlet;
import org.labkey.api.view.WebPartFactory;
import org.labkey.api.webdav.WebdavResolverImpl;
import org.labkey.api.wiki.WikiRendererType;
import org.labkey.api.writer.ContainerUser;
import org.labkey.filters.ContentSecurityPolicyFilter;

Expand Down Expand Up @@ -562,6 +563,7 @@ public void registerServlets(ServletContext servletCtx)
UserManager.TestCase.class,
ViewCategoryManager.TestCase.class,
WebdavResolverImpl.TestCase.class,
WikiRendererType.TestCase.class,
WorkbookContainerType.TestCase.class,
WriteableLookAndFeelProperties.TestCase.class
);
Expand Down
6 changes: 0 additions & 6 deletions api/src/org/labkey/api/admin/AdminBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.labkey.api.data.ContainerManager;
import org.labkey.api.data.CoreSchema;
import org.labkey.api.data.DbScope;
import org.labkey.api.module.Module;
import org.labkey.api.module.ModuleLoader;
import org.labkey.api.security.UserManager;
import org.labkey.api.settings.AppProps;
Expand All @@ -43,7 +42,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
Expand Down Expand Up @@ -82,7 +80,6 @@ public static class RecentUser
public static final String sessionTimeout = Formats.commaf0.format(ModuleLoader.getServletContext().getSessionTimeout());
public static final String buildTime = ModuleLoader.getInstance().getCoreModule().getBuildTime();
public static final String serverStartupTime = DateUtil.formatDateTime(ContainerManager.getRoot());
public static final List<Module> modules;

public static String asserts = "disabled";

Expand Down Expand Up @@ -116,9 +113,6 @@ public static class RecentUser

//noinspection ConstantConditions,AssertWithSideEffects
assert null != (asserts = "enabled");

modules = new ArrayList<>(ModuleLoader.getInstance().getModules());
modules.sort(Comparator.comparing(Module::getName, String.CASE_INSENSITIVE_ORDER));
}

private static @Nullable String getValue(Field field)
Expand Down
13 changes: 13 additions & 0 deletions api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,19 @@ public boolean supportsNativeGreatestAndLeast()
return true;
}

@Override
public boolean supportsIsNumeric()
{
return true;
}

@Override
public SQLFragment isNumericExpr(SQLFragment expression)
{
return new SQLFragment("(CASE WHEN CAST((").append(expression)
.append(") AS TEXT) ~ '^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$' THEN 1 ELSE 0 END)");
}

private class PostgreSqlColumnMetaDataReader extends ColumnMetaDataReader
{
private final TableInfo _table;
Expand Down
28 changes: 19 additions & 9 deletions api/src/org/labkey/api/data/dialect/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -863,15 +863,25 @@ public SQLFragment getNumericCast(SQLFragment expression)
* @param arguments Arguments passed from the LK SQL
* @return the dialect equivalent SQLFragrment
*/
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
{
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

public void handleCreateDatabaseException(SQLException e) throws ServletException
{
throw(new ServletException("Can't create database", e));
}
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
{
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

public boolean supportsIsNumeric()
{
return false;
}

public SQLFragment isNumericExpr(SQLFragment expression)
{
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

public void handleCreateDatabaseException(SQLException e) throws ServletException
{
throw(new ServletException("Can't create database", e));
}

/**
* Wrap one or more INSERT statements to allow explicit specification
Expand Down
9 changes: 9 additions & 0 deletions api/src/org/labkey/api/mcp/AbstractAgentAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,18 @@ protected String handleEscape(String prompt)
@Override
public void validateForm(F form, Errors errors)
{
if (!McpService.get().isAIFeaturesEnabled())
{
errors.reject(ERROR_GENERIC, "Agent actions are not available.");
return;
}

String prompt = form.getPrompt();
if (prompt != null && prompt.length() > MAX_PROMPT_CHAR_LENGTH)
{
errors.rejectValue("prompt", ERROR_GENERIC, "Prompt cannot exceed " + MAX_PROMPT_CHAR_LENGTH + " characters.");
return;
}

// Only honor a client-supplied conversationId if this agent previously issued this session that
// id. Otherwise, generate a fresh one. This prevents a same-session caller from splicing into
Expand Down
37 changes: 34 additions & 3 deletions api/src/org/labkey/api/mcp/McpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

///
Expand Down Expand Up @@ -94,8 +95,11 @@
///
public interface McpService extends ToolCallbackProvider
{
String VECTOR_SCHEMA = "vector_indexes"; // for pgvector extension, see core-26.005-26.006.sql

Logger LOG = LogHelper.getLogger(McpService.class, "MCP registration exceptions");
String ENABLE_MCP_SERVER_FLAG = "enableMcpServer";
String ENABLE_AI_FEATURES = "enableAIFeatures";

// Interface for MCP classes that we will "ingest" using Spring annotations. Provides a few helper methods.
interface McpImpl
Expand All @@ -117,9 +121,6 @@ default User getUser(ToolContext toolContext)
// Every MCP resource should call this on every invocation
default void incrementResourceRequestCount(String resource)
{
if (!get().isEnabled())
throw new RuntimeException("The MCP server is not enabled for external requests. Consider toggling the optional feature flag.");

get().incrementResourceRequestCount(resource);
}
}
Expand All @@ -142,8 +143,19 @@ default boolean isEnabled()
return OptionalFeatureService.get().isFeatureEnabled(ENABLE_MCP_SERVER_FLAG);
}

default boolean isAIFeaturesEnabled()
{
return OptionalFeatureService.get().isFeatureEnabled(ENABLE_AI_FEATURES);
}

boolean isReady();

// Convenience for in-product AI features: the AI feature flag is on AND the service has started.
default boolean isAIFeaturesReady()
{
return isAIFeaturesEnabled() && isReady();
}

// Register MCPs in Module.startup()
default void register(McpImpl mcp)
{
Expand Down Expand Up @@ -187,6 +199,8 @@ default ChatClient getChat(HttpSession session, String conversationName, Supplie

record MessageResponse(String contentType, String text, HtmlString html) {}

record VectorDocument(String id, String text, Map<String, Object> metadata) {}

/** get a consolidated response (good for many text-oriented agents/use-cases) */
MessageResponse sendMessage(ChatClient chat, String message);

Expand All @@ -201,4 +215,21 @@ default List<MessageResponse> sendMessageEx(ChatClient chat, String message)
* CONSIDER: Is it possible to implement VectorStoreRetriever wrapper for SearchService???
*/
VectorStore getVectorStore();

/** Returns true if the vector store exists and contains at least one document. */
boolean isVectorStorePopulated(@NotNull VectorStore vs);

/**
* Adds documents to the vector store, automatically splitting any document whose token
* count exceeds the embedding model's input limit. Prefer this over
* {@code getVectorStore().add(...)} for indexing — it prevents the
* {@code IllegalArgumentException} that {@code TokenCountBatchingStrategy} throws on
* oversized inputs.
*/
void addDocuments(List<VectorDocument> documents);

void saveVectorStore();

/** Drop and recreate the vector store table. Use when the embedding model has changed and dimensions no longer match. */
void resetVectorStore();
}
27 changes: 27 additions & 0 deletions api/src/org/labkey/api/mcp/NoopMcpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public boolean isEnabled()
return false;
}

@Override
public boolean isAIFeaturesEnabled()
{
return false;
}

@Override
public boolean isReady()
{
Expand Down Expand Up @@ -101,4 +107,25 @@ public VectorStore getVectorStore()
{
return null;
}

@Override
public boolean isVectorStorePopulated(@NotNull VectorStore vs)
{
return false;
}

@Override
public void addDocuments(List<VectorDocument> documents)
{
}

@Override
public void saveVectorStore()
{
}

@Override
public void resetVectorStore()
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,11 @@ default File getFileForModuleResource(Module module, Path path)
return null;
return FileUtil.appendPath(resources, path);
}

// used by ModuleResourceProvider@AllModuleResourcesRoot() (_webdav/@modules) which wants to soo
// the whole module source directory (e.g. when using SourcePath)
default File getModuleRoot(Module module, @Nullable List<String> messages)
{
return null;
}
}
Loading
Loading