1"""Hybrid synthesizer implementation.
2
3Combines extractive and abstractive approaches for robust synthesis.
4"""
5
6from __future__ import annotations
7
8from lexigram.ai.rag.synthesis.synthesizers.abstractive import (
9 AbstractiveSynthesizer,
10)
11from lexigram.ai.rag.synthesis.synthesizers.base import AbstractSynthesizer
12from lexigram.ai.rag.synthesis.synthesizers.extractive import (
13 ExtractiveSynthesizer,
14)
15from lexigram.ai.rag.synthesis.types import (
16 ContextChunk,
17 SynthesisResult,
18 SynthesisStrategy,
19)
20from lexigram.contracts import (
21 LLMClientProtocol,
22)
23
24
25class HybridSynthesizer(AbstractSynthesizer):
26 """Hybrid extractive + abstractive synthesizer.
27
28 This synthesizer first uses extractive methods to reduce and rank context,
29 then uses abstractive generation for the final response.
30
31 Attributes:
32 extractive: Extractive synthesizer for context reduction
33 abstractive: Abstractive synthesizer for final generation
34 extraction_ratio: Ratio of content to extract (0-1)
35 """
36
37 def __init__(
38 self,
39 llm_client: LLMClientProtocol,
40 extractive_weight: float = 0.3,
41 abstractive_weight: float = 0.7,
42 min_extractive_score: float = 0.5,
43 extraction_ratio: float = 0.5,
44 max_extractive_sentences: int = 5,
45 ):
46 """Initialize the hybrid synthesizer.
47
48 Args:
49 llm_client: LLM client implementing LLMClientProtocol protocol
50 extractive_weight: Weight for extractive component (0-1)
51 abstractive_weight: Weight for abstractive component (0-1)
52 min_extractive_score: Minimum score to use extractive results
53 extraction_ratio: Fraction of content to extract before abstraction
54 max_extractive_sentences: Max sentences to extract in extractive phase
55 """
56 self.extractive_weight = extractive_weight
57 self.abstractive_weight = abstractive_weight
58 self.min_extractive_score = min_extractive_score
59 self.extraction_ratio = extraction_ratio
60
61 self.extractive = ExtractiveSynthesizer(
62 max_sentences=max_extractive_sentences,
63 reorder_sentences=True,
64 )
65 self.abstractive = AbstractiveSynthesizer(
66 llm_client=llm_client,
67 temperature=0.3, # Default temperature
68 )
69
70 async def _synthesize_internal(
71 self,
72 query: str,
73 context_chunks: list[ContextChunk],
74 **kwargs,
75 ) -> SynthesisResult:
76 """Synthesize response using hybrid approach.
77
78 Args:
79 query: The user query
80 context_chunks: Retrieved context chunks
81 **kwargs: Additional parameters
82
83 Returns:
84 SynthesisResult with hybrid-synthesized response
85
86 Raises:
87 ValueError: If query is empty or no context chunks provided
88 """
89 if not query:
90 msg = "Query cannot be empty"
91 raise ValueError(msg)
92 if not context_chunks:
93 msg = "No context chunks provided"
94 raise ValueError(msg)
95
96 # Phase 1: Extractive reduction
97 # Extract most relevant sentences to reduce context size
98 extractive_result = await self.extractive._synthesize_internal(
99 query=query,
100 context_chunks=context_chunks,
101 )
102
103 # Create condensed chunks from extracted content
104 condensed_chunk = ContextChunk(
105 text=extractive_result.response,
106 source="extracted_content",
107 score=1.0,
108 metadata={
109 "num_sentences": extractive_result.metadata.get("num_sentences", 0),
110 "original_chunks": len(context_chunks),
111 },
112 )
113
114 # Phase 2: Abstractive synthesis
115 # Use LLM to generate final response from condensed content
116 abstractive_result = await self.abstractive._synthesize_internal(
117 query=query,
118 context_chunks=[condensed_chunk],
119 **kwargs,
120 )
121
122 # Combine metadata from both phases
123 metadata = {
124 "extraction_phase": extractive_result.metadata,
125 "abstraction_phase": abstractive_result.metadata,
126 "original_chunks": len(context_chunks),
127 "extractive_weight": self.extractive_weight,
128 "abstractive_weight": self.abstractive_weight,
129 }
130
131 return SynthesisResult(
132 query=query,
133 response=abstractive_result.response,
134 strategy=SynthesisStrategy.HYBRID,
135 context_chunks=extractive_result.context_chunks, # Original chunks used
136 citations=abstractive_result.citations,
137 metadata=metadata,
138 )