Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/hyde/base.py: 89%
62 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Base HyDE generator with common functionality."""
3from __future__ import annotations
5from abc import ABC, abstractmethod
6from typing import Any
8from lexigram.ai.rag.hyde.protocols import EmbeddingClientProtocol
9from lexigram.ai.rag.hyde.types import HyDEResult, HypotheticalDocument
10from lexigram.contracts import (
11 ChatMessage,
12 LLMClientProtocol,
13)
16class AbstractHyDEGenerator(ABC):
17 """Base class for HyDE generators."""
19 def __init__(
20 self,
21 llm_client: LLMClientProtocol,
22 embedding_client: EmbeddingClientProtocol | None = None,
23 ):
24 """Initialize generator.
26 Args:
27 llm_client: Client for generating hypothetical documents
28 embedding_client: Optional client for generating embeddings
29 """
30 self.llm_client = llm_client
31 self.embedding_client = embedding_client
33 @abstractmethod
34 async def generate(
35 self,
36 query: str,
37 num_documents: int = 1,
38 **kwargs: Any,
39 ) -> HyDEResult:
40 """Generate hypothetical documents for query.
42 Args:
43 query: User query
44 num_documents: Number of hypothetical documents to generate
45 **kwargs: Additional parameters
47 Returns:
48 HyDE result with hypothetical documents
49 """
51 def _build_prompt(
52 self,
53 query: str,
54 context: str = "",
55 domain: str = "",
56 ) -> str:
57 """Build prompt for generating hypothetical document.
59 Args:
60 query: User query
61 context: Optional context
62 domain: Optional domain specification
64 Returns:
65 Formatted prompt
66 """
67 prompt = (
68 "Generate a detailed, informative passage that would answer this query:\n\n"
69 )
70 prompt += f"Query: {query}\n\n"
72 if domain:
73 prompt += f"Domain: {domain}\n\n"
75 if context:
76 prompt += f"Context: {context}\n\n"
78 prompt += (
79 "Write a passage that directly answers this query with specific details, "
80 "facts, and relevant information. Do not include the query itself in your response."
81 )
83 return prompt
85 async def _generate_single_document(
86 self,
87 query: str,
88 temperature: float = 0.7,
89 max_tokens: int = 200,
90 **kwargs: Any,
91 ) -> str:
92 """Generate a single hypothetical document.
94 Args:
95 query: User query
96 temperature: Sampling temperature
97 max_tokens: Maximum tokens to generate
98 **kwargs: Additional parameters
100 Returns:
101 Generated hypothetical document content
102 """
103 prompt = self._build_prompt(
104 query,
105 context=kwargs.get("context", ""),
106 domain=kwargs.get("domain", ""),
107 )
109 messages = [ChatMessage(role="user", content=prompt)]
111 result = await self.llm_client.complete(
112 messages,
113 temperature=temperature,
114 max_tokens=max_tokens,
115 )
116 if result.is_err():
117 raise result.unwrap_err()
118 response = result.unwrap()
120 # Extract content from response
121 content = self._extract_content(response)
122 return content.strip()
124 def _extract_content(self, response: Any) -> str:
125 """Extract content from LLM response.
127 Args:
128 response: LLM response object
130 Returns:
131 Extracted content string
132 """
133 # Handle different response formats
134 if hasattr(response, "content"):
135 return response.content
136 if hasattr(response, "text"):
137 return response.text
138 if hasattr(response, "choices"):
139 return response.choices[0].message.content
140 if isinstance(response, str):
141 return response
142 return str(response)
144 async def _embed_documents(
145 self,
146 documents: list[HypotheticalDocument],
147 ) -> list[list[float]]:
148 """Generate embeddings for hypothetical documents.
150 Args:
151 documents: List of hypothetical documents
153 Returns:
154 List of embeddings
155 """
156 if not self.embedding_client:
157 msg = "Embedding client required for embedding documents"
158 raise ValueError(msg)
160 texts = [doc.content for doc in documents]
161 return await self.embedding_client.embed(texts)
163 def _aggregate_embeddings(
164 self,
165 embeddings: list[list[float]],
166 weights: list[float] | None = None,
167 ) -> list[float]:
168 """Aggregate multiple embeddings into one.
170 Args:
171 embeddings: List of embedding vectors
172 weights: Optional weights for each embedding
174 Returns:
175 Aggregated embedding vector
176 """
177 if not embeddings:
178 return []
180 if weights is None:
181 weights = [1.0] * len(embeddings)
183 # Normalize weights
184 total_weight = sum(weights)
185 normalized_weights = [w / total_weight for w in weights]
187 # Weighted average
188 embedding_dim = len(embeddings[0])
189 aggregated = [0.0] * embedding_dim
191 for embedding, weight in zip(embeddings, normalized_weights, strict=False):
192 for i, val in enumerate(embedding):
193 aggregated[i] += val * weight
195 # Normalize vector
196 magnitude = sum(x * x for x in aggregated) ** 0.5
197 if magnitude > 0:
198 aggregated = [x / magnitude for x in aggregated]
200 return aggregated