Repository navigation
Phase 2: domain models #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c7492b
dc7e311
6f7b261
6e61a8c
1f25912
8d53fb2
890341a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """Domain models — pure Pydantic, no infrastructure imports.""" | ||
|
|
||
| from openrag.core.models.catalog import DocumentRecord, DocumentStatus, IndexationJob, JobStatus | ||
| from openrag.core.models.chunk import Chunk, ChunkType | ||
| from openrag.core.models.contextualization import ContextualizedQuery | ||
| from openrag.core.models.conversation import Conversation, Message | ||
| from openrag.core.models.document import Document, DocumentType, ImageBlock, ProcessedDocument, TextBlock | ||
| from openrag.core.models.prompt import Prompt, PromptType | ||
| from openrag.core.models.query import RetrievalQuery | ||
| from openrag.core.models.retrieval_response import RetrievalResponse | ||
| from openrag.core.models.retrieval_result import RetrievalResult, ScoredChunk | ||
| from openrag.core.models.user import OIDCSession, PartitionRole, User, UserPartition | ||
|
|
||
| __all__ = [ | ||
| "Chunk", | ||
| "ChunkType", | ||
| "ContextualizedQuery", | ||
| "Conversation", | ||
| "Document", | ||
| "DocumentRecord", | ||
| "DocumentStatus", | ||
| "DocumentType", | ||
| "ImageBlock", | ||
| "IndexationJob", | ||
| "JobStatus", | ||
| "Message", | ||
| "OIDCSession", | ||
| "PartitionRole", | ||
| "ProcessedDocument", | ||
| "Prompt", | ||
| "PromptType", | ||
| "RetrievalQuery", | ||
| "RetrievalResponse", | ||
| "RetrievalResult", | ||
| "ScoredChunk", | ||
| "TextBlock", | ||
| "User", | ||
| "UserPartition", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Catalog domain models — document records, indexation jobs, status tracking.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import uuid | ||
| from datetime import UTC, datetime | ||
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class DocumentStatus(str, Enum): | ||
| QUEUED = "QUEUED" | ||
| SERIALIZING = "SERIALIZING" | ||
| CHUNKING = "CHUNKING" | ||
| INSERTING = "INSERTING" | ||
| COMPLETED = "COMPLETED" | ||
| FAILED = "FAILED" | ||
| CANCELLED = "CANCELLED" | ||
|
|
||
|
|
||
| class JobStatus(str, Enum): | ||
| QUEUED = "QUEUED" | ||
| RUNNING = "RUNNING" | ||
| SUCCESS = "SUCCESS" | ||
| FAILED = "FAILED" | ||
| PARTIAL = "PARTIAL" | ||
|
|
||
|
|
||
| class DocumentRecord(BaseModel): | ||
| """A document entry in the catalog (PostgreSQL).""" | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| file_id: str = "" | ||
| filename: str = "" | ||
| partition: str = "default" | ||
| metadata: dict[str, Any] = Field(default_factory=dict) | ||
| status: DocumentStatus = DocumentStatus.QUEUED | ||
| error_message: str | None = None | ||
| created_by: int | None = None | ||
| relationship_id: str | None = None | ||
| parent_id: str | None = None | ||
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | ||
|
|
||
|
|
||
| class IndexationJob(BaseModel): | ||
| """An indexation job tracking batch document processing.""" | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| status: JobStatus = JobStatus.QUEUED | ||
| total_documents: int = 0 | ||
| partition: str = "default" | ||
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | ||
| started_at: datetime | None = None | ||
| completed_at: datetime | None = None |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| """Chunk — the unit of indexable and retrievable text.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import uuid | ||
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class ChunkType(str, Enum): | ||
| TEXT = "text" | ||
| IMAGE_CAPTION = "image_caption" | ||
| TABLE = "table" | ||
| CONTEXTUALIZED = "contextualized" | ||
|
|
||
|
|
||
| class Chunk(BaseModel): | ||
| """A chunk of text extracted from a document, optionally embedded.""" | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| document_id: str = "" | ||
| text: str = "" | ||
| chunk_index: int = 0 | ||
| chunk_type: ChunkType = ChunkType.TEXT | ||
| embedding: list[float] | None = None | ||
| metadata: dict[str, Any] = Field(default_factory=dict) | ||
| partition: str = "default" | ||
| page_number: int | None = None | ||
| token_count: int | None = None | ||
| header: str | None = None | ||
| context: str | None = None | ||
| content: str | None = None | ||
|
|
||
| def with_embedding(self, embedding: list[float]) -> Chunk: | ||
| """Return a copy with the embedding set.""" | ||
| return self.model_copy(update={"embedding": embedding}) | ||
|
|
||
| @classmethod | ||
| def from_langchain(cls, doc: Any) -> Chunk: | ||
| """Convert a LangChain Document to a Chunk. | ||
|
|
||
| Import is deferred to method body so core/ stays pure at import time. | ||
| """ | ||
| metadata = dict(doc.metadata) if doc.metadata else {} | ||
| return cls( | ||
| id=metadata.pop("_id", str(uuid.uuid4())), | ||
| document_id=metadata.pop("file_id", ""), | ||
| text=doc.page_content, | ||
| partition=metadata.pop("partition", "default"), | ||
| page_number=metadata.pop("page", None), | ||
| chunk_type=ChunkType(metadata.pop("chunk_type", "text")), | ||
| metadata=metadata, | ||
| ) | ||
|
|
||
| def to_langchain(self) -> Any: | ||
| """Convert back to a LangChain Document. | ||
|
|
||
| Import is deferred to method body so core/ stays pure at import time. | ||
| """ | ||
| from langchain_core.documents.base import Document | ||
|
|
||
| metadata = { | ||
| **self.metadata, | ||
| "_id": self.id, | ||
| "file_id": self.document_id, | ||
| "partition": self.partition, | ||
| "page": self.page_number, | ||
| "chunk_type": self.chunk_type.value, | ||
| } | ||
| return Document(page_content=self.text, metadata=metadata) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| """Contextualized query models — query rewriting, HyDE, reasoning.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class ContextualizedQuery(BaseModel): | ||
| """A user query after contextualization by the LLM. | ||
|
|
||
| May include rewritten variants, a hypothetical document (HyDE), | ||
| and sub-queries for multi-step retrieval. | ||
| """ | ||
|
|
||
| original: str = "" | ||
| query_list: list[str] = Field(default_factory=list) | ||
| intent: str = "qa" | ||
| hypothetical_doc: str | None = None | ||
| sub_queries: list[str] = Field(default_factory=list) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """Conversation and message domain models.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import uuid | ||
| from datetime import UTC, datetime | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class Message(BaseModel): | ||
| """A single message within a conversation.""" | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| conversation_id: str = "" | ||
| role: str = "user" | ||
| content: str = "" | ||
| sources_json: list[dict[str, Any]] | None = None | ||
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | ||
|
|
||
|
|
||
| class Conversation(BaseModel): | ||
| """A persistent conversation between a user and the RAG system.""" | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| user_id: int = 0 | ||
| partition_scope: str = "default" | ||
| title: str = "" | ||
| messages: list[Message] = Field(default_factory=list) | ||
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | ||
| updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,112 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| """Document — the input to the indexing pipeline.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import UTC, datetime | ||||||||||||||||||||||||||||||||||||||||||||||
| from enum import Enum | ||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel, Field | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class DocumentType(str, Enum): | ||||||||||||||||||||||||||||||||||||||||||||||
| PDF = "pdf" | ||||||||||||||||||||||||||||||||||||||||||||||
| TEXT = "text" | ||||||||||||||||||||||||||||||||||||||||||||||
| HTML = "html" | ||||||||||||||||||||||||||||||||||||||||||||||
| MARKDOWN = "markdown" | ||||||||||||||||||||||||||||||||||||||||||||||
| IMAGE = "image" | ||||||||||||||||||||||||||||||||||||||||||||||
| AUDIO = "audio" | ||||||||||||||||||||||||||||||||||||||||||||||
| VIDEO = "video" | ||||||||||||||||||||||||||||||||||||||||||||||
| DOCX = "docx" | ||||||||||||||||||||||||||||||||||||||||||||||
| PPTX = "pptx" | ||||||||||||||||||||||||||||||||||||||||||||||
| DOC = "doc" | ||||||||||||||||||||||||||||||||||||||||||||||
| EML = "eml" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class TextBlock(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||
| """A block of text extracted from a document.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| text: str | ||||||||||||||||||||||||||||||||||||||||||||||
| page_number: int | None = None | ||||||||||||||||||||||||||||||||||||||||||||||
| block_type: str = "paragraph" | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata: dict[str, Any] = Field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class ImageBlock(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||
| """An image extracted from a document.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| image_bytes: bytes = Field(exclude=True, repr=False) | ||||||||||||||||||||||||||||||||||||||||||||||
| page_number: int | None = None | ||||||||||||||||||||||||||||||||||||||||||||||
| caption: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||
| mime_type: str = "image/png" | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata: dict[str, Any] = Field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class Document(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||
| """A document before or during indexing.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||||||||||||||||||||||||||||||||||||||||||||||
| filename: str = "" | ||||||||||||||||||||||||||||||||||||||||||||||
| content_type: DocumentType = DocumentType.TEXT | ||||||||||||||||||||||||||||||||||||||||||||||
| text: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||
| raw_bytes: bytes | None = Field(None, exclude=True) | ||||||||||||||||||||||||||||||||||||||||||||||
| partition: str = "default" | ||||||||||||||||||||||||||||||||||||||||||||||
| tags: list[str] = Field(default_factory=list) | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata: dict[str, Any] = Field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||
| def detect_content_type(filename: str) -> DocumentType: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Detect DocumentType from filename extension.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" | ||||||||||||||||||||||||||||||||||||||||||||||
| mapping = { | ||||||||||||||||||||||||||||||||||||||||||||||
| "pdf": DocumentType.PDF, | ||||||||||||||||||||||||||||||||||||||||||||||
| "txt": DocumentType.TEXT, | ||||||||||||||||||||||||||||||||||||||||||||||
| "md": DocumentType.MARKDOWN, | ||||||||||||||||||||||||||||||||||||||||||||||
| "html": DocumentType.HTML, | ||||||||||||||||||||||||||||||||||||||||||||||
| "htm": DocumentType.HTML, | ||||||||||||||||||||||||||||||||||||||||||||||
| "png": DocumentType.IMAGE, | ||||||||||||||||||||||||||||||||||||||||||||||
| "jpg": DocumentType.IMAGE, | ||||||||||||||||||||||||||||||||||||||||||||||
| "jpeg": DocumentType.IMAGE, | ||||||||||||||||||||||||||||||||||||||||||||||
| "mp3": DocumentType.AUDIO, | ||||||||||||||||||||||||||||||||||||||||||||||
| "wav": DocumentType.AUDIO, | ||||||||||||||||||||||||||||||||||||||||||||||
| "mp4": DocumentType.VIDEO, | ||||||||||||||||||||||||||||||||||||||||||||||
| "docx": DocumentType.DOCX, | ||||||||||||||||||||||||||||||||||||||||||||||
| "pptx": DocumentType.PPTX, | ||||||||||||||||||||||||||||||||||||||||||||||
| "doc": DocumentType.DOC, | ||||||||||||||||||||||||||||||||||||||||||||||
| "eml": DocumentType.EML, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return mapping.get(ext, DocumentType.TEXT) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||||||
| def from_langchain(cls, doc: Any) -> Document: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Convert a LangChain Document to a domain Document.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata = dict(doc.metadata) if doc.metadata else {} | ||||||||||||||||||||||||||||||||||||||||||||||
| return cls( | ||||||||||||||||||||||||||||||||||||||||||||||
| filename=metadata.pop("source", ""), | ||||||||||||||||||||||||||||||||||||||||||||||
| text=doc.page_content, | ||||||||||||||||||||||||||||||||||||||||||||||
| partition=metadata.pop("partition", "default"), | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata=metadata, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
♻️ Proposed change def from_langchain(cls, doc: Any) -> Document:
"""Convert a LangChain Document to a domain Document."""
metadata = dict(doc.metadata) if doc.metadata else {}
+ filename = metadata.pop("source", "")
return cls(
- filename=metadata.pop("source", ""),
+ filename=filename,
+ content_type=cls.detect_content_type(filename),
text=doc.page_content,
partition=metadata.pop("partition", "default"),
metadata=metadata,
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def to_langchain(self) -> Any: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Convert back to a LangChain Document.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| from langchain_core.documents.base import Document as LCDocument | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| metadata = { | ||||||||||||||||||||||||||||||||||||||||||||||
| **self.metadata, | ||||||||||||||||||||||||||||||||||||||||||||||
| "source": self.filename, | ||||||||||||||||||||||||||||||||||||||||||||||
| "partition": self.partition, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return LCDocument(page_content=self.text or "", metadata=metadata) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class ProcessedDocument(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||
| """Document after parsing/extraction — contains text blocks and images.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| document_id: str = "" | ||||||||||||||||||||||||||||||||||||||||||||||
| text_blocks: list[TextBlock] = Field(default_factory=list) | ||||||||||||||||||||||||||||||||||||||||||||||
| images: list[ImageBlock] = Field(default_factory=list) | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata: dict[str, Any] = Field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||
| page_count: int = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """Prompt domain models.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import uuid | ||
| from datetime import UTC, datetime | ||
| from enum import Enum | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class PromptType(str, Enum): | ||
| SYS_PROMPT = "sys_prompt" | ||
| QUERY_CONTEXTUALIZER = "query_contextualizer" | ||
| CHUNK_CONTEXTUALIZER = "chunk_contextualizer" | ||
| IMAGE_CAPTIONING = "image_captioning" | ||
| HYDE = "hyde" | ||
| MULTI_QUERY = "multi_query" | ||
| SPOKEN_STYLE_ANSWER = "spoken_style_answer" | ||
|
|
||
|
|
||
| class Prompt(BaseModel): | ||
| """A prompt template stored in the library.""" | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| prompt_type: str = "" | ||
| name: str = "" | ||
| content: str = "" | ||
| is_default: bool = False | ||
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | ||
| updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||||||||||||||||||||||||||
| """Retrieval query domain model.""" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| from pydantic import BaseModel, Field | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| class RetrievalQuery(BaseModel): | ||||||||||||||||||||||||||||||
| """A user query with retrieval parameters.""" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| text: str | ||||||||||||||||||||||||||||||
| partition: str = "default" | ||||||||||||||||||||||||||||||
| top_k: int = 10 | ||||||||||||||||||||||||||||||
| similarity_threshold: float = 0.95 | ||||||||||||||||||||||||||||||
| filters: dict[str, Any] = Field(default_factory=dict) | ||||||||||||||||||||||||||||||
| include_related: bool = False | ||||||||||||||||||||||||||||||
| include_ancestors: bool = False | ||||||||||||||||||||||||||||||
| related_limit: int = 10 | ||||||||||||||||||||||||||||||
| max_ancestor_depth: int | None = None | ||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "query.py" | grep -E "openrag.*models.*query"Repository: linagora/openrag Length of output: 90 🏁 Script executed: cat -n openrag/core/models/query.py | head -50Repository: linagora/openrag Length of output: 808 🏁 Script executed: # Check if there are any validators or constraints already in place
rg -A 5 -B 5 "class RetrievalQuery" openrag/Repository: linagora/openrag Length of output: 591 🏁 Script executed: # Check how top_k, related_limit, max_ancestor_depth are used downstream
rg "top_k|related_limit|max_ancestor_depth" openrag/ -A 3 -B 1 --type py | head -100Repository: linagora/openrag Length of output: 6853 🏁 Script executed: # Find all usages of RetrievalQuery class
rg "RetrievalQuery" openrag/ --type pyRepository: linagora/openrag Length of output: 260 🏁 Script executed: # Check the vector DB implementation to see if it handles negative/unbounded values
rg "def.*search|def.*get_file_ancestors" openrag/ -A 10 --type py | grep -A 10 "top_k\|related_limit\|max_ancestor_depth" | head -80Repository: linagora/openrag Length of output: 5269 🏁 Script executed: # Look specifically at the vectordb implementation
find openrag -name "*vectordb*" -o -name "*milvus*" | head -5Repository: linagora/openrag Length of output: 214 🏁 Script executed: # Check how RetrievalQuery is actually used - is it instantiated anywhere?
rg "RetrievalQuery(" openrag/ --type pyRepository: linagora/openrag Length of output: 140 🏁 Script executed: # Check the actual vectordb implementation and how it uses these parameters
cat -n openrag/components/indexer/vectordb/vectordb.py | head -200Repository: linagora/openrag Length of output: 7472 🏁 Script executed: # Look at the _expand_with_related_chunks implementation which uses related_limit and max_ancestor_depth
rg -B 5 -A 20 "_expand_with_related_chunks" openrag/ --type pyRepository: linagora/openrag Length of output: 10198 🏁 Script executed: # Get the full implementation of _expand_with_related_chunks
rg -A 80 "^async def _expand_with_related_chunks" openrag/components/retriever.pyRepository: linagora/openrag Length of output: 3030 🏁 Script executed: # Check the get_file_ancestors implementation in vectordb to see if it validates max_ancestor_depth
rg -B 3 -A 20 "async def get_file_ancestors\|def get_file_ancestors" openrag/ --type pyRepository: linagora/openrag Length of output: 42 🏁 Script executed: # Look at how top_k is used in the actual Milvus search implementation
rg -B 5 -A 25 "def async_search" openrag/components/indexer/vectordb/vectordb.py | head -150Repository: linagora/openrag Length of output: 2103 🏁 Script executed: # Get more of the async_search implementation to see how top_k and similarity_threshold are used
rg -A 80 "async def async_search" openrag/components/indexer/vectordb/vectordb.py | tail -100Repository: linagora/openrag Length of output: 4045 🏁 Script executed: # Look for where top_k is passed to Milvus search
rg "search_request\|AnnSearchRequest\|top_k" openrag/components/indexer/vectordb/vectordb.py | head -40Repository: linagora/openrag Length of output: 42 🏁 Script executed: # Check if there's validation in get_ancestor_chunks or get_related_chunks for the limits
rg -B 5 -A 20 "async def get_ancestor_chunks\|async def get_related_chunks" openrag/components/indexer/vectordb/vectordb.py | head -100Repository: linagora/openrag Length of output: 42 🏁 Script executed: # Search for any direct instantiation of RetrievalQuery
rg "RetrievalQuery\(" openrag/ --type py -iRepository: linagora/openrag Length of output: 121 🏁 Script executed: # Check if there are any imports of RetrievalQuery elsewhere
rg "from.*RetrievalQuery|import.*RetrievalQuery" openrag/ --type pyRepository: linagora/openrag Length of output: 144 🏁 Script executed: # Look at the actual Milvus parameters more carefully - check what happens with negative top_k or out-of-range threshold
cat -n openrag/components/indexer/vectordb/vectordb.py | sed -n '300,450p'Repository: linagora/openrag Length of output: 7049 🏁 Script executed: # Check if RetrievalQuery or other models in openrag/core/models/ use ConfigDict
rg "ConfigDict|model_config" openrag/core/models/ --type pyRepository: linagora/openrag Length of output: 42 🏁 Script executed: # Check the complete RetrievalQuery definition to ensure we have the full picture
cat openrag/core/models/query.pyRepository: linagora/openrag Length of output: 647 🏁 Script executed: # Verify if RetrievalQuery is actually used anywhere in production code paths
rg "RetrievalQuery" openrag/ --type py -B 2 -A 2Repository: linagora/openrag Length of output: 1082 🏁 Script executed: # Final check: search for any direct instantiation of RetrievalQuery in the codebase
rg "RetrievalQuery\s*\(" openrag/ --type pyRepository: linagora/openrag Length of output: 121 🏁 Script executed: # Check if negative/unbounded values would cause issues at the Milvus level
# Look at what happens when top_k is negative or zero
cat -n openrag/components/indexer/vectordb/vectordb.py | sed -n '400,500p'Repository: linagora/openrag Length of output: 4653 🏁 Script executed: # Confirm the complete picture by checking the Milvus search to see what happens with top_k
cat -n openrag/components/indexer/vectordb/vectordb.py | sed -n '540,600p'Repository: linagora/openrag Length of output: 3059 Add field validators to constrain caller-controlled retrieval limits. The domain model Suggested Pydantic constraints- top_k: int = 10
- similarity_threshold: float = 0.95
+ top_k: int = Field(default=10, gt=0, le=100)
+ similarity_threshold: float = Field(default=0.95, ge=0.0, le=1.0)
filters: dict[str, Any] = Field(default_factory=dict)
include_related: bool = False
include_ancestors: bool = False
- related_limit: int = 10
- max_ancestor_depth: int | None = None
+ related_limit: int = Field(default=10, ge=0, le=100)
+ max_ancestor_depth: int | None = Field(default=None, ge=0)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| with_surrounding_chunks: bool = True | ||||||||||||||||||||||||||||||
| rerank: bool = True | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make
chunk_typeconversion tolerant of legacy metadata.Line 53 can raise
ValueErrorfor existing LangChain documents whosechunk_typeis not exactly one of the new enum values. The existing chunker path inopenrag/components/indexer/chunker/chunker.py:245-260can pass through non-standard entity metadata, so one chunk can abort the whole bridge conversion.Suggested defensive conversion
metadata = dict(doc.metadata) if doc.metadata else {} + raw_chunk_type = metadata.pop("chunk_type", ChunkType.TEXT.value) + try: + chunk_type = raw_chunk_type if isinstance(raw_chunk_type, ChunkType) else ChunkType(raw_chunk_type) + except ValueError: + metadata["chunk_type"] = raw_chunk_type + chunk_type = ChunkType.TEXT + return cls( id=metadata.pop("_id", str(uuid.uuid4())), document_id=metadata.pop("file_id", ""), text=doc.page_content, partition=metadata.pop("partition", "default"), page_number=metadata.pop("page", None), - chunk_type=ChunkType(metadata.pop("chunk_type", "text")), + chunk_type=chunk_type, metadata=metadata, )🤖 Prompt for AI Agents