feat(graph-rag): implement full query runtime - #32
Conversation
📝 WalkthroughWalkthroughAdds a framework-neutral LightRAG query runtime with validated contracts, keyword planning, authorized retrieval, chunk reranking, context assembly, structured results, in-memory conformance coverage, and Spring AI model adapters. ChangesLightRAG query runtime
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant LightRagQueryEngine
participant KeywordPlanningModel
participant AuthorizedQueryProjection
participant ChunkReranker
participant QueryAnswerModel
Client->>LightRagQueryEngine: execute(request)
LightRagQueryEngine->>KeywordPlanningModel: complete(prompt)
LightRagQueryEngine->>AuthorizedQueryProjection: retrieve authorized graph and chunks
LightRagQueryEngine->>ChunkReranker: rerank(chunk candidates)
LightRagQueryEngine->>QueryAnswerModel: answer(grounded request)
QueryAnswerModel-->>LightRagQueryEngine: answer response
LightRagQueryEngine-->>Client: LightRagQueryResult
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java`:
- Around line 488-499: Update the reranked stream in LightRagQueryEngine so
missing rerank scores do not receive top priority or bypass minimumRerankScore.
Exclude null-scored ChunkState entries before applying the chunkTopK limit,
while preserving the existing scored-item threshold, ordering, and tie-breakers.
- Around line 68-70: Update the empty-keyword guard in LightRagQueryEngine so it
returns no results only for pure graph modes; allow MIX and NAIVE modes to
continue into their chunk-seed or vectorChunks retrieval paths. Preserve the
existing noResults behavior when a graph-only mode has empty keywords.
In
`@components/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.java`:
- Around line 304-333: Update the visible(Chunk) overload to require
chunk.evidence().projectionGeneration() to equal snapshot.generation(),
alongside its existing scope and organization checks. Ensure searchChunks,
rankChunks, and loadChunks continue using this visibility predicate so all
exposed chunks are pinned to the requested projection snapshot.
In
`@components/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.java`:
- Around line 66-67: Add conformance fixtures in
LightRagQueryRuntimeConformanceTests using GENERATION - 1 alongside the existing
GENERATION data, covering stale entities/relations and chunks. For a snapshot
pinned to GENERATION, assert the stale fixture is absent from both returned
references and context, validating generation filtering across all projected
data types.
In
`@integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.java`:
- Around line 43-46: Update the Prompt ChatOptions construction in
SpringAiKeywordPlanningModel.java (lines 43-46) and the comparable construction
in SpringAiQueryAnswerModel.java (lines 68-70) to derive from
chatModel.getOptions().mutate(), overriding only the intended model and
temperature values while preserving configured provider options such as apiKey,
baseUrl, and timeout.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 35533b62-a359-49ed-bd4f-118e30dcdd31
⛔ Files ignored due to path filters (3)
docs/increments/active/2026-07-23-full-lightrag-semantic-port/design.mdis excluded by!docs/**docs/increments/active/2026-07-23-full-lightrag-semantic-port/plan.mdis excluded by!docs/**docs/research/lightrag-v1.5.4-parity-manifest.mdis excluded by!docs/**
📒 Files selected for processing (21)
components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/AuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/ChunkReranker.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlan.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlanningModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagKeywordPlanner.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryRequest.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryResult.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryAnswerModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryOutputMode.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagKeywordPlannerTests.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagWeightedPollingTests.javacomponents/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.javacomponents/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/GraphQueryModelException.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModel.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/StructuredKeywordPlanResponse.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModelTests.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModelTests.java
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
- GitHub Check: Backend · Java 25
🧰 Additional context used
📓 Path-based instructions (3)
**/*
📄 CodeRabbit inference engine (CLAUDE.md)
**/*: Before changing unfamiliar Spring Boot 4, Spring Modulith 2, Spring AI 2, Gradle, React, Vite, Tailwind, or TypeScript APIs, consult Context7/current official documentation and the projectorgmemory-*verification skills.
Readdocs/guidelines/agent-safety.mdbefore retrieval, AI, MCP, permission, upload, graph, or export work.
Never commit.envfiles, provider keys, tokens, or customer data.
Run the relevant gates fromdocs/guidelines/testing-harness.md; use a terminating clean test as the context gate, and do not treatbootRunas verification.
Current behavior belongs in architecture/specs only after it exists in code; intent belongs in vision, roadmap, or an active increment, and repository state must not be duplicated across documents.
Files:
components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryOutputMode.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/GraphQueryModelException.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlanningModel.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagKeywordPlannerTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/StructuredKeywordPlanResponse.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModelTests.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagWeightedPollingTests.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModelTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModel.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryAnswerModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryRequest.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlan.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/ChunkReranker.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/AuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagKeywordPlanner.javacomponents/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryResult.javacomponents/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java
**/*.{java,sql}
📄 CodeRabbit inference engine (CLAUDE.md)
Pair JPA schema changes with a Flyway migration.
Files:
components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryOutputMode.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/GraphQueryModelException.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlanningModel.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagKeywordPlannerTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/StructuredKeywordPlanResponse.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModelTests.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagWeightedPollingTests.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModelTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModel.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryAnswerModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryRequest.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlan.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/ChunkReranker.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/AuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagKeywordPlanner.javacomponents/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryResult.javacomponents/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java
**/*.java
📄 CodeRabbit inference engine (CLAUDE.md)
JetBrains IDE inspection is a Java-backend gate only.
Files:
components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryOutputMode.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/GraphQueryModelException.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlanningModel.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagKeywordPlannerTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/StructuredKeywordPlanResponse.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModelTests.javacomponents/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagWeightedPollingTests.javaintegrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModelTests.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModel.javaintegrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryAnswerModel.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryRequest.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlan.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/ChunkReranker.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/AuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagKeywordPlanner.javacomponents/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryResult.javacomponents/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.javacomponents/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java
🔇 Additional comments (20)
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/GraphQueryModelException.java (1)
1-12: LGTM!integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/StructuredKeywordPlanResponse.java (1)
1-9: LGTM!integrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModelTests.java (1)
1-67: LGTM!integrations/graph-rag-spring-ai/src/test/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModelTests.java (1)
1-104: LGTM!components/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.java (2)
40-303: LGTM!
335-347: LGTM!components/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.java (1)
1-263: LGTM!Also applies to: 344-602
components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/AuthorizedQueryProjection.java (1)
84-129: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/ChunkReranker.java (1)
9-30: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.java (1)
12-26: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryRequest.java (1)
19-125: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryAnswerModel.java (1)
15-62: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryResult.java (1)
18-138: LGTM!components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagWeightedPollingTests.java (1)
12-46: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/QueryOutputMode.java (1)
1-7: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlan.java (1)
1-67: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/KeywordPlanningModel.java (1)
1-11: LGTM!components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagKeywordPlanner.java (2)
24-43: LGTM!
45-65: 🔒 Security & PrivacyConfirm agent-safety guidance was applied for this AI-integration surface.
prompt()interpolates raw, unescaped user query text directly into the LLM prompt sent viaKeywordPlanningModel. This is new retrieval/AI-call surface; please confirmdocs/guidelines/agent-safety.mdguidance on handling untrusted input in prompts (e.g., injection resistance beyond the---Constraints---section) was consulted for this design.As per coding guidelines, "Read
docs/guidelines/agent-safety.mdbefore retrieval, AI, MCP, permission, upload, graph, or export work."Source: Coding guidelines
components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagKeywordPlannerTests.java (1)
1-77: LGTM!
| if (request.options().mode().usesGraph() && keywords.empty()) { | ||
| return noResults(request, keywords, List.of(), "keywords_empty"); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
LightRAG mix mode empty keywords fallback vector retrieval
💡 Result:
In LightRAG, the mix mode is a comprehensive retrieval strategy that integrates results from local graph retrieval, global graph retrieval, and naive (vector-based) retrieval [1][2][3]. Regarding keyword handling and fallback mechanisms: 1. Keyword Usage: Users can provide hl_keywords (high-level) and ll_keywords (low-level) to prioritize specific retrieval focuses [4][5]. Providing these lists acts as an manual override; when they are provided, LightRAG treats them as explicit instructions for the graph retrieval components, bypassing automatic keyword extraction [6]. 2. Empty Keywords Fallback: If these keyword lists are left empty (the default), LightRAG employs an internal process to automatically extract keywords from the user query before proceeding with graph retrieval [6]. 3. Vector Retrieval Behavior: While keywords primarily steer the knowledge graph traversal, the mix mode simultaneously performs a parallel vector similarity search using the raw query text (optionally augmented with conversation history) [7]. Importantly, direct chunk-vector retrieval in mix mode uses the raw query, ensuring that retrieval remains robust even if automatic keyword extraction processes are empty or suboptimal [6]. In specific implementations or forks (such as those managing local/global fallback logic), if automatic extraction yields no usable keywords, the system defaults to using the raw query to maintain retrieval continuity [6]. In summary, the mix mode does not "fail" if keywords are empty; instead, it either automatically generates them or relies on the parallel raw-query vector retrieval path to ensure retrieval coverage [6][7].
Citations:
- 1: https://github.com/hkuds/lightrag
- 2: https://github.com/HKUDS/LightRAG/blob/HEAD/README.md
- 3: https://github.com/hkuds/lightrag?tab=readme-ov-file
- 4: https://github.com/HKUDS/LightRAG/blob/v1.4.15/lightrag/base.py
- 5: https://github.com/HKUDS/LightRAG/blob/828d1c01/lightrag/api/routers/query_routes.py
- 6: https://github.com/pctapia/lightrag-mvn
- 7: https://neo4j.com/blog/developer/under-the-covers-with-lightrag-retrieval/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate file:"
fd -a 'LightRagQueryEngine.java' . || true
echo
echo "File outline:"
ast-grep outline components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java --view expanded || true
echo
echo "Relevant lines 1-140:"
sed -n '1,140p' components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java | nl -ba
echo
echo "Search mode definitions/usages:"
rg -n "enum Query[A-Za-z0-9_]*|MIX|usesGraph|usesChunkSeeds|LightRagQueryMode|QueryMode" components/graph-rag-core/src/main/java -SRepository: kl3inIT/OrgMemory
Length of output: 2545
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant lines 1-140:"
sed -n '1,140p' components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java | cat -n
echo
echo "Search mode definitions/usages:"
rg -n "enum Query[A-Za-z0-9_]*|MIX|usesGraph|usesChunkSeeds|LightRagQueryMode|QueryMode" components/graph-rag-core/src/main/java -S
echo
echo "Search LightRagQueryMode files:"
fd -i 'LightRagQueryMode|QueryMode' components/graph-rag-core/src/main/java -e javaRepository: kl3inIT/OrgMemory
Length of output: 10572
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "LightRagQueryMode.java:"
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.java
echo
echo "RetrievalStrategy.java:"
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/RetrievalStrategy.java
echo
echo "LightRagQueryRequest.java around options:"
sed -n '1,130p' components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryRequest.java | cat -n
echo
echo "Tests mentioning MIX, keywords_empty, usesChunkSeeds, or empty keyword plan:"
rg -n "MIX|keywords_empty|usesChunkSeeds|usesGraph|LightRagQueryMode|keyword" --glob '*.java' .Repository: kl3inIT/OrgMemory
Length of output: 40451
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant line 99 and vectorChunks implementation:"
sed -n '90,115p;150,170p;448,495p' components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java | cat -n
echo
echo "LightRagQueryRuntimeConformanceTests relevant sections:"
sed -n '90,135p;170,225p;233,255p;335,385p' components/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.java | cat -n
echo
echo "KeywordPlan tests related to empty/high/low/source:"
sed -n '1,120p' components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagKeywordPlannerTests.java | cat -n
echo
echo "Read-only semantic probe for current mode predicates and proposed guard:"
python3 - <<'PY'
from pathlib import Path
text = Path('components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java').read_text()
mode = Path('components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryMode.java').read_text()
checks = {
"line_68_usesGraph_shortcircuit": "request.options().mode().usesGraph() && keywords.empty()" in text,
"line_99_mixed_usesChunkSeeds_vectorChunks": "request.options().mode().usesChunkSeeds()\n ? vectorChunks(request, embeddingPlan.query())" in text,
"mix_usesChunkSeeds_predicate": "public boolean usesChunkSeeds()\n return this == NAIVE || this == MIX;" in mode,
"mix_usesGraph_predicate": "public boolean usesGraph()\n return this == LOCAL || this == GLOBAL || this == HYBRID || this == MIX;" in mode,
"sec_mix_channels_chunk": "RetrievalStrategy.SECURE_MIX(List.of(\n RetrievalChannel.ENTITY,\n RetrievalChannel.RELATION,\n RetrievalChannel.CHUNK));" in Path('components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/RetrievalStrategy.java').read_text(),
}
for key, val in checks.items():
print(f"{key}: {val}")
PYRepository: kl3inIT/OrgMemory
Length of output: 17116
Keep MIX chunk-seed retrieval when keyword planning is empty.
MIX has chunk-seed retrieval, but the usesGraph() && keywords.empty() guard returns NO_RESULTS before the vectorChunks() path can run. Pure graph modes should still require keywords; bypass only this guard for MIX or NAIVE.
🐛 Proposed fix
- if (request.options().mode().usesGraph() && keywords.empty()) {
+ if (request.options().mode().usesGraph()
+ && !request.options().mode().usesChunkSeeds()
+ && keywords.empty()) {
return noResults(request, keywords, List.of(), "keywords_empty");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (request.options().mode().usesGraph() && keywords.empty()) { | |
| return noResults(request, keywords, List.of(), "keywords_empty"); | |
| } | |
| if (request.options().mode().usesGraph() | |
| && !request.options().mode().usesChunkSeeds() | |
| && keywords.empty()) { | |
| return noResults(request, keywords, List.of(), "keywords_empty"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java`
around lines 68 - 70, Update the empty-keyword guard in LightRagQueryEngine so
it returns no results only for pure graph modes; allow MIX and NAIVE modes to
continue into their chunk-seed or vectorChunks retrieval paths. Preserve the
existing noResults behavior when a graph-only mode has empty keywords.
| List<ChunkState> reranked = chunks.stream() | ||
| .map(item -> item.withRerank(byChunk.get(item.chunk().id()))) | ||
| .filter(item -> item.rerankScore() == null | ||
| || item.rerankScore() >= request.options().minimumRerankScore()) | ||
| .sorted(Comparator | ||
| .comparingDouble((ChunkState item) -> | ||
| item.rerankScore() == null ? 1.0 : item.rerankScore()) | ||
| .reversed() | ||
| .thenComparingInt(ChunkState::order) | ||
| .thenComparing(item -> item.chunk().id())) | ||
| .limit(request.options().chunkTopK()) | ||
| .toList(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate files:"
fd -a 'LightRagQueryEngine.java|ChunkReranker.java|ChunkState.java|ChunkRerankers.java' . || true
echo
echo "Git status/stat:"
git status --short
git diff --stat
echo
echo "Outline LightRagQueryEngine:"
ast-grep outline components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java --view compact || true
echo
echo "Relevant LightRagQueryEngine lines 450-530:"
sed -n '450,530p' components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java | nl -ba -v450
echo
echo "Search rerank symbols:"
rg -n "interface ChunkReranker|class .*ChunkReranker|record ChunkState|chunkTopK|minimumRerankScore|rerankScore|rerank\\(" components/graph-rag-core/src/main/java tests -S || true
echo
echo "Check dependency/docs for rerankers:"
rg -n "Rerank|rerank|chunkTopK|limit" docs components/graph-rag-core/src/main/java -S || trueRepository: kl3inIT/OrgMemory
Length of output: 674
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant ChunkReranker:"
sed -n '1,240p' components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/ChunkReranker.java
echo
echo "Relevant ChunkState/ChunkState-ish definitions:"
rg -n "record ChunkState|class ChunkState|rerankScore|withRerank" components/graph-rag-core/src/main/java -S
echo
echo "Relevant LightRagQueryEngine rerank section with line numbers:"
python3 - <<'PY'
from pathlib import Path
p = Path('components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java')
lines = p.read_text().splitlines()
for i in range(450, 530):
if i <= len(lines):
print(f"{i}: {lines[i-1]}")
PY
echo
echo "Search rerank option usage:"
rg -n "chunkTopK|minimumRerankScore|rerank\\(|ChunkReranker|reranked|withRerank|rerankScore" components/graph-rag-core/src/main/java tests -S || trueRepository: kl3inIT/OrgMemory
Length of output: 13977
🌐 Web query:
Context7 current official documentation ChunkReranker.rerank limit return score for every candidate
💡 Result:
There is no "ChunkReranker" class or rerank method in the current Context7 official documentation [1][2][3]. Recent updates to Context7 have moved away from manual configuration of result limits, pagination, and reranking parameters [4]. The service now performs server-side, LLM-powered reranking automatically [4][3]. Developers describe their needs via a query string, and the system autonomously determines the relevant documentation to return [4]. Historically, older versions or different retrieval libraries (such as those for RedisVL or Elasticsearch) used classes like ChunkReranker or CohereReranker with explicit limit and return_score parameters [5][6][7][8]. However, these are not part of the Context7 API [1][4][3]. In the current Context7 framework, the API manages context token budgets and relevance scoring internally to optimize results without requiring manual tuning from the user [4][9].
Citations:
- 1: https://www.mintlify.com/upstash/context7/mcp/tools-reference
- 2: https://upstash-context7.mintlify.app/introduction
- 3: https://deepwiki.com/upstash/context7/5-public-api-reference
- 4: https://upstash.com/blog/new-context7
- 5: https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers/text-similarity-reranker-retriever
- 6: https://docs.redisvl.com/en/v0.16.0/api/reranker.html
- 7: Add RerankRequestChunker elastic/elasticsearch#130485
- 8: Text similarity reranker chunks and scores snippets elastic/elasticsearch#133576
- 9: https://memo.d.foundation/breakdown/context7
🌐 Web query:
OrgMemory graph rag ChunkReranker rerank limit documentation return Score list
💡 Result:
In the context of technical RAG (Retrieval-Augmented Generation) systems that employ a reranker (specifically referenced in the RAGFlow ecosystem which uses a "RERANK_LIMIT" parameter), the rerank limit is a backend constraint designed to prevent exceeding the input size capacity of reranking models [1][2]. Key aspects of the rerank limit and its relation to score lists include: 1. Function: The rerank limit defines the maximum number of documents or chunks that the system will pass to the reranker model for scoring in a single batch [3][4]. 2. Calculation and Constraints: - It is typically calculated based on pagination parameters (like page_size) and internal model API limits (e.g., 64) [3][5]. - Systems often enforce a logic such as min(calculated_limit, top_k, max_api_limit) to ensure user-defined Top-K settings are respected without violating provider-imposed API constraints (e.g., a 64-document limit) [1][2]. 3. Return Score List: After the reranker processes these candidates, it returns a list of results (the "score list") that has been re-ordered based on the model's relevance scores [3][4]. In most implementations, these scores are normalized or used to re-sort the initial retrieval pool before the final, paginated response is returned to the user [3][4]. 4. Pagination Issues: There is a known technical challenge in some implementations where the rerank limit must remain a multiple of the page_size to ensure that pagination math remains accurate [5]. Clamping the limit to satisfy API constraints (like the 64-doc limit) can inadvertently break this alignment, leading to dropped results or "short pages" during retrieval [5]. If you are encountering errors or unexpected behavior, it is likely due to the system's backend clamping the RERANK_LIMIT to meet API safety constraints, which may conflict with the page_size or top_k settings configured in your UI [1][5]. Patching the logic to strictly enforce min(RERANK_LIMIT, top_k) is a common developer-recommended workaround [1][2].
Citations:
- 1: [Bug]: Hardcoded RERANK_LIMIT logic causes API failures (400) and ignores UI Top-K settings infiniflow/ragflow#14081
- 2: Fix: rerank overflow by enforcing top_k and 64 cap infiniflow/ragflow#14084
- 3: https://github.com/infiniflow/ragflow/blob/384fa6fc/rag/nlp/search.py
- 4: [Question]: Doubt Regarding Function Confusion Between top_k and page_size, and the Control of Input Quantity for the Rerank Model infiniflow/ragflow#9833
- 5: [Bug] Reranked retrieval drops results and returns short pages on deep pagination infiniflow/ragflow#15433
Treat missing rerank scores as lowest priority instead of top priority.
ChunkReranker.rerank(..., limit) is a top-limit selection contract, so when there are more candidates than chunkTopK, omitted chunks get null scores. They currently pass minimumRerankScore unconditionally and sort first because null is mapped to 1.0, so the final limit(chunkTopK) can return mostly unscored chunks in original order. Drop them here, or sort them after scored chunks that meet the threshold.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/LightRagQueryEngine.java`
around lines 488 - 499, Update the reranked stream in LightRagQueryEngine so
missing rerank scores do not receive top priority or bypass minimumRerankScore.
Exclude null-scored ChunkState entries before applying the chunkTopK limit,
while preserving the existing scored-item threshold, ordering, and tie-breakers.
| private static boolean visible( | ||
| AuthorizedEvidenceScope scope, | ||
| ProjectionSnapshot snapshot, | ||
| EntityContribution contribution) { | ||
| return scope.includes( | ||
| contribution.provenance().organizationId(), | ||
| contribution.provenance().knowledgeAssetId()) | ||
| && contribution.provenance().projectionGeneration() == snapshot.generation(); | ||
| } | ||
|
|
||
| private static boolean visible( | ||
| AuthorizedEvidenceScope scope, | ||
| ProjectionSnapshot snapshot, | ||
| RelationContribution contribution) { | ||
| return scope.includes( | ||
| contribution.provenance().organizationId(), | ||
| contribution.provenance().knowledgeAssetId()) | ||
| && contribution.provenance().projectionGeneration() == snapshot.generation(); | ||
| } | ||
|
|
||
| private static boolean visible( | ||
| AuthorizedEvidenceScope scope, | ||
| ProjectionSnapshot snapshot, | ||
| Chunk chunk) { | ||
| return scope.includes( | ||
| chunk.evidence().organizationId(), | ||
| chunk.evidence().knowledgeAssetId()) | ||
| && snapshot.namespace().organizationId().equals( | ||
| chunk.evidence().organizationId()); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f 'AuthorizedQueryProjection.java|EvidenceReference.java|EvidenceProvenance.java'
rg -n -A 15 'interface AuthorizedQueryProjection' --type=java
rg -n -A 12 'record Chunk' --type=java
rg -n -A 10 'record EvidenceReference' --type=java
rg -n -A 10 'record EvidenceProvenance' --type=javaRepository: kl3inIT/OrgMemory
Length of output: 553
🏁 Script executed:
#!/bin/bash
set -u
echo "== authorized projection file outline =="
ast-grep outline components/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.java --view expanded || true
echo "== relevant testkit file sections =="
cat -n components/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.java | sed -n '1,220p;300,355p'
echo "== core authorized projection sections =="
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/query/AuthorizedQueryProjection.java | sed -n '1,220p;300,360p'
echo "== evidence/model records =="
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/model/EvidenceReference.java
echo
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/model/EvidenceProvenance.javaRepository: kl3inIT/OrgMemory
Length of output: 24866
🏁 Script executed:
#!/bin/bash
set -u
echo "== locate conformance tests and generation constants =="
rg -n -C 4 'GENERATION|projectionGeneration|ProjectionSnapshot|InMemoryAuthorizedQueryProjection' components docs --type=java --glob '!**/graph-rag-testkit/**' || true
echo "== search for projection snapshot/generation model fields =="
rg -n -C 3 'record ProjectionSnapshot|class ProjectionSnapshot|ProjectionSnapshot' --type=java --type=sql || true
echo "== search for chunk projection generation persistence =="
rg -n -C 3 'projection_generation|projectionGeneration|Chunk|EvidenceReference|EvidenceProvenance' components --type=java | sed -n '1,260p' || true
echo "== deterministic behavior probe of visible methods from source text =="
python3 - <<'PY'
from pathlib import Path
s = Path('components/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.java').read_text()
checks = {
"entity_visible_has_projectionGeneration": "contribution.provenance().projectionGeneration() == snapshot.generation()" in s,
"relation_visible_has_projectionGeneration": "contribution.provenance().projectionGeneration() == snapshot.generation()" in s,
"chunk_visible_has_projectionGeneration": "chunk.evidence().projectionGeneration()" in s or "projectionGeneration" in s[s.find("private static boolean visible("):s.find("private void begin", s.find("private static boolean visible(")))],
"searchChunks_uses_chunksKeySet": "rankVisibleChunks(scope, snapshot, search, chunks.keySet())" in s,
"chunk_has_aclGeneration": "long aclGeneration" in Path('components/graph-rag-core/src/main/java/com/orgmemory/graphrag/model/EvidenceReference.java').read_text(),
"chunk_has_projectionGeneration": "projectionGeneration" in Path('components/graph-rag-core/src/main/java/com/orgmemory/graphrag/model/EvidenceReference.java').read_text(),
}
for k,v in checks.items():
print(f"{k}={v}")
PYRepository: kl3inIT/OrgMemory
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -u
echo "== projection snapshot definitions =="
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/storage/ProjectionSnapshot.java
echo "== projection namespace definitions =="
cat -n components/graph-rag-core/src/main/java/com/orgmemory/graphrag/storage/ProjectionNamespace.java
echo "== production AuthorizedQueryProjection implementations =="
fd -t f '.*AuthorizedQueryProjection.*\.java' components --exec sh -c 'echo "--- $1"; cat -n "$1" | sed -n "1,260p"' sh {}
echo "== chunk generation accessors/usages =="
rg -n -C 3 'aclGeneration|projectionGeneration|evidence\(\)\.aclGeneration|snapshot\.generation\(\)' components --type=java | sed -n '1,260p'
echo "== conformance fixture generation references =="
rg -n -C 3 'InMemoryAuthorizedQueryProjection|new Chunk|EvidenceReference\(|GENERATION|projectionGeneration|projection generation' components/graph-rag-core components/graph-rag-testkit --type=java | sed -n '1,260p'Repository: kl3inIT/OrgMemory
Length of output: 50373
Pin chunk visibility to the projection snapshot generation.
visible(EntityContribution) and visible(RelationContribution) filter by contribution.provenance().projectionGeneration() == snapshot.generation(), but visible(Chunk) only checks organization membership. Add and apply the same generation filter before exposing chunks from searchChunks, rankChunks, and loadChunks; otherwise chunks from a different published projection can be returned for a snapshot-pinned query.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@components/graph-rag-testkit/src/main/java/com/orgmemory/graphrag/testkit/InMemoryAuthorizedQueryProjection.java`
around lines 304 - 333, Update the visible(Chunk) overload to require
chunk.evidence().projectionGeneration() to equal snapshot.generation(),
alongside its existing scope and organization checks. Ensure searchChunks,
rankChunks, and loadChunks continue using this visibility predicate so all
exposed chunks are pinned to the requested projection snapshot.
| private static final long GENERATION = 7; | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
No conformance coverage for stale/mismatched snapshot generation.
Every fixture is seeded under the same constant GENERATION (Line 66), so the suite never asserts that data from a different projection generation is excluded from results — for entities/relations (which the projection does filter by generation) or for chunks (which currently don't get filtered by generation at all; see companion comment on InMemoryAuthorizedQueryProjection.java). Adding a fixture seeded at GENERATION - 1 alongside the existing data, with an assertion that it never appears in references/context for a snapshot pinned to GENERATION, would directly validate the snapshot-pinning guarantee this PR is meant to provide.
Also applies to: 287-342
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@components/graph-rag-testkit/src/test/java/com/orgmemory/graphrag/testkit/LightRagQueryRuntimeConformanceTests.java`
around lines 66 - 67, Add conformance fixtures in
LightRagQueryRuntimeConformanceTests using GENERATION - 1 alongside the existing
GENERATION data, covering stale entities/relations and chunks. For a snapshot
pinned to GENERATION, assert the stale fixture is absent from both returned
references and context, validating generation filtering across all projected
data types.
| ChatOptions.builder() | ||
| .model(modelId) | ||
| .temperature(0.0) | ||
| .build())); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n -C2 'ChatOptions\.builder\(\)|getOptions\(\)\.mutate\(\)' \
integrations/graph-rag-spring-ai/src
fd -HI 'libs.versions.toml|build.gradle|build.gradle.kts|settings.gradle|settings.gradle.kts' . \
-x rg -n 'spring-ai|springAi' {}Repository: kl3inIT/OrgMemory
Length of output: 5324
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== Locate Spring AI provider model configuration files =="
rg -n -i "spring\.ai|chatmodel|model-|temperature|seed|max-tokens|frequency|presence|top|stop|tools" \
-g 'application*.yml' -g 'application*.yaml' -g 'application*.properties' -g 'application*.conf' -g '*.java' .
echo
echo "== Inspect candidate Spring AI model interfaces/classes around getOptions and call =="
for f in \
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.java \
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModel.java \
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiDescriptionSummaryModel.java \
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiExtractionModel.java \
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiMultimodalAnalyzer.java
do
echo "--- $f"
sed -n '1,110p' "$f"
done
echo
echo "== Check dependency metadata for Spring AI 2.0.0 provider config classes =="
fd -HI 'pom.xml|build.gradle.kts|build.gradle|settings.gradle|libs.versions.toml|dependency-reduced-pom.xml' . | while read -r f; do
echo "--- $f"
rg -n 'spring-ai|gradle-plugin|java|plugins|org' "$f" | head -n 120
doneRepository: kl3inIT/OrgMemory
Length of output: 50374
🌐 Web query:
Spring AI 2.0.0 Prompt options model startup options override ChatOptions builder mutate getOptions documentation
💡 Result:
In Spring AI 2.0.0, ChatOptions are strictly immutable [1]. To modify existing options, you must use the mutate method, which returns a new builder initialized with the current values [2][3]. Key concepts and usage for Spring AI 2.0.0: 1. Mutating Options: Because options are immutable, the copy and fromOptions methods have been removed [1]. To create a modified version of an existing options instance, use the mutate method [2][1]: // Example of mutating options ChatOptions newOptions = originalOptions.mutate.temperature(0.7).model("gpt-4o").build; 2. ChatClient Options Configuration: The ChatClient's.options and.defaultOptions methods now require a ChatOptions.Builder (or a provider-specific subtype) rather than a fully built ChatOptions instance [1][4]. This builder is merged with the model's default options at request time, ensuring that only the fields you explicitly set will override the defaults [1][4]. 3. Merging Options: The ChatOptions.Builder provides a combineWith(ChatOptions.Builder other) method, which allows you to mutate a builder by taking non-null values from another builder while retaining existing values [5]. 4. Important Migration Note: Configuration property names for model options have changed; the.options prefix has been removed (e.g., spring.ai.openai.embedding.options.model is now spring.ai.openai.embedding.model) [4]. Additionally, default values for these options have been moved into the options constructors [4].
Citations:
- 1: https://docs.spring.io/spring-ai/reference/2.0/upgrade-notes.html
- 2: https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/prompt/ChatOptions.html
- 3: https://docs.spring.io/spring-ai/docs/2.0.0-M3/api/org/springframework/ai/chat/prompt/ChatOptions.html
- 4: https://docs.spring.io/spring-ai/reference/upgrade-notes.html
- 5: https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/prompt/ChatOptions.Builder.html
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== Application Spring AI model configs =="
sed -n '35,95p' apps/worker/src/main/resources/application.yml
sed -n '65,95p' apps/api/src/main/resources/application.yml
echo
echo "== ChatModelPort definition =="
sed -n '1,80p' core/src/main/java/com/orgmemory/core/ai/ChatModelPort.java
echo
echo "== OpenAI model provider implementation =="
fd -HI 'OpenAiCompatibleChatModelProvider.java|Open*Chat*Model*.java' . -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}Repository: kl3inIT/OrgMemory
Length of output: 6972
🌐 Web query:
Spring AI 2.0.0 ChatModel defaultOptions Prompt options merge getOptions mutate non-null Prompt options override
💡 Result:
In Spring AI 2.0.0, the approach to handling ChatOptions depends on whether you are using the low-level ChatModel interface or the high-level ChatClient abstraction [1][2]. ChatModel Level Behavior: The ChatModel interface does not perform any merging of options [1][3]. The behavior is strictly as follows [2][3]: - If the ChatOptions provided in the Prompt is non-null, it is used exactly as-is, fully overriding the model's default options [1][3]. - If the ChatOptions provided in the Prompt is null, the model's own default options are used instead [1][3]. No partial merging or mutation occurs at the ChatModel level; you must provide a fully constructed instance of the concrete ChatOptions type required by the model (e.g., AnthropicChatOptions) if you wish to define custom settings [1][4]. ChatClient Level Behavior: In contrast, the ChatClient abstraction supports merging options [2][5]. When using ChatClient, you provide a ChatOptions.Builder (or a provider-specific subtype) via methods like.options [1][6][4]. This builder is merged with the model’s default options before the request is executed, allowing for an incremental, delta-based override where only the fields you explicitly set in the builder are applied [1][4]. Key Summary of Mechanisms: - Immutability: ChatOptions classes are strictly immutable [4]. - Mutation: The mutate method on a ChatOptions instance returns a new ChatOptions.Builder, which allows you to initialize a new builder based on existing values [7][8]. - Merging: Merging is a feature of the ChatClient and its builder system (e.g., via methods designed for incremental updates), not the ChatModel call [2][5]. - DefaultOptions: ChatModel.getDefaultOptions is deprecated in favor of ChatModel.getOptions, which retrieves the model's current configured defaults [1].
Citations:
- 1: https://docs.spring.io/spring-ai/reference/2.0/upgrade-notes.html
- 2: Restore options replacing instead of merging in
ChatModelspring-projects/spring-ai#6336 - 3: spring-projects/spring-ai@836d691
- 4: https://docs.spring.io/spring-ai/reference/upgrade-notes.html
- 5: Move options merging to ChatClient level via builder combineWith() method spring-projects/spring-ai#5725
- 6: https://docs.spring.io/spring-ai/reference/2.0/api/chatclient.html
- 7: https://docs.spring.io/spring-ai/reference/2.0/api/chatmodel.html
- 8: https://docs.spring.io/spring-ai/docs/2.0.0-M3/api/org/springframework/ai/chat/prompt/ChatOptions.html
Preserve provider options when overriding model options through Prompt.
With Spring AI 2.0.0 ChatModel, a non-null Prompt options value is used exactly as-is, replacing the model’s configured defaults rather than being merged. Since this path is constructed with OpenAI-specific defaults (apiKey, baseUrl, timeout, model), derive from chatModel.getOptions().mutate() and only override the fields that are intentionally supplied here.
Apply the same change to the comparable sites in SpringAiDescriptionSummaryModel.java, SpringAiExtractionModel.java, and SpringAiMultimodalAnalyzer.java.
Proposed direction
- ChatOptions.builder()
+ chatModel.getOptions().mutate()📍 Affects 2 files
integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.java#L43-L46(this comment)integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiQueryAnswerModel.java#L68-L70
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@integrations/graph-rag-spring-ai/src/main/java/com/orgmemory/integrations/graphrag/springai/SpringAiKeywordPlanningModel.java`
around lines 43 - 46, Update the Prompt ChatOptions construction in
SpringAiKeywordPlanningModel.java (lines 43-46) and the comparable construction
in SpringAiQueryAnswerModel.java (lines 68-70) to derive from
chatModel.getOptions().mutate(), overriding only the intended model and
temperature values while preserving configured provider options such as apiKey,
baseUrl, and timeout.
What changed
Why
PR7 owns executable query semantics while keeping provider and storage effects behind explicit ports. This preserves LightRAG behavior without allowing Spring AI, PostgreSQL, or an authorization adapter to leak into the algorithmic core.
Validation
git diff --check./gradlew.bat --no-daemon compileJava./gradlew.bat --no-daemon test:apps:worker:test --rerun-tasksafter a transient Gradle binary-results raceMerge target
This PR intentionally targets
light-rag. It must not be merged directly intomain.Summary by CodeRabbit
New Features
Tests