wenjin272 commented on code in PR #1060:
URL: https://github.com/apache/flink-agents/pull/1060#discussion_r4022080527


##########
api/src/main/java/org/apache/flink/agents/api/chat/messages/MediaBlock.java:
##########
@@ -0,0 +1,207 @@
+/*
+ * 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.flink.agents.api.chat.messages;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Shared shape for binary media blocks: modality is the concrete type, 
encoding is the media type
+ * (RFC 6838; historically called a MIME type).
+ *
+ * <p>Media blocks are immutable, and every construction path — the {@code 
fromBase64}/{@code
+ * fromUrl} factories, the full constructors, and Jackson deserialization — 
runs the same
+ * validation, so a block that exists carries exactly one of base64 {@code 
data} or an externally
+ * managed {@code url}. URL-backed content is externally managed: URLs may 
expire, may not be
+ * reachable by the model provider, and may be invalid after recovery from a 
checkpoint.
+ *
+ * <p>The optional {@code name}/{@code sizeBytes}/{@code sha256} metadata also 
serves the Event Log,
+ * which records media metadata instead of payload bytes — see {@link 
#sanitize()}.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public abstract class MediaBlock extends ContentBlock {
+
+    @JsonProperty("media_type")
+    private final String mediaType;
+
+    @Nullable private final String data;
+
+    @Nullable private final String url;

Review Comment:
   Could we model the payload location as a typed `MediaSource` instead of 
keeping mutually exclusive nullable `data` and `url` fields directly on 
`MediaBlock`?
   
   The current XOR validation works for the two initial sources, but it makes 
invalid states representable and does not scale well. Discussion #1031 
explicitly leaves room for a future managed blob/reference source. With the 
current flat shape, adding `blob_id`, provider file IDs, or another source 
would require expanding the N-way exclusivity validation in Java and Python, as 
well as updating serializers and bridge conversions.
   
   A discriminated source shape would make the invariant structural, for 
example:
   
   ```json
   {
     "type": "image",
     "media_type": "image/png",
     "source": {
       "type": "base64",
       "data": "..."
     }
   }
   ```
   
   This PR only needs `Base64Source` and `UrlSource`; a managed `BlobSource` 
can remain future work. This is similar to AgentScope's `Source` model and 
gives us an explicit extension point without implementing blob storage now. 
Providers would still explicitly convert or reject supported source types.



##########
runtime/src/main/java/org/apache/flink/agents/runtime/eventlog/ChatMessageEventLogSerializer.java:
##########
@@ -0,0 +1,67 @@
+/*
+ * 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.flink.agents.runtime.eventlog;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.apache.flink.agents.api.chat.messages.ChatMessage;
+import org.apache.flink.agents.api.chat.messages.ContentBlock;
+
+import java.io.IOException;
+
+/**
+ * The Event Log's {@link ChatMessage} serializer: each content block is 
written through its own
+ * {@link ContentBlock#sanitize()} projection, so media payload bytes and 
unsanitized URLs never
+ * reach the log — at any log level, VERBOSE included. Everything else (role, 
tool calls, extra
+ * args) keeps the regular shape, and the level-dependent {@link 
JsonTruncator} still applies to the
+ * result afterwards at STANDARD.
+ *
+ * <p>This serializer is registered only on the Event Log mappers via {@link 
#module()}; the global
+ * {@link ChatMessage} wire format — the Java/Python bridge, event 
serialization, state recovery —
+ * is untouched and preserves the complete payload. Logged output is not a 
faithful {@link
+ * ChatMessage} and must never be reconstructed into one: an inline-backed 
media block drops its
+ * {@code data} (so reconstruction fails loudly), and a URL-backed one carries 
only the stripped
+ * URL.
+ */
+public class ChatMessageEventLogSerializer extends JsonSerializer<ChatMessage> 
{
+
+    /** The module Event Log mappers register to apply the sanitized {@link 
ChatMessage} shape. */
+    public static Module module() {
+        return new SimpleModule("flink-agents-event-log-chat-messages")
+                .addSerializer(ChatMessage.class, new 
ChatMessageEventLogSerializer());

Review Comment:
   Python-originated built-in events are currently deserialized as the base 
`Event` at the cross-language boundary. Therefore, infrastructure that runs 
before the action, including Event Log serialization, cannot see the concrete 
event type, and the `ChatMessage`-specific serializer may be bypassed.
   
   This is a broader cross-language event restoration issue rather than 
something specific to this PR. I opened #1125 to track the framework-level fix 
for 0.4, so I don't think it needs to block this PR.



##########
python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py:
##########
@@ -426,18 +426,14 @@ def chat(
             ]
 
             extra_args["anthropic_content_blocks"] = message.content
-            return ChatMessage(
-                role=MessageRole(message.role),
-                content=text,
+            return ChatMessage.of(MessageRole(message.role), text,

Review Comment:
   Please rebase this PR onto the latest `main`, resolve the current merge 
conflict, and rerun CI. The current failures appear to come from call sites and 
tests on `main` that have not been migrated to the new `ChatMessage` API.



##########
python/flink_agents/api/chat_message.py:
##########
@@ -43,6 +44,88 @@ class MessageRole(str, Enum):
     TOOL = "tool"
 
 
+class TextBlock(BaseModel):
+    """A plain-text, immutable part of a ChatMessage."""
+
+    model_config = ConfigDict(frozen=True)
+
+    type: Literal["text"] = "text"
+    text: str = ""
+
+    def __str__(self) -> str:
+        return self.text
+
+
+class MediaBlock(BaseModel):
+    """Shared shape for binary media blocks: modality is the concrete type,
+    encoding is the media type (RFC 6838; historically called a MIME type).
+
+    Media blocks are immutable, and the payload is carried by exactly one of
+    base64 ``data`` or an externally managed ``url``. URL-backed content is
+    externally managed: URLs may expire, may not be reachable by the model
+    provider, and may be invalid after recovery from a checkpoint. The optional
+    ``name``/``size_bytes``/``sha256`` metadata also serves the Event Log,
+    which records media metadata instead of payload bytes.
+    """
+
+    # Frozen keeps sharing a block (e.g. across a routing context copy) safe,
+    # and matches the validated immutable construction on the Java side.
+    model_config = ConfigDict(frozen=True)
+
+    media_type: str

Review Comment:
   Java rejects both `null` and an empty `media_type`, but the Python model 
currently uses an unconstrained `str`, so this is accepted:
   
   ```python
   ImageBlock(media_type="", data="aGk=")
   ```
   
   The object is therefore valid in Python but fails later when it crosses the 
Java bridge and is reconstructed as a Java `MediaBlock`. Could we enforce the 
same non-empty constraint in Python, for example with `Field(min_length=1)`, 
and add a negative cross-language test? The public Java and Python APIs should 
agree on which wire values are valid.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to