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]
