weiqingy commented on code in PR #924:
URL: https://github.com/apache/flink-agents/pull/924#discussion_r3745291890


##########
python/flink_agents/cli/trace_tree.py:
##########
@@ -93,28 +162,15 @@ def read_event_records(
 
     for log_file in log_files:
         for record in read_json_objects(log_file, warnings):
-            event_id: str | None = None
-            invalid_reason: str | None = None
-            if not isinstance(record, dict):
-                invalid_reason = "record must be a JSON object"
-            else:
-                event = record.get("event")
-                event_type = record.get("eventType")
-                if not isinstance(event, dict):
-                    invalid_reason = "field 'event' must be a JSON object"
-                elif not isinstance(event.get("id"), str) or not event["id"]:
-                    invalid_reason = "field 'event.id' must be a non-empty 
string"
-                elif not isinstance(event_type, str) or not event_type:
-                    invalid_reason = "field 'eventType' must be a non-empty 
string"
-                else:
-                    event_id = event["id"]
-                    for field_name in ("upstreamEventId", 
"upstreamActionName"):
-                        field_value = event.get(field_name)
-                        if field_value is not None and not 
isinstance(field_value, str):
-                            invalid_reason = (
-                                f"field 'event.{field_name}' must be a string 
or null"
-                            )
-                            break
+            if (
+                isinstance(record, dict)
+                and record.get("eventType") in EXECUTION_LIFECYCLE_EVENT_TYPES

Review Comment:
   This skip drops the record and writes nothing, so it is the one place the 
reader discards something without leaving a warning behind. If a user's own 
Event happens to use one of these type strings it disappears from the tree, and 
its children then report `MISSING_PARENT` (`:316-324`) for a parent that is 
sitting right there in the log.
   
   Nothing reserves the `_` prefix today: `Event.java:82-84` only checks for 
null or empty, and Python's `event.py:59-71` has no check at all. So 
`_execution_*` is a convention, the same way `_input_event` is 
(`EventUtil.java:27-29` matches that one on the raw string too). None of that 
changes in this PR. What does change is that these names now live in two 
languages: `EntityTypes`, `ProblemCategories` and `ToolExecutionMetadataKeys` 
all got mirrored into Python, but these four are typed out again at `:26-33` 
and listed a third time in `monitoring.md:395-398`, with nothing keeping the 
copies in step.
   
   So is the namespace part of the contract? If it is, would a shared constant 
plus a warning on this skip be worth adding?



##########
plan/src/main/java/org/apache/flink/agents/plan/actions/ChatModelAction.java:
##########
@@ -368,16 +370,27 @@ public ChatMessage call() throws Exception {
 
         for (int attempt = 0; attempt < numRetries + 1; attempt++) {
             try {
-                response =
-                        chatAsync
-                                ? ctx.durableExecuteAsync(callable)
-                                : ctx.durableExecute(callable);
+                ExecutionReporters.started(ctx, 
ExecutionReporter.EntityTypes.LLM, model);
+                try {
+                    response =
+                            chatAsync
+                                    ? ctx.durableExecuteAsync(callable)
+                                    : ctx.durableExecute(callable);
+                    Objects.requireNonNull(response, "ChatModel returned a 
null response.");
+                } catch (Exception modelError) {
+                    ExecutionReporters.failed(
+                            ctx,
+                            ExecutionReporter.EntityTypes.LLM,
+                            model,
+                            modelError,
+                            
ExecutionReporter.ProblemCategories.MODEL_CALL_FAILED);
+                    throw modelError;
+                }
+                ExecutionReporters.succeeded(ctx, 
ExecutionReporter.EntityTypes.LLM, model);
                 recordChatTokenMetrics(chatModel, response);

Review Comment:
   `recordChatTokenMetrics` sits outside the durable boundary. On a cache hit, 
`durableExecuteCompletionOnly` returns at `RunnerContextImpl.java:384` and 
never reaches the real call at `:390`, but the cached `ChatMessage` still 
carries its `promptTokens` / `completionTokens`. So the documented counters 
(`monitoring.md:50-51`) go up for a model call that never happened. Python does 
the same (`chat_model_action.py:377-388`).
   
   The window is small. A finished action is skipped whole at 
`ActionExecutionOperator.java:362-383` and never gets here, so this is only the 
replay of an action that did not finish, and only when 
`actionStateStoreBackend` is set (default `null`, 
`AgentConfigOptions.java:65-66`).
   
   It also predates this PR: `main:375` is the same line in the same spot, so 
there is nothing to change here. Noting it for a follow-up issue, since these 
counters are user-facing.



##########
plan/src/main/java/org/apache/flink/agents/plan/actions/ChatModelAction.java:
##########
@@ -368,16 +370,27 @@ public ChatMessage call() throws Exception {
 
         for (int attempt = 0; attempt < numRetries + 1; attempt++) {
             try {
-                response =
-                        chatAsync
-                                ? ctx.durableExecuteAsync(callable)
-                                : ctx.durableExecute(callable);
+                ExecutionReporters.started(ctx, 
ExecutionReporter.EntityTypes.LLM, model);

Review Comment:
   Discussion #929 asks for the resolved model, the provider, and per-call 
token usage to land in `entityMetadata`. Today neither report carries any: this 
line and `:389` both pass just the model, so an LLM record has no 
`entityMetadata` at all, and `entityName` is only the alias 
`_default_chat_model`. One line further down at `:390` the model name and the 
token counts are both in hand, and they go only to the cumulative counters. 
Python is the same (`chat_model_action.py:375`, `:377-388`).
   
   Model and provider look easy, since both are known before the call, so the 
start and the end report would carry the same value. Usage is the tricky one: 
`entityMetadata` is part of what matches an end report back to its start 
(`RunnerContextImpl.java:264-286`, `ReportedExecutionKey.java:56-67`), and 
`ExecutionReporter.java:28-29` asks it to stay the same across both. A number 
known only after the response would not match, so the end report would come out 
under a fresh `executionId` instead of closing the one it started.
   
   The record shape is being settled here, so would it be worth carrying model 
and provider now, and letting usage find its own home later?



##########
python/flink_agents/cli/trace_tree.py:
##########
@@ -82,6 +90,67 @@ def read_json_objects(path: Path, warnings: list[dict[str, 
Any]]) -> Iterator[An
         yield record
 
 
+def _normalize_event_record(
+    record: Any,
+) -> tuple[dict[str, Any] | None, str | None, str | None]:
+    """Normalize flat and legacy Event Log records for lineage 
reconstruction."""
+    if not isinstance(record, dict):
+        return None, None, "record must be a JSON object"
+
+    event_type = record.get("eventType")
+    flat_record = "eventId" in record or "eventAttributes" in record
+    if flat_record:
+        event_id_value = record.get("eventId")
+        event_attributes = record.get("eventAttributes")
+        upstream_event_id = record.get("upstreamEventId")
+        upstream_action_name = record.get("upstreamActionName")
+        event_id_field = "eventId"
+        attributes_field = "eventAttributes"
+        lineage_prefix = ""
+    else:
+        event = record.get("event")
+        if not isinstance(event, dict):
+            return None, None, "field 'event' must be a JSON object"
+        event_id_value = event.get("id")
+        event_attributes = event.get("attributes")
+        upstream_event_id = event.get("upstreamEventId")
+        upstream_action_name = event.get("upstreamActionName")
+        event_id_field = "event.id"
+        attributes_field = "event.attributes"
+        lineage_prefix = "event."
+
+    if not isinstance(event_id_value, str) or not event_id_value:
+        return None, None, f"field '{event_id_field}' must be a non-empty 
string"
+    if not isinstance(event_type, str) or not event_type:
+        return None, None, "field 'eventType' must be a non-empty string"
+    if not isinstance(event_attributes, dict):
+        return None, None, f"field '{attributes_field}' must be a JSON object"

Review Comment:
   Nit: `event_id_value` is checked at `:122`, but this return and the one at 
`:125` both pass `None` as the id, so a record with a perfectly good `eventId` 
shows up as `MALFORMED_RECORD` with nothing to search on. This runs after both 
shape branches close, so it hits flat and legacy records alike. The lineage 
check just below at `:134-138` does pass the id through.



-- 
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