1"""Retrieval stage for fetching relevant context chunks."""
2
3from __future__ import annotations
4
5from lexigram.ai.rag.config import RetrievalConfig
6from lexigram.ai.rag.pipeline.types import PipelineContext
7from lexigram.ai.rag.synthesis import ContextChunk
8from lexigram.contracts.ai.vector import DocumentVectorStoreProtocol
9from lexigram.logging import (
10 get_logger,
11)
12
13logger = get_logger(__name__)
14
15
16class RetrievalStage:
17 """Pipeline stage for retrieving relevant context chunks.
18
19 This stage retrieves relevant chunks based on the query,
20 using vector search, knowledge graph, or multi-hop reasoning
21 as configured.
22 """
23
24 def __init__(
25 self,
26 config: RetrievalConfig,
27 vector_store: DocumentVectorStoreProtocol | None = None,
28 ):
29 """Initialize the retrieval stage.
30
31 Args:
32 config: Retrieval configuration
33 vector_store: Optional vector store for similarity search
34 """
35 self.config = config
36 self.vector_store = vector_store
37
38 @property
39 def name(self) -> str:
40 """Get stage name."""
41 return "retrieval"
42
43 async def process(self, context: PipelineContext) -> PipelineContext:
44 """Process the retrieval stage.
45
46 Args:
47 context: Pipeline context with query and chunks
48
49 Returns:
50 Updated context with retrieved chunks
51 """
52 if not self.config.enabled:
53 logger.info("Retrieval stage disabled, skipping")
54 return context
55
56 try:
57 logger.info(
58 "Starting retrieval",
59 extra={
60 "request_id": context.request_id,
61 "query": context.query,
62 "available_chunks": len(context.chunks),
63 },
64 )
65
66 # For now, use a simple mock retrieval
67 # In a full implementation, this would use:
68 # - Vector store for similarity search
69 # - Knowledge graph for entity-based retrieval
70 # - Multi-hop reasoning for complex queries
71
72 retrieved_chunks = await self._retrieve_chunks(context)
73
74 context.retrieved_chunks = retrieved_chunks
75
76 logger.info(
77 "Retrieval completed",
78 extra={
79 "request_id": context.request_id,
80 "retrieved_count": len(retrieved_chunks),
81 },
82 )
83
84 except Exception as e:
85 logger.exception(
86 "Retrieval failed",
87 extra={
88 "request_id": context.request_id,
89 "error": str(e),
90 },
91 )
92 raise
93
94 return context
95
96 async def _retrieve_chunks(
97 self,
98 context: PipelineContext,
99 ) -> list[ContextChunk]:
100 """Retrieve relevant chunks based on query.
101
102 Args:
103 context: Pipeline context
104
105 Returns:
106 List of retrieved context chunks
107 """
108 # Simple mock retrieval: convert chunks to ContextChunks
109 # In a real implementation, this would:
110 # 1. Compute query embeddings
111 # 2. Search vector store
112 # 3. Rank by relevance
113 # 4. Apply filtering
114
115 if not context.chunks:
116 logger.warning("No chunks available for retrieval")
117 return []
118
119 # Convert to ContextChunks
120 retrieved: list[ContextChunk] = []
121
122 for i, chunk in enumerate(context.chunks[: self.config.top_k]):
123 md = chunk.metadata or {}
124 context_chunk = ContextChunk(
125 text=chunk.text,
126 source=md.get("source", f"chunk_{i}"),
127 score=1.0 - (i * 0.1), # Mock score
128 metadata=md,
129 rank=i,
130 )
131 retrieved.append(context_chunk)
132
133 return retrieved