feat: resolve block LLM credentials from user vault
This commit is contained in:
parent
d7783e0d23
commit
86dfe1bb98
|
|
@ -5,5 +5,6 @@ public record AssistantConfigView(
|
|||
String defaultModel,
|
||||
String availableProvidersRetrieverUrl,
|
||||
String availableModelsRetrieverUrlTemplate,
|
||||
AssistantModelSelection defaultPhaseModels) {
|
||||
AssistantModelSelection defaultPhaseModels,
|
||||
String providerCatalogUrl) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package it.cnr.isti.workflow.manager.configurations.retrievers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import it.cnr.isti.workflow.manager.auth.repo.LoginEntity;
|
||||
import it.cnr.isti.workflow.manager.vault.UserSecretService;
|
||||
import it.cnr.isti.workflow.manager.vault.model.VaultSecretView;
|
||||
|
||||
@Component
|
||||
public class UserSecretsFieldRetriever implements SecureDynamicFieldRetriever {
|
||||
|
||||
private final UserSecretService userSecretService;
|
||||
|
||||
public UserSecretsFieldRetriever(UserSecretService userSecretService) {
|
||||
this.userSecretService = userSecretService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "UserSecrets";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RetrieverItem> retrieve(String parameter, Map<String, String> params, LoginEntity user) {
|
||||
if (!"forProvider".equals(parameter)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Unknown UserSecrets retriever parameter: " + parameter);
|
||||
}
|
||||
if (user == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authenticated user is required");
|
||||
}
|
||||
String provider = params == null ? null : params.get("provider");
|
||||
return userSecretService.list(user.getUsername()).stream()
|
||||
.filter(VaultSecretView::active)
|
||||
.filter(secret -> !StringUtils.hasText(provider) || secret.provider().equalsIgnoreCase(provider))
|
||||
.map(secret -> new RetrieverItem(
|
||||
new RetrieverItemDescriptor(secret.label(), secret.description(), metadata(secret)),
|
||||
secret.id(), false, true, List.of()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> metadata(VaultSecretView secret) {
|
||||
java.util.LinkedHashMap<String, Object> metadata = new java.util.LinkedHashMap<>();
|
||||
metadata.put("id", secret.id());
|
||||
metadata.put("provider", secret.provider());
|
||||
if (secret.lastUsedAt() != null) {
|
||||
metadata.put("lastUsedAt", secret.lastUsedAt());
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ public class AssistantController {
|
|||
|
||||
private static final String PROVIDERS_RETRIEVER_URL = "/retriever/LLM/providers";
|
||||
private static final String MODELS_RETRIEVER_URL_TEMPLATE = "/retriever/LLM/models?provider={provider}";
|
||||
private static final String PROVIDER_CATALOG_URL = "/llm/providers";
|
||||
|
||||
@Autowired
|
||||
private FlowAssistantService flowAssistantService;
|
||||
|
|
@ -121,7 +122,8 @@ public class AssistantController {
|
|||
defaultAssistantModel,
|
||||
PROVIDERS_RETRIEVER_URL,
|
||||
MODELS_RETRIEVER_URL_TEMPLATE,
|
||||
new AssistantModelSelection(defaultPlanningModel, defaultJsonModel, defaultRepairModel));
|
||||
new AssistantModelSelection(defaultPlanningModel, defaultJsonModel, defaultRepairModel),
|
||||
PROVIDER_CATALOG_URL);
|
||||
}
|
||||
|
||||
@PostMapping("/sessions")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package it.cnr.isti.workflow.manager.controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMProviderCatalogService;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMProviderMetadata;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/llm/providers")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public class LLMProviderCatalogController {
|
||||
|
||||
private final LLMProviderCatalogService catalogService;
|
||||
|
||||
public LLMProviderCatalogController(LLMProviderCatalogService catalogService) {
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "List LLM provider capabilities", description = "Returns non-sensitive provider metadata for assistant and block configuration UIs.")
|
||||
public List<LLMProviderMetadata> list() {
|
||||
return catalogService.list();
|
||||
}
|
||||
}
|
||||
|
|
@ -1495,14 +1495,11 @@ public class ExecutionsService {
|
|||
return;
|
||||
}
|
||||
LLMProvider provider = resolveProvider(descriptor.provider());
|
||||
// LLM credentials are always references to the user vault and are resolved at call time.
|
||||
// Do not expose a raw authorization requirement in an execution view or snapshot.
|
||||
if (provider == null || !provider.requiresAuthorization()) {
|
||||
return;
|
||||
}
|
||||
String key = provider.authorizationKey();
|
||||
requirements.computeIfAbsent(key,
|
||||
ignored -> new RequirementAccumulator(key, provider.getName(), provider.authorizationFieldName(),
|
||||
provider.authorizationDescription()))
|
||||
.addStepReference(block == null ? null : block.getId(), stepName);
|
||||
}
|
||||
|
||||
private void collectHttpRequirement(Map<String, RequirementAccumulator> requirements, Block<?> block) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import it.cnr.isti.workflow.manager.executions.steps.Input;
|
|||
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.ChatMessage;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Component
|
||||
|
|
@ -34,6 +35,9 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<ChatInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -57,7 +61,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
|
||||
LLMDescriptor chatDescriptor = configuration.getLlmDescriptor();
|
||||
LLMProvider chatProvider = resolveProvider(chatDescriptor.provider());
|
||||
String chatAuthorization = resolveAuthorization(chatProvider, chatDescriptor, authorizations);
|
||||
String chatAuthorization = resolveAuthorization(chatProvider, chatDescriptor, executionVariables);
|
||||
if (eventLogger != null) {
|
||||
eventLogger.info(ExecutionEventType.LLM_REQUEST,
|
||||
"Calling chat model " + chatDescriptor.model(),
|
||||
|
|
@ -65,7 +69,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
}
|
||||
|
||||
LLMProvider simulatorProvider = resolveProvider(simulatorDescriptor.provider());
|
||||
String simulatorAuthorization = resolveAuthorization(simulatorProvider, simulatorDescriptor, authorizations);
|
||||
String simulatorAuthorization = resolveAuthorization(simulatorProvider, simulatorDescriptor, executionVariables);
|
||||
|
||||
String resolvedGoal = resolvePlaceholders(configuration.getGoalDescription(), inputs, executionVariables);
|
||||
List<String> history = new ArrayList<>();
|
||||
|
|
@ -115,11 +119,7 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
"Calling chat model " + llmDescriptor.model(),
|
||||
Map.of("provider", llmDescriptor.provider(), "model", llmDescriptor.model()));
|
||||
}
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey))))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
String authorization = resolveAuthorization(llmProvider, llmDescriptor, executionVariables);
|
||||
|
||||
if (interaction.containsKey(ChatInteractionBlockFactory.INTERACTION_FIELD)) {
|
||||
Object messageValue = interaction.get(ChatInteractionBlockFactory.INTERACTION_FIELD);
|
||||
|
|
@ -136,9 +136,9 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
.collect(Collectors.toList());
|
||||
messages.add(new ChatMessage(ChatMessage.Role.USER, resolvedMessage));
|
||||
|
||||
String assistantResponse = llmProvider.requiresAuthorization()
|
||||
? llmProvider.chat(llmDescriptor.model(), List.copyOf(messages), String.valueOf(authorizations.get(authKey)))
|
||||
: llmProvider.chat(llmDescriptor.model(), List.copyOf(messages));
|
||||
String assistantResponse = authorization == null
|
||||
? llmProvider.chat(llmDescriptor.model(), List.copyOf(messages))
|
||||
: llmProvider.chat(llmDescriptor.model(), List.copyOf(messages), authorization);
|
||||
|
||||
List<String> updatedHistory = new ArrayList<>(history);
|
||||
updatedHistory.add(formatConversationLine(ChatMessage.Role.USER, resolvedMessage));
|
||||
|
|
@ -174,15 +174,9 @@ public class ChatInteractionExecutor implements BlockExecutor<ChatInteractionBlo
|
|||
return llmProvider;
|
||||
}
|
||||
|
||||
private String resolveAuthorization(LLMProvider provider, LLMDescriptor descriptor, Map<String, Object> authorizations) {
|
||||
if (!provider.requiresAuthorization()) {
|
||||
return null;
|
||||
}
|
||||
String authKey = provider.authorizationKey();
|
||||
if (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey)))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + descriptor.provider());
|
||||
}
|
||||
return String.valueOf(authorizations.get(authKey));
|
||||
private String resolveAuthorization(LLMProvider provider, LLMDescriptor descriptor,
|
||||
Map<String, Object> executionVariables) {
|
||||
return credentialResolver.resolve(provider, descriptor, executionVariables);
|
||||
}
|
||||
|
||||
private String generateSimulatorMessage(LLMProvider simulatorProvider, LLMDescriptor simulatorDescriptor,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import it.cnr.isti.workflow.manager.executions.executors.BooleanLlmResponseParse
|
|||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Component
|
||||
|
|
@ -45,6 +46,9 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<ConditionalBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -93,16 +97,12 @@ public class ConditionalExecutor implements BlockExecutor<ConditionalBlockType>
|
|||
Map<String, Object> authorizations, Map<String, Object> executionVariables) {
|
||||
LLMDescriptor llmDescriptor = config.getLlmDescriptor();
|
||||
LLMProvider llmProvider = resolveProvider(llmDescriptor.provider());
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(authorizations.get(authKey).toString()))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
String authorization = credentialResolver.resolve(llmProvider, llmDescriptor, executionVariables);
|
||||
|
||||
String prompt = buildLlmPrompt(config, inputValues, executionVariables);
|
||||
String response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString())
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
String response = authorization == null
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt)
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt, authorization);
|
||||
try {
|
||||
return parseBooleanResponse(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import it.cnr.isti.workflow.manager.executions.InteractionResult;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Component
|
||||
|
|
@ -29,6 +30,9 @@ public class HumanDecisionExecutor implements BlockExecutor<HumanDecisionBlockTy
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<HumanDecisionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -56,11 +60,7 @@ public class HumanDecisionExecutor implements BlockExecutor<HumanDecisionBlockTy
|
|||
if (llmProvider == null) {
|
||||
throw new IllegalArgumentException("Provider not found: " + simulatorDescriptor.provider());
|
||||
}
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey))))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + simulatorDescriptor.provider());
|
||||
}
|
||||
String authorization = credentialResolver.resolve(llmProvider, simulatorDescriptor, executionVariables);
|
||||
|
||||
String context = inputs.stream()
|
||||
.map(input -> "%s= %s".formatted(input.getDescriptor().getName(), input.getValue()))
|
||||
|
|
@ -81,9 +81,9 @@ public class HumanDecisionExecutor implements BlockExecutor<HumanDecisionBlockTy
|
|||
configuration.isRationaleRequired() ? "RATIONALE: <short rationale>" : "");
|
||||
prompt = BiasRuntimeSupport.decoratePrompt(prompt, executionVariables);
|
||||
|
||||
String response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(simulatorDescriptor.model(), prompt, String.valueOf(authorizations.get(authKey)))
|
||||
: llmProvider.generate(simulatorDescriptor.model(), prompt);
|
||||
String response = authorization == null
|
||||
? llmProvider.generate(simulatorDescriptor.model(), prompt)
|
||||
: llmProvider.generate(simulatorDescriptor.model(), prompt, authorization);
|
||||
if (!StringUtils.hasText(response)) {
|
||||
throw new IllegalArgumentException("Simulated HumanDecision produced an empty response");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import it.cnr.isti.workflow.manager.executions.InteractionResult;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Component
|
||||
|
|
@ -27,6 +28,9 @@ public class HumanInteractionExecutor implements BlockExecutor<HumanInteractionB
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<HumanInteractionBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -61,17 +65,11 @@ public class HumanInteractionExecutor implements BlockExecutor<HumanInteractionB
|
|||
if (llmProvider == null) {
|
||||
throw new IllegalArgumentException("Provider not found: " + llmDescriptor.provider());
|
||||
}
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization() && (!authorizations.containsKey(authKey) || !StringUtils.hasText(authorizations.get(authKey).toString()))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
String authorization = credentialResolver.resolve(llmProvider, llmDescriptor, executionVariables);
|
||||
|
||||
String response;
|
||||
if (llmProvider.requiresAuthorization()) {
|
||||
response = llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString());
|
||||
} else {
|
||||
response = llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
}
|
||||
String response = authorization == null
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt)
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt, authorization);
|
||||
|
||||
return Map.of("output", response);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import it.cnr.isti.workflow.manager.executions.ExecutionTemplateResolver;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
import it.cnr.isti.workflow.manager.skills.SkillPromptService;
|
||||
|
||||
|
|
@ -34,6 +35,9 @@ public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
|||
@Autowired
|
||||
private SkillPromptService skillPromptService;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<LLMBlockType> block, List<Input> inputs, Map<String, Object> authorizations,
|
||||
Map<String, Object> executionVariables, Map<String, ExecutionVariableDescriptor> executionVariableDescriptors,
|
||||
|
|
@ -68,17 +72,11 @@ public class LLMExecutor implements BlockExecutor<LLMBlockType> {
|
|||
if (llmProvider == null) {
|
||||
throw new IllegalArgumentException("Provider not found: " + llmDescriptor.provider());
|
||||
}
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization() && (!authorizations.containsKey(authKey) || !StringUtils.hasText(authorizations.get(authKey).toString()))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
String authorization = credentialResolver.resolve(llmProvider, llmDescriptor, executionVariables);
|
||||
|
||||
String response;
|
||||
if (llmProvider.requiresAuthorization()) {
|
||||
response = llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString());
|
||||
} else {
|
||||
response = llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
}
|
||||
String response = authorization == null
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt)
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt, authorization);
|
||||
|
||||
if (eventLogger != null) {
|
||||
eventLogger.info(ExecutionEventType.LLM_REQUEST, "Called LLM model " + llmDescriptor.model(),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import it.cnr.isti.workflow.manager.executions.InteractionResult;
|
|||
import it.cnr.isti.workflow.manager.executions.steps.Input;
|
||||
import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPAgentService;
|
||||
import it.cnr.isti.workflow.manager.mcp.MCPSharedSessionRegistry;
|
||||
|
|
@ -40,6 +41,9 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<MCPAgentChatBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -66,7 +70,7 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
throw new IllegalArgumentException("Missing simulation descriptor for MCPAgentChat execution");
|
||||
}
|
||||
LLMProvider simulatorProvider = resolveProvider(simulatorDescriptor.provider());
|
||||
String simulatorAuthorization = resolveAuthorization(simulatorProvider, simulatorDescriptor, authorizations);
|
||||
String simulatorAuthorization = resolveAuthorization(simulatorProvider, simulatorDescriptor, executionVariables);
|
||||
String resolvedGoal = resolvePlaceholders(configuration.getGoalDescription(), inputs, executionVariables);
|
||||
resolvedGoal = BiasRuntimeSupport.decoratePrompt(resolvedGoal, executionVariables);
|
||||
List<String> history = new ArrayList<>();
|
||||
|
|
@ -225,15 +229,9 @@ public class MCPAgentChatExecutor implements BlockExecutor<MCPAgentChatBlockType
|
|||
.orElseThrow(() -> new IllegalArgumentException("Provider not found: " + providerName));
|
||||
}
|
||||
|
||||
private String resolveAuthorization(LLMProvider provider, LLMDescriptor descriptor, Map<String, Object> authorizations) {
|
||||
if (!provider.requiresAuthorization()) {
|
||||
return null;
|
||||
}
|
||||
String authKey = provider.authorizationKey();
|
||||
if (!authorizations.containsKey(authKey) || !StringUtils.hasText(String.valueOf(authorizations.get(authKey)))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + descriptor.provider());
|
||||
}
|
||||
return String.valueOf(authorizations.get(authKey));
|
||||
private String resolveAuthorization(LLMProvider provider, LLMDescriptor descriptor,
|
||||
Map<String, Object> executionVariables) {
|
||||
return credentialResolver.resolve(provider, descriptor, executionVariables);
|
||||
}
|
||||
|
||||
private String generateSimulatorMessage(LLMProvider simulatorProvider, LLMDescriptor simulatorDescriptor,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import it.cnr.isti.workflow.manager.executions.bias.runtime.BiasRuntimeSupport;
|
|||
import it.cnr.isti.workflow.manager.ios.IODescriptor;
|
||||
import it.cnr.isti.workflow.manager.ios.IOType;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMCredentialResolver;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Component
|
||||
|
|
@ -51,6 +52,9 @@ public class SwitchExecutor implements BlockExecutor<SwitchBlockType> {
|
|||
@Autowired
|
||||
private Map<String, LLMProvider> llmProviders;
|
||||
|
||||
@Autowired
|
||||
private LLMCredentialResolver credentialResolver;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Block<SwitchBlockType> block, List<Input> inputs,
|
||||
Map<String, Object> authorizations, Map<String, Object> executionVariables,
|
||||
|
|
@ -119,16 +123,12 @@ public class SwitchExecutor implements BlockExecutor<SwitchBlockType> {
|
|||
Map<String, Object> authorizations, Map<String, Object> executionVariables, Set<String> allowedOutputs) {
|
||||
LLMDescriptor llmDescriptor = config.getLlmDescriptor();
|
||||
LLMProvider llmProvider = resolveProvider(llmDescriptor.provider());
|
||||
String authKey = llmProvider.authorizationKey();
|
||||
if (llmProvider.requiresAuthorization()
|
||||
&& (!authorizations.containsKey(authKey) || !StringUtils.hasText(authorizations.get(authKey).toString()))) {
|
||||
throw new IllegalArgumentException("Missing authorization for provider: " + llmDescriptor.provider());
|
||||
}
|
||||
String authorization = credentialResolver.resolve(llmProvider, llmDescriptor, executionVariables);
|
||||
|
||||
String prompt = buildLlmPrompt(config, inputValues, executionVariables, allowedOutputs);
|
||||
String response = llmProvider.requiresAuthorization()
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt, authorizations.get(authKey).toString())
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt);
|
||||
String response = authorization == null
|
||||
? llmProvider.generate(llmDescriptor.model(), prompt)
|
||||
: llmProvider.generate(llmDescriptor.model(), prompt, authorization);
|
||||
return parseSelectedOutput(response, allowedOutputs);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package it.cnr.isti.workflow.manager.llms;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionRuntimeContextSupport;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
import it.cnr.isti.workflow.manager.vault.UserSecretService;
|
||||
|
||||
/** Resolves a flow's credential reference without ever putting plaintext in its persisted state. */
|
||||
@Component
|
||||
public class LLMCredentialResolver {
|
||||
|
||||
private final UserSecretService userSecretService;
|
||||
|
||||
public LLMCredentialResolver(UserSecretService userSecretService) {
|
||||
this.userSecretService = userSecretService;
|
||||
}
|
||||
|
||||
public String resolve(LLMProvider provider, LLMDescriptor descriptor, Map<String, Object> executionVariables) {
|
||||
if (!provider.requiresAuthorization()) {
|
||||
return null;
|
||||
}
|
||||
if (!StringUtils.hasText(descriptor.credentialId())) {
|
||||
throw new IllegalArgumentException("Missing credentialId for provider: " + descriptor.provider());
|
||||
}
|
||||
String owner = executionVariables == null ? null
|
||||
: asText(executionVariables.get(ExecutionRuntimeContextSupport.EXECUTION_OWNER));
|
||||
if (!StringUtils.hasText(owner)) {
|
||||
throw new IllegalArgumentException(
|
||||
"A user-owned execution is required to resolve credential for provider: " + provider.getName());
|
||||
}
|
||||
return userSecretService.resolveValue(owner, descriptor.credentialId().trim(), provider.getName());
|
||||
}
|
||||
|
||||
private String asText(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
package it.cnr.isti.workflow.manager.llms;
|
||||
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever;
|
||||
import it.cnr.isti.workflow.manager.configurations.annotations.UiDescription;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public record LLMDescriptor(
|
||||
@NotBlank @FieldRetriever(name = "LLM", url = "/retriever/LLM/providers") String provider,
|
||||
@NotBlank @FieldRetriever(name = "LLM", url = "/retriever/LLM/models", dependsOn = {"provider"}) String model) {}
|
||||
@NotBlank @FieldRetriever(name = "LLM", url = "/retriever/LLM/models", dependsOn = {"provider"}) String model,
|
||||
@UiDescription("Saved user credential. Required when the selected provider declares requiresCredential.")
|
||||
@FieldRetriever(name = "UserSecrets", url = "/secure-retriever/UserSecrets/forProvider/items", dependsOn = {"provider"}, requiresAuth = true)
|
||||
String credentialId) {
|
||||
|
||||
public LLMDescriptor(String provider, String model) {
|
||||
this(provider, model, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package it.cnr.isti.workflow.manager.llms;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
|
||||
@Service
|
||||
public class LLMProviderCatalogService {
|
||||
|
||||
private final Map<String, LLMProvider> providers;
|
||||
|
||||
public LLMProviderCatalogService(Map<String, LLMProvider> providers) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
public List<LLMProviderMetadata> list() {
|
||||
return providers.values().stream()
|
||||
.map(provider -> new LLMProviderMetadata(provider.getName(), provider.requiresAuthorization()))
|
||||
.filter(provider -> StringUtils.hasText(provider.name()))
|
||||
.distinct()
|
||||
.sorted(java.util.Comparator.comparing(LLMProviderMetadata::name, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package it.cnr.isti.workflow.manager.llms;
|
||||
|
||||
/** Public, non-sensitive capabilities used by every LLM selection UI. */
|
||||
public record LLMProviderMetadata(String name, boolean requiresCredential) {
|
||||
}
|
||||
|
|
@ -708,6 +708,7 @@ public class AssistantControllerTest {
|
|||
assertEquals(MODEL, config.defaultModel());
|
||||
assertEquals("/retriever/LLM/providers", config.availableProvidersRetrieverUrl());
|
||||
assertEquals("/retriever/LLM/models?provider={provider}", config.availableModelsRetrieverUrlTemplate());
|
||||
assertEquals("/llm/providers", config.providerCatalogUrl());
|
||||
assertNotNull(config.defaultPhaseModels());
|
||||
assertEquals(MODEL, config.defaultPhaseModels().planningModel());
|
||||
assertEquals(MODEL, config.defaultPhaseModels().jsonModel());
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import it.cnr.isti.workflow.manager.executions.ExecutionEvent;
|
|||
import it.cnr.isti.workflow.manager.executions.ExecutionEventType;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionSimulationRequest;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionStatus;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionAuthorizationValueRequest;
|
||||
import it.cnr.isti.workflow.manager.executions.ExecutionsService;
|
||||
import it.cnr.isti.workflow.manager.executions.api.ExecutionGroupView;
|
||||
import it.cnr.isti.workflow.manager.executions.api.ExecutionView;
|
||||
|
|
@ -47,6 +46,9 @@ import it.cnr.isti.workflow.manager.ios.IOType;
|
|||
import it.cnr.isti.workflow.manager.llms.ChatMessage;
|
||||
import it.cnr.isti.workflow.manager.llms.LLMDescriptor;
|
||||
import it.cnr.isti.workflow.manager.llms.providers.LLMProvider;
|
||||
import it.cnr.isti.workflow.manager.vault.UserSecretService;
|
||||
import it.cnr.isti.workflow.manager.vault.model.VaultSecretCreateRequest;
|
||||
import it.cnr.isti.workflow.manager.vault.model.VaultSecretView;
|
||||
|
||||
@SpringBootTest
|
||||
@TestPropertySource(locations = "classpath:test.properties")
|
||||
|
|
@ -91,6 +93,36 @@ public class ExecutionControllerTest {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LLMProvider vaultProvider() {
|
||||
return new LLMProvider() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VaultProvider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRegisteredModels() {
|
||||
return List.of("vault-model");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String model, String prompt) {
|
||||
throw new IllegalArgumentException("VaultProvider requires a user credential");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String model, String prompt, String authorization) {
|
||||
return "authorized:" + authorization;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresAuthorization() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
|
@ -105,6 +137,9 @@ public class ExecutionControllerTest {
|
|||
@Autowired
|
||||
private ExecutionsService executionsService;
|
||||
|
||||
@Autowired
|
||||
private UserSecretService userSecretService;
|
||||
|
||||
@Test
|
||||
public void createFlowAndExecution(){
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
|
|
@ -649,7 +684,7 @@ public class ExecutionControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void executionExposesRequiredAuthorizationsAndAcceptsProvidedValues() {
|
||||
public void externalLlmExecutionDoesNotExposeRawAuthorizationRequirements() {
|
||||
LLMDescriptor llmDescriptor = LLMDescriptor.builder()
|
||||
.provider("Gemini")
|
||||
.model("gemini-2.0-flash")
|
||||
|
|
@ -663,32 +698,40 @@ public class ExecutionControllerTest {
|
|||
|
||||
FlowCreateRequest request = new FlowCreateRequest(
|
||||
"Gemini Flow",
|
||||
"Flow requiring provider authorization",
|
||||
"Flow using an external provider",
|
||||
FlowData.builder().block(llmBlock).build());
|
||||
|
||||
ResponseEntity<FlowView> createdFlow = flowController.createFlow(request, new LoginEntity("testuser", "testpassword"));
|
||||
ExecutionView executionObject = executionsController.create(createdFlow.getBody().id(),
|
||||
new LoginEntity("testuser", "testpassword"));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, executionObject.getRequiredAuthorizations().size());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("LLMProvider::Gemini::authorization",
|
||||
executionObject.getRequiredAuthorizations().getFirst().key());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.getRequiredAuthorizations().isEmpty());
|
||||
}
|
||||
|
||||
String executionId = executionObject.getId();
|
||||
@Test
|
||||
public void executionResolvesProviderCredentialFromTheOwnerVault() {
|
||||
VaultSecretView secret = userSecretService.create("testuser", new VaultSecretCreateRequest(
|
||||
"vault-provider-" + java.util.UUID.randomUUID(), "VaultProvider", null, "vault-api-key"));
|
||||
LLMDescriptor descriptor = new LLMDescriptor("VaultProvider", "vault-model", secret.id());
|
||||
Block<LLMBlockType> block = blocksController.create(LLMBlockConfiguration.builder()
|
||||
.name("Vault-backed LLM")
|
||||
.llmDescriptor(descriptor)
|
||||
.prompt("Analyze ${{candidate}}")
|
||||
.build());
|
||||
ResponseEntity<FlowView> flow = flowController.createFlow(new FlowCreateRequest(
|
||||
"Vault LLM flow", "Uses a saved credential", FlowData.builder().block(block).build()), testUser());
|
||||
ExecutionView execution = executionsController.create(flow.getBody().id(), testUser());
|
||||
|
||||
executionsController.prepareStringInputs(executionId, llmBlock.getId(),
|
||||
llmBlock.getInputs().getFirst().getName(), "Ada Lovelace", testUser());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(execution.getRequiredAuthorizations().isEmpty());
|
||||
executionsController.prepareStringInputs(execution.getId(), block.getId(),
|
||||
block.getInputs().getFirst().getName(), "Ada", testUser());
|
||||
executionsController.start(execution.getId(), testUser());
|
||||
waitForExecutionStatus(execution, ExecutionStatus.SUCCESS);
|
||||
|
||||
IllegalStateException missingAuthorization = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executionsController.start(executionId, testUser()));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(missingAuthorization.getMessage().contains("LLMProvider::Gemini::authorization"));
|
||||
|
||||
executionObject = executionsController.provideAuthorization(executionId,
|
||||
new ExecutionAuthorizationValueRequest("LLMProvider::Gemini::authorization", "test-api-key"), testUser());
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertTrue(executionObject.getMissingAuthorizationKeys().isEmpty());
|
||||
org.junit.jupiter.api.Assertions.assertEquals(ExecutionStatus.READY, executionObject.getContext().getStatus());
|
||||
it.cnr.isti.workflow.manager.executions.ExecutionObject completed = executionsService.getExecution(execution.getId());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("authorized:vault-api-key",
|
||||
completed.getContext().getResult()
|
||||
.get(new it.cnr.isti.workflow.manager.executions.FieldKey(block.getId(), "response")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in New Issue