google / google/adk-java

[FEATURE] Port SaveFilesAsArtifactsPlugin from adk-python

Aperta
#1,411 1 commento 0 reazioni 1 assegnatario Rivendicata da @hemasekhar-p Vedi su GitHub
needs review
Lingua principale
Java
Stelle
1.7k
Fork
420
Merge medio
4g 12h
PR unite (30g)
31

Descrizione

**Please make sure you read the contribution guide and file the issues in the right place.**
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)

## 🔴 Required Information

### Is your feature request related to a specific problem?

adk-python deprecated `save_input_blobs_as_artifacts` in favour of `SaveFilesAsArtifactsPlugin`.
adk-java has the parameter but not the plugin, so `RunConfig.saveInputBlobsAsArtifacts(true)` is the
only way to keep uploaded bytes out of every later LLM request — and it loses two things the plugin
provides.

**1. The uploaded file name is discarded.** `Runner` names every artifact
`artifact_{invocationId}_{index}` and never reads `Blob.displayName`.

```java
// Runner, in the blob-offload loop
String fileName = "artifact_" + invocationContext.invocationId() + "_" + i;
```

An upload of `report.pdf` is stored as `artifact__1`, so anything that later loads it
must work from an opaque id that appears nowhere but the placeholder text.

**2. Nothing is reported to the session.** The user event carries a state delta only, so
`EventActions.artifactDelta` stays empty and the session's artifact bookkeeping never records the
upload.

```java
// Runner.appendNewMessageToSession — no artifactDelta is set on this event
if (stateDelta != null && !stateDelta.isEmpty()) {
eventBuilder.actions(EventActions.builder().stateDelta(new ConcurrentHashMap<>(stateDelta)).build());
}
```
Both behaviours live in the plugin.

### Describe the Solution You'd Like

`SaveFilesAsArtifactsPlugin` in `com.google.adk.plugins`, registered on a `Runner` like any other
plugin. For each `inlineData` part of the incoming user message it would:

- save the part to the configured `BaseArtifactService`, named from `Blob.displayName`, falling back
to `artifact_{invocationId}_{index}` when the blob carries no name;
- replace it with `[Uploaded Artifact: ""]` in the message that reaches the model and is
appended to the session, matching adk-python's wording;
- report the saved versions through `EventActions.artifactDelta`;
- on a failed save, keep the original part and log, without failing the invocation — as in
adk-python, the log is the only signal, so an unreachable artifact service degrades silently to no
offload.

**`attach_file_reference` would not be ported:** it needs
`get_artifact_version(...).canonical_uri`, which has no equivalent on `BaseArtifactService`, so it
would require an SPI change across every implementation. Separate change.

### Impact on your work

Applications wanting adk-python's behaviour must reimplement it in application code, where it will
drift from upstream. Not blocking, and no timeline — this is a parity gap, not an outage.

### Willingness to contribute

Yes. A PR follows immediately after this issue: one new plugin class plus a small package-private helper, with tests. No existing file is modified, and the only new public surface is the plugin class itself.

---

## 🟡 Recommended Information

### Describe Alternatives You've Considered

**Teach the existing flag to read `Blob.displayName` and set `artifactDelta`.** Smaller diff, but it
adds behaviour to the parameter adk-python is steering users away from, and adk-java would still have
no plugin — widening the divergence rather than closing it.

**Implement the plugin in application code.** This works using public API only, and is how the
behaviour below was verified. It is per-application boilerplate for something a plugin surface exists
to ship once.

### Proposed API / Implementation

Registration needs no new API — the existing surfaces already accept it:

```java
Runner runner = Runner.builder()
.agent(agent)
.appName("my-app")
.artifactService(new InMemoryArtifactService())
.sessionService(new InMemorySessionService())
.plugins(new SaveFilesAsArtifactsPlugin()) // also App.Builder.plugins(...) and
.build(); // InMemoryRunner(agent, appName, plugins)
```

Two hooks, both already wired into the runtime:

```java
public class SaveFilesAsArtifactsPlugin extends BasePlugin {

@Override
public Maybe onUserMessageCallback(InvocationContext ctx, Content userMessage) {
// save each inlineData part, swap it for [Uploaded Artifact: ""],
// stash {fileName: version} under a temp: state key, return the rebuilt Content
}

@Override
public Maybe beforeAgentCallback(BaseAgent agent, CallbackContext callbackContext) {
// drain the stash into callbackContext.eventActions().artifactDelta(), return Maybe.empty()
}
}
```

The state hand-off exists because `onUserMessageCallback` runs before any `EventActions` exists, so
the saved versions cannot be reported from there. adk-python solves it the same way.

### Additional Context

Same upload, same runner, differing only in which mechanism performs the offload:

```text
--- Run A: RunConfig.saveInputBlobsAsArtifacts(true) ---
user attached : blob with displayName="report.pdf"
artifacts stored in the session : artifact_e-eb41628a-9812-43a0-88d0-7b121706190b_1
EventActions.artifactDelta : (none)
message appended to the session :
- text: "read the attachment"
- text: "Uploaded file: artifact_e-eb41628a-…_1. It has been saved to the artifacts"

--- Run B: SaveFilesAsArtifactsPlugin (the proposed port) ---
user attached : blob with displayName="report.pdf"
artifacts stored in the session : report.pdf
EventActions.artifactDelta : {report.pdf=0}
message appended to the session :
- text: "read the attachment"
- text: "[Uploaded Artifact: "report.pdf"]"
```

Both offload the payload losslessly and both show the model a placeholder instead of the bytes — the
gap is the file name and the bookkeeping, not the offload.

Observed on `1.7.2-SNAPSHOT`, Windows 11 (not OS-specific), with a stub model and with
`gemini-3.5-flash`; both mechanisms run before the model call.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.