io.modelcontextprotocol.sdk
mcp
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/ConfluentMcpProxyApplication.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/AgentProxyApplication.java
similarity index 78%
rename from agent-proxy/src/main/java/io/confluent/pas/agent/proxy/ConfluentMcpProxyApplication.java
rename to agent-proxy/src/main/java/io/confluent/pas/agent/proxy/AgentProxyApplication.java
index 5531522..7ecde16 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/ConfluentMcpProxyApplication.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/AgentProxyApplication.java
@@ -8,10 +8,10 @@
@EnableAsync
@SpringBootApplication
@EnableConfigurationProperties
-public class ConfluentMcpProxyApplication {
+public class AgentProxyApplication {
public static void main(String[] args) {
- SpringApplication.run(ConfluentMcpProxyApplication.class, args);
+ SpringApplication.run(AgentProxyApplication.class, args);
}
}
\ No newline at end of file
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RegistrationCoordinator.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RegistrationCoordinator.java
index 3f3e67c..f6c1283 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RegistrationCoordinator.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RegistrationCoordinator.java
@@ -1,10 +1,7 @@
package io.confluent.pas.agent.proxy.registration;
import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;
-import io.confluent.pas.agent.common.services.KafkaConfiguration;
-import io.confluent.pas.agent.common.services.KafkaPropertiesFactory;
-import io.confluent.pas.agent.common.services.RegistrationService;
-import io.confluent.pas.agent.common.services.RegistrationServiceHandler;
+import io.confluent.pas.agent.common.services.*;
import io.confluent.pas.agent.common.services.schemas.Registration;
import io.confluent.pas.agent.common.services.schemas.RegistrationKey;
import io.confluent.pas.agent.proxy.registration.events.DeletedRegistrationEvent;
@@ -19,39 +16,46 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* The RegistrationCoordinator is the central component responsible for managing
- * tool registrations.
+ * tool registrations in a thread-safe and concurrent manner.
*
* It coordinates the following processes:
* - Listening for new registrations on the registration topic
- * - Processing incoming registrations and unregistrations
+ * - Processing incoming registrations and un-registrations concurrently
* - Creating and managing handlers for each registered tool
- * - Maintaining the lifecycle of registrations
+ * - Maintaining the lifecycle of registrations with proper synchronization
* - Broadcasting registration events to other components
+ * - Handling registration conflicts and updates atomically
*
* This class acts as the bridge between the Kafka-based registration system and
- * the protocol-specific servers (MCP, REST) that handle client communications.
+ * the protocol-specific servers (MCP, REST, A2A) that handle client communications.
+ * It ensures thread-safe operations when managing multiple concurrent registrations
+ * and unregistrations.
*/
@Slf4j
@Component
public class RegistrationCoordinator implements DisposableBean {
/**
- * MCP server for handling Model Context Protocol communications
+ * MCP server for handling Model Context Protocol communications asynchronously
*/
@Getter
private final McpAsyncServer mcpServer;
+ /**
+ * Agent-to-Agent async server for handling inter-agent communications
+ */
@Getter
private final A2AAsyncServer a2AAsyncServer;
/**
- * REST server for handling HTTP/REST communications
+ * REST server for handling synchronous HTTP communications with agents
*/
@Getter
private final AgentAsyncServer restServer;
@@ -63,7 +67,9 @@ public class RegistrationCoordinator implements DisposableBean {
private final RequestResponseHandler requestResponseHandler;
/**
- * Thread-safe map of registration handlers indexed by registration name
+ * Thread-safe map of registration handlers indexed by registration name.
+ * Uses ConcurrentHashMap to ensure atomic operations and visibility across threads
+ * when adding, removing, or accessing handlers.
*/
private final Map handlers = new ConcurrentHashMap<>();
@@ -105,7 +111,7 @@ public RegistrationCoordinator(KafkaConfiguration kafkaConfiguration,
// Create a registration handler that will forward registration events to our
// onRegistration method
// This avoids the circular reference issue during construction
- RegistrationServiceHandler.Handler registrationHandler = registrations -> {
+ CacheHandler.Handler registrationHandler = registrations -> {
if (registrations != null) {
onRegistration(registrations);
}
@@ -240,14 +246,16 @@ private void handleRegistration(RegistrationKey key, Registration registration)
// Handle registration update or creation
if (handlers.containsKey(registrationName)) {
log.info("Registration already exists, updating: {}", registrationName);
- // First unregister the existing handler to clean up resources
+ // First, unregister the existing handler to clean up resources
unregisterHandler(registrationName);
} else {
log.info("Received new registration: {}", registrationName);
}
// Create a new handler for the registration
- createHandler(registration, registrationName);
+ createHandler(registration, registrationName)
+ .doOnError(e -> log.error("Error creating handler for registration: {}", registrationName, e))
+ .block();
}
/**
@@ -259,7 +267,7 @@ private void handleRegistration(RegistrationKey key, Registration registration)
* @param registration The registration details
* @param registrationName The name of the registration
*/
- private void createHandler(Registration registration, String registrationName) {
+ private Mono createHandler(Registration registration, String registrationName) {
try {
// Create a new composite handler for the registration
final CompositeHandler handler = new CompositeHandler(
@@ -269,13 +277,11 @@ private void createHandler(Registration registration, String registrationName) {
this);
// Initialize the handler asynchronously and block until complete
- handler.initialize()
+ return handler.initialize()
.doOnSuccess(v -> handleSuccessfulRegistration(registrationName, handler, registration))
- .doOnError(e -> handleFailedRegistration(registrationName, e))
- .block();
+ .doOnError(e -> handleFailedRegistration(registrationName, e));
} catch (Exception e) {
- // Handle any exceptions that occur during handler creation
- log.error("Error creating handler for registration: {}", registrationName, e);
+ return Mono.error(e);
}
}
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseChannel.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseChannel.java
index ca1bcf6..198f456 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseChannel.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseChannel.java
@@ -70,8 +70,7 @@ void process(RequestResponseChannel channel,
*
* @param correlationId unique ID to correlate requests with responses
* @param registration service registration details
- * @param requestResponseHandler handler for sending requests and receiving
- * responses
+ * @param requestResponseHandler handler for sending requests and receiving responses
* @param processor callback for processing received responses
*/
public RequestResponseChannel(String correlationId,
@@ -96,8 +95,7 @@ public void close() {
}
/**
- * Registers success and error handlers for responses with the request-response
- * handler.
+ * Registers success and error handlers for responses with the request-response handler.
* This sets up the callback mechanism for processing responses.
*/
private void registerResponseHandler() {
@@ -136,8 +134,7 @@ private ConsumerService.ResponseHandler createSuccessResponseHandler(ResponsePro
}
/**
- * Creates a handler for error responses or exceptions that occur during
- * processing.
+ * Creates a handler for error responses or exceptions that occur during processing.
* This handler formats errors and forwards them to the processor.
*
* @param processor callback to forward the error information to
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandler.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandler.java
index ae5416d..f0f89b2 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandler.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandler.java
@@ -6,7 +6,6 @@
import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
import io.confluent.pas.agent.proxy.registration.kafka.ProducerService;
import io.confluent.pas.agent.proxy.registration.kafka.ConsumerService;
-import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,7 +16,11 @@
import java.util.Collection;
/**
- * Handle requests and responses
+ * Handles the request-response communication pattern using Kafka topics.
+ * This component manages the lifecycle of Kafka producers and consumers,
+ * handles message routing, and maintains registration of response handlers.
+ * It provides functionality to send requests and handle corresponding responses
+ * asynchronously using correlation IDs.
*/
@Slf4j
@Component
@@ -25,29 +28,49 @@ public class RequestResponseHandler implements DisposableBean {
private final ProducerService producerService;
private final ConsumerService consumerService;
- private final ObservationRegistry observationRegistry;
+ /**
+ * Creates a new RequestResponseHandler with auto-configured services.
+ *
+ * @param kafkaConfiguration The Kafka configuration to use for producers and consumers
+ * @param responseTimeout The maximum time to wait for responses in milliseconds (default: 10000)
+ */
@Autowired
public RequestResponseHandler(KafkaConfiguration kafkaConfiguration,
- ObservationRegistry observationRegistry,
@Value("${kafka.response.timeout:10000}") long responseTimeout) {
this(new ProducerService(kafkaConfiguration),
- new ConsumerService(kafkaConfiguration, responseTimeout),
- observationRegistry);
+ new ConsumerService(kafkaConfiguration, responseTimeout));
}
+ /**
+ * Creates a new RequestResponseHandler with pre-configured services.
+ *
+ * @param producerService The service responsible for sending messages to Kafka
+ * @param consumerService The service responsible for consuming messages from Kafka
+ */
public RequestResponseHandler(ProducerService producerService,
- ConsumerService consumerService,
- ObservationRegistry observationRegistry) {
+ ConsumerService consumerService) {
this.producerService = producerService;
this.consumerService = consumerService;
- this.observationRegistry = observationRegistry;
}
+ /**
+ * Adds multiple registrations to the consumer service for message handling.
+ *
+ * @param registrations Collection of registrations to be added
+ */
public void addRegistrations(Collection registrations) {
consumerService.addRegistrations(registrations);
}
+ /**
+ * Registers a response handler for a specific registration and correlation ID.
+ *
+ * @param registration The registration details for the handler
+ * @param correlationId The unique identifier to correlate requests with responses
+ * @param handler The handler for successful responses
+ * @param errorHandler The handler for error responses
+ */
public void registerHandler(Registration registration,
String correlationId,
ConsumerService.ResponseHandler handler,
@@ -59,16 +82,35 @@ public void registerHandler(Registration registration,
errorHandler);
}
+ /**
+ * Removes a previously registered response handler.
+ *
+ * @param registration The registration details for the handler
+ * @param correlationId The correlation ID of the handler to remove
+ */
public void unregisterHandler(Registration registration, String correlationId) {
consumerService.unregisterResponseHandler(registration, correlationId);
}
+ /**
+ * Sends a request message to the specified Kafka topic.
+ *
+ * @param registration The registration containing the topic information
+ * @param key The message key
+ * @param request The request payload as JsonNode
+ * @return A Mono that completes when the message is sent
+ */
public Mono sendRequest(Registration registration,
Key key,
JsonNode request) {
return producerService.send(registration.getRequestTopicName(), key, request);
}
+ /**
+ * Implements DisposableBean to properly close Kafka resources when the bean is destroyed.
+ *
+ * @throws Exception if an error occurs during resource cleanup
+ */
@Override
public void destroy() throws Exception {
consumerService.close();
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/handlers/mcp/McpResourceHandler.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/handlers/mcp/McpResourceHandler.java
index 08cbb89..f809487 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/handlers/mcp/McpResourceHandler.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/handlers/mcp/McpResourceHandler.java
@@ -17,6 +17,7 @@
import javax.naming.OperationNotSupportedException;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* Handler for Model Context Protocol (MCP) resource registration and request
@@ -45,8 +46,7 @@ public McpResourceHandler(ResourceRegistration registration,
RequestResponseHandler requestResponseHandler,
McpAsyncServer mcpServer) {
super(registration, schemas, mcpServer, requestResponseHandler, (payload) -> {
- final ResourceResponse.ResponseType responseType = ResourceResponse.ResponseType.fromValue(
- payload.get("type").asText());
+ final String responseType = payload.get("type").asText();
final McpSchema.ResourceContents content = createResourceContents(
JsonUtils.toMap(payload),
@@ -127,9 +127,9 @@ private McpServerFeatures.AsyncResourceSpecification createResourceRegistration(
*/
private static McpSchema.ResourceContents createResourceContents(
Map response,
- ResourceResponse.ResponseType responseType) {
+ String responseType) {
- if (responseType == ResourceResponse.ResponseType.BLOB) {
+ if (Objects.equals(responseType, ResourceResponse.BLOB_TYPE)) {
return createBlobContents(response);
} else {
return createTextContents(response);
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/Consumer.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/Consumer.java
index 6c5376a..8699f3f 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/Consumer.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/Consumer.java
@@ -1,225 +1,64 @@
package io.confluent.pas.agent.proxy.registration.kafka;
-import io.confluent.pas.agent.common.services.KafkaConfiguration;
-import io.confluent.pas.agent.common.services.KafkaPropertiesFactory;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
-import org.apache.kafka.common.errors.WakeupException;
import java.io.Closeable;
-import java.io.IOException;
-import java.time.Duration;
-import java.util.*;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
+import java.util.Collection;
+
/**
- * Consumer class for managing Kafka topic subscriptions and message processing.
+ * Interface for managing Kafka topic subscriptions and message consumption.
*
* @param Key type for Kafka messages
* @param Value type for Kafka messages
*/
-@Slf4j
-public class Consumer implements Closeable {
-
- @FunctionalInterface
- public interface TimeoutChecker {
- void checkTimeouts(long lastCheck);
- }
-
- private final ExecutorService executorSvc = Executors.newSingleThreadExecutor();
- private final KafkaConsumer kafkaConsumer;
-
- private final ConsumerHandler consumerHandler;
- private final List topics = Collections.synchronizedList(new ArrayList<>());
- private final TimeoutChecker timeoutChecker;
- private volatile boolean subscriptionUpdated = false;
- private volatile boolean stopRequested = false;
-
+public interface Consumer extends Closeable {
/**
- * Constructs a Consumer instance with the specified Kafka configuration and
- * message types.
- *
- * @param kafkaConfiguration Kafka configuration containing connection and auth details
- * @param requestClass Class type for message values
- * @param timeoutChecker The timeout checker to use
+ * Functional interface for checking timeouts in message processing.
*/
- public Consumer(KafkaConfiguration kafkaConfiguration,
- Class requestClass,
- ConsumerHandler consumerHandler,
- TimeoutChecker timeoutChecker) {
- this(consumerHandler,
- new KafkaConsumer<>(KafkaPropertiesFactory.getConsumerProperties(
- kafkaConfiguration,
- false,
- Key.class,
- requestClass)),
- timeoutChecker);
+ @FunctionalInterface
+ interface TimeoutChecker {
+ /**
+ * Check for any timeouts since the last check.
+ *
+ * @param lastCheck timestamp of the last timeout check
+ */
+ void checkTimeouts(long lastCheck);
}
/**
- * Constructs a Consumer instance with the specified Kafka consumer and handler.
+ * Checks if there is an active subscription for the specified topic.
*
- * @param consumerHandler The handler to process messages
- * @param kafkaConsumer The Kafka consumer to use
- * @param timeoutChecker The timeout checker to use
+ * @param topic the topic to check
+ * @return true if subscribed to the topic, false otherwise
*/
- public Consumer(ConsumerHandler consumerHandler,
- KafkaConsumer kafkaConsumer,
- TimeoutChecker timeoutChecker) {
- this.kafkaConsumer = kafkaConsumer;
- this.consumerHandler = consumerHandler;
- this.timeoutChecker = timeoutChecker;
-
- executorSvc.submit(this::runLoop);
- }
-
- public boolean isSubscribed(String topic) {
- return topics.contains(topic);
- }
+ boolean isSubscribed(String topic);
/**
- * Subscribes to a Kafka topic with the specified handler.
+ * Subscribes to a single Kafka topic.
*
- * @param topic The topic to subscribe to
+ * @param topic the topic to subscribe to
*/
- public void subscribe(String topic) {
- if (topics.contains(topic)) {
- throw new IllegalArgumentException("Subscription already exists for topic: " + topic);
- }
-
- topics.add(topic);
- subscriptionUpdated = true;
- }
+ void subscribe(String topic);
/**
- * Subscribes to a Kafka topic with the specified handler.
+ * Subscribes to multiple Kafka topics.
*
- * @param topicsToAdd The topics to subscribe to
+ * @param topicsToAdd collection of topics to subscribe to
*/
- public void subscribe(Collection topicsToAdd) {
- topics.addAll(topicsToAdd);
- subscriptionUpdated = true;
- }
+ void subscribe(Collection topicsToAdd);
/**
* Unsubscribes from a Kafka topic.
*
- * @param topic The topic to unsubscribe from
+ * @param topic the topic to unsubscribe from
*/
- public void unsubscribe(String topic) {
- topics.remove(topic);
- subscriptionUpdated = true;
- }
+ void unsubscribe(String topic);
/**
- * Closes the consumer, stopping the polling loop and releasing resources.
+ * Processes a single Kafka record.
*
- * @throws IOException if an error occurs while closing the consumer
+ * @param record the Kafka record to process
*/
- @Override
- public void close() throws IOException {
- stopRequested = true;
-
- kafkaConsumer.wakeup();
-
- try {
- executorSvc.shutdown();
-
- boolean done = executorSvc.awaitTermination(10, TimeUnit.SECONDS);
- if (!done) {
- log.error("Executor did not terminate");
- }
- } catch (InterruptedException e) {
- log.error("Error waiting for executor to terminate", e);
- }
- }
-
- /**
- * Main loop for polling Kafka messages and processing them.
- */
- void runLoop() {
- log.info("Starting Consumer Service");
-
- long lastCheck = System.currentTimeMillis();
-
- while (!stopRequested) {
- updateSubscriptions();
- if (topics.isEmpty()) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- log.warn("Interrupted while sleeping", e);
- Thread.currentThread().interrupt();
- }
-
- continue;
- }
-
- try {
- var records = kafkaConsumer.poll(Duration.ofMillis(100));
-
- // Process the records before checking for timeouts
- records.forEach(this::processRecord);
-
- try {
- // Check for timeouts in the registered handlers
- timeoutChecker.checkTimeouts(lastCheck);
-
- // Update the last check time
- lastCheck = System.currentTimeMillis();
- } catch (Exception e) {
- log.error("Error checking timeouts", e);
- }
-
- } catch (WakeupException e) {
- if (!stopRequested) {
- log.error("Unexpected WakeupException", e);
- throw e;
- }
- }
- }
-
- kafkaConsumer.close();
-
- log.info("Consumer Service stopped");
- }
-
- /**
- * Updates the Kafka topic subscriptions based on the current handlers.
- */
- private void updateSubscriptions() {
- if (subscriptionUpdated) {
- log.info("Updating subscriptions");
- kafkaConsumer.unsubscribe();
- if (!topics.isEmpty()) {
- kafkaConsumer.subscribe(topics);
- } else {
- log.info("No topics to subscribe to");
- }
- subscriptionUpdated = false;
- }
- }
-
- /**
- * Processes a single Kafka record by invoking the appropriate handler.
- *
- * @param record The Kafka record to process
- */
- void processRecord(ConsumerRecord record) {
- if (!topics.contains(record.topic())) {
- log.warn("Received message from unregistered topic: {}", record.topic());
- return;
- }
-
- try {
- log.info("Processing message from topic: {}", record.topic());
- consumerHandler.onMessage(record.topic(), record.key(), record.value());
- } catch (Exception e) {
- log.error("Failed to process message", e);
- }
- }
-}
\ No newline at end of file
+ void processRecord(ConsumerRecord record);
+}
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerService.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerService.java
index db1725f..752dc15 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerService.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerService.java
@@ -4,6 +4,7 @@
import io.confluent.pas.agent.common.services.KafkaConfiguration;
import io.confluent.pas.agent.common.services.schemas.Registration;
import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
+import io.confluent.pas.agent.proxy.registration.kafka.impl.ConsumerImpl;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@@ -17,8 +18,7 @@
/**
* Service that handles responses from Kafka topics by routing messages to
- * appropriate handlers
- * based on correlation IDs. This service:
+ * appropriate handlers based on correlation IDs. This service:
*
* - Subscribes to response topics for registered services
* - Routes messages to the correct handler using correlation IDs
@@ -82,8 +82,7 @@ public record RegistrationItem(
}
/**
- * Map of topic names to their registration items, which include handlers
- * indexed by correlation ID.
+ * Map of topic names to their registration items, which include handlers indexed by correlation ID.
*/
@Getter
private final Map responseHandlers = new ConcurrentHashMap<>();
@@ -103,7 +102,7 @@ public record RegistrationItem(
* timing out
*/
public ConsumerService(KafkaConfiguration kafkaConfiguration, long responseTimeout) {
- this.consumer = new Consumer<>(
+ this.consumer = new ConsumerImpl<>(
kafkaConfiguration,
JsonNode.class,
this::handleResponse,
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/impl/ConsumerImpl.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/impl/ConsumerImpl.java
new file mode 100644
index 0000000..8ffb2ff
--- /dev/null
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/impl/ConsumerImpl.java
@@ -0,0 +1,227 @@
+package io.confluent.pas.agent.proxy.registration.kafka.impl;
+
+import io.confluent.pas.agent.common.services.KafkaConfiguration;
+import io.confluent.pas.agent.common.services.KafkaPropertiesFactory;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
+import io.confluent.pas.agent.proxy.registration.kafka.Consumer;
+import io.confluent.pas.agent.proxy.registration.kafka.ConsumerHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.errors.WakeupException;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Consumer class for managing Kafka topic subscriptions and message processing.
+ *
+ * @param Key type for Kafka messages
+ * @param Value type for Kafka messages
+ */
+@Slf4j
+public class ConsumerImpl implements Closeable, Consumer {
+
+ private final ExecutorService executorSvc = Executors.newSingleThreadExecutor();
+ private final KafkaConsumer kafkaConsumer;
+ private final ConsumerHandler consumerHandler;
+ private final List topics = Collections.synchronizedList(new ArrayList<>());
+ private final TimeoutChecker timeoutChecker;
+ private volatile boolean subscriptionUpdated = false;
+ private volatile boolean stopRequested = false;
+
+ /**
+ * Constructs a Consumer instance with the specified Kafka configuration and
+ * message types.
+ *
+ * @param kafkaConfiguration Kafka configuration containing connection and auth details
+ * @param requestClass Class type for message values
+ * @param timeoutChecker The timeout checker to use
+ */
+ public ConsumerImpl(KafkaConfiguration kafkaConfiguration,
+ Class requestClass,
+ ConsumerHandler consumerHandler,
+ TimeoutChecker timeoutChecker) {
+ this(consumerHandler,
+ new KafkaConsumer<>(KafkaPropertiesFactory.getConsumerProperties(
+ kafkaConfiguration,
+ false,
+ Key.class,
+ requestClass)),
+ timeoutChecker);
+ }
+
+ /**
+ * Constructs a Consumer instance with the specified Kafka consumer and handler.
+ *
+ * @param consumerHandler The handler to process messages
+ * @param kafkaConsumer The Kafka consumer to use
+ * @param timeoutChecker The timeout checker to use
+ */
+ public ConsumerImpl(ConsumerHandler consumerHandler,
+ KafkaConsumer kafkaConsumer,
+ TimeoutChecker timeoutChecker) {
+ this.kafkaConsumer = kafkaConsumer;
+ this.consumerHandler = consumerHandler;
+ this.timeoutChecker = timeoutChecker;
+
+ executorSvc.submit(this::runLoop);
+ }
+
+ @Override
+ public boolean isSubscribed(String topic) {
+ return topics.contains(topic);
+ }
+
+ /**
+ * Subscribes to a Kafka topic with the specified handler.
+ *
+ * @param topic The topic to subscribe to
+ */
+ @Override
+ public void subscribe(String topic) {
+ if (topics.contains(topic)) {
+ throw new IllegalArgumentException("Subscription already exists for topic: " + topic);
+ }
+
+ topics.add(topic);
+ subscriptionUpdated = true;
+ }
+
+ /**
+ * Subscribes to a Kafka topic with the specified handler.
+ *
+ * @param topicsToAdd The topics to subscribe to
+ */
+ @Override
+ public void subscribe(Collection topicsToAdd) {
+ topics.addAll(topicsToAdd);
+ subscriptionUpdated = true;
+ }
+
+ /**
+ * Unsubscribes from a Kafka topic.
+ *
+ * @param topic The topic to unsubscribe from
+ */
+ @Override
+ public void unsubscribe(String topic) {
+ topics.remove(topic);
+ subscriptionUpdated = true;
+ }
+
+ /**
+ * Closes the consumer, stopping the polling loop and releasing resources.
+ *
+ * @throws IOException if an error occurs while closing the consumer
+ */
+ @Override
+ public void close() throws IOException {
+ stopRequested = true;
+
+ kafkaConsumer.wakeup();
+
+ try {
+ executorSvc.shutdown();
+
+ boolean done = executorSvc.awaitTermination(10, TimeUnit.SECONDS);
+ if (!done) {
+ log.error("Executor did not terminate");
+ }
+ } catch (InterruptedException e) {
+ log.error("Error waiting for executor to terminate", e);
+ }
+ }
+
+ /**
+ * Main loop for polling Kafka messages and processing them.
+ */
+ public void runLoop() {
+ log.info("Starting Consumer Service");
+
+ long lastCheck = System.currentTimeMillis();
+
+ while (!stopRequested) {
+ updateSubscriptions();
+ if (topics.isEmpty()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ log.warn("Interrupted while sleeping", e);
+ Thread.currentThread().interrupt();
+ }
+
+ continue;
+ }
+
+ try {
+ var records = kafkaConsumer.poll(Duration.ofMillis(100));
+
+ // Process the records before checking for timeouts
+ records.forEach(this::processRecord);
+
+ try {
+ // Check for timeouts in the registered handlers
+ timeoutChecker.checkTimeouts(lastCheck);
+
+ // Update the last check time
+ lastCheck = System.currentTimeMillis();
+ } catch (Exception e) {
+ log.error("Error checking timeouts", e);
+ }
+
+ } catch (WakeupException e) {
+ if (!stopRequested) {
+ log.error("Unexpected WakeupException", e);
+ throw e;
+ }
+ }
+ }
+
+ kafkaConsumer.close();
+
+ log.info("Consumer Service stopped");
+ }
+
+ /**
+ * Updates the Kafka topic subscriptions based on the current handlers.
+ */
+ private void updateSubscriptions() {
+ if (subscriptionUpdated) {
+ log.info("Updating subscriptions");
+ kafkaConsumer.unsubscribe();
+ if (!topics.isEmpty()) {
+ kafkaConsumer.subscribe(topics);
+ } else {
+ log.info("No topics to subscribe to");
+ }
+
+ subscriptionUpdated = false;
+ }
+ }
+
+ /**
+ * Processes a single Kafka record by invoking the appropriate handler.
+ *
+ * @param record The Kafka record to process
+ */
+ @Override
+ public void processRecord(ConsumerRecord record) {
+ if (!topics.contains(record.topic())) {
+ log.warn("Received message from unregistered topic: {}", record.topic());
+ return;
+ }
+
+ try {
+ log.info("Processing message from topic: {}", record.topic());
+ consumerHandler.onMessage(record.topic(), record.key(), record.value());
+ } catch (Exception e) {
+ log.error("Failed to process message", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/impl/DistributedConsumer.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/impl/DistributedConsumer.java
new file mode 100644
index 0000000..edd6f88
--- /dev/null
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/kafka/impl/DistributedConsumer.java
@@ -0,0 +1,28 @@
+package io.confluent.pas.agent.proxy.registration.kafka.impl;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import io.confluent.pas.agent.common.services.KafkaConfiguration;
+import io.confluent.pas.agent.proxy.registration.kafka.ConsumerHandler;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+
+import java.io.Closeable;
+
+public class DistributedConsumer extends ConsumerImpl implements Closeable {
+
+ private final Cache> cache = Caffeine.newBuilder()
+ .maximumSize(10000)
+ .expireAfterWrite(5, java.util.concurrent.TimeUnit.MINUTES)
+ .build();
+
+ public DistributedConsumer(KafkaConfiguration kafkaConfiguration, Class requestClass, ConsumerHandler consumerHandler, TimeoutChecker timeoutChecker) {
+ super(kafkaConfiguration, requestClass, consumerHandler, timeoutChecker);
+ }
+
+ public DistributedConsumer(ConsumerHandler consumerHandler, KafkaConsumer kafkaConsumer, TimeoutChecker timeoutChecker) {
+ super(consumerHandler, kafkaConsumer, timeoutChecker);
+ }
+
+
+}
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/models/WorkItem.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/models/WorkItem.java
new file mode 100644
index 0000000..e766995
--- /dev/null
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/models/WorkItem.java
@@ -0,0 +1,19 @@
+package io.confluent.pas.agent.proxy.registration.models;
+
+import io.confluent.pas.agent.proxy.frameworks.java.models.Request;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class WorkItem {
+
+ private Request request;
+ private Response response;
+
+}
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/models/WorkItems.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/models/WorkItems.java
new file mode 100644
index 0000000..1e3bf78
--- /dev/null
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/registration/models/WorkItems.java
@@ -0,0 +1,20 @@
+package io.confluent.pas.agent.proxy.registration.models;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class WorkItems {
+
+ private String correlationId;
+ private List workItems = new ArrayList<>();
+
+}
diff --git a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/rest/a2a/A2AController.java b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/rest/a2a/A2AController.java
index 77d4ce8..30fc504 100644
--- a/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/rest/a2a/A2AController.java
+++ b/agent-proxy/src/main/java/io/confluent/pas/agent/proxy/rest/a2a/A2AController.java
@@ -9,7 +9,7 @@
import java.util.List;
-//@RestController
+@RestController
@Tag(name = "A2A", description = "Google A2A compliant API")
public class A2AController {
private final static String AGENT_PATH = "/a2a/";
diff --git a/agent-proxy/src/main/resources/application.yaml b/agent-proxy/src/main/resources/application.yaml
index bf2c57a..8831955 100644
--- a/agent-proxy/src/main/resources/application.yaml
+++ b/agent-proxy/src/main/resources/application.yaml
@@ -3,7 +3,7 @@ spring:
activate:
on-profile: "production"
application:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
mvc:
async:
request-timeout: 60000
@@ -11,7 +11,7 @@ spring:
web-application-type: reactive
mcp:
server:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
version: 1.0.0
mode: "sse"
kafka:
@@ -48,7 +48,7 @@ spring:
activate:
on-profile: "default"
application:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
mvc:
async:
request-timeout: 60000
@@ -56,7 +56,7 @@ spring:
web-application-type: reactive
mcp:
server:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
version: 1.0.0
mode: "sse"
kafka:
@@ -87,13 +87,13 @@ spring:
activate:
on-profile: "stdio"
application:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
main:
banner-mode: off
web-application-type: none
mcp:
server:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
version: 1.0.0
mode: "stdio"
kafka:
diff --git a/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandlerTest.java b/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandlerTest.java
index 181b521..df4e617 100644
--- a/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandlerTest.java
+++ b/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/RequestResponseHandlerTest.java
@@ -44,7 +44,7 @@ void setUp() {
when(kafkaConfiguration.topicConfiguration()).thenReturn(new KafkaConfiguration.DefaultTopicConfiguration());
when(kafkaConfiguration.saslMechanism()).thenReturn(KafkaConfiguration.DEFAULT_SASL_MECHANISM);
- requestResponseHandler = new RequestResponseHandler(producerService, consumerService, observationRegistry);
+ requestResponseHandler = new RequestResponseHandler(producerService, consumerService);
}
@Test
diff --git a/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerServiceTest.java b/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerServiceTest.java
index eff04a9..0ceb139 100644
--- a/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerServiceTest.java
+++ b/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerServiceTest.java
@@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.confluent.pas.agent.common.services.schemas.Registration;
import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
+import io.confluent.pas.agent.proxy.registration.kafka.impl.ConsumerImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
diff --git a/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerTest.java b/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerTest.java
index 5de4470..c1105a6 100644
--- a/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerTest.java
+++ b/agent-proxy/src/test/java/io/confluent/pas/agent/proxy/registration/kafka/ConsumerTest.java
@@ -1,6 +1,7 @@
package io.confluent.pas.agent.proxy.registration.kafka;
import io.confluent.pas.agent.common.services.KafkaConfiguration;
+import io.confluent.pas.agent.proxy.registration.kafka.impl.ConsumerImpl;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.KafkaConsumer;
@@ -28,7 +29,7 @@ class ConsumerTest {
@Mock
private KafkaConfiguration kafkaConfiguration;
- private Consumer consumer;
+ private ConsumerImpl consumer;
@BeforeEach
void setUp() {
@@ -44,7 +45,7 @@ void setUp() {
when(kafkaConfiguration.saslMechanism()).thenReturn(KafkaConfiguration.DEFAULT_SASL_MECHANISM);
- consumer = new Consumer<>(consumerHandler, kafkaConsumer, mock(Consumer.TimeoutChecker.class));
+ consumer = new ConsumerImpl<>(consumerHandler, kafkaConsumer, mock(ConsumerImpl.TimeoutChecker.class));
}
@Test
diff --git a/agent-proxy/src/test/resources/application.yaml b/agent-proxy/src/test/resources/application.yaml
index fb98d7a..77bbd99 100644
--- a/agent-proxy/src/test/resources/application.yaml
+++ b/agent-proxy/src/test/resources/application.yaml
@@ -1,6 +1,6 @@
spring:
application:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
mvc:
async:
request-timeout: 60000
@@ -8,7 +8,7 @@ spring:
web-application-type: reactive
mcp:
server:
- name: "Confluent MCP Proxy"
+ name: "Confluent Agent Proxy"
version: 1.0.0
mode: "sse"
authentication:
diff --git a/common/pom.xml b/common/pom.xml
index bc5211f..b42539c 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -22,6 +22,17 @@
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
+
+
+ org.apache.kafka
+ kafka-streams
+
+
com.github.victools
jsonschema-generator
@@ -46,6 +57,10 @@
io.kcache
kcache
+
+ io.kcache
+ kcache-caffeine
+
org.mockito
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/Cache.java b/common/src/main/java/io/confluent/pas/agent/common/services/Cache.java
new file mode 100644
index 0000000..9e3d787
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/Cache.java
@@ -0,0 +1,187 @@
+package io.confluent.pas.agent.common.services;
+
+import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer;
+import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializerConfig;
+import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializer;
+import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializerConfig;
+import io.confluent.pas.agent.common.services.cache.LocalCache;
+import io.kcache.CacheUpdateHandler;
+import io.kcache.KafkaCache;
+import org.apache.kafka.common.serialization.Serde;
+import org.apache.kafka.common.serialization.Serdes;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A generic cache implementation that wraps KafkaCache to provide distributed caching capabilities.
+ * This cache uses Kafka as a backing store and supports JSON schema serialization/deserialization.
+ *
+ * @param The type of keys in the cache
+ * @param The type of values in the cache
+ */
+public class Cache implements Map {
+
+ private final KafkaCache cache;
+
+ /**
+ * Creates a new Cache instance with the specified configuration.
+ *
+ * @param kafkaConfiguration The Kafka configuration settings
+ * @param cacheName The name of the cache
+ * @param kClass The class type of the keys
+ * @param vClass The class type of the values
+ * @param handler The handler for cache updates
+ * @param readOnly Whether the cache is read-only
+ * @param topicName The name of the Kafka topic to use
+ */
+ public Cache(KafkaConfiguration kafkaConfiguration,
+ String cacheName,
+ Class kClass,
+ Class vClass,
+ CacheHandler.Handler handler,
+ boolean readOnly,
+ String topicName) {
+ this.cache = initialize(
+ kafkaConfiguration,
+ cacheName,
+ kClass,
+ vClass,
+ handler,
+ readOnly,
+ topicName);
+ }
+
+ public Cache(KafkaCache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public int size() {
+ return cache.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return cache.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return cache.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return cache.containsValue(value);
+ }
+
+ @Override
+ public V get(Object key) {
+ return cache.get(key);
+ }
+
+ @Override
+ public @Nullable V put(K key, V value) {
+ return cache.put(key, value);
+ }
+
+ @Override
+ public V remove(Object key) {
+ return cache.remove(key);
+ }
+
+ @Override
+ public void putAll(@NotNull Map extends K, ? extends V> m) {
+ cache.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public @NotNull Set keySet() {
+ return cache.keySet();
+ }
+
+ @Override
+ public @NotNull Collection values() {
+ return cache.values();
+ }
+
+ @Override
+ public @NotNull Set> entrySet() {
+ return cache.entrySet();
+ }
+
+ /**
+ * Initializes the KafkaCache with the specified configuration.
+ * Sets up JSON schema serialization/deserialization and configures the cache storage.
+ *
+ * @param kafkaConfiguration The Kafka configuration settings
+ * @param cacheName The name of the cache
+ * @param kClass The class type of the keys
+ * @param vClass The class type of the values
+ * @param handler The handler for cache updates
+ * @param readOnly Whether the cache is read-only
+ * @param topicName The name of the Kafka topic to use
+ * @return The initialized KafkaCache instance
+ */
+ private KafkaCache initialize(KafkaConfiguration kafkaConfiguration,
+ String cacheName,
+ Class kClass,
+ Class vClass,
+ CacheHandler.Handler handler,
+ boolean readOnly,
+ String topicName) {
+ // Retrieve the Schema Registry configuration settings from KafkaPropertiesFactory.
+ final Map srConfig = KafkaPropertiesFactory.getSchemaRegistryConfig(kafkaConfiguration);
+ srConfig.put(KafkaJsonSchemaDeserializerConfig.JSON_KEY_TYPE, kClass);
+ srConfig.put(KafkaJsonSchemaDeserializerConfig.JSON_VALUE_TYPE, vClass);
+ srConfig.put(KafkaJsonSchemaSerializerConfig.AUTO_REGISTER_SCHEMAS, true);
+
+ // Create and configure the Serde for the registration key using Kafka JSON schema serializer and deserializer.
+ final Serde keySerdes = new Serdes.WrapperSerde<>(
+ new KafkaJsonSchemaSerializer<>(),
+ new KafkaJsonSchemaDeserializer<>()
+ );
+ keySerdes.configure(srConfig, true);
+
+ // Create and configure the Serde for the registration value using Kafka JSON schema serializer and deserializer.
+ final Serde valueSerdes = new Serdes.WrapperSerde<>(
+ new KafkaJsonSchemaSerializer<>(),
+ new KafkaJsonSchemaDeserializer<>()
+ );
+ valueSerdes.configure(srConfig, false);
+
+ final CacheUpdateHandler serviceHandler = handler != null
+ ? new CacheHandler<>(handler)
+ : null;
+
+ final KafkaCache cache = new KafkaCache<>(
+ KafkaPropertiesFactory.getCacheConfig(
+ kafkaConfiguration,
+ readOnly,
+ topicName,
+ cacheName),
+ keySerdes,
+ valueSerdes,
+ serviceHandler,
+ new LocalCache<>(
+ 1000,
+ Duration.of(5, ChronoUnit.MINUTES),
+ null)
+ );
+
+ cache.init();
+
+ return cache;
+ }
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/CacheHandler.java b/common/src/main/java/io/confluent/pas/agent/common/services/CacheHandler.java
new file mode 100644
index 0000000..5f658a1
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/CacheHandler.java
@@ -0,0 +1,115 @@
+package io.confluent.pas.agent.common.services;
+
+import io.kcache.CacheUpdateHandler;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.common.TopicPartition;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Handles cache updates and initialization for a Kafka-based caching system.
+ * This class implements CacheUpdateHandler to manage cache events and updates,
+ * providing a way to accumulate and process cache entries during initialization.
+ *
+ * @param The type of keys in the cache
+ * @param The type of values in the cache
+ */
+@Slf4j
+public class CacheHandler implements CacheUpdateHandler {
+
+ /**
+ * Interface defining the contract for handling cache values.
+ * Implementations process batches of key-value pairs from the cache.
+ *
+ * @param The type of keys in the cache
+ * @param The type of values in the cache
+ */
+ public interface Handler {
+ /**
+ * Processes a batch of cache entries.
+ *
+ * @param registrations Map of key-value pairs to process
+ */
+ void onValues(Map registrations);
+ }
+
+ /**
+ * Handler for processing cache values
+ */
+ private final Handler handler;
+ /**
+ * Temporary storage for cache entries during initialization
+ */
+ private final Map accumulator = new HashMap<>();
+ /**
+ * Flag indicating if the cache has been initialized
+ */
+ private boolean initialized = false;
+
+ /**
+ * Flag indicating if the cache is empty after initialization
+ */
+ @Getter
+ private boolean empty;
+
+ public CacheHandler(Handler handler) {
+ this.handler = handler;
+ }
+
+ /**
+ * Called when the cache is initialized.
+ * Processes accumulated entries if any exist, or marks the cache as empty if no entries were found.
+ *
+ * @param count Number of entries in the cache
+ * @param checkpoints Map of topic partitions and their offsets
+ */
+ @Override
+ public void cacheInitialized(int count, Map checkpoints) {
+ initialized = true;
+
+ if (count == 0) {
+ // No event, we might need to register the schemas
+ empty = true;
+ } else if (!accumulator.isEmpty()) {
+ handler.onValues(accumulator);
+ accumulator.clear();
+ }
+
+ log.info("Registration cache initialized.");
+ }
+
+ /**
+ * Handles updates to the cache entries.
+ * If the cache is initialized, processes updates immediately.
+ * Otherwise, accumulates updates for later processing.
+ *
+ * @param key Key being updated
+ * @param value New value
+ * @param oldValue Previous value
+ * @param tp Topic partition
+ * @param offset Kafka offset
+ * @param ts Timestamp of the update
+ */
+ @Override
+ public void handleUpdate(K key,
+ V value,
+ V oldValue,
+ TopicPartition tp,
+ long offset,
+ long ts) {
+ // If the cache is not initialized, store the value in the accumulator
+ // This will help to keep the last event for each key
+ if (initialized) {
+ handler.onValues(Map.of(key, value));
+ } else {
+ if (value == null) {
+ accumulator.remove(key);
+ } else {
+ accumulator.put(key, value);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/KafkaPropertiesFactory.java b/common/src/main/java/io/confluent/pas/agent/common/services/KafkaPropertiesFactory.java
index bcc9bcc..a33afec 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/KafkaPropertiesFactory.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/KafkaPropertiesFactory.java
@@ -146,11 +146,28 @@ public static Properties getConsumerProperties(KafkaConfiguration configration,
* @param readOnly Whether the cache should be read-only
* @return KafkaCacheConfig configured for the cache
*/
- public static KafkaCacheConfig getCacheConfig(KafkaConfiguration configration, boolean readOnly) {
+ public static KafkaCacheConfig getRegistrationCacheConfig(KafkaConfiguration configration, boolean readOnly) {
+ return getCacheConfig(configration, readOnly, configration.registrationTopicName(), "registration");
+ }
+
+ /**
+ * Creates configuration for a Kafka cache.
+ * Sets up topic, client ID, and group ID with appropriate suffixes.
+ *
+ * @param configration The Kafka configuration containing connection and auth details
+ * @param readOnly Whether the cache should be read-only
+ * @param topicName The name of the topic
+ * @param clientIdSuffix The suffix to add to the client ID to create a unique ID for the cache
+ * @return KafkaCacheConfig configured for the cache
+ */
+ public static KafkaCacheConfig getCacheConfig(KafkaConfiguration configration,
+ boolean readOnly,
+ String topicName,
+ String clientIdSuffix) {
Properties properties = getDefaultProperties(configration, "kafkacache.");
- properties.put(KafkaCacheConfig.KAFKACACHE_TOPIC_CONFIG, configration.registrationTopicName());
- properties.put(KafkaCacheConfig.KAFKACACHE_CLIENT_ID_CONFIG, configration.applicationId() + "-registration" + "-" + configration.clientId());
- properties.put(KafkaCacheConfig.KAFKACACHE_GROUP_ID_CONFIG, configration.applicationId() + "-registration" + "-group");
+ properties.put(KafkaCacheConfig.KAFKACACHE_TOPIC_CONFIG, topicName);
+ properties.put(KafkaCacheConfig.KAFKACACHE_CLIENT_ID_CONFIG, configration.applicationId() + "-" + clientIdSuffix + "-" + configration.clientId());
+ properties.put(KafkaCacheConfig.KAFKACACHE_GROUP_ID_CONFIG, configration.applicationId() + "-" + clientIdSuffix + "-group");
properties.put(KafkaCacheConfig.KAFKACACHE_TOPIC_READ_ONLY_CONFIG, readOnly);
return new KafkaCacheConfig(properties);
}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/RegistrationService.java b/common/src/main/java/io/confluent/pas/agent/common/services/RegistrationService.java
index 2d2149b..7b89509 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/RegistrationService.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/RegistrationService.java
@@ -1,17 +1,8 @@
package io.confluent.pas.agent.common.services;
-import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;
-import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer;
-import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializerConfig;
-import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializer;
-import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializerConfig;
import io.confluent.pas.agent.common.services.schemas.Registration;
import io.confluent.pas.agent.common.services.schemas.RegistrationKey;
-import io.confluent.pas.agent.common.utils.SchemaUtils;
-import io.kcache.KafkaCache;
import lombok.extern.slf4j.Slf4j;
-import org.apache.kafka.common.serialization.Serde;
-import org.apache.kafka.common.serialization.Serdes;
import java.io.Closeable;
import java.io.IOException;
@@ -42,12 +33,15 @@ public RegistrationService(KafkaConfiguration kafkaConfiguration,
Class registrationKeyClass,
Class registrationClass,
boolean readOnly,
- RegistrationServiceHandler.Handler handler) {
- this(initialize(kafkaConfiguration,
+ CacheHandler.Handler handler) {
+ registrationCache = new Cache<>(
+ kafkaConfiguration,
+ "registration-cache",
registrationKeyClass,
registrationClass,
+ handler,
readOnly,
- handler));
+ kafkaConfiguration.registrationTopicName());
}
/**
@@ -70,24 +64,12 @@ public RegistrationService(Map registrationCache) {
public RegistrationService(KafkaConfiguration kafkaConfiguration,
Class registrationKeyClass,
Class registrationClass,
- RegistrationServiceHandler.Handler handler) {
- this(kafkaConfiguration, registrationKeyClass, registrationClass, false, handler);
- }
-
-
- /**
- * Constructor for RegistrationService without a handler.
- *
- * @param kafkaConfiguration the Kafka configuration
- * @param registrationKeyClass the class type of the registration key
- * @param registrationClass the class type of the registration
- * @param readOnly whether the service is read-only
- */
- public RegistrationService(KafkaConfiguration kafkaConfiguration,
- Class registrationKeyClass,
- Class registrationClass,
- boolean readOnly) {
- this(kafkaConfiguration, registrationKeyClass, registrationClass, readOnly, null);
+ CacheHandler.Handler handler) {
+ this(kafkaConfiguration,
+ registrationKeyClass,
+ registrationClass,
+ false,
+ handler);
}
/**
@@ -164,73 +146,4 @@ public void unregister(K key) {
// Remove the registration associated with the given key from the Kafka cache.
registrationCache.remove(key);
}
-
-
- /**
- * Initialize the registration service.
- * Configures the Kafka serializers and deserializers, and initializes the Kafka cache.
- *
- * @param kafkaConfiguration the Kafka configuration
- * @param registrationKeyClass the class type of the registration key
- * @param registrationClass the class type of the registration
- * @param readOnly whether the service is read-only
- * @param handler the handler for processing registration updates
- * (optional, can be null)
- * @return the initialized Kafka cache
- */
- private static KafkaCache initialize(
- KafkaConfiguration kafkaConfiguration,
- Class registrationKeyClass,
- Class registrationClass,
- boolean readOnly,
- RegistrationServiceHandler.Handler handler) {
- // Retrieve the Schema Registry configuration settings from KafkaPropertiesFactory.
- final Map srConfig = KafkaPropertiesFactory.getSchemaRegistryConfig(kafkaConfiguration);
- srConfig.put(KafkaJsonSchemaDeserializerConfig.JSON_KEY_TYPE, registrationKeyClass);
- srConfig.put(KafkaJsonSchemaDeserializerConfig.JSON_VALUE_TYPE, registrationClass);
- srConfig.put(KafkaJsonSchemaSerializerConfig.AUTO_REGISTER_SCHEMAS, true);
-
- // Create and configure the Serde for the registration key using Kafka JSON schema serializer and deserializer.
- final Serde keySerdes = new Serdes.WrapperSerde<>(
- new KafkaJsonSchemaSerializer<>(),
- new KafkaJsonSchemaDeserializer<>()
- );
- keySerdes.configure(srConfig, true);
-
- // Create and configure the Serde for the registration value using Kafka JSON schema serializer and deserializer.
- final Serde valueSerdes = new Serdes.WrapperSerde<>(
- new KafkaJsonSchemaSerializer<>(),
- new KafkaJsonSchemaDeserializer<>()
- );
- valueSerdes.configure(srConfig, false);
-
- final RegistrationServiceHandler serviceHandler = handler != null
- ? new RegistrationServiceHandler<>(handler)
- : null;
-
- final KafkaCache cache = new KafkaCache<>(
- KafkaPropertiesFactory.getCacheConfig(kafkaConfiguration, readOnly),
- keySerdes,
- valueSerdes,
- serviceHandler,
- null
- );
-
- cache.init();
-
- if (serviceHandler != null && serviceHandler.isEmpty()) {
- final String registrationTopic = kafkaConfiguration.registrationTopicName();
-
- // If a service handler is provided and is empty, register the necessary schemas in the Schema Registry.
- try (SchemaRegistryClient schemaRegistryClient = KafkaPropertiesFactory.getSchemRegistryClient(kafkaConfiguration)) {
- SchemaUtils.registerSchemaIfMissing(registrationTopic, registrationKeyClass, true, schemaRegistryClient);
- SchemaUtils.registerSchemaIfMissing(registrationTopic, registrationClass, false, schemaRegistryClient);
- } catch (Throwable e) {
- log.error("Error registering schemas", e);
- }
-
- }
-
- return cache;
- }
}
\ No newline at end of file
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/Streaming.java b/common/src/main/java/io/confluent/pas/agent/common/services/Streaming.java
new file mode 100644
index 0000000..f445826
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/Streaming.java
@@ -0,0 +1,32 @@
+package io.confluent.pas.agent.common.services;
+
+import io.confluent.pas.agent.common.services.kstream.StreamingHandler;
+
+/**
+ * Interface for managing streaming operations with Kafka.
+ *
+ * @param The type of the message key
+ * @param The type of the input message value
+ * @param The type of the output message value
+ */
+public interface Streaming extends AutoCloseable {
+
+ /**
+ * Initializes the streaming process with the given configuration and topics.
+ *
+ * @param kafkaConfiguration Configuration for Kafka connection
+ * @param inTopicName Name of the input topic
+ * @param outTopicName Name of the output topic
+ * @param streamingHandler Handler for processing the stream
+ */
+ void init(KafkaConfiguration kafkaConfiguration,
+ String inTopicName,
+ String outTopicName,
+ StreamingHandler streamingHandler);
+
+ /**
+ * Starts the streaming process.
+ * This method initializes and begins the Kafka streams processing.
+ */
+ void start();
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/cache/LocalCache.java b/common/src/main/java/io/confluent/pas/agent/common/services/cache/LocalCache.java
new file mode 100644
index 0000000..a3673ef
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/cache/LocalCache.java
@@ -0,0 +1,170 @@
+package io.confluent.pas.agent.common.services.cache;
+
+import com.github.benmanes.caffeine.cache.*;
+import io.kcache.CacheLoader;
+import io.kcache.utils.InMemoryCache;
+import org.jetbrains.annotations.NotNull;
+
+import java.time.Duration;
+import java.util.Comparator;
+import java.util.Map;
+
+/**
+ * A local caching implementation that extends InMemoryCache with Caffeine caching capabilities.
+ * This cache supports maximum size limits, expiration policies, and optional loading functionality.
+ *
+ * @param The type of keys maintained by this cache
+ * @param The type of mapped values
+ */
+public class LocalCache extends InMemoryCache {
+ private final Cache cache;
+ /**
+ * The loader used to load values when they are not present in the cache.
+ * Can be null if no loading functionality is required.
+ */
+ private final CacheLoader loader;
+
+ /**
+ * Constructs a LocalCache with a specified comparator for key ordering.
+ *
+ * @param comparator the comparator to determine the ordering of keys
+ */
+ public LocalCache(Comparator super K> comparator) {
+ this(null, null, null, comparator);
+ }
+
+ /**
+ * Constructs a LocalCache with size limits, expiration policy, and loading functionality.
+ *
+ * @param maximumSize the maximum number of entries the cache may contain
+ * @param expireAfterWrite the time after which entries should be automatically removed
+ * @param loader the cache loader to use when entries are not found
+ */
+ public LocalCache(Integer maximumSize, Duration expireAfterWrite, CacheLoader loader) {
+ this.loader = loader;
+ this.cache = this.createCache(maximumSize, expireAfterWrite);
+ }
+
+ /**
+ * Constructs a LocalCache with size limits, expiration policy, loading functionality, and key ordering.
+ *
+ * @param maximumSize the maximum number of entries the cache may contain
+ * @param expireAfterWrite the time after which entries should be automatically removed
+ * @param loader the cache loader to use when entries are not found
+ * @param comparator the comparator to determine the ordering of keys
+ */
+ public LocalCache(Integer maximumSize, Duration expireAfterWrite, CacheLoader loader, Comparator super K> comparator) {
+ super(comparator);
+ this.loader = loader;
+ this.cache = this.createCache(maximumSize, expireAfterWrite);
+ }
+
+ /**
+ * Returns true if this cache contains a mapping for the specified key.
+ *
+ * @param key key whose presence in this cache is to be tested
+ * @return true if this cache contains a mapping for the specified key
+ */
+ public boolean containsKey(Object key) {
+ return this.get(key) != null;
+ }
+
+ /**
+ * Returns the value associated with the specified key, or null if there is no cached value.
+ * If a loader is configured and the key isn't present, it will attempt to load the value.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value associated with the specified key, or null if none
+ */
+ @SuppressWarnings("unchecked")
+ public V get(Object key) {
+ return (cache instanceof LoadingCache loadingCache)
+ ? loadingCache.get((K) key)
+ : super.get(key);
+ }
+
+ /**
+ * Associates the specified value with the specified key in this cache.
+ *
+ * @param key key with which the specified value is to be associated
+ * @param value value to be associated with the specified key
+ * @return the previous value associated with key, or null if there was no mapping
+ */
+ public V put(K key, V value) {
+ V originalValue = this.get(key);
+
+ this.cache.put(key, value);
+ delegate().put(key, value);
+
+ return originalValue;
+ }
+
+ /**
+ * Copies all entries from the specified map to this cache.
+ *
+ * @param entries mappings to be stored in this cache
+ */
+ public void putAll(@NotNull Map extends K, ? extends V> entries) {
+ this.cache.putAll(entries);
+ for (Map.Entry extends K, ? extends V> entry : entries.entrySet()) {
+ delegate().put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Removes the entry for the specified key if present.
+ *
+ * @param key key whose mapping is to be removed from the cache
+ * @return the previous value associated with key, or null if there was no mapping
+ */
+ @SuppressWarnings("unchecked")
+ public V remove(Object key) {
+ V originalValue = this.get(key);
+ if (key != null) {
+ this.cache.invalidate((K) key);
+ }
+
+ return originalValue;
+ }
+
+ /**
+ * Removes all entries from this cache.
+ */
+ public void clear() {
+ this.cache.invalidateAll();
+ }
+
+ /**
+ * Creates a new Caffeine cache instance with the specified configuration.
+ *
+ * @param maximumSize the maximum number of entries the cache may contain
+ * @param expireAfterWrite the time after which entries should be automatically removed
+ * @return a new configured Cache instance
+ */
+ private Cache createCache(Integer maximumSize, Duration expireAfterWrite) {
+ Caffeine caffeine = Caffeine
+ .newBuilder()
+ .evictionListener((key, value, cause) -> this.delegate().remove(key, value));
+
+ if (maximumSize != null && maximumSize >= 0) {
+ caffeine.maximumSize((long) maximumSize);
+ }
+
+ if (expireAfterWrite != null && !expireAfterWrite.isNegative()) {
+ caffeine.scheduler(Scheduler.systemScheduler()).expireAfterWrite(expireAfterWrite);
+ }
+
+ if (this.loader != null) {
+ return caffeine.build((key) -> {
+ V value = this.loader.load(key);
+ if (value != null) {
+ this.delegate().put(key, value);
+ }
+
+ return value;
+ });
+ }
+
+ return caffeine.build();
+ }
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingHandler.java b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingHandler.java
new file mode 100644
index 0000000..5a2f9c7
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingHandler.java
@@ -0,0 +1,48 @@
+package io.confluent.pas.agent.common.services.kstream;
+
+import reactor.core.publisher.Mono;
+
+import java.util.function.Consumer;
+
+/**
+ * Interface defining handler for processing streaming data.
+ * Implementations of this interface handle the processing logic for Kafka Streams.
+ *
+ * @param The type of the message key
+ * @param The type of the input message value
+ * @param The type of the output message value
+ */
+public interface StreamingHandler {
+
+ /**
+ * Gets the Class object representing the key type.
+ *
+ * @return The Class object for the key type
+ */
+ Class getKeyClass();
+
+ /**
+ * Gets the Class object representing the input type.
+ *
+ * @return The Class object for the input type
+ */
+ Class getInClass();
+
+ /**
+ * Gets the Class object representing the output type.
+ *
+ * @return The Class object for the output type
+ */
+ Class getOutClass();
+
+
+ /**
+ * Handles the processing of a single message in the stream.
+ *
+ * @param key The key of the message to be processed
+ * @param in The input value to be processed
+ * @param sink Consumer function to emit the processed output value
+ */
+ void handle(Key key, In in, Consumer sink);
+
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingImpl.java b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingImpl.java
new file mode 100644
index 0000000..c829b36
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingImpl.java
@@ -0,0 +1,132 @@
+package io.confluent.pas.agent.common.services.kstream;
+
+import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer;
+import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializer;
+import io.confluent.pas.agent.common.services.KafkaConfiguration;
+import io.confluent.pas.agent.common.services.KafkaPropertiesFactory;
+import io.confluent.pas.agent.common.services.Streaming;
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.common.serialization.Serdes;
+import org.apache.kafka.streams.KafkaStreams;
+import org.apache.kafka.streams.StreamsBuilder;
+import org.apache.kafka.streams.Topology;
+import org.apache.kafka.streams.kstream.Consumed;
+import org.apache.kafka.streams.kstream.Produced;
+
+import java.util.Map;
+import java.util.Properties;
+
+
+/**
+ * Implementation of the Streaming interface for Kafka Streams processing.
+ *
+ * @param The type of the message key
+ * @param The type of the input message value
+ * @param The type of the output message value
+ */
+@Slf4j
+public class StreamingImpl implements Streaming {
+
+ private KafkaStreams kafkaStreams;
+
+ /**
+ * Initializes the streaming process with the given configuration and topics.
+ *
+ * @param kafkaConfiguration Configuration for Kafka connection
+ * @param inTopicName Name of the input topic
+ * @param outTopicName Name of the output topic
+ * @param streamingHandler Handler for processing the stream
+ */
+ @Override
+ public void init(KafkaConfiguration kafkaConfiguration,
+ String inTopicName,
+ String outTopicName,
+ StreamingHandler streamingHandler) {
+ this.kafkaStreams = setupAndStartKafkaStreams(
+ kafkaConfiguration,
+ inTopicName,
+ outTopicName,
+ streamingHandler);
+ }
+
+ /**
+ * Starts the Kafka Streams processing if the streams instance is initialized.
+ */
+ @Override
+ public void start() {
+ if (kafkaStreams != null) {
+ kafkaStreams.start();
+ log.info("Kafka Streams started");
+ }
+ }
+
+ /**
+ * Closes the Kafka Streams instance and releases all resources.
+ */
+ public void close() {
+ if (kafkaStreams != null) {
+ kafkaStreams.close();
+ log.info("Kafka Streams closed");
+ }
+ }
+
+ /**
+ * Creates a Serde (Serializer/Deserializer) for a specific class type.
+ *
+ * @param kafkaConfiguration Configuration for Kafka connection
+ * @param valueClass Class type for which to create the Serde
+ * @param isKey Whether this Serde is for a key or value
+ * @return A configured WrapperSerde instance
+ */
+ private Serdes.WrapperSerde createSerde(KafkaConfiguration kafkaConfiguration,
+ Class valueClass,
+ boolean isKey) {
+ final Map configuration =
+ KafkaPropertiesFactory.getSchemaRegistryConfig(kafkaConfiguration, valueClass, isKey);
+
+ final Serdes.WrapperSerde serde = new Serdes.WrapperSerde<>(
+ new KafkaJsonSchemaSerializer<>(),
+ new KafkaJsonSchemaDeserializer<>());
+
+ serde.configure(configuration, isKey);
+ return serde;
+ }
+
+ /**
+ * Sets up and initializes a KafkaStreams instance with the specified configuration and topology.
+ *
+ * @param kafkaConfiguration Configuration for Kafka connection
+ * @param inTopicName Name of the input topic
+ * @param outTopicName Name of the output topic
+ * @param streamingHandler Handler for processing the stream
+ * @return Configured KafkaStreams instance
+ */
+ private KafkaStreams setupAndStartKafkaStreams(KafkaConfiguration kafkaConfiguration,
+ String inTopicName,
+ String outTopicName,
+ StreamingHandler streamingHandler) {
+ final Serdes.WrapperSerde keySerde = createSerde(kafkaConfiguration,
+ streamingHandler.getKeyClass(),
+ true);
+ final Serdes.WrapperSerde inSerde = createSerde(
+ kafkaConfiguration,
+ streamingHandler.getInClass(),
+ false);
+ final Serdes.WrapperSerde outSerde = createSerde(kafkaConfiguration,
+ streamingHandler.getOutClass(),
+ false);
+
+ StreamsBuilder builder = new StreamsBuilder();
+
+ builder.stream(inTopicName, Consumed.with(keySerde, inSerde))
+ .process(new StreamingSupplier<>(streamingHandler))
+ .to(outTopicName, Produced.with(keySerde, outSerde));
+
+ final Properties configuration = KafkaPropertiesFactory.getKStreamsProperties(kafkaConfiguration);
+ final Topology topology = builder.build();
+ kafkaStreams = new KafkaStreams(topology, configuration);
+
+ return kafkaStreams;
+ }
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingProcessor.java b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingProcessor.java
new file mode 100644
index 0000000..f9c6556
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingProcessor.java
@@ -0,0 +1,56 @@
+package io.confluent.pas.agent.common.services.kstream;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.streams.processor.api.Processor;
+import org.apache.kafka.streams.processor.api.ProcessorContext;
+import org.apache.kafka.streams.processor.api.Record;
+
+/**
+ * A generic Kafka Streams processor that handles stream processing operations.
+ *
+ * @param The type of the record key
+ * @param The type of the input record value
+ * @param The type of the output record value
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class StreamingProcessor implements Processor {
+
+ /**
+ * Handler responsible for processing the streaming records
+ */
+ private final StreamingHandler handler;
+ /**
+ * Processor context for forwarding processed records
+ */
+ private ProcessorContext context;
+
+ /**
+ * Initializes the processor with the given context.
+ *
+ * @param context The processor context used for forwarding records
+ */
+ @Override
+ public void init(ProcessorContext context) {
+ this.context = context;
+ }
+
+ /**
+ * Processes the input record by delegating to the handler and forwarding the result.
+ *
+ * @param record The input record to process
+ */
+ @Override
+ public void process(Record record) {
+ try {
+ handler.handle(record.key(), record.value(), out -> {
+ log.debug("Forwarding record: {}", record);
+ context.forward(record.withValue(out));
+ });
+ } catch (Exception e) {
+ log.error("Error processing record: {}", record, e);
+ // TODO: Handle error appropriately
+ }
+ }
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingSupplier.java b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingSupplier.java
new file mode 100644
index 0000000..e61d438
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/kstream/StreamingSupplier.java
@@ -0,0 +1,34 @@
+package io.confluent.pas.agent.common.services.kstream;
+
+import lombok.AllArgsConstructor;
+import org.apache.kafka.streams.processor.api.Processor;
+import org.apache.kafka.streams.processor.api.ProcessorSupplier;
+
+
+/**
+ * A supplier class for streaming processors that handles the creation of processor instances
+ * based on a given streaming handler.
+ *
+ * @param the type of the message key
+ * @param the type of the input value
+ * @param the type of the output value
+ */
+@AllArgsConstructor
+public class StreamingSupplier implements ProcessorSupplier {
+
+ /**
+ * The streaming handler that will be used by the created processors
+ * to process the input messages.
+ */
+ private final StreamingHandler streamingHandler;
+
+ /**
+ * Creates a new processor instance using the configured streaming handler.
+ *
+ * @return a new processor instance that will use the configured streaming handler
+ */
+ @Override
+ public Processor get() {
+ return new StreamingProcessor<>(streamingHandler);
+ }
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/AbstractSchema.java b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/AbstractSchema.java
new file mode 100644
index 0000000..9e179b1
--- /dev/null
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/AbstractSchema.java
@@ -0,0 +1,49 @@
+package io.confluent.pas.agent.common.services.schemas;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@AllArgsConstructor(access = lombok.AccessLevel.PROTECTED)
+@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
+public abstract class AbstractSchema {
+ private Map metaData;
+
+ @JsonProperty(value = "metaData")
+ public Map getMetaData() {
+ return metaData;
+ }
+
+ @JsonProperty(value = "metaData")
+ public void setMetaData(Map metaData) {
+ this.metaData = metaData;
+ }
+
+ protected T getMetaDataValue(String key, Class type) {
+ if (metaData == null || !metaData.containsKey(key)) {
+ return null;
+ }
+
+ Object value = metaData.get(key);
+ if (value == null) {
+ return null;
+ }
+
+ if (type.isInstance(value)) {
+ return type.cast(value);
+ }
+
+ throw new IllegalArgumentException("Meta data value for key '" + key + "' is not of type " + type.getName());
+ }
+
+ protected void setMetaDataValue(String key, Object value) {
+ if (metaData == null) {
+ metaData = new HashMap<>();
+ }
+
+ metaData.put(key, value);
+ }
+}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/BlobResourceResponse.java b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/BlobResourceResponse.java
index eb890ed..57b9386 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/BlobResourceResponse.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/BlobResourceResponse.java
@@ -1,73 +1,28 @@
package io.confluent.pas.agent.common.services.schemas;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import io.confluent.kafka.schemaregistry.annotations.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-@Schema(value = """
- {
- "properties":{
- "type":{
- "connect.index":0,
- "enum":[
- "text",
- "blob"
- ],
- "default": "text",
- "type":"string"
- },
- "uri":{
- "connect.index":1,
- "type":"string"
- },
- "mimeType":{
- "connect.index":2,
- "type":"string"
- },
- "text":{
- "connect.index":3,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- },
- "blob":{
- "connect.index":4,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- }
- },
- "required":[
- "type",
- "uri",
- "mimeType"
- ],
- "title":"Record",
- "type":"object"
- }""", refs = {})
@Getter
@Setter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class BlobResourceResponse extends ResourceResponse {
- @JsonProperty(value = "blob", required = true)
- private String blob;
+ private final static String BLOB = "blob";
+
+ public String getBlob() {
+ return getMetaDataValue(BLOB, String.class);
+ }
+
+ public void setBlob(String blob) {
+ setMetaDataValue(BLOB, blob);
+ }
public BlobResourceResponse(String uri, String mimeType, String blob) {
- super(ResponseType.BLOB, uri, mimeType);
- this.blob = blob;
+ super(ResourceResponse.BLOB_TYPE, uri, mimeType);
+
+ setBlob(blob);
}
}
\ No newline at end of file
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/Registration.java b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/Registration.java
index 5b94f05..c4f80da 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/Registration.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/Registration.java
@@ -5,6 +5,9 @@
import lombok.*;
import org.apache.commons.lang3.StringUtils;
+import java.util.HashMap;
+import java.util.Map;
+
@Schema(value = """
{
"properties":{
@@ -12,17 +15,6 @@
"connect.index":5,
"type":"string"
},
- "correlationIdFieldName":{
- "connect.index":4,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- },
"description":{
"connect.index":1,
"type":"string"
@@ -33,25 +25,19 @@
},
"requestTopicName":{
"connect.index":2,
- "type":"string"
+ "type":"string"
},
"responseTopicName":{
"connect.index":3,
"type":"string"
},
- "mimeType":{
- "connect.index":6,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
+ "metaData":{
+ "connect.index":4,
+ "type":"object",
+ "additionalProperties":true
},
- "url":{
- "connect.index":7,
+ "version":{
+ "connect.index":6,
"oneOf":[
{
"type":"null"
@@ -59,20 +45,9 @@
{
"type":"string"
}
- ]
- },
- "version":{
- "connect.index":8,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ],
- "default":"N/A"
- }
+ ],
+ "default":"N/A"
+ }
},
"required":[
"name",
@@ -88,17 +63,16 @@
@Getter
@Setter
@NoArgsConstructor
-@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "registrationType", defaultImpl = Registration.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = ResourceRegistration.class, name = Registration.RESOURCE),
@JsonSubTypes.Type(value = Registration.class, name = Registration.TOOL)})
@Builder
-public class Registration {
+public class Registration extends AbstractSchema {
public final static String TOOL = "tool";
public final static String RESOURCE = "resource";
- public final static String CORRELATION_ID_FIELD_NAME = "correlationId";
+ public final static String AGENT = "agent";
@JsonProperty(value = "registrationType", required = true, defaultValue = TOOL)
private String registrationType;
@@ -113,12 +87,45 @@ public class Registration {
@JsonProperty(value = "version", defaultValue = "N/A")
private String version;
- public Registration(String name, String description, String requestTopicName, String responseTopicName) {
- this(TOOL, name, description, requestTopicName, responseTopicName, "N/A");
+ public Registration(String name,
+ String description,
+ String requestTopicName,
+ String responseTopicName) {
+ this(TOOL, name, description, requestTopicName, responseTopicName, "N/A", new HashMap<>());
+ }
+
+ public Registration(String registrationType, String name, String description, String requestTopicName, String responseTopicName, String version) {
+ super(new HashMap<>());
+
+ this.registrationType = registrationType;
+ this.name = name;
+ this.description = description;
+ this.requestTopicName = requestTopicName;
+ this.responseTopicName = responseTopicName;
+ this.version = version;
+ }
+
+ protected Registration(String registrationType,
+ String name,
+ String description,
+ String requestTopicName,
+ String responseTopicName,
+ String version,
+ Map metaData) {
+ super(metaData);
+
+ this.registrationType = registrationType;
+ this.name = name;
+ this.description = description;
+ this.requestTopicName = requestTopicName;
+ this.responseTopicName = responseTopicName;
+ this.version = version;
}
@JsonIgnore
public boolean isResource() {
return StringUtils.equals(registrationType, RESOURCE);
}
+
+
}
\ No newline at end of file
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceRegistration.java b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceRegistration.java
index 08dd065..5807001 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceRegistration.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceRegistration.java
@@ -2,113 +2,115 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import io.confluent.kafka.schemaregistry.annotations.Schema;
import io.confluent.pas.agent.common.utils.UriUtils;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
import lombok.NoArgsConstructor;
-import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
-@Setter
-@Getter
-@Schema(value = """
- {
- "properties":{
- "registrationType":{
- "connect.index":5,
- "type":"string"
- },
- "correlationIdFieldName":{
- "connect.index":4,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- },
- "description":{
- "connect.index":1,
- "type":"string"
- },
- "name":{
- "connect.index":0,
- "type":"string"
- },
- "requestTopicName":{
- "connect.index":2,
- "type":"string"
- },
- "responseTopicName":{
- "connect.index":3,
- "type":"string"
- },
- "mimeType":{
- "connect.index":6,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- },
- "url":{
- "connect.index":7,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- }
- },
- "required":[
- "name",
- "description",
- "registrationType",
- "requestTopicName",
- "responseTopicName"
- ],
- "additionalProperties":false,
- "title":"Record",
- "type":"object"
- }""", refs = {})
-@AllArgsConstructor
+import java.util.HashMap;
+
+/**
+ * Represents a resource registration in the system, extending the base Registration class.
+ * This class manages resource-specific attributes such as URLs and MIME types.
+ *
+ * Each resource registration contains:
+ * - A URL identifying the resource location
+ * - A MIME type specifying the resource format
+ * - Metadata storage for additional resource properties
+ *
+ * The class provides methods to manage these attributes while ensuring data validity
+ * through input validation. It supports template URLs and follows RESTful principles
+ * for resource identification.
+ */
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResourceRegistration extends Registration {
- private String mimeType;
- private String url;
+ /**
+ * Key used to store and retrieve the MIME type in the metadata map.
+ * This constant ensures consistent access to MIME type information.
+ */
+ private final static String MIME_TYPE = "mimeType";
+
+ /**
+ * Key used to store and retrieve the URL in the metadata map.
+ * This constant ensures consistent access to URL information.
+ */
+ private final static String URL = "url";
+
+ /**
+ * Retrieves the URL associated with this resource.
+ *
+ * @return The URL string stored in metadata
+ */
+ public String getUrl() {
+ return getMetaDataValue(URL, String.class);
+ }
- // TODO: Should not require to remove the leading slash
+ /**
+ * Sets the URL for this resource after validation.
+ *
+ * @param url The URL to set
+ * @throws IllegalArgumentException if the URL is blank
+ */
public void setUrl(String url) {
if (StringUtils.isBlank(url)) {
throw new IllegalArgumentException("url cannot be blank");
}
- this.url = url.startsWith("/") ? url.substring(1) : url;
+ setMetaDataValue(URL, url.startsWith("/") ? url.substring(1) : url);
+ }
+
+ /**
+ * Retrieves the MIME type of this resource.
+ *
+ * @return The MIME type string stored in metadata
+ */
+ public String getMimeType() {
+ return getMetaDataValue(MIME_TYPE, String.class);
}
+ /**
+ * Sets the MIME type for this resource after validation.
+ *
+ * @param mimeType The MIME type to set
+ * @throws IllegalArgumentException if the MIME type is blank
+ */
+ public void setMimeType(String mimeType) {
+ if (StringUtils.isBlank(mimeType)) {
+ throw new IllegalArgumentException("mimeType cannot be blank");
+ }
+
+ setMetaDataValue(MIME_TYPE, mimeType);
+ }
+
+ /**
+ * Checks if the resource URL is a template.
+ *
+ * @return true if the URL contains template parameters, false otherwise
+ */
@JsonIgnore
public boolean isTemplate() {
- return UriUtils.isTemplate(url);
+ return UriUtils.isTemplate(getUrl());
}
+ /**
+ * Creates a new ResourceRegistration with the specified parameters.
+ *
+ * @param name The name of the resource
+ * @param description The description of the resource
+ * @param requestTopicName The name of the request topic
+ * @param responseTopicName The name of the response topic
+ * @param mimeType The MIME type of the resource
+ * @param url The URL of the resource
+ */
public ResourceRegistration(String name,
String description,
String requestTopicName,
String responseTopicName,
String mimeType,
String url) {
- super(RESOURCE, name, description, requestTopicName, responseTopicName, "N/A");
- this.mimeType = mimeType;
+ super(RESOURCE, name, description, requestTopicName, responseTopicName, "N/A", new HashMap<>());
+
+ setMimeType(mimeType);
setUrl(url);
}
}
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceResponse.java b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceResponse.java
index fb65191..896e4cc 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceResponse.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/ResourceResponse.java
@@ -7,103 +7,80 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
+import java.util.HashMap;
+import java.util.Map;
+
@Schema(value = """
- {
- "properties":{
- "type":{
- "connect.index":0,
- "enum":[
- "text",
- "blob"
- ],
- "default": "text",
- "type":"string"
- },
- "uri":{
- "connect.index":1,
- "type":"string"
- },
- "mimeType":{
- "connect.index":2,
- "type":"string"
- },
- "text":{
- "connect.index":3,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- },
- "blob":{
- "connect.index":4,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- }
- },
- "required":[
- "type",
- "uri",
- "mimeType"
- ],
- "title":"Record",
- "type":"object"
- }""", refs = {})
+ {
+ "properties":{
+ "type":{
+ "connect.index":0,
+ "enum":[
+ "text",
+ "blob"
+ ],
+ "default": "text",
+ "type":"string"
+ },
+ "uri":{
+ "connect.index":1,
+ "type":"string"
+ },
+ "mimeType":{
+ "connect.index":2,
+ "type":"string"
+ },
+ "metaData":{
+ "connect.index":3,
+ "type":"object",
+ "additionalProperties":true
+ }
+ },
+ "required":[
+ "type",
+ "uri",
+ "mimeType"
+ ],
+ "title":"Record",
+ "type":"object"
+ }""", refs = {})
@Getter
@Setter
-@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = TextResourceResponse.class)
@JsonSubTypes({
- @JsonSubTypes.Type(value = TextResourceResponse.class, name = "text"),
- @JsonSubTypes.Type(value = BlobResourceResponse.class, name = "blob")})
-public class ResourceResponse {
- public enum ResponseType {
- TEXT("text"),
- BLOB("blob");
-
- private final String value;
-
- ResponseType(String value) {
- this.value = value;
- }
-
- @JsonValue
- public String getValue() {
- return value;
- }
-
- @Override
- public String toString() {
- return value;
- }
-
- @JsonCreator
- public static ResourceResponse.ResponseType fromValue(String value) {
- for (ResourceResponse.ResponseType e : ResourceResponse.ResponseType.values()) {
- if (e.value.equals(value)) {
- return e;
- }
- }
-
- throw new IllegalArgumentException("Unexpected value '" + value + "'");
- }
- }
+ @JsonSubTypes.Type(value = TextResourceResponse.class, name = ResourceResponse.TEXT_TYPE),
+ @JsonSubTypes.Type(value = BlobResourceResponse.class, name = ResourceResponse.BLOB_TYPE)})
+public class ResourceResponse extends AbstractSchema {
+ public static final String TEXT_TYPE = "text";
+ public static final String BLOB_TYPE = "blob";
@JsonProperty(value = "type", required = true)
- private ResponseType type;
+ private String type;
@JsonProperty(value = "uri", required = true)
private String uri;
@JsonProperty(value = "mimeType", required = true)
private String mimeType;
+
+ protected ResourceResponse(String type,
+ String uri,
+ String mimeType,
+ Map metaData) {
+ super(metaData);
+
+ this.type = type;
+ this.uri = uri;
+ this.mimeType = mimeType;
+ }
+
+ protected ResourceResponse(String type,
+ String uri,
+ String mimeType) {
+ super(new HashMap<>());
+
+ this.type = type;
+ this.uri = uri;
+ this.mimeType = mimeType;
+ }
}
\ No newline at end of file
diff --git a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/TextResourceResponse.java b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/TextResourceResponse.java
index dca54a7..1f3be33 100644
--- a/common/src/main/java/io/confluent/pas/agent/common/services/schemas/TextResourceResponse.java
+++ b/common/src/main/java/io/confluent/pas/agent/common/services/schemas/TextResourceResponse.java
@@ -1,73 +1,30 @@
package io.confluent.pas.agent.common.services.schemas;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import io.confluent.kafka.schemaregistry.annotations.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-@Schema(value = """
- {
- "properties":{
- "type":{
- "connect.index":0,
- "enum":[
- "text",
- "blob"
- ],
- "default": "text",
- "type":"string"
- },
- "uri":{
- "connect.index":1,
- "type":"string"
- },
- "mimeType":{
- "connect.index":2,
- "type":"string"
- },
- "text":{
- "connect.index":3,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- },
- "blob":{
- "connect.index":4,
- "oneOf":[
- {
- "type":"null"
- },
- {
- "type":"string"
- }
- ]
- }
- },
- "required":[
- "type",
- "uri",
- "mimeType"
- ],
- "title":"Record",
- "type":"object"
- }""", refs = {})
+import java.util.HashMap;
+
@Getter
@Setter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class TextResourceResponse extends ResourceResponse {
- @JsonProperty(value = "text", required = true)
- private String text;
+ private final static String TEXT = "text";
+
+ public String getText() {
+ return getMetaDataValue(TEXT, String.class);
+ }
+
+ public void setText(String text) {
+ setMetaDataValue(TEXT, text);
+ }
public TextResourceResponse(String uri, String mimeType, String text) {
- super(ResponseType.TEXT, uri, mimeType);
- this.text = text;
+ super(ResourceResponse.TEXT_TYPE, uri, mimeType, new HashMap<>());
+
+ setText(text);
}
}
\ No newline at end of file
diff --git a/examples/JavaAgent/README.md b/examples/JavaAgent/README.md
index 0f8dce4..195c931 100644
--- a/examples/JavaAgent/README.md
+++ b/examples/JavaAgent/README.md
@@ -114,9 +114,9 @@ llm gemini
### Example Interaction in MCP Shell
```sh
-shell:> mcp list tools "Confluent MCP Proxy"
+shell:> mcp list tools "Confluent Agent Proxy"
--------------------
-Tools for server: Confluent MCP Proxy
+Tools for server: Confluent Agent Proxy
....................
Name: JavaSentimentAgent
Description: This agent analyzes sentiment in human queries.
diff --git a/examples/client_info.md b/examples/client_info.md
index 77ad24e..4439c23 100644
--- a/examples/client_info.md
+++ b/examples/client_info.md
@@ -212,10 +212,10 @@ llm gemini
Below is an example of how the shell interacts with the MCP proxy and LLM:
```
-Connected to server: Confluent MCP Proxy (1.0.0)
-shell:>mcp list tools "Confluent MCP Proxy"
+Connected to server: Confluent Agent Proxy (1.0.0)
+shell:>mcp list tools "Confluent Agent Proxy"
--------------------
-Tools for server: Confluent MCP Proxy
+Tools for server: Confluent Agent Proxy
....................
Name: findLastName
Description: Find the last name of a given user
diff --git a/frameworks/agent-proxy-framework/pom.xml b/frameworks/agent-proxy-framework/pom.xml
index d54a35f..fc2a78f 100644
--- a/frameworks/agent-proxy-framework/pom.xml
+++ b/frameworks/agent-proxy-framework/pom.xml
@@ -16,7 +16,6 @@
4.38.0
1.0.0-M7
- 7.9.0-ce
diff --git a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/RequestResponseHandler.java b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/RequestResponseHandler.java
new file mode 100644
index 0000000..ff747c1
--- /dev/null
+++ b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/RequestResponseHandler.java
@@ -0,0 +1,52 @@
+package io.confluent.pas.agent.proxy.frameworks.java;
+
+import io.confluent.pas.agent.common.services.kstream.StreamingHandler;
+import io.confluent.pas.agent.common.utils.JsonUtils;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Request;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
+import io.confluent.pas.agent.proxy.frameworks.java.models.ResponseStatus;
+import io.confluent.pas.agent.proxy.frameworks.java.subscription.SubscriptionRequest;
+import io.confluent.pas.agent.proxy.frameworks.java.subscription.SubscriptionResponse;
+
+import java.util.function.Consumer;
+
+public record RequestResponseHandler(Class requestClass, Class responseClass,
+ SubscriptionHandler.RequestHandler subscriptionHandler)
+ implements StreamingHandler {
+
+ @Override
+ public Class getKeyClass() {
+ return Key.class;
+ }
+
+ @Override
+ public Class getInClass() {
+ return Request.class;
+ }
+
+ @Override
+ public Class getOutClass() {
+ return Response.class;
+ }
+
+ @Override
+ public void handle(Key key, Request request, Consumer sink) {
+ final REQ req = JsonUtils.toObject(request.getPayload(), requestClass);
+
+ final SubscriptionRequest subscriptionRequest = new SubscriptionRequest<>(
+ key,
+ req,
+ subscriptionResponse -> sendResponse(subscriptionResponse, sink));
+
+ subscriptionHandler.onRequest(subscriptionRequest);
+ }
+
+ private void sendResponse(SubscriptionResponse subscriptionResponse, Consumer sink) {
+ final Response response = new Response();
+ response.setStatus(ResponseStatus.COMPLETED);
+ response.setPayload(JsonUtils.toMap(subscriptionResponse.response()));
+
+ sink.accept(response);
+ }
+}
diff --git a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandler.java b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandler.java
index add71b4..f5105b4 100644
--- a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandler.java
+++ b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandler.java
@@ -1,11 +1,8 @@
package io.confluent.pas.agent.proxy.frameworks.java;
import io.confluent.kafka.schemaregistry.json.JsonSchema;
-import io.confluent.kafka.serializers.json.KafkaJsonSchemaDeserializer;
-import io.confluent.kafka.serializers.json.KafkaJsonSchemaSerializer;
-import io.confluent.pas.agent.common.services.KafkaConfiguration;
-import io.confluent.pas.agent.common.services.KafkaPropertiesFactory;
-import io.confluent.pas.agent.common.services.RegistrationService;
+import io.confluent.pas.agent.common.services.*;
+import io.confluent.pas.agent.common.services.kstream.StreamingImpl;
import io.confluent.pas.agent.common.services.schemas.Registration;
import io.confluent.pas.agent.common.services.schemas.RegistrationKey;
import io.confluent.pas.agent.proxy.frameworks.java.kafka.TopicManagement;
@@ -15,16 +12,8 @@
import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
import io.confluent.pas.agent.proxy.frameworks.java.subscription.SubscriptionRequest;
import lombok.extern.slf4j.Slf4j;
-import org.apache.kafka.common.serialization.Serdes;
-import org.apache.kafka.streams.KafkaStreams;
-import org.apache.kafka.streams.StreamsBuilder;
-import org.apache.kafka.streams.Topology;
-import org.apache.kafka.streams.kstream.Consumed;
-import org.apache.kafka.streams.kstream.Produced;
import java.io.Closeable;
-import java.util.Map;
-import java.util.Properties;
import java.util.function.Supplier;
/**
@@ -41,13 +30,6 @@
@Slf4j
public class SubscriptionHandler implements Closeable {
- /**
- * Supplier for creating Kafka Streams instances.
- */
- public interface KStreamsSupplier {
- KafkaStreams get(Topology topology, Properties kStreamsProperties);
- }
-
/**
* Interface for handling incoming requests from Kafka topics.
*
@@ -62,12 +44,8 @@ public interface RequestHandler {
private final RegistrationService registrationService;
private final Class requestClass;
private final Class responseClass;
- private final Serdes.WrapperSerde keySerde;
- private final Serdes.WrapperSerde requestSerde;
- private final Serdes.WrapperSerde responseSerde;
private final Supplier topicManagementSupplier;
- private final KStreamsSupplier kafkaStreamsSupplier;
- private KafkaStreams kafkaStreams;
+ private final Streaming streaming;
/**
* Creates a new subscription handler with the specified message types.
@@ -87,33 +65,32 @@ public SubscriptionHandler(KafkaConfiguration kafkaConfiguration,
RegistrationKey.class,
Registration.class),
() -> new TopicManagementImpl(kafkaConfiguration),
- KafkaStreams::new
+ new StreamingImpl<>()
);
}
/**
- * Creates a new subscription handler with the specified message types.
+ * Creates a new subscription handler with the specified message types and registration service.
*
- * @param kafkaConfiguration Kafka cluster configuration
- * @param requestClass Class type for request payloads
- * @param responseClass Class type for response payloads
- * @param registrationService Registration service for storing capabilities
+ * @param kafkaConfiguration Kafka cluster configuration
+ * @param requestClass Class type for request payloads
+ * @param responseClass Class type for response payloads
+ * @param registrationService Registration service for managing registrations
+ * @param topicManagementSupplier Supplier for topic management
+ * @param streaming Streaming service for processing requests
*/
public SubscriptionHandler(KafkaConfiguration kafkaConfiguration,
Class requestClass,
Class responseClass,
RegistrationService registrationService,
Supplier topicManagementSupplier,
- KStreamsSupplier kafkaStreamsSupplier) {
+ Streaming streaming) {
this.kafkaConfiguration = kafkaConfiguration;
this.requestClass = requestClass;
this.responseClass = responseClass;
this.registrationService = registrationService;
- this.keySerde = createSerde(Key.class, true);
- this.requestSerde = createSerde(Request.class, false);
- this.responseSerde = createSerde(Response.class, false);
this.topicManagementSupplier = topicManagementSupplier;
- this.kafkaStreamsSupplier = kafkaStreamsSupplier;
+ this.streaming = streaming;
}
/**
@@ -165,9 +142,9 @@ public void subscribeWith(Registration registration,
public void close() {
log.info("Closing subscription handler resources");
- if (kafkaStreams != null) {
+ if (streaming != null) {
try {
- kafkaStreams.close();
+ streaming.close();
log.debug("Kafka Streams closed successfully");
} catch (Exception e) {
log.warn("Error closing Kafka Streams", e);
@@ -182,32 +159,12 @@ public void close() {
}
}
- /**
- * Creates a Kafka Serde for serialization/deserialization.
- *
- * @param valueClass Class to create serde for
- * @param isKey Whether this is for a key (true) or value (false)
- * @return Configured Serde instance
- */
- private Serdes.WrapperSerde createSerde(Class valueClass, boolean isKey) {
- final Map configuration =
- KafkaPropertiesFactory.getSchemaRegistryConfig(kafkaConfiguration, valueClass, isKey);
-
- final Serdes.WrapperSerde serde = new Serdes.WrapperSerde<>(
- new KafkaJsonSchemaSerializer<>(),
- new KafkaJsonSchemaDeserializer<>());
-
- serde.configure(configuration, isKey);
- return serde;
- }
-
/**
* Creates topics using class types.
*/
private void createTopics(Registration registration,
Class requestClass,
Class responseClass) throws Exception {
-
final JsonSchema reqSchema = Request.getSchema(requestClass);
final JsonSchema resSchema = Response.getSchema(responseClass);
@@ -240,7 +197,11 @@ private void createTopicsWithSchemas(Registration registration,
private void startSubscription(Registration registration,
RequestHandler handler) {
registerCapability(registration);
- setupAndStartKafkaStreams(registration, handler);
+ streaming.init(kafkaConfiguration,
+ registration.getRequestTopicName(),
+ registration.getResponseTopicName(),
+ new RequestResponseHandler<>(requestClass, responseClass, handler));
+ streaming.start();
}
/**
@@ -257,22 +218,4 @@ private void registerCapability(Registration registration) {
}
}
- /**
- * Sets up and starts the Kafka Streams topology.
- */
- private void setupAndStartKafkaStreams(Registration registration,
- RequestHandler handler) {
- StreamsBuilder builder = new StreamsBuilder();
-
- builder.stream(registration.getRequestTopicName(), Consumed.with(keySerde, requestSerde))
- .process(new SubscriptionHandlerSupplier<>(handler, requestClass))
- .to(registration.getResponseTopicName(), Produced.with(keySerde, responseSerde));
-
- final Topology topology = builder.build();
- kafkaStreams = kafkaStreamsSupplier.get(topology,
- KafkaPropertiesFactory.getKStreamsProperties(kafkaConfiguration));
-
- log.info("Starting Kafka Streams for registration: {}", registration.getName());
- kafkaStreams.start();
- }
}
\ No newline at end of file
diff --git a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerProcessor.java b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerProcessor.java
deleted file mode 100644
index e98d42f..0000000
--- a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerProcessor.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package io.confluent.pas.agent.proxy.frameworks.java;
-
-import io.confluent.pas.agent.common.utils.JsonUtils;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Request;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
-import io.confluent.pas.agent.proxy.frameworks.java.models.ResponseStatus;
-import io.confluent.pas.agent.proxy.frameworks.java.subscription.SubscriptionRequest;
-import io.confluent.pas.agent.proxy.frameworks.java.subscription.SubscriptionResponse;
-import org.apache.kafka.streams.processor.api.Processor;
-import org.apache.kafka.streams.processor.api.ProcessorContext;
-import org.apache.kafka.streams.processor.api.Record;
-
-public class SubscriptionHandlerProcessor implements Processor {
-
- private final SubscriptionHandler.RequestHandler subscriptionHandler;
- private final Class requestClass;
-
- private ProcessorContext context;
-
- public SubscriptionHandlerProcessor(SubscriptionHandler.RequestHandler subscriptionHandler,
- Class requestClass) {
- this.subscriptionHandler = subscriptionHandler;
- this.requestClass = requestClass;
- }
-
- @Override
- public void init(ProcessorContext context) {
- this.context = context;
- }
-
- @Override
- public void process(Record record) {
- final REQ request = JsonUtils.toObject(record.value().getPayload(), requestClass);
-
- final SubscriptionRequest subscriptionRequest = new SubscriptionRequest<>(
- record.key(),
- request,
- this::sendResponse);
-
- subscriptionHandler.onRequest(subscriptionRequest);
- }
-
- void sendResponse(SubscriptionResponse subscriptionResponse) {
- final Response response = new Response();
- response.setStatus(ResponseStatus.COMPLETED);
- response.setPayload(JsonUtils.toMap(subscriptionResponse.response()));
-
- context.forward(new Record<>(
- subscriptionResponse.key(),
- response,
- System.currentTimeMillis()));
- }
-}
diff --git a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerSupplier.java b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerSupplier.java
deleted file mode 100644
index 366bbde..0000000
--- a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerSupplier.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.confluent.pas.agent.proxy.frameworks.java;
-
-import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Request;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
-import lombok.AllArgsConstructor;
-import org.apache.kafka.streams.processor.api.Processor;
-import org.apache.kafka.streams.processor.api.ProcessorSupplier;
-
-@AllArgsConstructor
-public class SubscriptionHandlerSupplier implements ProcessorSupplier {
-
- private final SubscriptionHandler.RequestHandler subscriptionHandler;
- private final Class requestClass;
-
- @Override
- public Processor get() {
- return new SubscriptionHandlerProcessor<>(subscriptionHandler, requestClass);
- }
-}
diff --git a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/spring/autoconfig/McpRegistrationAutoConfiguration.java b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/spring/autoconfig/McpRegistrationAutoConfiguration.java
index 38c44a6..4174451 100644
--- a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/spring/autoconfig/McpRegistrationAutoConfiguration.java
+++ b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/spring/autoconfig/McpRegistrationAutoConfiguration.java
@@ -9,7 +9,6 @@
import org.springframework.context.annotation.Bean;
import io.confluent.pas.agent.common.services.schemas.Registration;
-import static io.confluent.pas.agent.common.services.schemas.Registration.CORRELATION_ID_FIELD_NAME;
/**
* Auto-configuration class for MCP (Model Control Protocol) agent registration.
diff --git a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/subscription/SubscriptionRequest.java b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/subscription/SubscriptionRequest.java
index 13cf159..40a3456 100644
--- a/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/subscription/SubscriptionRequest.java
+++ b/frameworks/agent-proxy-framework/src/main/java/io/confluent/pas/agent/proxy/frameworks/java/subscription/SubscriptionRequest.java
@@ -1,7 +1,6 @@
package io.confluent.pas.agent.proxy.frameworks.java.subscription;
import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
-import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
diff --git a/frameworks/agent-proxy-framework/src/test/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerTest.java b/frameworks/agent-proxy-framework/src/test/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerTest.java
index 557901f..c69a588 100644
--- a/frameworks/agent-proxy-framework/src/test/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerTest.java
+++ b/frameworks/agent-proxy-framework/src/test/java/io/confluent/pas/agent/proxy/frameworks/java/SubscriptionHandlerTest.java
@@ -3,12 +3,14 @@
import io.confluent.kafka.schemaregistry.json.JsonSchema;
import io.confluent.pas.agent.common.services.KafkaConfiguration;
import io.confluent.pas.agent.common.services.RegistrationService;
+import io.confluent.pas.agent.common.services.Streaming;
import io.confluent.pas.agent.common.services.schemas.Registration;
import io.confluent.pas.agent.common.services.schemas.RegistrationKey;
import io.confluent.pas.agent.proxy.frameworks.java.kafka.TopicManagement;
import io.confluent.pas.agent.proxy.frameworks.java.models.Key;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Request;
+import io.confluent.pas.agent.proxy.frameworks.java.models.Response;
import org.apache.commons.lang3.RandomStringUtils;
-import org.apache.kafka.streams.KafkaStreams;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
@@ -18,10 +20,10 @@
public class SubscriptionHandlerTest {
- private record Request(int a, int b) {
+ private record HandlingRequest(int a, int b) {
}
- private record Response(int result) {
+ private record HandlingResponse(int result) {
}
@@ -35,9 +37,9 @@ private record Response(int result) {
private TopicManagement topicManagement;
@Mock
- private KafkaStreams kStreams;
+ private Streaming kStreams;
- private SubscriptionHandler subscriptionHandler;
+ private SubscriptionHandler subscriptionHandler;
@BeforeEach
public void setUp() {
@@ -55,11 +57,11 @@ public void setUp() {
subscriptionHandler = new SubscriptionHandler<>(
kafkaConfiguration,
- Request.class,
- Response.class,
+ HandlingRequest.class,
+ HandlingResponse.class,
registrationService,
() -> topicManagement,
- (topology, properties) -> kStreams);
+ kStreams);
}
@Test
@@ -73,7 +75,7 @@ public void testSubscribeWith() throws Exception {
"responseTopic"
),
(request) -> {
- request.respond(new Response(request.getRequest().a() + request.getRequest().b()))
+ request.respond(new HandlingResponse(request.getRequest().a() + request.getRequest().b()))
.block();
}
);
@@ -100,7 +102,7 @@ public void testSubscribeWithSchema() throws Exception {
new JsonSchema(reqSchema),
new JsonSchema(resSchema),
(request) -> {
- request.respond(new Response(request.getRequest().a() + request.getRequest().b()))
+ request.respond(new HandlingResponse(request.getRequest().a() + request.getRequest().b()))
.block();
}
);
diff --git a/pom.xml b/pom.xml
index cb175ce..6e9ac54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,8 @@
3.7.5
1.21.0
0.9.0
+ 7.9.0-ce
+ 3.2.0
@@ -69,6 +71,18 @@
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+
+
+
+ org.apache.kafka
+ kafka-streams
+ ${kstream.version}
+
+
io.modelcontextprotocol.sdk
mcp
@@ -112,6 +126,11 @@
kcache
${kcache.version}
+
+ io.kcache
+ kcache-caffeine
+ ${kcache.version}
+
org.projectlombok
lombok
diff --git a/shell/src/main/resources/application.yaml b/shell/src/main/resources/application.yaml
index ed6ef7a..a9c4668 100644
--- a/shell/src/main/resources/application.yaml
+++ b/shell/src/main/resources/application.yaml
@@ -1,6 +1,6 @@
spring:
application:
- name: "Confluent MCP Proxy Client"
+ name: "Confluent Agent Proxy Client"
shell:
interactive:
enabled: true