Repository navigation
[camel-main] Fixes #9280. Add filter.* and modality=media to langchain4j-ingest - #9290
JiriOndrusek wants to merge 3 commits into
Conversation
jamesnetherton
left a comment
There was a problem hiding this comment.
Reviewed locally (latest commit only; the first commit is from #9282). A few comments inline. The most important is the first: the raw-size guard is skipped for media pipelines.
| */ | ||
| private static ProcessorDefinition<?> parseSteps(ProcessorDefinition<?> route, String name, String parser, | ||
| int maxDocumentSize, boolean directory, IdempotentRepository register, String includeId, String excludeId) { | ||
| if (parser == null) { |
There was a problem hiding this comment.
With modality=media there is no parser, so parseSteps returns here and the bounded raw-size read (max + 1 bytes, not trusting CamelFileLength) never runs for media pipelines, including consumer-fed ones (source.uri / Source.endpoint).
The component's own check only trusts the CamelFileLength header before calling getMandatoryBody(byte[]). So a large body sent with a forged CamelFileLength gets past max-document-size and is loaded into the heap in full. Should the size guard also apply when media is true?
There was a problem hiding this comment.
Partly true: an oversized media payload is still refused, because MediaIngestService re-checks bytes.length. But that happens only after the whole body is read, and a forged CamelFileLength passes the up-front check.
maxDocumentSize is the component's option, so the fix belongs there rather than in a media copy of the guard here: CAMEL-25463. With it, a body not yet in memory will be read only one byte (media) or one character (text) past the cap, then refused. CamelFileLength will still reject early, but it will no longer decide how much is read. That will cover every user of the component, not only Camel Quarkus, and keep another copy of the size logic out of the extension.
I'll add a test here to verify it, so this PR will wait for the fix. That's no extra delay: the fix targets Camel 4.23.0, which this PR already waits for.
Why that will be enough:
- A directory pipeline is already safe: the file consumer's
CamelFileLengthis checked before the file is read. - On a consumer-fed pipeline, a guard in the extension could not do more. Stream caching (on by default, in memory) reads a stream whole before any of our steps run. That needs a limit at the consumer (e.g.
quarkus.http.limits.max-body-size) or spooling, as the component docs in the fix will say.
The extension keeps its own guard only for parser pipelines, since the parse runs before the sink.
| } | ||
|
|
||
| /** Comma-separated id patterns, parsed as the component parses them; null when none remain. */ | ||
| private static String[] patterns(String value) { |
There was a problem hiding this comment.
patterns() and the AntPathMatcher check before parsing re-implement the component's private parsePatterns/idAccepted. If the component's matching changes (case, a leading /, splitting, where the id comes from), the advisory filter and the authoritative one could disagree. Worth a comment linking the two, or a test that checks they agree?
There was a problem hiding this comment.
True. I added a comment pointing to LangChain4jIngestProducer.parsePatterns/idAccepted ("keep them in step"). The component stays authoritative; the copy only spares the parse. Exposing them from the component would remove the copy, and I can raise that in Camel.
| } | ||
|
|
||
| @Override | ||
| public FilterRunTimeConfig filter() { |
There was a problem hiding this comment.
filter() returns null, so this PipelineRunTimeConfig is only partly implemented. Any later code that reads runtime.filter() will hit an NPE, and only for @Ingest pipelines. Could it return an empty FilterRunTimeConfig, or delegate to the configured pipeline's filter?
There was a problem hiding this comment.
Agreed. The builder view now delegates to the configured filter.*, or returns an empty filter when there is none. IngestRoutes reads runtime.filter() for both declaration styles.
| // filter.* comes from configuration for both declaration styles, the builder having no | ||
| // filter API, and the component enforces it | ||
| IngestRunTimeConfig.PipelineRunTimeConfig configured = runTimeConfig.pipelines().get(name); | ||
| String includeId = configured == null ? "" : configured.filter().includeId().orElse(""); |
There was a problem hiding this comment.
filter.* adds the pipeline name to the runtime map. A filter property for a misspelled @Ingest name (e.g. datasheet.filter.exclude-id when the pipeline is datasheets) creates a phantom configured pipeline. Startup then fails with a missing source.directory error rather than "unknown pipeline", and the real pipeline runs without the filter. Could a name that only has filter.* set be detected and reported?
There was a problem hiding this comment.
Partly. The misleading error is real, but the start fails, so the real pipeline doesn't run without the filter.
The error now names the Java-declared pipelines: Ingestion pipeline 'datasheet' has no source.directory, and no @Ingest method declares it (declared in Java: [datasheets]). Fix the name, or set ...source.directory. Test: IngestBuilderFilterTypoTest.
| if (configured.filter().minDocumentSize() != 0) { | ||
| sink.put("minDocumentSize", String.valueOf(configured.filter().minDocumentSize())); | ||
| } | ||
| configured.filter().documentFilter().ifPresent(bean -> sink.put("documentFilter", "#bean:" + bean)); |
There was a problem hiding this comment.
When filter.document-filter is unset it isn't passed, so an application-wide camel.kamelet.langchain4j-ingest-sink.documentFilter still applies to every pipeline. The docs do say not to set those properties, but binding a neutral always-true Predicate as the default would pin the option, as the PR intends.
There was a problem hiding this comment.
Done. An unset filter.document-filter now goes as an always-true predicate. The IT poison set gains camel.kamelet.langchain4j-ingest-sink.documentFilter=#bean:rejectAll. documentSplitter and idempotentRepository stay unpinned, since no neutral bean exists for them.
| * document id's file extension. The twin of the {@code content-type} configuration property. | ||
| */ | ||
| public IngestPipeline contentType(String contentType) { | ||
| if (contentType == null || contentType.isBlank()) { |
There was a problem hiding this comment.
This only rejects blank values. The component drops everything after ; and treats an empty result as unset, so contentType("; codecs=opus") passes here and then silently falls back to the file extension. Could this apply the same normalization before validating?
There was a problem hiding this comment.
Fixed for both paths: a value with nothing before ; is rejected by IngestPipeline.contentType() and, for content-type, at build time. Tests extended.
| sink.put("maxSegmentSize", String.valueOf(maxSegmentSize)); | ||
| sink.put("maxOverlapSize", String.valueOf(maxOverlapSize)); | ||
| sink.put("embeddingBatchSize", String.valueOf(embeddingBatchSize)); | ||
| sink.put("modality", modality); |
There was a problem hiding this comment.
The modality is forwarded exactly as typed ("Media", "MEDIA", both used in the tests), so this depends on the component's IngestModality enum conversion ignoring case. modality.toLowerCase(Locale.ROOT) here would remove that dependency.
There was a problem hiding this comment.
Done: it is lower-cased before it goes to the Kamelet.
| ---- | ||
|
|
||
| The MIME type handed to the model comes from `content-type` or, when unset, from the file extension through Camel's MIME table (wav, mp3, flac, ogg, opus, m4a, aac and aiff for audio; png, jpg, gif and webp for images; mp4, mov and webm for video; pdf); a file whose type cannot be determined fails the exchange, and `content-type` without `modality=media` is rejected like the parser and splitter conflicts. | ||
| On a directory pipeline such a file — `notes.txt`, `Thumbs.db` — fails again on every poll, so restrict the ids with `filter.include-id`, as above: it is checked before the media type. |
There was a problem hiding this comment.
On a directory pipeline, a file whose media type can't be determined fails on every poll, for ever, unless filter.include-id is set. Since retrying can never succeed, could it be committed in the register as filtered/rejected (like an empty parse is) instead of only being documented?
There was a problem hiding this comment.
True: the rollback releases the file's key. I'd keep it documented for now:
- The extension can't tell this failure from the others without copying the component's MIME table (
MediaTypesis package-private). - An oversized file is retried the same way on purpose (
Langchain4jIngestTikaTest). - With a persistent register, a committed key would keep the file skipped even after a config fix.
Answering filtered for an untypable id fits better in the component. I can open a Camel issue.
|
Thanks for the review, @jamesnetherton. The last commit addresses it. I kept it separate for an easier re-review and will squash it before merge.
That test fails until CAMEL-25463 is in the Camel snapshot, so CI will be red on it and the PR stays a draft. It now waits for:
Verified locally against a build of the fix: deployment tests, JVM and native ITs pass. |
21bc352 to
82a5858
Compare
…Kamelets The extension's own route builder is replaced by a composition of the catalog Kamelets: langchain4j-ingest-file-source, tika-extract-text-action or docling-convert-action, and langchain4j-ingest-sink, always loaded from the classpath. Ingestion results stay the same apart from the edge cases listed in the 4.0 migration guide. Each Kamelet runs as its own route, named langchain4j-ingest-<name>-source, -parser or -sink, and the extension registers the four Kamelets for native itself. New dependencies: camel-kamelets, camel-quarkus-kamelet and camel-quarkus-yaml-dsl. Also: an invalid @ingest pipeline name now fails the build, and with two store or model beans the error for a Java-declared pipeline names the IngestPipeline setter. Pipelines with colliding route ids are rejected. Needs the camel-kamelets 4.23.0 release, the first with these Kamelets; until then the root pom pins 4.23.0-SNAPSHOT (marked TODO). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds on the Kamelet composition of apache#9152. filter.include-id, filter.exclude-id, filter.min-document-size and filter.document-filter go to the sink Kamelet and are enforced by the component (CAMEL-24753), for configured and @ingest pipelines alike. With a parser, the id patterns are also checked before the parse, so an excluded document is never parsed. modality=media, with an optional content-type, embeds each document whole through the sink's media path (CAMEL-25027); IngestPipeline gets modality() and contentType(). A configured pipeline's modality and content-type are checked at build time; the builder setters reject bad values. The raw-size guard before the parse stays in the extension: it reads CamelFileLength on a directory pipeline and at most max + 1 bytes otherwise. The parser actions' own maxDocumentSize (apache/camel-kamelets#3087) is pinned to 0. modality, contentType, includeId, excludeId and the parsers' maxDocumentSize are always passed ("" when unset), so app-wide camel.kamelet.<id>.* properties cannot change them. Needs the camel-kamelets 4.23.0 release including apache/camel-kamelets#3085 (modality and contentType on langchain4j-ingest-sink). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Test that a consumer-fed media stream is read only up to the cap (needs CAMEL-25463) - Pin an unset documentFilter to an always-true predicate - The builder view's filter() delegates to configuration instead of null - Name the @ingest pipelines when filter.* targets an unknown name - Reject a content-type that is only parameters, in the builder and at build time - Lower-case modality before it goes to the sink - Link the advisory id filter to the component's matcher Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f249cfa to
b23e833
Compare
|
Superseded by #9340 (rebased on main after #9315). Generated with Claude Code on behalf of @JiriOndrusek |
|
superseded by #9340 |
Fixes #9280
Builds on #9282: only the last commit is new.
filter.*options go to the sink Kamelet, which enforces them. With a parser, the id patterns are also checked before the parse.modality=media(optionalcontent-type) embeds each document whole, as one vector; the builder getsmodality()andcontentType(). Conflicting settings fail the build.Not mergeable yet: needs #9282 and the camel-kamelets 4.23.0 release with apache/camel-kamelets#3085.
Tested locally: deployment tests, JVM and native integration tests, with camel-kamelets built from apache/camel-kamelets#3085 and apache/camel-kamelets#3087.
Claude Code on behalf of @JiriOndrusek. AI-generated; please verify before merging.
🤖 Generated with Claude Code