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
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
Expand Down Expand Up @@ -792,12 +793,25 @@
return Collections.emptyList();
}

return request.getTools().stream()
.map(this::convertToolDefinition)
.collect(Collectors.toList());
Map<String, ToolDefinition> uniqueTools = new LinkedHashMap<>();
for (ToolDefinition tool : request.getTools()) {
if (tool == null || tool.getName() == null || tool.getName().isBlank()) {
log.warn("[LLM] Dropping tool with blank name before request serialization");
continue;
}
ToolDefinition previous = uniqueTools.putIfAbsent(tool.getName(), tool);
if (previous != null) {
log.warn("[LLM] Dropping duplicate tool definition '{}' before request serialization", tool.getName());
}
}

List<ToolSpecification> tools = new ArrayList<>(uniqueTools.size());
for (ToolDefinition tool : uniqueTools.values()) {
tools.add(convertToolDefinition(tool));
}
return tools;
}

@SuppressWarnings("unchecked")
private ToolSpecification convertToolDefinition(ToolDefinition tool) {
ToolSpecification.Builder builder = ToolSpecification.builder()
.name(tool.getName())
Expand All @@ -806,15 +820,21 @@
// Convert input schema to JsonObjectSchema parameters
if (tool.getInputSchema() != null) {
Map<String, Object> schema = tool.getInputSchema();
Map<String, Object> properties = (Map<String, Object>) schema.get(SCHEMA_KEY_PROPERTIES);
List<String> required = (List<String>) schema.get("required");

Map<String, Object> properties = stringObjectMap(schema.get(SCHEMA_KEY_PROPERTIES),
tool.getName(), SCHEMA_KEY_PROPERTIES);
List<String> required = stringList(schema.get("required"), tool.getName(), "required");
if (properties != null) {
JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
String paramName = entry.getKey();
Map<String, Object> paramSchema = (Map<String, Object>) entry.getValue();
schemaBuilder.addProperty(paramName, toJsonSchemaElement(paramSchema));
Map<String, Object> paramSchema = stringObjectMap(entry.getValue(), tool.getName(),
SCHEMA_KEY_PROPERTIES + "." + paramName);
if (paramSchema == null) {
log.warn("[LLM] Dropping invalid schema for tool '{}' param '{}'", tool.getName(), paramName);
continue;
}
schemaBuilder.addProperty(paramName, toJsonSchemaElement(tool.getName(),
SCHEMA_KEY_PROPERTIES + "." + paramName, paramSchema));
}
if (required != null && !required.isEmpty()) {
schemaBuilder.required(required);
Expand All @@ -826,11 +846,10 @@
return builder.build();
}

@SuppressWarnings("unchecked")
private JsonSchemaElement toJsonSchemaElement(Map<String, Object> paramSchema) {
String type = (String) paramSchema.get("type");
String description = (String) paramSchema.get("description");
List<String> enumValues = (List<String>) paramSchema.get("enum");
private JsonSchemaElement toJsonSchemaElement(String toolName, String path, Map<String, Object> paramSchema) {

Check warning on line 849 in src/main/java/me/golemcore/bot/adapter/outbound/llm/Langchain4jAdapter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 89 to 64, Complexity from 32 to 14, Nesting Level from 5 to 2, Number of Variables from 18 to 6.

See more on https://sonarcloud.io/project/issues?id=alexk-dev_golemcore-bot&issues=AZ0HzVPAEwPRkYN49-em&open=AZ0HzVPAEwPRkYN49-em&pullRequest=194

Check failure on line 849 in src/main/java/me/golemcore/bot/adapter/outbound/llm/Langchain4jAdapter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 47 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=alexk-dev_golemcore-bot&issues=AZ0HzVPAEwPRkYN49-ei&open=AZ0HzVPAEwPRkYN49-ei&pullRequest=194
String type = stringValue(paramSchema.get("type"), toolName, path + ".type");
String description = stringValue(paramSchema.get("description"), toolName, path + ".description");
List<String> enumValues = stringList(paramSchema.get("enum"), toolName, path + ".enum");

// Enum values take priority
if (enumValues != null && !enumValues.isEmpty()) {
Expand Down Expand Up @@ -880,8 +899,10 @@
builder.description(description);
}
if (paramSchema.containsKey("items")) {
Map<String, Object> items = (Map<String, Object>) paramSchema.get("items");
builder.items(toJsonSchemaElement(items));
Map<String, Object> items = stringObjectMap(paramSchema.get("items"), toolName, path + ".items");
if (items != null) {
builder.items(toJsonSchemaElement(toolName, path + ".items", items));
}
}
return builder.build();
}
Expand All @@ -891,9 +912,20 @@
builder.description(description);
}
if (paramSchema.containsKey(SCHEMA_KEY_PROPERTIES)) {
Map<String, Object> nestedProps = (Map<String, Object>) paramSchema.get(SCHEMA_KEY_PROPERTIES);
for (Map.Entry<String, Object> entry : nestedProps.entrySet()) {
builder.addProperty(entry.getKey(), toJsonSchemaElement((Map<String, Object>) entry.getValue()));
Map<String, Object> nestedProps = stringObjectMap(paramSchema.get(SCHEMA_KEY_PROPERTIES),
toolName, path + "." + SCHEMA_KEY_PROPERTIES);
if (nestedProps != null) {
for (Map.Entry<String, Object> entry : nestedProps.entrySet()) {
Map<String, Object> nestedSchema = stringObjectMap(entry.getValue(), toolName,
path + "." + SCHEMA_KEY_PROPERTIES + "." + entry.getKey());
if (nestedSchema == null) {
log.warn("[LLM] Dropping invalid nested schema for tool '{}' at {}", toolName,
path + "." + SCHEMA_KEY_PROPERTIES + "." + entry.getKey());
continue;
}
builder.addProperty(entry.getKey(), toJsonSchemaElement(toolName,
path + "." + SCHEMA_KEY_PROPERTIES + "." + entry.getKey(), nestedSchema));
}
}
}
return builder.build();
Expand All @@ -909,6 +941,57 @@
}
}

@SuppressWarnings("unchecked")
private Map<String, Object> stringObjectMap(Object rawValue, String toolName, String path) {
if (!(rawValue instanceof Map<?, ?> rawMap)) {
if (rawValue != null) {
log.warn("[LLM] Invalid schema object for tool '{}' at {}: {}", toolName, path,
rawValue.getClass().getSimpleName());
}
return null;

Check warning on line 951 in src/main/java/me/golemcore/bot/adapter/outbound/llm/Langchain4jAdapter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty map instead of null.

See more on https://sonarcloud.io/project/issues?id=alexk-dev_golemcore-bot&issues=AZ0HzVPAEwPRkYN49-ej&open=AZ0HzVPAEwPRkYN49-ej&pullRequest=194
}
Map<String, Object> casted = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
if (!(entry.getKey() instanceof String key)) {
log.warn("[LLM] Dropping non-string schema key for tool '{}' at {}", toolName, path);
continue;
}
casted.put(key, (Object) entry.getValue());

Check warning on line 959 in src/main/java/me/golemcore/bot/adapter/outbound/llm/Langchain4jAdapter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary cast to "Object".

See more on https://sonarcloud.io/project/issues?id=alexk-dev_golemcore-bot&issues=AZ0HzVPAEwPRkYN49-ek&open=AZ0HzVPAEwPRkYN49-ek&pullRequest=194
}
return casted;
}

private List<String> stringList(Object rawValue, String toolName, String path) {
if (!(rawValue instanceof List<?> rawList)) {
if (rawValue != null) {
log.warn("[LLM] Invalid schema list for tool '{}' at {}: {}", toolName, path,
rawValue.getClass().getSimpleName());
}
return null;

Check warning on line 970 in src/main/java/me/golemcore/bot/adapter/outbound/llm/Langchain4jAdapter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty collection instead of null.

See more on https://sonarcloud.io/project/issues?id=alexk-dev_golemcore-bot&issues=AZ0HzVPAEwPRkYN49-el&open=AZ0HzVPAEwPRkYN49-el&pullRequest=194
}
List<String> values = new ArrayList<>(rawList.size());
for (Object item : rawList) {
if (item instanceof String stringValue && !stringValue.isBlank()) {
values.add(stringValue);
} else {
log.warn("[LLM] Dropping non-string schema list item for tool '{}' at {}", toolName, path);
}
}
return values;
}

private String stringValue(Object rawValue, String toolName, String path) {
if (rawValue == null) {
return null;
}
if (rawValue instanceof String stringValue) {
return stringValue;
}
log.warn("[LLM] Invalid schema string for tool '{}' at {}: {}", toolName, path,
rawValue.getClass().getSimpleName());
return null;
}

private LlmResponse convertResponse(ChatResponse response, boolean compatibilityFlatteningApplied,
boolean geminiApiType) {
AiMessage aiMessage = response.aiMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ private ContextAttributes() {
/** Boolean ? compatibility fallback flattened tool history for this turn. */
public static final String LLM_COMPAT_FLATTEN_FALLBACK_USED = "llm.compat.flatten.fallback.used";

/**
* Map<String, ToolComponent> ? context-scoped tools available only in the
* current turn.
*/
public static final String CONTEXT_SCOPED_TOOLS = "context.scoped.tools";

/** Boolean — tools were executed in this iteration. */

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,19 +340,48 @@ private void enqueueFollowUp(Message inbound) {
}

if (isQueueModeAll(runtimeConfigService.getTurnQueueFollowUpMode())) {
enqueueWithBound(queuedFollowUpMessages, inbound, "follow-up");
enqueueFollowUpWithBound(inbound, "follow-up");
return;
}

if (queuedFollowUpMessages.isEmpty()) {
queuedFollowUpMessages.addLast(inbound);
} else {
Message replaced = queuedFollowUpMessages.removeFirst();
Message replaced = removeOldestRegularFollowUpLocked();
if (replaced != null) {
rejectPendingCompletion(replaced, "Replaced by a newer follow-up message");
queuedFollowUpMessages.addLast(inbound);
log.debug(
"[SessionRunCoordinator] follow-up queue mode one-at-a-time: replaced pending follow-up message");
}
enqueueFollowUpWithBound(inbound, "follow-up");
}

private void enqueueFollowUpWithBound(Message inbound, String queueLabel) {
if (queuedFollowUpMessages.size() >= MAX_QUEUED_MESSAGES_PER_SESSION) {
Message dropped = removeOldestRegularFollowUpLocked();
if (dropped == null) {
dropped = queuedFollowUpMessages.removeFirst();
}
rejectPendingCompletion(dropped,
"Dropped oldest pending " + queueLabel + " message due to queue limit");
log.warn(
"[SessionRunCoordinator] {} queue limit reached ({}), dropped oldest message: channel={}, chatId={}",
queueLabel,
MAX_QUEUED_MESSAGES_PER_SESSION,
key.channelType(),
key.chatId());
}
queuedFollowUpMessages.addLast(inbound);
}

private Message removeOldestRegularFollowUpLocked() {
java.util.Iterator<Message> iterator = queuedFollowUpMessages.iterator();
while (iterator.hasNext()) {
Message queued = iterator.next();
if (ContextAttributes.TURN_QUEUE_KIND_INTERNAL_RETRY.equals(resolveQueueKind(queued))) {
continue;
}
iterator.remove();
return queued;
}
return null;
}

private void enqueueWithBound(Deque<Message> queue, Message inbound, String queueLabel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public void save(AgentSession session) {
log.debug("Saved session: {}", session.getId());
} catch (Exception e) {
log.error("Failed to save session: {}", session.getId(), e);
throw new IllegalStateException("Failed to save session: " + session.getId(), e);
}
}

Expand Down Expand Up @@ -280,7 +281,10 @@ private Optional<AgentSession> load(String sessionId) {
enrichSessionFields(loaded, sessionId + PROTO_EXTENSION);
return Optional.of(loaded);
}
} catch (IOException | RuntimeException e) { // NOSONAR - intentionally catch all for fallback
} catch (IOException e) {
log.error("Failed to parse protobuf session {}: {}", sessionId, e.getMessage());
throw new IllegalStateException("Failed to parse protobuf session: " + sessionId, e);
} catch (RuntimeException e) { // NOSONAR - intentionally catch all for storage fallback
log.debug("Failed protobuf load for session {}: {}", sessionId, e.getMessage());
}
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import me.golemcore.bot.domain.loop.AgentContextHolder;
import me.golemcore.bot.domain.model.AgentContext;
import me.golemcore.bot.domain.model.Attachment;
import me.golemcore.bot.domain.model.ContextAttributes;
import me.golemcore.bot.domain.model.Message;
import me.golemcore.bot.domain.model.ToolArtifact;
import me.golemcore.bot.domain.model.ToolFailureKind;
Expand All @@ -19,6 +20,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -71,7 +73,7 @@ public ToolCallExecutionResult execute(AgentContext context, Message.ToolCall to
}
}

ToolResult rawResult = executeToolCall(toolCall);
ToolResult rawResult = executeToolCall(context, toolCall);
Attachment attachment = extractAttachment(context, rawResult, toolCall.getName());
ToolResult result = enrichToolResult(context, rawResult, toolCall.getName(), attachment);
context.addToolResult(toolCall.getId(), result);
Expand Down Expand Up @@ -126,12 +128,12 @@ private boolean requestConfirmation(AgentContext context, Message.ToolCall toolC
}
}

private ToolResult executeToolCall(Message.ToolCall toolCall) {
private ToolResult executeToolCall(AgentContext context, Message.ToolCall toolCall) {
String toolName = sanitizeToolName(toolCall.getName());
ToolComponent tool = toolRegistry.get(toolName);
ToolComponent tool = resolveTool(context, toolName);

if (tool == null) {
String available = String.join(", ", toolRegistry.keySet());
String available = String.join(", ", resolveAvailableToolNames(context));
return ToolResult.failure(ToolFailureKind.POLICY_DENIED,
"Unknown tool: " + toolName + ". Available tools: " + available);
}
Expand All @@ -150,6 +152,36 @@ private ToolResult executeToolCall(Message.ToolCall toolCall) {
}
}

@SuppressWarnings("unchecked")
private ToolComponent resolveTool(AgentContext context, String toolName) {
if (context != null) {
Object scopedTools = context.getAttribute(ContextAttributes.CONTEXT_SCOPED_TOOLS);
if (scopedTools instanceof Map<?, ?> map) {
Object candidate = map.get(toolName);
if (candidate instanceof ToolComponent tool) {
return tool;
}
}
}
return toolRegistry.get(toolName);
}

@SuppressWarnings("unchecked")
private List<String> resolveAvailableToolNames(AgentContext context) {
TreeSet<String> names = new TreeSet<>(toolRegistry.keySet());
if (context != null) {
Object scopedTools = context.getAttribute(ContextAttributes.CONTEXT_SCOPED_TOOLS);
if (scopedTools instanceof Map<?, ?> map) {
for (Object key : map.keySet()) {
if (key instanceof String name && !name.isBlank()) {
names.add(name);
}
}
}
}
return List.copyOf(names);
}

private static String safeCauseMessage(Throwable error) {
if (error == null) {
return "unknown";
Expand Down
Loading
Loading