content: /*
   * Copyright 2025 the original author or authors.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      https://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.alibaba.cloud.ai.studio.core.agent;
  
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.ChatMessage;
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.ContentType;
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.MessageRole;
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.MultimodalContent;
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.ToolCall;
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.ToolCallType;
  import com.alibaba.cloud.ai.studio.runtime.domain.chat.Usage;
  import com.alibaba.cloud.ai.studio.runtime.exception.BizException;
  import com.alibaba.cloud.ai.studio.runtime.enums.ErrorCode;
  import com.alibaba.cloud.ai.studio.runtime.domain.RequestContext;
  import com.alibaba.cloud.ai.studio.runtime.domain.agent.AgentRequest;
  import com.alibaba.cloud.ai.studio.runtime.domain.agent.AgentResponse;
  import com.alibaba.cloud.ai.studio.runtime.domain.agent.AgentStatus;
  import com.alibaba.cloud.ai.studio.runtime.domain.app.AgentConfig;
  import com.alibaba.cloud.ai.studio.runtime.domain.app.FileSearchOptions;
  import com.alibaba.cloud.ai.studio.runtime.domain.knowledgebase.DocumentChunk;
  import com.alibaba.cloud.ai.studio.runtime.utils.JsonUtils;
  import com.alibaba.cloud.ai.studio.core.base.service.McpServerService;
  import com.alibaba.cloud.ai.studio.core.base.service.PluginService;
  import com.alibaba.cloud.ai.studio.core.base.service.ToolExecutionService;
  import com.alibaba.cloud.ai.studio.core.config.CommonConfig;
  import com.alibaba.cloud.ai.studio.core.context.RequestContextHolder;
  import com.alibaba.cloud.ai.studio.core.model.llm.ModelFactory;
  import com.alibaba.cloud.ai.studio.core.base.manager.AppComponentManager;
  import com.alibaba.cloud.ai.studio.core.base.manager.DocumentRetrieverManager;
  import com.alibaba.cloud.ai.studio.core.base.manager.FileManager;
  import com.alibaba.cloud.ai.studio.core.rag.advisor.KnowledgeBaseRetrievalAdvisor;
  import com.alibaba.cloud.ai.studio.core.agent.tool.AgentToolCallback;
  import com.alibaba.cloud.ai.studio.core.agent.tool.CompositeToolCallbackProvider;
  import com.alibaba.cloud.ai.studio.core.agent.tool.ToolArgumentsHelper;
  import com.alibaba.cloud.ai.studio.core.rag.DocumentChunkConverter;
  import com.alibaba.cloud.ai.studio.core.utils.io.FileUtils;
  import com.alibaba.cloud.ai.studio.core.utils.common.IdGenerator;
  import lombok.RequiredArgsConstructor;
  import org.apache.commons.lang3.ArrayUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.ai.chat.client.ChatClient;
  import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
  import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
  import org.springframework.ai.chat.client.advisor.api.Advisor;
  import org.springframework.ai.chat.memory.ChatMemory;
  import org.springframework.ai.chat.messages.*;
  import org.springframework.ai.chat.metadata.ChatResponseMetadata;
  import org.springframework.ai.chat.model.ChatModel;
  import org.springframework.ai.chat.model.ChatResponse;
  import org.springframework.ai.chat.model.Generation;
  import org.springframework.ai.chat.prompt.Prompt;
  import org.springframework.ai.chat.prompt.SystemPromptTemplate;
  import org.springframework.ai.content.Media;
  import org.springframework.ai.document.Document;
  import org.springframework.ai.model.tool.ToolCallingChatOptions;
  import org.springframework.ai.model.tool.ToolCallingManager;
  import org.springframework.ai.model.tool.ToolExecutionResult;
  import org.springframework.ai.openai.OpenAiChatOptions;
  import org.springframework.ai.rag.retrieval.search.DocumentRetriever;
  import org.springframework.ai.tool.ToolCallback;
  import org.springframework.ai.tool.ToolCallbackProvider;
  import org.springframework.beans.factory.annotation.Qualifier;
  import org.springframework.core.io.ByteArrayResource;
  import org.springframework.core.io.Resource;
  import org.springframework.http.MediaType;
  import org.springframework.stereotype.Service;
  import org.springframework.util.Assert;
  import org.springframework.util.CollectionUtils;
  import reactor.core.publisher.Flux;
  import reactor.core.publisher.Mono;
  
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.util.*;
  import java.util.stream.Collectors;
  
  import static com.alibaba.cloud.ai.studio.core.rag.RagConstants.FILE_SEARCH_CALL;
  import static com.alibaba.cloud.ai.studio.core.rag.RagConstants.FILE_SEARCH_RESULT;
  import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;
  
  /**
   * Basic agent executor implementation that handles agent interactions and tool
   * executions. Provides functionality for both streaming and non-streaming agent
   * responses.
   *
   * @since 1.0.0.3
   */
  @Service()
  @Qualifier("basicAgentExecutor")
  @RequiredArgsConstructor
  public class BasicAgentExecutor extends AbstractAgentExecutor {
  
  	/** Service for executing tools */
  	private final ToolExecutionService toolExecutionService;
  
  	/** Service for managing plugins */
  	private final PluginService pluginService;
  
  	/** Service for MCP server interactions */
  	private final McpServerService mcpServerService;
  
  	/** Manager for app components */
  	private final AppComponentManager appComponentManager;
  
  	/** Manager for document retrieval */
  	private final DocumentRetrieverManager documentRetrieverManager;
  
  	/** Chat memory for conversation history */
  	private final ChatMemory chatMemory;
  
  	/** Common configuration settings */
  	private final CommonConfig commonConfig;
  
  	/** Factory for creating chat models */
  	private final ModelFactory modelFactory;
  
  	/** Manager for file operations */
  	private final FileManager fileManager;
  
  	/**
  	 * Executes the agent request in streaming mode
  	 * @param context The agent context
  	 * @param request The agent request
  	 * @return Flux of agent responses
  	 */
  	@Override
  	public Flux<AgentResponse> streamExecute(AgentContext context, AgentRequest request) {
  		// TODO: Spring AI chat client design has limited extensibility for business use,
  		// consider redesigning by ourselves
  		// build chat options
  		RequestContext requestContext = RequestContextHolder.getRequestContext();
  		AgentConfig config = context.getConfig();
  		ToolCallingChatOptions chatOptions = buildChatOptions(config);
  
  		// build tool callback provider
  		ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
  		CompositeToolCallbackProvider toolCallbackProvider = buildToolCallbackProvider(config, request.getExtraPrams());
  
  		// build messages
  		List<Message> messages = buildMessages(context);
  
  		// build chat client
  		ChatClient.Builder chatClientBuilder = buildChatClient(context, chatOptions, toolCallbackProvider);
  
  		final Prompt prompt = new Prompt(messages, chatOptions);
  		return chatClientBuilder.build()
  			.prompt(prompt)
  			.stream()
  			.chatResponse()
  			.concatMap(response -> processToolCallsRecursively(chatClientBuilder, response, prompt, toolCallingManager,
  					toolCallbackProvider, requestContext, chatOptions));
  	}
  
  	/**
  	 * Executes the agent request in non-streaming mode
  	 * @param context The agent context
  	 * @param request The agent request
  	 * @return Agent response
  	 */
  	@Override
  	public AgentResponse execute(AgentContext context, AgentRequest request) {
  		AgentConfig config = context.getConfig();
  		// build chat options
  		ToolCallingChatOptions chatOptions = buildChatOptions(config);
  
  		// build tool callback provider
  		ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
  		CompositeToolCallbackProvider toolCallbackProvider = buildToolCallbackProvider(config, request.getExtraPrams());
  
  		// build messages
  		List<Message> messages = buildMessages(context);
  
  		// build chat client
  		ChatClient.Builder chatClientBuilder = buildChatClient(context, chatOptions, toolCallbackProvider);
  
  		Prompt prompt = new Prompt(messages, chatOptions);
  		ChatResponse response = chatClientBuilder.build().prompt(prompt).options(chatOptions).call().chatResponse();
  
  		Assert.notNull(response, "response can not be null");
  		if (response.hasToolCalls()) {
  			ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, response);
  
  			Prompt newPrompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);
  			ChatResponse chatResponse = chatClientBuilder.build().prompt(newPrompt).call().chatResponse();
  
  			Assert.notNull(chatResponse, "chat response can not be null");
  			AgentResponse agentResponse = convertResponse(chatResponse, toolCallbackProvider).block();
  
  			Assert.notNull(agentResponse, "agent response can not be null");
  
  			// handle tool results
  			Generation generation = response.getResults().get(0);
  			List<AssistantMessage.ToolCall> assistantToolCalls = generation.getOutput().getToolCalls();
  			List<ToolCall> toolCalls = new ArrayList<>(convertToolCall(assistantToolCalls, toolCallbackProvider));
  
  			List<ToolCall> toolCallResults = convertToolResult(toolExecutionResult, toolCallbackProvider);
  			toolCalls.addAll(toolCallResults);
  
  			if (!CollectionUtils.isEmpty(toolCalls)) {
  				agentResponse.getMessage().setToolCalls(toolCalls);
  			}
  
  			return agentResponse;
  		}
  
  		return convertResponse(response, toolCallbackProvider).block();
  	}
  
  	/**
  	 * Builds chat options based on agent configuration
  	 * @param config Agent configuration
  	 * @return OpenAiChatOptions instance
  	 */
  	private OpenAiChatOptions buildChatOptions(AgentConfig config) {
  		OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder()
  			.model(config.getModel())
  			.streamUsage(true)
  			.internalToolExecutionEnabled(false);
  
  		if (config.getParameter() != null) {
  			builder.maxTokens(config.getParameter().getMaxTokens())
  				.temperature(config.getParameter().getTemperature())
  				.topP(config.getParameter().getTopP())
  				.presencePenalty(config.getParameter().getRepetitionPenalty());
  		}
  
  		return builder.build();
  	}
  
  	/**
  	 * Creates a chat model based on configuration
  	 * @param config Agent configuration
  	 * @return ChatModel instance
  	 */
  	private ChatModel buildChatModel(AgentConfig config) {
  		return modelFactory.getChatModel(config.getModelProvider());
  	}
  
  	/**
  	 * Builds a chat client with necessary advisors and configurations
  	 * @param context Agent context
  	 * @param chatOptions Chat options
  	 * @param toolCallbackProvider Tool callback provider
  	 * @return ChatClient.Builder instance
  	 */
  	private ChatClient.Builder buildChatClient(AgentContext context, ToolCallingChatOptions chatOptions,
  			ToolCallbackProvider toolCallbackProvider) {
  		AgentConfig config = context.getConfig();
  		AgentRequest request = context.getRequest();
  
  		ChatModel chatModel = buildChatModel(config);
  		ChatClient chatClient = ChatClient.builder(chatModel).defaultAdvisors(new SimpleLoggerAdvisor()).build();
  
  		// Add chat memory advisor
  		ChatClient.Builder chatClientBuilder = chatClient.mutate();
  		if (context.isMemoryEnabled()) {
  			MessageChatMemoryAdvisor advisor = MessageChatMemoryAdvisor.builder(chatMemory).build();
  			int dialogRound = config.getMemory().getDialogRound();
  			chatClientBuilder.defaultAdvisors(advisor);
  			String conversationId = String.format("%s_%s", context.getAppId(), request.getConversationId());
  			// chatClientBuilder
  			// .defaultAdvisors(memoryAdvisor -> memoryAdvisor.param(CONVERSATION_ID,
  			// conversationId)
  			// .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, dialogRound));
  			chatClientBuilder.defaultAdvisors(memoryAdvisor -> memoryAdvisor.param(CONVERSATION_ID, conversationId));
  		}
  
  		// Add document retriever
  		FileSearchOptions fileSearchOptions = config.getFileSearch();
  		if (fileSearchOptions != null && fileSearchOptions.getEnableSearch()
  				&& !CollectionUtils.isEmpty(fileSearchOptions.getKbIds())) {
  			DocumentRetriever documentRetriever = documentRetrieverManager.getDocumentRetriever(fileSearchOptions);
  			Advisor retrievalAdvisor = KnowledgeBaseRetrievalAdvisor.builder()
  				.documentRetriever(documentRetriever)
  				.agentContext(context)
  				.commonConfig(commonConfig)
  				.build();
  			chatClientBuilder.defaultAdvisors(retrievalAdvisor);
  		}
  
  		// Add tool callbacks
  		ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();
  		if (!ArrayUtils.isEmpty(toolCallbacks)) {
  			chatOptions.setToolCallbacks(Arrays.stream(toolCallbacks).toList());
  		}
  
  		return chatClientBuilder;
  	}
  
  	/**
  	 * Builds message list from agent context
  	 * @param context Agent context
  	 * @return List of messages
  	 */
  	protected List<Message> buildMessages(AgentContext context) {
  		AgentConfig config = context.getConfig();
  		AgentRequest request = context.getRequest();
  
  		List<Message> messages = new ArrayList<>();
  		List<ChatMessage> chatMessages = request.getMessages();
  		if (StringUtils.isNotBlank(config.getInstructions())
  				&& !MessageRole.SYSTEM.getValue().equals(chatMessages.get(0).getRole().getValue())) {
  			Message message = buildInstructions(context, config.getInstructions());
  			messages.add(message);
  		}
  
  		for (ChatMessage chatMessage : chatMessages) {
  			Message message = null;
  			switch (chatMessage.getRole()) {
  				case SYSTEM -> message = buildInstructions(context, String.valueOf(chatMessage.getContent()));
  				case USER -> {
  					if (chatMessage.getContentType() == ContentType.TEXT) {
  						message = new UserMessage(String.valueOf(chatMessage.getContent()));
  					}
  					else if (chatMessage.getContentType() == ContentType.MULTIMODAL) {
  						message = buildMultimodelMessage(chatMessage.getContent());
  					}
  					else {
  						throw new BizException(ErrorCode.INVALID_PARAMS.toError("content_type",
  								chatMessage.getContentType().getValue() + "not supported"));
  					}
  				}
  				case ASSISTANT -> message = new AssistantMessage(String.valueOf(chatMessage.getContent()));
  			}
  
  			messages.add(message);
  		}
  
  		return messages;
  	}
  
  	/**
  	 * Builds system instructions message
  	 * @param context Agent context
  	 * @param instructions System instructions
  	 * @return Message instance
  	 */
  	private Message buildInstructions(AgentContext context, String instructions) {
  		AgentConfig config = context.getConfig();
  		AgentRequest request = context.getRequest();
  
  		// Composite file search prompts
  		FileSearchOptions searchOptions = config.getFileSearch();
  		boolean enableFileSearch = searchOptions != null && searchOptions.getEnableSearch();
  		if (enableFileSearch) {
  			if (StringUtils.isBlank(instructions)) {
  				instructions = commonConfig.getFileSearchPrompt();
  			}
  
  			if (searchOptions.getEnableCitation() != null && searchOptions.getEnableCitation()) {
  				instructions = commonConfig.getCitationPrompt() + "\n\n" + instructions;
  			}
  		}
  
  		// Handle custom prompt variables
  		List<AgentConfig.PromptVariable> promptVariables = config.getPromptVariables();
  		Map<String, Object> map = new HashMap<>();
  		if (!CollectionUtils.isEmpty(promptVariables)) {
  			for (AgentConfig.PromptVariable promptVariable : promptVariables) {
  				String variableName = promptVariable.getName();
  				String defaultValue = promptVariable.getDefaultValue();
  				if (StringUtils.isNotBlank(variableName)) {
  					map.put(variableName, defaultValue);
  				}
  			}
  
  			// Override default value
  			if (!CollectionUtils.isEmpty(request.getPromptVariables())) {
  				for (Map.Entry<String, String> variables : request.getPromptVariables().entrySet()) {
  					if (map.containsKey(variables.getKey())) {
  						map.put(variables.getKey(), variables.getValue());
  					}
  				}
  			}
  
  			// Remove empty value
  			map.keySet().removeIf(key -> {
  				Object value = map.get(key);
  				return value == null || StringUtils.isBlank(String.valueOf(value));
  			});
  		}
  
  		if (enableFileSearch) {
  			context.setPromptVariables(map);
  			return new SystemMessage(instructions);
  		}
  
  		return new SystemPromptTemplate(instructions).createMessage(map);
  	}
  
  	/**
  	 * Builds a multimodal message from content
  	 * @param obj Content object
  	 * @return Message instance
  	 */
  	private Message buildMultimodelMessage(Object obj) {
  		if (!(obj instanceof List)) {
  			throw new BizException(ErrorCode.INVALID_PARAMS.toError("content", "content must be list"));
  		}
  
  		List<MultimodalContent> content = (List<MultimodalContent>) obj;
  		String text = null;
  		List<Media> media = new ArrayList<>();
  		try {
  			for (MultimodalContent item : content) {
  				switch (item.getType()) {
  					case TEXT -> text = item.getText();
  					case IMAGE -> {
  						if (StringUtils.isNotBlank(item.getUrl())) {
  							String contentType = FileUtils.getContentType(item.getUrl());
  							MediaType mediaType = MediaType.parseMediaType(contentType);
  							media.add(Media.builder().mimeType(mediaType).data(new URL(item.getUrl())).build());
  						}
  						else if (StringUtils.isNotBlank(item.getPath())) {
  							String contentType = FileUtils.getContentType(item.getPath());
  							MediaType mediaType = MediaType.parseMediaType(contentType);
  							Resource resource = fileManager.loadFile(item.getPath());
  							media.add(new Media(mediaType, resource));
  						}
  						else if (StringUtils.isNotBlank(item.getData())) {
  							// format should be data:image/png;base64,iVBORw0KGgoSUhEUgAAA
  							int semicolonIndex = item.getData().indexOf(';');
  							if (semicolonIndex == -1) {
  								throw new IllegalArgumentException("Invalid data format");
  							}
  
  							String mimeType = item.getData().substring(0, semicolonIndex);
  							MediaType mediaType = MediaType.parseMediaType(mimeType);
  							media.add(new Media(mediaType, new ByteArrayResource(item.getData().getBytes())));
  						}
  						else {
  							throw new BizException(ErrorCode.INVALID_PARAMS.toError("content",
  									"image content must be url or path or data"));
  						}
  					}
  				}
  			}
  		}
  		catch (MalformedURLException e) {
  			throw new RuntimeException(e);
  		}
  
  		return UserMessage.builder().text(text).media(media).build();
  	}
  
  	/**
  	 * Converts chat response to agent response
  	 * @param chatResponse Chat response
  	 * @param toolCallbackProvider Tool callback provider
  	 * @return Mono of agent response
  	 */
  	protected Mono<AgentResponse> convertResponse(ChatResponse chatResponse,
  			CompositeToolCallbackProvider toolCallbackProvider) {
  		if (chatResponse == null) {
  			return Mono.empty();
  		}
  
  		org.springframework.ai.chat.metadata.Usage usage = chatResponse.getMetadata().getUsage();
  
  		AgentResponse.AgentResponseBuilder responseBuilder = AgentResponse.builder()
  			.model(chatResponse.getMetadata().getModel());
  
  		if (!CollectionUtils.isEmpty(chatResponse.getResults())) {
  			Generation generation = chatResponse.getResults().get(0);
  			String finishReason = generation.getMetadata().getFinishReason();
  
  			// Process reasoning content response
  			Object reasoningContentObj = generation.getOutput().getMetadata().get("reasoningContent");
  			String reasoningContent = reasoningContentObj == null ? null : String.valueOf(reasoningContentObj);
  			reasoningContent = StringUtils.isBlank(reasoningContent) ? null : reasoningContent;
  
  			ChatMessage.ChatMessageBuilder messageBuilder = ChatMessage.builder()
  				.role(MessageRole.ASSISTANT)
  				.content(generation.getOutput().getText())
  				.reasoningContent(reasoningContent);
  
  			List<ToolCall> toolCalls = new ArrayList<>();
  			if (!CollectionUtils.isEmpty(generation.getOutput().getToolCalls())) {
  				toolCalls.addAll(convertToolCall(generation.getOutput().getToolCalls(), toolCallbackProvider));
  			}
  
  			if (chatResponse.getMetadata() != null) {
  				toolCalls.addAll(convertToolCall(chatResponse.getMetadata()));
  			}
  
  			if (!CollectionUtils.isEmpty(toolCalls)) {
  				messageBuilder.toolCalls(toolCalls);
  			}
  
  			AgentStatus status = AgentStatus.toAgentStatus(finishReason);
  			responseBuilder.status(status).message(messageBuilder.build());
  
  			if (usage.getPromptTokens() > 0 || usage.getCompletionTokens() > 0 || usage.getTotalTokens() > 0) {
  				responseBuilder.usage(Usage.builder()
  					.promptTokens(usage.getPromptTokens())
  					.completionTokens(usage.getCompletionTokens())
  					.totalTokens(usage.getTotalTokens())
  					.build());
  			}
  		}
  		else {
  			return Mono.empty();
  		}
  
  		return Mono.just(responseBuilder.build());
  	}
  
  	/**
  	 * Converts tool calls to agent tool calls
  	 * @param toolCalls List of assistant tool calls
  	 * @param toolCallbackProvider tool callback provider
  	 * @return List of tool calls
  	 */
  	private List<ToolCall> convertToolCall(List<AssistantMessage.ToolCall> toolCalls,
  			CompositeToolCallbackProvider toolCallbackProvider) {
  		if (CollectionUtils.isEmpty(toolCalls)) {
  			return new ArrayList<>(0);
  		}
  
  		Map<String, AgentToolCallback> toolCallbackMap = getAgentToolCallback(toolCallbackProvider);
  
  		return toolCalls.stream().map(toolCall -> {
  			AgentToolCallback toolCallback = toolCallbackMap.get(toolCall.name());
  			ToolCallType type = ToolCallType.FUNCTION;
  			String arguments = toolCall.arguments();
  			if (toolCallback != null) {
  				type = toolCallback.getToolCallType();
  				Map<String, Object> argumentsMap = ToolArgumentsHelper.mergeToolArguments(toolCall.arguments(),
  						toolCallbackProvider.getExtraParams(), toolCallback.getId());
  				arguments = JsonUtils.toJson(argumentsMap);
  			}
  
  			return ToolCall.builder()
  				.id(toolCall.id())
  				.type(type)
  				.function(ToolCall.Function.builder().name(toolCall.name()).arguments(arguments).build())
  				.build();
  		}).collect(Collectors.toCollection(ArrayList::new));
  	}
  
  	/**
  	 * Converts chat response metadata to tool calls, especially file search call
  	 * @param metadata Chat response metadata
  	 * @return List of tool calls
  	 */
  	private List<ToolCall> convertToolCall(ChatResponseMetadata metadata) {
  		List<ToolCall> toolCalls = new ArrayList<>();
  		if (metadata.containsKey(FILE_SEARCH_CALL)) {
  			ToolCall toolCall = ToolCall.builder()
  				.id(IdGenerator.uuid())
  				.type(ToolCallType.FILE_SEARCH_CALL)
  				.function(
  						ToolCall.Function.builder().arguments(JsonUtils.toJson(metadata.get(FILE_SEARCH_CALL))).build())
  				.build();
  			toolCalls.add(toolCall);
  		}
  
  		if (metadata.containsKey(FILE_SEARCH_RESULT) && metadata.get(FILE_SEARCH_RESULT) != null) {
  			List<Document> documents = metadata.get(FILE_SEARCH_RESULT);
  			Assert.notNull(documents, "RAG_DOCUMENT_CONTEXT is not a list");
  
  			List<DocumentChunk> chunks = new ArrayList<>();
  			for (Document document : documents) {
  				chunks.add(DocumentChunkConverter.toDocumentChunk(document));
  			}
  
  			ToolCall toolCall = ToolCall.builder()
  				.id(IdGenerator.uuid())
  				.type(ToolCallType.FILE_SEARCH_RESULT)
  				.function(ToolCall.Function.builder().output(JsonUtils.toJson(chunks)).build())
  				.build();
  			toolCalls.add(toolCall);
  		}
  
  		return toolCalls;
  	}
  
  	/**
  	 * Converts tool execution result to tool calls
  	 * @param toolExecutionResult Tool execution result
  	 * @param toolCallbackProvider tool callback provider
  	 * @return List of tool calls
  	 */
  	private List<ToolCall> convertToolResult(ToolExecutionResult toolExecutionResult,
  			CompositeToolCallbackProvider toolCallbackProvider) {
  		List<ToolCall> toolResults = new ArrayList<>();
  		List<Message> conversationHistory = toolExecutionResult.conversationHistory();
  
  		Map<String, AgentToolCallback> toolCallbackMap = getAgentToolCallback(toolCallbackProvider);
  
  		if (conversationHistory
  			.get(conversationHistory.size() - 1) instanceof ToolResponseMessage toolResponseMessage) {
  			toolResponseMessage.getResponses().forEach(response -> {
  				// Map tool type to tool result type
  				AgentToolCallback toolCallback = toolCallbackMap.get(response.name());
  				ToolCallType type = toolCallback == null ? ToolCallType.TOOL_RESULT : toolCallback.getToolCallType();
  				ToolCallType resultType;
  				switch (type) {
  					case MCP_TOOL_CALL -> resultType = ToolCallType.MCP_TOOL_RESULT;
  					case COMPONENT_TOOL_CALL -> resultType = ToolCallType.COMPONENT_TOOL_RESULT;
  					default -> resultType = ToolCallType.TOOL_RESULT;
  				}
  
  				ToolCall toolCallOutput = ToolCall.builder()
  					.id(response.id())
  					.type(resultType)
  					.function(ToolCall.Function.builder().name(response.name()).output(response.responseData()).build())
  					.build();
  				toolResults.add(toolCallOutput);
  			});
  		}
  
  		return toolResults;
  	}
  
  	/**
  	 * Converts tool execution result to agent response
  	 * @param response Chat response
  	 * @param toolExecutionResult Tool execution result
  	 * @param toolCallbackProvider tool callback provider
  	 * @return Agent response
  	 */
  	protected AgentResponse convertToolResult(ChatResponse response, ToolExecutionResult toolExecutionResult,
  			CompositeToolCallbackProvider toolCallbackProvider) {
  		String model = response.getMetadata().getModel();
  		List<ToolCall> toolResults = convertToolResult(toolExecutionResult, toolCallbackProvider);
  
  		return AgentResponse.builder()
  			.model(model)
  			.message(ChatMessage.builder().role(MessageRole.ASSISTANT).content("").toolCalls(toolResults).build())
  			.status(AgentStatus.IN_PROGRESS)
  			.build();
  	}
  
  	/**
  	 * Builds tool callback provider
  	 * @param config Agent configuration
  	 * @param extraParams Extra parameters
  	 * @return CompositeToolCallbackProvider instance
  	 */
  	private CompositeToolCallbackProvider buildToolCallbackProvider(AgentConfig config,
  			Map<String, Object> extraParams) {
  		return new CompositeToolCallbackProvider(config, pluginService, toolExecutionService, mcpServerService,
  				appComponentManager, extraParams);
  	}
  
  	private Flux<AgentResponse> processToolCallsRecursively(ChatClient.Builder chatClientBuilder, ChatResponse response,
  			Prompt originalPrompt, ToolCallingManager toolCallingManager,
  			CompositeToolCallbackProvider toolCallbackProvider, RequestContext requestContext,
  			ToolCallingChatOptions chatOptions) {
  
  		if (!response.hasToolCalls()) {
  			return convertResponse(response, toolCallbackProvider).flux();
  		}
  
  		// Handle tool calling process
  		return Flux.concat(
  				// 1. Send tool call request
  				convertResponse(response, toolCallbackProvider).flux(),
  				// 2. Send tool execution result
  				Mono.fromCallable(() -> {
  					RequestContextHolder.setRequestContext(requestContext);
  					try {
  						ToolExecutionResult result = toolCallingManager.executeToolCalls(originalPrompt, response);
  						return new Object[] { result, convertToolResult(response, result, toolCallbackProvider) };
  					}
  					finally {
  						RequestContextHolder.clearRequestContext();
  					}
  				}).flux().flatMap(array -> {
  					ToolExecutionResult result = (ToolExecutionResult) array[0];
  					AgentResponse toolResult = (AgentResponse) array[1];
  
  					// Create new prompt with the tool execution result
  					Prompt newPrompt = new Prompt(result.conversationHistory(), chatOptions);
  
  					return Flux.concat(
  							// Send tool execution result
  							Flux.just(toolResult),
  							// 3. Send new prompt and recursively process any further tool
  							// calls
  							chatClientBuilder.build()
  								.prompt(newPrompt)
  								.stream()
  								.chatResponse()
  								// Key change: Recursively process the response
  								.concatMap(newResponse -> processToolCallsRecursively(chatClientBuilder, newResponse,
  										newPrompt, toolCallingManager, toolCallbackProvider, requestContext,
  										chatOptions)));
  				}));
  	}
  
  	/**
  	 * Get agent tool callback
  	 * @param toolCallbackProvider tool callback provider
  	 * @return agent tool callback
  	 */
  	private Map<String, AgentToolCallback> getAgentToolCallback(ToolCallbackProvider toolCallbackProvider) {
  		Map<String, AgentToolCallback> agentToolCallbackMap = new HashMap<>();
  		ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();
  		if (ArrayUtils.isEmpty(toolCallbacks)) {
  			return agentToolCallbackMap;
  		}
  
  		for (ToolCallback toolCallback : toolCallbacks) {
  			if (toolCallback instanceof AgentToolCallback agentToolCallback) {
  				agentToolCallbackMap.put(agentToolCallback.getToolDefinition().name(), agentToolCallback);
  			}
  		}
  
  		return agentToolCallbackMap;
  	}
  
  }
  