This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12233-3339132550594fe3128534cf37c86f0cbbe435cc
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit 7b88e187059226a64ff09dea3f4fc74de402e901
Author: Yohanes <[email protected]>
AuthorDate: Sun Sep 13 13:53:01 2026 +0000

    feat(connector): add Mem0 Platform V3 ADD sink (#12233)
    
    Signed-off-by: Yohanes <[email protected]>
    Co-authored-by: Yohanes <[email protected]>
---
 config/plugin_config                               |   1 +
 docs/en/connectors/changelog/connector-mem0.md     |   7 +
 docs/en/connectors/sink/Mem0.md                    |  33 ++++
 docs/zh/connectors/changelog/connector-mem0.md     |   7 +
 docs/zh/connectors/sink/Mem0.md                    |  30 ++++
 plugin-mapping.properties                          |   1 +
 .../connector-http/connector-mem0/pom.xml          |  35 ++++
 .../seatunnel/mem0/config/Mem0SinkOptions.java     |  68 ++++++++
 .../connectors/seatunnel/mem0/sink/Mem0Sink.java   |  94 ++++++++++
 .../seatunnel/mem0/sink/Mem0SinkFactory.java       |  56 ++++++
 .../seatunnel/mem0/sink/Mem0SinkWriter.java        | 193 +++++++++++++++++++++
 .../seatunnel/mem0/sink/Mem0SinkWriterTest.java    | 166 ++++++++++++++++++
 seatunnel-connectors-v2/connector-http/pom.xml     |   1 +
 seatunnel-dist/pom.xml                             |   6 +
 14 files changed, 698 insertions(+)

diff --git a/config/plugin_config b/config/plugin_config
index f8d2f01e99..8e2b4992d2 100644
--- a/config/plugin_config
+++ b/config/plugin_config
@@ -66,6 +66,7 @@ connector-tiktok-ads
 connector-graphql
 connector-hive
 connector-http-base
+connector-mem0
 connector-http-feishu
 connector-http-gitlab
 connector-http-github
diff --git a/docs/en/connectors/changelog/connector-mem0.md 
b/docs/en/connectors/changelog/connector-mem0.md
new file mode 100644
index 0000000000..e931ca8a68
--- /dev/null
+++ b/docs/en/connectors/changelog/connector-mem0.md
@@ -0,0 +1,7 @@
+<details><summary> Change Log </summary>
+
+| Change | Commit | Version |
+| --- | --- | --- |
+| [Feature][Connector-V2] Add Mem0 Platform V3 ADD sink connector | - | Next |
+
+</details>
diff --git a/docs/en/connectors/sink/Mem0.md b/docs/en/connectors/sink/Mem0.md
new file mode 100644
index 0000000000..893c59281e
--- /dev/null
+++ b/docs/en/connectors/sink/Mem0.md
@@ -0,0 +1,33 @@
+import ChangeLog from '../changelog/connector-mem0.md';
+
+# Mem0
+
+The Mem0 sink sends SeaTunnel rows to the hosted Mem0 Platform V3 asynchronous
+add API. Phase 1 supports `ADD` only. A row is acknowledged only when the API
+returns a non-empty `event_id`; this is accepted at-least-once delivery, not
+completion of asynchronous processing.
+
+## Configuration
+
+```hocon
+sink {
+  Mem0 {
+    api_key = "${MEM0_API_KEY}"
+    messages_field = "messages"
+    user_id_field = "user_id"
+    api_base_url = "https://api.mem0.ai";
+  }
+}
+```
+
+`messages_field` must resolve to an array. At least one scope field among
+`user_id_field`, `agent_id_field`, `app_id_field`, and `run_id_field` must be
+configured and non-empty for each row. Optional `metadata_field` must resolve
+to a JSON object.
+
+The connector uses `POST /v3/memories/add/` with `Authorization: Token`,
+`Content-Type: application/json`, and `Accept: application/json`. Delete,
+self-hosted OSS endpoints, event polling, and a generic Mem0-compatible
+protocol are outside Phase 1.
+
+<ChangeLog />
diff --git a/docs/zh/connectors/changelog/connector-mem0.md 
b/docs/zh/connectors/changelog/connector-mem0.md
new file mode 100644
index 0000000000..e931ca8a68
--- /dev/null
+++ b/docs/zh/connectors/changelog/connector-mem0.md
@@ -0,0 +1,7 @@
+<details><summary> Change Log </summary>
+
+| Change | Commit | Version |
+| --- | --- | --- |
+| [Feature][Connector-V2] Add Mem0 Platform V3 ADD sink connector | - | Next |
+
+</details>
diff --git a/docs/zh/connectors/sink/Mem0.md b/docs/zh/connectors/sink/Mem0.md
new file mode 100644
index 0000000000..5709a2ffa1
--- /dev/null
+++ b/docs/zh/connectors/sink/Mem0.md
@@ -0,0 +1,30 @@
+import ChangeLog from '../changelog/connector-mem0.md';
+
+# Mem0
+
+Mem0 Sink 将 SeaTunnel 行写入托管版 Mem0 Platform V3 异步添加接口。第一阶段
+只支持 `ADD`。只有接口返回非空 `event_id` 时才确认该行;这表示“已接受的至少一次
+投递”,不代表 Mem0 异步处理已经完成。
+
+## 配置
+
+```hocon
+sink {
+  Mem0 {
+    api_key = "${MEM0_API_KEY}"
+    messages_field = "messages"
+    user_id_field = "user_id"
+    api_base_url = "https://api.mem0.ai";
+  }
+}
+```
+
+`messages_field` 必须解析为数组。每行至少要有
+`user_id_field`、`agent_id_field`、`app_id_field`、`run_id_field` 中的一个非空范围字段。
+可选的 `metadata_field` 必须解析为 JSON 对象。
+
+连接器使用 `POST /v3/memories/add/`,并发送 `Authorization: Token`、
+`Content-Type: application/json` 和 `Accept: application/json`。删除、自托管 OSS
+接口、事件轮询以及通用 Mem0 兼容协议不属于第一阶段范围。
+
+<ChangeLog />
diff --git a/plugin-mapping.properties b/plugin-mapping.properties
index e49dc101c9..538a91f225 100644
--- a/plugin-mapping.properties
+++ b/plugin-mapping.properties
@@ -171,6 +171,7 @@ seatunnel.source.MQTT = connector-mqtt
 seatunnel.sink.MQTT = connector-mqtt
 seatunnel.source.SNMP = connector-snmp
 seatunnel.sink.SNMP = connector-snmp
+seatunnel.sink.Mem0 = connector-mem0
 seatunnel.source.Python = connector-python
 seatunnel.source.Prometheus = connector-prometheus
 seatunnel.sink.Prometheus = connector-prometheus
diff --git a/seatunnel-connectors-v2/connector-http/connector-mem0/pom.xml 
b/seatunnel-connectors-v2/connector-http/connector-mem0/pom.xml
new file mode 100644
index 0000000000..3b9c198209
--- /dev/null
+++ b/seatunnel-connectors-v2/connector-http/connector-mem0/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.seatunnel</groupId>
+        <artifactId>connector-http</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <artifactId>connector-mem0</artifactId>
+    <name>SeaTunnel : Connectors V2 : Http : Mem0</name>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-http-base</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/config/Mem0SinkOptions.java
 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/config/Mem0SinkOptions.java
new file mode 100644
index 0000000000..ca6d03d0d6
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/config/Mem0SinkOptions.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.connectors.seatunnel.mem0.config;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+import org.apache.seatunnel.connectors.seatunnel.http.config.HttpCommonOptions;
+
+public class Mem0SinkOptions extends HttpCommonOptions {
+    public static final String DEFAULT_API_BASE_URL = "https://api.mem0.ai";;
+    public static final String ADD_PATH = "/v3/memories/add/";
+    public static final String AUTHORIZATION = "Authorization";
+    public static final String CONTENT_TYPE = "Content-Type";
+    public static final String ACCEPT = "Accept";
+    public static final String APPLICATION_JSON = "application/json";
+
+    public static final Option<String> API_BASE_URL =
+            Options.key("api_base_url")
+                    .stringType()
+                    .defaultValue(DEFAULT_API_BASE_URL)
+                    .withDescription("Mem0 Platform V3 API base URL");
+    public static final Option<String> API_KEY =
+            
Options.key("api_key").stringType().noDefaultValue().withDescription("Mem0 API 
key");
+    public static final Option<String> MESSAGES_FIELD =
+            Options.key("messages_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Input row field containing the messages 
payload");
+    public static final Option<String> USER_ID_FIELD =
+            Options.key("user_id_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Input row field mapped to Mem0 user_id");
+    public static final Option<String> AGENT_ID_FIELD =
+            Options.key("agent_id_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Optional input row field mapped to Mem0 
agent_id");
+    public static final Option<String> APP_ID_FIELD =
+            Options.key("app_id_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Optional input row field mapped to Mem0 
app_id");
+    public static final Option<String> RUN_ID_FIELD =
+            Options.key("run_id_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Optional input row field mapped to Mem0 
run_id");
+    public static final Option<String> METADATA_FIELD =
+            Options.key("metadata_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Optional input row field mapped to Mem0 
metadata");
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0Sink.java
 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0Sink.java
new file mode 100644
index 0000000000..ed5957d96c
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0Sink.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.connectors.seatunnel.mem0.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.sink.SinkWriter;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import 
org.apache.seatunnel.connectors.seatunnel.common.sink.AbstractSimpleSink;
+import org.apache.seatunnel.connectors.seatunnel.http.config.HttpParameter;
+import org.apache.seatunnel.connectors.seatunnel.mem0.config.Mem0SinkOptions;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class Mem0Sink extends AbstractSimpleSink<SeaTunnelRow, Void> {
+    private final CatalogTable catalogTable;
+    private final SeaTunnelRowType rowType;
+    private final HttpParameter httpParameter;
+    private final String messagesField;
+    private final String userIdField;
+    private final String agentIdField;
+    private final String appIdField;
+    private final String runIdField;
+    private final String metadataField;
+
+    public Mem0Sink(ReadonlyConfig config, CatalogTable catalogTable) {
+        this.catalogTable = catalogTable;
+        this.rowType = catalogTable.getSeaTunnelRowType();
+        this.messagesField = config.get(Mem0SinkOptions.MESSAGES_FIELD);
+        this.userIdField = config.get(Mem0SinkOptions.USER_ID_FIELD);
+        this.agentIdField = 
config.getOptional(Mem0SinkOptions.AGENT_ID_FIELD).orElse(null);
+        this.appIdField = 
config.getOptional(Mem0SinkOptions.APP_ID_FIELD).orElse(null);
+        this.runIdField = 
config.getOptional(Mem0SinkOptions.RUN_ID_FIELD).orElse(null);
+        this.metadataField = 
config.getOptional(Mem0SinkOptions.METADATA_FIELD).orElse(null);
+        String baseUrl = config.get(Mem0SinkOptions.API_BASE_URL);
+        this.httpParameter = new HttpParameter();
+        this.httpParameter.setUrl(normalizeBaseUrl(baseUrl) + 
Mem0SinkOptions.ADD_PATH);
+        Map<String, String> headers = new HashMap<>();
+        headers.put(Mem0SinkOptions.AUTHORIZATION, "Token " + 
config.get(Mem0SinkOptions.API_KEY));
+        headers.put(Mem0SinkOptions.CONTENT_TYPE, 
Mem0SinkOptions.APPLICATION_JSON);
+        headers.put(Mem0SinkOptions.ACCEPT, Mem0SinkOptions.APPLICATION_JSON);
+        this.httpParameter.setHeaders(headers);
+        
this.httpParameter.setRetry(config.getOptional(Mem0SinkOptions.RETRY).orElse(0));
+        this.httpParameter.setRetryBackoffMultiplierMillis(
+                config.get(Mem0SinkOptions.RETRY_BACKOFF_MULTIPLIER_MS));
+        this.httpParameter.setRetryBackoffMaxMillis(
+                config.get(Mem0SinkOptions.RETRY_BACKOFF_MAX_MS));
+    }
+
+    private static String normalizeBaseUrl(String value) {
+        return value.endsWith("/") ? value.substring(0, value.length() - 1) : 
value;
+    }
+
+    @Override
+    public String getPluginName() {
+        return "Mem0";
+    }
+
+    @Override
+    public Mem0SinkWriter createWriter(SinkWriter.Context context) throws 
IOException {
+        return new Mem0SinkWriter(
+                rowType,
+                httpParameter,
+                messagesField,
+                userIdField,
+                agentIdField,
+                appIdField,
+                runIdField,
+                metadataField);
+    }
+
+    @Override
+    public Optional<CatalogTable> getWriteCatalogTable() {
+        return Optional.ofNullable(catalogTable);
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkFactory.java
 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkFactory.java
new file mode 100644
index 0000000000..af6bb0868f
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.connectors.seatunnel.mem0.sink;
+
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.table.connector.TableSink;
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.TableSinkFactory;
+import org.apache.seatunnel.api.table.factory.TableSinkFactoryContext;
+import org.apache.seatunnel.connectors.seatunnel.mem0.config.Mem0SinkOptions;
+
+import com.google.auto.service.AutoService;
+
+@AutoService(Factory.class)
+public class Mem0SinkFactory implements TableSinkFactory {
+    @Override
+    public String factoryIdentifier() {
+        return "Mem0";
+    }
+
+    @Override
+    public OptionRule optionRule() {
+        return OptionRule.builder()
+                .required(Mem0SinkOptions.API_KEY, 
Mem0SinkOptions.MESSAGES_FIELD)
+                .optional(
+                        Mem0SinkOptions.API_BASE_URL,
+                        Mem0SinkOptions.USER_ID_FIELD,
+                        Mem0SinkOptions.AGENT_ID_FIELD,
+                        Mem0SinkOptions.APP_ID_FIELD,
+                        Mem0SinkOptions.RUN_ID_FIELD,
+                        Mem0SinkOptions.METADATA_FIELD,
+                        Mem0SinkOptions.RETRY,
+                        Mem0SinkOptions.RETRY_BACKOFF_MULTIPLIER_MS,
+                        Mem0SinkOptions.RETRY_BACKOFF_MAX_MS)
+                .build();
+    }
+
+    @Override
+    public TableSink createSink(TableSinkFactoryContext context) {
+        return () -> new Mem0Sink(context.getOptions(), 
context.getCatalogTable());
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkWriter.java
 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkWriter.java
new file mode 100644
index 0000000000..5692934889
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/main/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkWriter.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.connectors.seatunnel.mem0.sink;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.JsonNode;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.ObjectMapper;
+import 
org.apache.seatunnel.shade.com.fasterxml.jackson.databind.node.ObjectNode;
+
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import 
org.apache.seatunnel.connectors.seatunnel.common.sink.AbstractSinkWriter;
+import 
org.apache.seatunnel.connectors.seatunnel.http.client.HttpClientProvider;
+import org.apache.seatunnel.connectors.seatunnel.http.client.HttpResponse;
+import org.apache.seatunnel.connectors.seatunnel.http.config.HttpParameter;
+import org.apache.seatunnel.format.json.JsonSerializationSchema;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/** Writes SeaTunnel rows to the hosted Mem0 Platform V3 asynchronous add API. 
*/
+public class Mem0SinkWriter extends AbstractSinkWriter<SeaTunnelRow, Void> {
+    private final HttpClientProvider httpClient;
+    private final String url;
+    private final JsonSerializationSchema serializationSchema;
+    private final ObjectMapper objectMapper;
+    private final String messagesField;
+    private final String userIdField;
+    private final String agentIdField;
+    private final String appIdField;
+    private final String runIdField;
+    private final String metadataField;
+    private final int maxRetries;
+    private final int retryBackoffMultiplierMillis;
+    private final int retryBackoffMaxMillis;
+
+    public Mem0SinkWriter(
+            SeaTunnelRowType rowType,
+            HttpParameter parameter,
+            String messagesField,
+            String userIdField,
+            String agentIdField,
+            String appIdField,
+            String runIdField,
+            String metadataField) {
+        this.httpClient = new HttpClientProvider(parameter);
+        this.url = parameter.getUrl();
+        this.serializationSchema = new JsonSerializationSchema(rowType);
+        this.objectMapper = serializationSchema.getMapper();
+        this.messagesField = messagesField;
+        this.userIdField = userIdField;
+        this.agentIdField = agentIdField;
+        this.appIdField = appIdField;
+        this.runIdField = runIdField;
+        this.metadataField = metadataField;
+        this.headers = parameter.getHeaders();
+        this.maxRetries = Math.max(0, parameter.getRetry());
+        this.retryBackoffMultiplierMillis =
+                Math.max(0, parameter.getRetryBackoffMultiplierMillis());
+        this.retryBackoffMaxMillis = Math.max(0, 
parameter.getRetryBackoffMaxMillis());
+    }
+
+    private final java.util.Map<String, String> headers;
+
+    @Override
+    public void write(SeaTunnelRow element) throws IOException {
+        ObjectNode row = parseRow(element);
+        ObjectNode request = objectMapper.createObjectNode();
+        JsonNode messages = required(row, messagesField, "messages");
+        if (!messages.isArray()) {
+            throw new IOException("Mem0 messages field must contain a JSON 
array");
+        }
+        request.set("messages", messages);
+        boolean hasScope = false;
+        hasScope |= copyIfPresent(row, userIdField, "user_id", request);
+        hasScope |= copyIfPresent(row, agentIdField, "agent_id", request);
+        hasScope |= copyIfPresent(row, appIdField, "app_id", request);
+        hasScope |= copyIfPresent(row, runIdField, "run_id", request);
+        if (!hasScope) {
+            throw new IOException("Mem0 requires at least one scope field");
+        }
+        if (metadataField != null) {
+            JsonNode metadata = row.get(metadataField);
+            if (metadata != null && !metadata.isNull()) {
+                if (!metadata.isObject()) {
+                    throw new IOException("Mem0 metadata field must contain a 
JSON object");
+                }
+                request.set("metadata", metadata);
+            }
+        }
+        send(request);
+    }
+
+    private ObjectNode parseRow(SeaTunnelRow element) throws IOException {
+        return (ObjectNode) 
objectMapper.readTree(serializationSchema.serialize(element));
+    }
+
+    private JsonNode required(ObjectNode row, String field, String 
logicalName) throws IOException {
+        JsonNode value = row.get(field);
+        if (value == null
+                || value.isNull()
+                || (value.isTextual() && value.textValue().trim().isEmpty())) {
+            throw new IOException("Mem0 " + logicalName + " field is missing 
or empty");
+        }
+        return value;
+    }
+
+    private boolean copyIfPresent(ObjectNode row, String field, String name, 
ObjectNode target) {
+        if (field == null) {
+            return false;
+        }
+        JsonNode value = row.get(field);
+        if (value == null
+                || value.isNull()
+                || (value.isTextual() && value.textValue().trim().isEmpty())) {
+            return false;
+        }
+        target.set(name, value);
+        return true;
+    }
+
+    private void send(ObjectNode request) throws IOException {
+        final String body;
+        try {
+            body = objectMapper.writeValueAsString(request);
+            for (int attempt = 0; ; attempt++) {
+                HttpResponse response = httpClient.doPost(url, headers, body);
+                if (response.getCode() >= 200 && response.getCode() < 300) {
+                    validateAcceptedResponse(response);
+                    return;
+                }
+                if (!isRetryableStatus(response.getCode()) || attempt >= 
maxRetries) {
+                    throw new IOException(
+                            "Mem0 add request failed with HTTP status " + 
response.getCode());
+                }
+                waitBeforeRetry(attempt);
+            }
+        } catch (IOException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new IOException("Failed to send Mem0 add request", e);
+        }
+    }
+
+    private void validateAcceptedResponse(HttpResponse response) throws 
IOException {
+        JsonNode responseBody = objectMapper.readTree(response.getContent());
+        JsonNode eventId = responseBody == null ? null : 
responseBody.get("event_id");
+        if (eventId == null || eventId.isNull() || 
eventId.asText().trim().isEmpty()) {
+            throw new IOException("Mem0 add response did not contain a 
nonblank event_id");
+        }
+    }
+
+    static boolean isRetryableStatus(int statusCode) {
+        return statusCode == 408 || statusCode == 429 || statusCode >= 500;
+    }
+
+    private void waitBeforeRetry(int attempt) throws IOException {
+        long delay =
+                Math.min(
+                        (long) retryBackoffMultiplierMillis * (attempt + 1),
+                        (long) retryBackoffMaxMillis);
+        if (delay <= 0) {
+            return;
+        }
+        try {
+            TimeUnit.MILLISECONDS.sleep(delay);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IOException("Interrupted while retrying Mem0 add 
request", e);
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (Objects.nonNull(httpClient)) {
+            httpClient.close();
+        }
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-mem0/src/test/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkWriterTest.java
 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/test/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkWriterTest.java
new file mode 100644
index 0000000000..75185d4edd
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-mem0/src/test/java/org/apache/seatunnel/connectors/seatunnel/mem0/sink/Mem0SinkWriterTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.connectors.seatunnel.mem0.sink;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.JsonNode;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.seatunnel.api.table.type.ArrayType;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import 
org.apache.seatunnel.connectors.seatunnel.http.client.HttpClientProvider;
+import org.apache.seatunnel.connectors.seatunnel.http.client.HttpResponse;
+import org.apache.seatunnel.connectors.seatunnel.http.config.HttpParameter;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class Mem0SinkWriterTest {
+    @Mock private HttpClientProvider httpClient;
+    private SeaTunnelRowType rowType;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        rowType =
+                new SeaTunnelRowType(
+                        new String[] {"messages", "user_id", "agent_id"},
+                        new SeaTunnelDataType[] {
+                            ArrayType.of(BasicType.STRING_TYPE),
+                            BasicType.STRING_TYPE,
+                            BasicType.STRING_TYPE
+                        });
+    }
+
+    private Mem0SinkWriter writer() throws Exception {
+        HttpParameter parameter = new HttpParameter();
+        parameter.setUrl("https://api.mem0.ai/v3/memories/add/";);
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Token secret");
+        headers.put("Content-Type", "application/json");
+        headers.put("Accept", "application/json");
+        parameter.setHeaders(headers);
+        Mem0SinkWriter writer =
+                new Mem0SinkWriter(
+                        rowType, parameter, "messages", "user_id", null, null, 
null, null);
+        Field field = Mem0SinkWriter.class.getDeclaredField("httpClient");
+        field.setAccessible(true);
+        field.set(writer, httpClient);
+        return writer;
+    }
+
+    @Test
+    void sendsV3AddPayloadAndRequiresEventId() throws Exception {
+        when(httpClient.doPost(anyString(), any(), anyString()))
+                .thenReturn(new HttpResponse(202, "{\"event_id\":\"evt-1\"}"));
+
+        writer().write(new SeaTunnelRow(new Object[] {new String[] {"hello"}, 
"u-1", null}));
+
+        org.mockito.ArgumentCaptor<String> body = 
org.mockito.ArgumentCaptor.forClass(String.class);
+        verify(httpClient).doPost(anyString(), any(), body.capture());
+        JsonNode json = new ObjectMapper().readTree(body.getValue());
+        assertEquals("hello", json.get("messages").get(0).asText());
+        assertEquals("u-1", json.get("user_id").asText());
+    }
+
+    @Test
+    void rejectsAcceptedResponseWithoutEventId() throws Exception {
+        when(httpClient.doPost(anyString(), any(), anyString()))
+                .thenReturn(new HttpResponse(202, "{}"));
+
+        assertThrows(
+                java.io.IOException.class,
+                () ->
+                        writer().write(
+                                        new SeaTunnelRow(
+                                                new Object[] {
+                                                    new String[] {"hello"}, 
"u-1", null
+                                                })));
+    }
+
+    @Test
+    void retriesRateLimitAndServerResponses() throws Exception {
+        when(httpClient.doPost(anyString(), any(), anyString()))
+                .thenReturn(new HttpResponse(429, "{}"))
+                .thenReturn(new HttpResponse(503, "{}"))
+                .thenReturn(new HttpResponse(202, "{\"event_id\":\"evt-3\"}"));
+        HttpParameter parameter = parameter();
+        parameter.setRetry(2);
+        parameter.setRetryBackoffMultiplierMillis(1);
+        parameter.setRetryBackoffMaxMillis(2);
+        Mem0SinkWriter writer =
+                new Mem0SinkWriter(
+                        rowType, parameter, "messages", "user_id", null, null, 
null, null);
+        injectClient(writer);
+
+        writer.write(new SeaTunnelRow(new Object[] {new String[] {"hello"}, 
"u-1", null}));
+
+        verify(httpClient, org.mockito.Mockito.times(3)).doPost(anyString(), 
any(), anyString());
+    }
+
+    @Test
+    void onlyTransientStatusesAreRetryable() {
+        
org.junit.jupiter.api.Assertions.assertTrue(Mem0SinkWriter.isRetryableStatus(408));
+        
org.junit.jupiter.api.Assertions.assertTrue(Mem0SinkWriter.isRetryableStatus(429));
+        
org.junit.jupiter.api.Assertions.assertTrue(Mem0SinkWriter.isRetryableStatus(500));
+        
org.junit.jupiter.api.Assertions.assertFalse(Mem0SinkWriter.isRetryableStatus(400));
+        
org.junit.jupiter.api.Assertions.assertFalse(Mem0SinkWriter.isRetryableStatus(401));
+    }
+
+    @Test
+    void acceptsAnotherScopeWhenConfiguredUserIdIsNull() throws Exception {
+        when(httpClient.doPost(anyString(), any(), anyString()))
+                .thenReturn(new HttpResponse(202, "{\"event_id\":\"evt-2\"}"));
+        Mem0SinkWriter writer =
+                new Mem0SinkWriter(
+                        rowType, parameter(), "messages", "user_id", 
"agent_id", null, null, null);
+        injectClient(writer);
+        writer.write(new SeaTunnelRow(new Object[] {new String[] {"hello"}, 
null, "a-1"}));
+    }
+
+    private HttpParameter parameter() {
+        HttpParameter parameter = new HttpParameter();
+        parameter.setUrl("https://api.mem0.ai/v3/memories/add/";);
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Token secret");
+        headers.put("Content-Type", "application/json");
+        headers.put("Accept", "application/json");
+        parameter.setHeaders(headers);
+        return parameter;
+    }
+
+    private void injectClient(Mem0SinkWriter writer) throws Exception {
+        Field field = Mem0SinkWriter.class.getDeclaredField("httpClient");
+        field.setAccessible(true);
+        field.set(writer, httpClient);
+    }
+}
diff --git a/seatunnel-connectors-v2/connector-http/pom.xml 
b/seatunnel-connectors-v2/connector-http/pom.xml
index 133cf63a74..05f73a000c 100644
--- a/seatunnel-connectors-v2/connector-http/pom.xml
+++ b/seatunnel-connectors-v2/connector-http/pom.xml
@@ -48,6 +48,7 @@
         <module>connector-http-zendesk</module>
         <module>connector-http-stripe</module>
         <module>connector-http-linear</module>
+        <module>connector-mem0</module>
     </modules>
 
 </project>
diff --git a/seatunnel-dist/pom.xml b/seatunnel-dist/pom.xml
index 27ddbfc396..a029ecd37b 100644
--- a/seatunnel-dist/pom.xml
+++ b/seatunnel-dist/pom.xml
@@ -698,6 +698,12 @@
                     <version>${project.version}</version>
                     <scope>provided</scope>
                 </dependency>
+                <dependency>
+                    <groupId>org.apache.seatunnel</groupId>
+                    <artifactId>connector-mem0</artifactId>
+                    <version>${project.version}</version>
+                    <scope>provided</scope>
+                </dependency>
                 <dependency>
                     <groupId>org.apache.seatunnel</groupId>
                     <artifactId>connector-rabbitmq</artifactId>

Reply via email to