Skip to content

feat: add enhanced RAG validation with comprehensive checks and utili… - #12

Closed
jaffrey-deepsource wants to merge 1 commit into
mainfrom
feature/enhanced-rag-validation
Closed

feat: add enhanced RAG validation with comprehensive checks and utili…#12
jaffrey-deepsource wants to merge 1 commit into
mainfrom
feature/enhanced-rag-validation

Conversation

@jaffrey-deepsource

Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions github-actions Bot added the web label Feb 11, 2026
@deepsource-development

deepsource-development Bot commented Feb 11, 2026

Copy link
Copy Markdown

DeepSource Code Review

DeepSource reviewed changes in the commit range b76c8fa..63debb2 on this pull request. Below is the summary for the review, and you can see the individual issues we found as review comments.

For detailed review results, please see the PR on DeepSource ↗

PR Report Card

Security × 6 issues Overall PR Quality   

Focus Area: Reliability

Guidance
Multiple critical issues in `api/services/dataset_service.py` related to assigning function results without returns and misuse of context managers should be addressed first.

Grade capped at D due to multiple critical issues
Reliability × 28 issues
Complexity × 39 issues
Hygiene × 22 issues

Code Review Summary

Analyzer Status Summary Details
JavaScript 15 new issues detected. Review ↗
Python 81 new issues detected. Review ↗
Secrets No new issues detected. Review ↗
How are these analyzer statuses calculated?

Administrators can configure which issue categories are reported and cause analysis to be marked as failed when detected. This helps prevent bad and insecure code from being introduced in the codebase. If you're an administrator, you can modify this in the repository's settings.


💡 If you're a repository administrator, you can configure the quality gates from the settings.

}
}

const handleChange = (key: keyof RagValidationConfig, value: any) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of `any` type disables TypeScript type checking


The value parameter in the handleChange function is typed as any, which disables TypeScript's type checking and can introduce runtime errors if unexpected types are passed. This undermines the benefits of TypeScript's static type system and increases maintenance difficulty.
Replace the any type with more specific types, or use unknown to enforce explicit type checks before usage, improving code safety and predictability.

type="checkbox"
id="strict-validation"
checked={localConfig.enableStrictValidation}
onChange={(e) => handleChange('enableStrictValidation', e.target.checked)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline arrow function in JSX causes unnecessary re-renders


Defining the arrow function directly in the onChange prop creates a new function on each render, causing React to treat it as a new prop and potentially trigger unnecessary re-renders. This degrades performance especially in frequently updating or large component trees.

Use React.useCallback to memoize the callback outside of render or define the handler outside the JSX expression to maintain stable function references and reduce rendering overhead.

type="number"
id="max-topk"
value={localConfig.maxTopK}
onChange={(e) => handleChange('maxTopK', parseInt(e.target.value))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline arrow function in JSX causes re-creation on every render


The inline arrow function passed to the onChange prop is recreated on every render, which can make React treat it as a new callback causing unnecessary re-renders and impacting performance. This happens because React's prop shallow comparison detects a new function instance each time.

Use React.useCallback to memoize the callback so the function instance is stable across renders. Alternatively, define the handler function outside the render scope and reference it in JSX.

type="checkbox"
id="require-score-threshold"
checked={localConfig.requireScoreThreshold}
onChange={(e) => handleChange('requireScoreThreshold', e.target.checked)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline arrow function in JSX causes performance overhead


The arrow function (e) => handleChange('requireScoreThreshold', e.target.checked) is recreated on every render causing performance degradation by triggering unnecessary re-renders in child components that rely on prop reference equality. This affects the JSX property onChange in the flagged line.

Use React.useCallback to memoize the handler outside of the JSX return or define the callback once to avoid recreations and improve rendering efficiency.

errors: string[]
}

export const useRagValidation = (config: any) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using `any` disables TypeScript type safety checks


Using the any type for the config parameter bypasses all type checking, allowing any value to be passed without verification. This weakens type safety, making the code prone to bugs from unexpected or invalid data.
Replace the any type with specific types or use unknown to enforce explicit type checking and improve code robustness.

return totalScore / documents.length
}

export const filterDocuments = (documents: any[], threshold: number): any[] => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using `any` type disables TypeScript's type checks


The function filterDocuments is declared with parameters and return type as any[], which disables compile-time type checking. This allows any value to be passed or returned, increasing the risk of unexpected bugs or incorrect data handling at runtime.

Replace any[] with more specific or safer types like unknown[] or properly defined interfaces to enable TypeScript's type safety features and catch type errors early during development.

}

const RagPipelineValidation = ({ config, onConfigChange }: RagPipelineValidationProps) => {
const [localConfig, setLocalConfig] = useState(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component state `localConfig` doesn't update when `config` prop changes


The localConfig state is initialized with the config prop but is not updated when the prop changes. This can cause the UI to become out of sync with the application's state if the configuration is updated by the parent component.

Use the useEffect hook to synchronize the internal state with the config prop whenever it changes.

Comment on lines +17 to +24
const validateConfig = (cfg: RagValidationConfig) => {
if (cfg.maxTopK <= 0) {
throw new Error('Max Top K must be positive')
}
if (cfg.maxTopK > 1000) {
throw new Error('Max Top K cannot exceed 1000')
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`parseInt` can return `NaN` which bypasses validation checks


The validateConfig function fails to handle NaN values that result from parseInt(e.target.value) when the input is empty or non-numeric. This allows an invalid NaN value for maxTopK to be set in the state and passed to parent components, which can cause downstream errors.

Add a check for isNaN(cfg.maxTopK) in validateConfig to properly reject invalid number inputs and provide a more descriptive error message.

Comment on lines +14 to +16
if (config.topK <= 0) errors.push('Top K must be positive')
if (config.topK > 100) errors.push('Top K cannot exceed 100')
if (config.scoreThreshold < 0 || config.scoreThreshold > 1) errors.push('Score threshold must be between 0 and 1')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`config` properties are accessed without runtime validation


The hook accesses config.topK and config.scoreThreshold without verifying that config exists and that its properties are of the expected number type. This can cause a runtime crash if config is null, or incorrect validation behavior if a property is a non-numeric string, which would be coerced to NaN and bypass the checks.

Add checks to ensure config is not nullish and that topK and scoreThreshold are numbers before performing validation. This prevents runtime errors and ensures validation logic is correct.

Comment on lines +45 to +52
const avgLength = documents.reduce((sum, doc) => sum + (doc.content?.length || 0), 0) / totalDocs
const relevantDocs = documents.filter(doc => doc.score > 0.5).length

return {
totalDocuments: totalDocs,
averageContentLength: avgLength,
relevantDocuments: relevantDocs,
relevanceRatio: relevantDocs / totalDocs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division by `totalDocs` occurs without checking if it is zero


The calculations for avgLength and relevanceRatio do not handle the case where the documents array is empty, resulting in division by zero. This produces NaN values, which can propagate and cause unexpected behavior or crashes in other parts of the application.

Add a guard clause at the beginning of the function to check if documents.length is 0. If it is, return a default metrics object with all values set to 0.

if score_threshold < 0.0 or score_threshold > 1.0:
raise ValueError("Score threshold must be between 0.0 and 1.0")

def estimate_index_size(self, documents: list[Document]) -> int:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method lacks `@staticmethod`, causing unnecessary binding


The method estimate_index_size defined on line 110 does not use the self parameter within its body, meaning it does not need instance access. This causes Python to create a bound method every time an instance is made, incurring unnecessary memory overhead.

Add the @staticmethod decorator to estimate_index_size to avoid binding it to class instances, improving performance and reducing memory usage.

:return: Value or None
"""
return self.metadata.get(key) if self.metadata else None
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String statement has no effect


The string statement has not been assigned to anything. This is pointless and should be removed if not necessary. In case this is supposed to describe what's happening in the code, it is recommended to use comments or docstrings instead.

"""
raise NotImplementedError

def validate_rerank_params(self, query: str, documents: list[Document], top_n: int | None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance method without `self` usage wastes memory


The method validate_rerank_params is defined with self but does not use it, causing Python to create bound method objects for each instance. This wastes memory and adds overhead in method calls.

Decorate validate_rerank_params with @staticmethod to avoid binding to class instances and improve performance.

if top_n is not None and top_n <= 0:
raise ValueError("Top N must be positive")

def compute_rerank_score(self, query: str, document: Document) -> float:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance method ignores `self`, wasting binding overhead


The method compute_rerank_score is defined as an instance method but does not reference self. This causes Python to create a bound method for each instance, increasing memory and computation unnecessarily.

Add the @staticmethod decorator before the method to define it as a static method, avoiding binding overhead and clarifying its usage.

raise ValueError("Top K cannot exceed 100 to prevent excessive resource usage")

# Validate score threshold if enabled
if retrieve_config.score_threshold_enabled:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested `if` statements can be combined using `and`


The code contains nested if statements that can be merged into a single condition using the and operator, reducing unnecessary nesting and simplifying logic flows. This enhances readability by making it clear all conditions must be true for the block to execute.

Merge the nested if conditions into one line using the and operator to combine expressions into a single if statement.


return total_score / len(documents)

def _validate_query_input(self, query: str, inputs: Mapping[str, Any] | None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance method unused, wastes bound method overhead


The method _validate_query_input defines self as its first parameter but does not use it anywhere, creating an unneeded bound method. This causes Python to allocate extra resources during instantiation, affecting performance when creating class instances.

Add the @staticmethod decorator before _validate_query_input to remove the implicit self parameter, improving memory usage and call overhead for this utility method.

db.session.commit()
except Exception as e:
db.session.rollback()
raise e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undefined variable 'e'


The variable name is not defined where it is used.
This will lead to an error during the runtime.
Make sure there is no typo. If the name was supposed to be imported, verify that you've actually imported the name.

Comment on lines +17 to +24
def compute_document_similarity(doc1: DocumentContext, doc2: DocumentContext) -> float:
if not doc1 or not doc2:
return 0.0

# Simple similarity based on content length difference
len1 = len(doc1.content) if doc1.content else 0
len2 = len(doc2.content) if doc2.content else 0
return 1.0 / (1.0 + abs(len1 - len2))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`compute_document_similarity` uses only content length difference


The compute_document_similarity function provides a misleading implementation of document similarity. It only compares the lengths of document contents, which is not a reliable measure of semantic similarity and can lead to incorrect logic if a caller expects a meaningful comparison.

Consider replacing this with a standard text similarity algorithm like Jaccard similarity or cosine similarity on TF-IDF vectors. If this simple length-based metric is intentional, rename the function to compute_similarity_by_length to accurately reflect its behavior.

total_score = 0.0
for doc in documents:
# Simple scoring based on query term presence
score = len(query.split()) / len(doc.page_content.split()) if doc.page_content else 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`len(doc.page_content.split())` can be zero, causing `ZeroDivisionError`


The code len(doc.page_content.split()) is used as a divisor without checking if it's non-zero. If doc.page_content is a string containing only whitespace (e.g., " "), split() returns an empty list, which will cause a ZeroDivisionError and crash the request, leading to a potential denial-of-service.

To fix this, add a check to ensure doc.page_content contains non-whitespace characters before performing the division.

…ties

- Add validation for dataset retrieval config in backend
- Add relevance score calculation and query validation helpers
- Add document filtering and averaging utilities
- Add frontend validation utilities and hooks for RAG pipeline
- Add pipeline validator class for enhanced checks
- Add rerank parameter validation and scoring
- Add embedding input validation and similarity computation
- Add text splitter parameter validation and chunk estimation
- Add dataset config validation and statistics computation
- Add document validation and content utilities
- Add index parameter validation and size estimation
- Add RAG pipeline config validation and metrics calculation
return true
}

export const computeRagScore = (documents: any[], query: string): number => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declared variables unused waste memory and reduce clarity


The function computeRagScore declares variables that are not used anywhere, consuming memory and potentially confusing later developers about their purpose. This degrades code maintainability and may load unnecessary modules at runtime.
Remove or prefix unused variables with _ to explicitly show they are intentional or delete them to optimize performance and reduce code clutter.

}

const RagPipelineValidation = ({ config, onConfigChange }: RagPipelineValidationProps) => {
const [localConfig, setLocalConfig] = useState(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component state initialized from props is not updated on prop changes


The component's internal state localConfig is initialized from the config prop using useState(config). This initialization runs only once when the component mounts. If the config prop is updated by the parent component later, localConfig will become out of sync, causing the UI to display stale data.

Use a useEffect hook to synchronize localConfig with the config prop whenever it changes. This ensures the component always reflects the most current state provided by its parent.

type="number"
id="max-topk"
value={localConfig.maxTopK}
onChange={(e) => handleChange('maxTopK', parseInt(e.target.value))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`parseInt` on user input can produce `NaN`, bypassing validation


The onChange handler for the maxTopK input uses parseInt directly on e.target.value. If a user enters an empty or non-numeric string, parseInt returns NaN. This NaN value bypasses the checks in validateConfig (as NaN &lt;= 0 and NaN &gt; 1000 are both false), corrupting the component's state and causing "NaN" to be displayed in the input field.

To prevent state corruption, add validation to ensure that only valid numbers are passed to handleChange. Check the result of parseInt with isNaN and only update the state if the value is a valid number.

Comment on lines +8 to +16
export const useRagValidation = (config: any) => {
const [validation, setValidation] = useState<RagValidationState>({ isValid: true, errors: [] })

useEffect(() => {
const errors: string[] = []

if (config.topK <= 0) errors.push('Top K must be positive')
if (config.topK > 100) errors.push('Top K cannot exceed 100')
if (config.scoreThreshold < 0 || config.scoreThreshold > 1) errors.push('Score threshold must be between 0 and 1')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`config` parameter uses `any` and is accessed without null-safety checks


The config parameter is typed as any, which bypasses static type checking. This, combined with direct property access, creates two problems: 1) If config is null or undefined, the hook will crash with a TypeError. 2) If required properties like topK are missing, they are treated as undefined and incorrectly pass validation.

Replace any with a specific interface for config and add runtime checks to validate the presence and types of its properties before using them. This will prevent crashes and ensure validation logic is correct.

Comment on lines +44 to +53
const totalDocs = documents.length
const avgLength = documents.reduce((sum, doc) => sum + (doc.content?.length || 0), 0) / totalDocs
const relevantDocs = documents.filter(doc => doc.score > 0.5).length

return {
totalDocuments: totalDocs,
averageContentLength: avgLength,
relevantDocuments: relevantDocs,
relevanceRatio: relevantDocs / totalDocs
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division by `totalDocs` can result in `NaN`


The calculations for avgLength and relevanceRatio do not account for the case where the documents array is empty, leading to a division by zero. This results in NaN values being returned, which can cause rendering issues or runtime errors in consuming components.

Add a guard clause at the start of the function to check if totalDocs is 0 and return a default metrics object (e.g., with all values set to 0) in that case.

"""Asynchronous Embed query text."""
raise NotImplementedError

def validate_embedding_input(self, texts: list[str]) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance method without `self` usage wastes resources


The validate_embedding_input method is defined as an instance method but does not utilize the self parameter, so it unnecessarily creates a bound method for every class instance. This wastes memory and adds minor computational overhead during method calls.

Decorate validate_embedding_input with @staticmethod to avoid binding it to instances. This improves efficiency and clarifies that the method does not depend on instance state.

if not text or len(text.strip()) == 0:
raise ValueError("All texts must be non-empty")

def compute_embedding_similarity(self, embedding1: list[float], embedding2: list[float]) -> float:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance method without `self` usage wastes resources


The method compute_embedding_similarity is defined as an instance method but does not use the self parameter. Python creates a bound method for every class instance, requiring additional memory and CPU cycles when this is not needed.

Add the @staticmethod decorator above compute_embedding_similarity to remove the implicit self and avoid bound method instantiation, improving resource efficiency for all class instances.

Comment on lines +17 to +24
def compute_document_similarity(doc1: DocumentContext, doc2: DocumentContext) -> float:
if not doc1 or not doc2:
return 0.0

# Simple similarity based on content length difference
len1 = len(doc1.content) if doc1.content else 0
len2 = len(doc2.content) if doc2.content else 0
return 1.0 / (1.0 + abs(len1 - len2))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`compute_document_similarity` compares document lengths, not content


The compute_document_similarity function's name is misleading as it suggests a content-based comparison, while its logic is solely based on the difference in content length. This can lead to incorrect behavior in features that rely on this function for semantic similarity.

To fix this, either rename the function to more accurately reflect its behavior (e.g., compute_length_based_similarity), or replace the current implementation with a proper content similarity algorithm like Jaccard similarity.

total_score = 0.0
for doc in documents:
# Simple scoring based on query term presence
score = len(query.split()) / len(doc.page_content.split()) if doc.page_content else 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`len(doc.page_content.split())` can be zero, causing a crash


The scoring logic len(query.split()) / len(doc.page_content.split()) is vulnerable to a division-by-zero error. If doc.page_content is a string that results in an empty list when split (e.g., an empty string or only whitespace), the denominator becomes zero. This leads to an unhandled ZeroDivisionError and a potential denial of service.

To fix this, add a check to ensure the list of words from doc.page_content is not empty before performing the division. Assign a score of 0.0 in cases where there is no content.

Comment on lines +176 to +177
effective_size = chunk_size - overlap
return (text_length - overlap) // effective_size + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`chunk_size - overlap` can be zero, causing `ZeroDivisionError`


The calculation of effective_size does not handle the case where self._chunk_overlap is greater than or equal to self._chunk_size. This can lead to a ZeroDivisionError if effective_size is zero, crashing the application. An attacker might exploit this by providing configuration that leads to this state, causing a denial of service.

Add a validation check before the division to ensure effective_size is positive. For example, raise a ValueError if chunk_size &lt;= overlap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant