solana-agent 27.1.0__py3-none-any.whl → 27.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solana_agent/adapters/{llm_adapter.py → openai_adapter.py} +59 -5
- solana_agent/adapters/pinecone_adapter.py +496 -0
- solana_agent/client/solana_agent.py +145 -1
- solana_agent/factories/agent_factory.py +74 -8
- solana_agent/interfaces/client/client.py +75 -5
- solana_agent/interfaces/providers/llm.py +20 -0
- solana_agent/interfaces/providers/vector_storage.py +59 -0
- solana_agent/interfaces/services/knowledge_base.py +86 -0
- solana_agent/services/knowledge_base.py +771 -0
- solana_agent/services/query.py +33 -2
- {solana_agent-27.1.0.dist-info → solana_agent-27.3.0.dist-info}/METADATA +153 -72
- {solana_agent-27.1.0.dist-info → solana_agent-27.3.0.dist-info}/RECORD +14 -10
- {solana_agent-27.1.0.dist-info → solana_agent-27.3.0.dist-info}/LICENSE +0 -0
- {solana_agent-27.1.0.dist-info → solana_agent-27.3.0.dist-info}/WHEEL +0 -0
@@ -3,7 +3,7 @@ LLM provider adapters for the Solana Agent system.
|
|
3
3
|
|
4
4
|
These adapters implement the LLMProvider interface for different LLM services.
|
5
5
|
"""
|
6
|
-
from typing import AsyncGenerator, Literal, Optional, Type, TypeVar
|
6
|
+
from typing import AsyncGenerator, List, Literal, Optional, Type, TypeVar
|
7
7
|
|
8
8
|
from openai import AsyncOpenAI
|
9
9
|
from pydantic import BaseModel
|
@@ -14,16 +14,25 @@ from solana_agent.interfaces.providers.llm import LLMProvider
|
|
14
14
|
|
15
15
|
T = TypeVar('T', bound=BaseModel)
|
16
16
|
|
17
|
+
DEFAULT_CHAT_MODEL = "gpt-4.1-mini"
|
18
|
+
DEFAULT_PARSE_MODEL = "gpt-4.1-nano"
|
19
|
+
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
|
20
|
+
DEFAULT_EMBEDDING_DIMENSIONS = 3072
|
21
|
+
DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe"
|
22
|
+
DEFAULT_TTS_MODEL = "tts-1"
|
23
|
+
|
17
24
|
|
18
25
|
class OpenAIAdapter(LLMProvider):
|
19
26
|
"""OpenAI implementation of LLMProvider with web search capabilities."""
|
20
27
|
|
21
28
|
def __init__(self, api_key: str):
|
22
29
|
self.client = AsyncOpenAI(api_key=api_key)
|
23
|
-
self.parse_model =
|
24
|
-
self.text_model =
|
25
|
-
self.transcription_model =
|
26
|
-
self.tts_model =
|
30
|
+
self.parse_model = DEFAULT_PARSE_MODEL
|
31
|
+
self.text_model = DEFAULT_CHAT_MODEL
|
32
|
+
self.transcription_model = DEFAULT_TRANSCRIPTION_MODEL
|
33
|
+
self.tts_model = DEFAULT_TTS_MODEL
|
34
|
+
self.embedding_model = DEFAULT_EMBEDDING_MODEL
|
35
|
+
self.embedding_dimensions = DEFAULT_EMBEDDING_DIMENSIONS
|
27
36
|
|
28
37
|
async def tts(
|
29
38
|
self,
|
@@ -248,3 +257,48 @@ class OpenAIAdapter(LLMProvider):
|
|
248
257
|
raise ValueError(
|
249
258
|
f"Failed to generate structured output: {e}. All fallbacks failed."
|
250
259
|
) from e
|
260
|
+
|
261
|
+
async def embed_text(
|
262
|
+
self,
|
263
|
+
text: str,
|
264
|
+
model: Optional[str] = None,
|
265
|
+
dimensions: Optional[int] = None
|
266
|
+
) -> List[float]: # pragma: no cover
|
267
|
+
"""Generate an embedding for the given text using OpenAI.
|
268
|
+
|
269
|
+
Args:
|
270
|
+
text: The text to embed.
|
271
|
+
model: The embedding model to use (defaults to text-embedding-3-large).
|
272
|
+
dimensions: Desired output dimensions for the embedding.
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
A list of floats representing the embedding vector.
|
276
|
+
"""
|
277
|
+
if not text:
|
278
|
+
raise ValueError("Text cannot be empty")
|
279
|
+
|
280
|
+
try:
|
281
|
+
# Use provided model/dimensions or fall back to defaults
|
282
|
+
embedding_model = model or self.embedding_model
|
283
|
+
embedding_dimensions = dimensions or self.embedding_dimensions
|
284
|
+
|
285
|
+
# Replace newlines with spaces as recommended by OpenAI
|
286
|
+
text = text.replace("\n", " ")
|
287
|
+
|
288
|
+
response = await self.client.embeddings.create(
|
289
|
+
input=[text],
|
290
|
+
model=embedding_model,
|
291
|
+
dimensions=embedding_dimensions
|
292
|
+
)
|
293
|
+
|
294
|
+
if response.data and response.data[0].embedding:
|
295
|
+
return response.data[0].embedding
|
296
|
+
else:
|
297
|
+
raise ValueError(
|
298
|
+
"Failed to retrieve embedding from OpenAI response")
|
299
|
+
|
300
|
+
except Exception as e:
|
301
|
+
print(f"Error generating embedding: {e}")
|
302
|
+
import traceback
|
303
|
+
print(traceback.format_exc())
|
304
|
+
raise
|
@@ -0,0 +1,496 @@
|
|
1
|
+
from typing import List, Dict, Any, Optional, Literal, Union
|
2
|
+
from pinecone import PineconeAsyncio, ServerlessSpec
|
3
|
+
from pinecone.exceptions import PineconeApiException
|
4
|
+
import asyncio
|
5
|
+
|
6
|
+
from solana_agent.interfaces.providers.vector_storage import VectorStorageProvider
|
7
|
+
# LLMProvider is no longer needed here
|
8
|
+
|
9
|
+
# Type definitions remain useful
|
10
|
+
PineconeRerankModel = Literal[
|
11
|
+
"cohere-rerank-3.5",
|
12
|
+
"bge-reranker-v2-m3",
|
13
|
+
"pinecone-rerank-v0",
|
14
|
+
]
|
15
|
+
# Kept for potential future use, but not used internally now
|
16
|
+
InputType = Literal["query", "passage"]
|
17
|
+
TruncateType = Literal["END", "NONE"] # Kept for potential future use
|
18
|
+
|
19
|
+
|
20
|
+
class PineconeAdapter(VectorStorageProvider):
|
21
|
+
"""
|
22
|
+
Adapter for interacting with Pinecone vector database using PineconeAsyncio.
|
23
|
+
Assumes embeddings are generated externally (e.g., via OpenAI).
|
24
|
+
Supports Pinecone native reranking.
|
25
|
+
Follows context management patterns for Pinecone client v3+.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
api_key: Optional[str] = None,
|
31
|
+
index_name: Optional[str] = None,
|
32
|
+
# Default for OpenAI text-embedding-3-large, MUST match external embedder
|
33
|
+
embedding_dimensions: int = 3072,
|
34
|
+
cloud_provider: str = "aws",
|
35
|
+
region: str = "us-east-1",
|
36
|
+
metric: str = "cosine",
|
37
|
+
create_index_if_not_exists: bool = True,
|
38
|
+
# Reranking Config
|
39
|
+
use_reranking: bool = False,
|
40
|
+
rerank_model: Optional[PineconeRerankModel] = None,
|
41
|
+
rerank_top_k: int = 3, # Final number of results after reranking
|
42
|
+
# Multiplier for initial fetch before rerank
|
43
|
+
initial_query_top_k_multiplier: int = 5,
|
44
|
+
# Metadata field containing text for reranking
|
45
|
+
rerank_text_field: str = "text",
|
46
|
+
):
|
47
|
+
"""
|
48
|
+
Initialize the Pinecone Adapter.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
api_key: Pinecone API key.
|
52
|
+
index_name: Name of the Pinecone index.
|
53
|
+
embedding_dimensions: Dimension of the embeddings generated externally. MUST match the index dimension.
|
54
|
+
cloud_provider: Cloud provider for the index (e.g., 'aws', 'gcp').
|
55
|
+
region: Region for the index.
|
56
|
+
metric: Distance metric for the index (e.g., 'cosine', 'dotproduct', 'euclidean').
|
57
|
+
create_index_if_not_exists: Attempt to create the index if it doesn't exist.
|
58
|
+
use_reranking: Enable Pinecone native reranking.
|
59
|
+
rerank_model: The reranking model to use (required if use_reranking is True).
|
60
|
+
rerank_top_k: Final number of results to return after reranking.
|
61
|
+
initial_query_top_k_multiplier: Fetch top_k * multiplier results initially for reranking.
|
62
|
+
rerank_text_field: The key in vector metadata containing the text content for reranking.
|
63
|
+
"""
|
64
|
+
self.api_key = api_key
|
65
|
+
self.index_name = index_name
|
66
|
+
# Crucial: Must match external embedder and index
|
67
|
+
self.embedding_dimensions = embedding_dimensions
|
68
|
+
self.cloud_provider = cloud_provider
|
69
|
+
self.region = region
|
70
|
+
self.metric = metric
|
71
|
+
self.create_index_if_not_exists = create_index_if_not_exists
|
72
|
+
|
73
|
+
# Reranking Config
|
74
|
+
self.use_reranking = use_reranking
|
75
|
+
self.rerank_model = rerank_model
|
76
|
+
self.rerank_top_k = rerank_top_k
|
77
|
+
# Calculate how many results to fetch initially if reranking
|
78
|
+
self.initial_query_top_k_multiplier = initial_query_top_k_multiplier
|
79
|
+
|
80
|
+
self.rerank_text_field = rerank_text_field
|
81
|
+
|
82
|
+
self.pinecone: Optional[PineconeAsyncio] = None
|
83
|
+
# Store index host for connections
|
84
|
+
self.index_host: Optional[str] = None
|
85
|
+
|
86
|
+
# --- Validation ---
|
87
|
+
if not self.api_key:
|
88
|
+
raise ValueError("Pinecone API key is required.")
|
89
|
+
if not self.index_name:
|
90
|
+
raise ValueError("Pinecone index name is required.")
|
91
|
+
if self.embedding_dimensions <= 0:
|
92
|
+
raise ValueError(
|
93
|
+
"embedding_dimensions must be a positive integer.")
|
94
|
+
if self.use_reranking and not self.rerank_model:
|
95
|
+
raise ValueError(
|
96
|
+
"rerank_model must be specified when use_reranking is True.")
|
97
|
+
|
98
|
+
print(
|
99
|
+
f"PineconeAdapter configured for index '{self.index_name}' using external embeddings with dimension {self.embedding_dimensions}.")
|
100
|
+
if self.use_reranking:
|
101
|
+
print(f"Reranking enabled using model '{self.rerank_model}'.")
|
102
|
+
|
103
|
+
self._init_lock = asyncio.Lock()
|
104
|
+
self._initialized = False
|
105
|
+
|
106
|
+
async def _initialize_async(self):
|
107
|
+
"""Asynchronously initialize the Pinecone client and get index host."""
|
108
|
+
async with self._init_lock:
|
109
|
+
if self._initialized:
|
110
|
+
return
|
111
|
+
|
112
|
+
try:
|
113
|
+
print("Initializing PineconeAsyncio client...")
|
114
|
+
self.pinecone = PineconeAsyncio(api_key=self.api_key)
|
115
|
+
|
116
|
+
if self.create_index_if_not_exists:
|
117
|
+
await self._create_index_if_not_exists_async()
|
118
|
+
|
119
|
+
print(
|
120
|
+
f"Describing Pinecone index '{self.index_name}' to get host...")
|
121
|
+
index_description = await self.pinecone.describe_index(self.index_name)
|
122
|
+
self.index_host = index_description.host
|
123
|
+
if not self.index_host:
|
124
|
+
raise RuntimeError(
|
125
|
+
f"Could not obtain host for index '{self.index_name}'.")
|
126
|
+
print(f"Obtained index host: {self.index_host}")
|
127
|
+
|
128
|
+
# Validate index dimension matches configured dimension
|
129
|
+
index_dimension = index_description.dimension
|
130
|
+
if index_dimension != 0 and index_dimension != self.embedding_dimensions:
|
131
|
+
# This is a critical mismatch
|
132
|
+
raise ValueError(
|
133
|
+
f"CRITICAL MISMATCH: Pinecone index dimension ({index_dimension}) "
|
134
|
+
f"does not match configured embedding dimension ({self.embedding_dimensions}). "
|
135
|
+
f"Ensure the index was created with the correct dimension or update the adapter configuration."
|
136
|
+
)
|
137
|
+
elif index_dimension == 0:
|
138
|
+
print(
|
139
|
+
f"Warning: Pinecone index dimension reported as 0. Cannot verify match with configured dimension ({self.embedding_dimensions}).")
|
140
|
+
|
141
|
+
print("Attempting to get index stats...")
|
142
|
+
stats = await self.describe_index_stats()
|
143
|
+
print(f"Successfully retrieved index stats: {stats}")
|
144
|
+
|
145
|
+
total_vector_count = stats.get("total_vector_count", 0)
|
146
|
+
print(
|
147
|
+
f"Current index '{self.index_name}' contains {total_vector_count} vectors.")
|
148
|
+
|
149
|
+
self._initialized = True
|
150
|
+
print("Pinecone adapter initialization complete.")
|
151
|
+
|
152
|
+
except PineconeApiException as e:
|
153
|
+
print(f"Pinecone API error during async initialization: {e}")
|
154
|
+
self.pinecone = None
|
155
|
+
self.index_host = None
|
156
|
+
raise
|
157
|
+
except Exception as e:
|
158
|
+
print(
|
159
|
+
f"Failed to initialize Pinecone async adapter for index '{self.index_name}': {e}")
|
160
|
+
self.pinecone = None
|
161
|
+
self.index_host = None
|
162
|
+
raise
|
163
|
+
|
164
|
+
async def _create_index_if_not_exists_async(self) -> None:
|
165
|
+
"""Create the Pinecone index asynchronously if it doesn't already exist."""
|
166
|
+
if not self.pinecone:
|
167
|
+
raise RuntimeError(
|
168
|
+
"Pinecone client not initialized before creating index.")
|
169
|
+
try:
|
170
|
+
indexes_response = await self.pinecone.list_indexes()
|
171
|
+
existing_indexes = indexes_response.get('indexes', [])
|
172
|
+
existing_names = [idx.get('name') for idx in existing_indexes]
|
173
|
+
|
174
|
+
if self.index_name not in existing_names:
|
175
|
+
print(
|
176
|
+
f"Creating Pinecone index '{self.index_name}' with dimension {self.embedding_dimensions}...")
|
177
|
+
|
178
|
+
spec_data = {
|
179
|
+
"cloud": self.cloud_provider,
|
180
|
+
"region": self.region
|
181
|
+
}
|
182
|
+
|
183
|
+
create_params = {
|
184
|
+
"name": self.index_name,
|
185
|
+
"dimension": self.embedding_dimensions, # Use configured dimension
|
186
|
+
"metric": self.metric,
|
187
|
+
# Assuming serverless, adjust if needed
|
188
|
+
"spec": ServerlessSpec(**spec_data)
|
189
|
+
}
|
190
|
+
|
191
|
+
await self.pinecone.create_index(**create_params)
|
192
|
+
print(
|
193
|
+
f"✅ Successfully initiated creation of Pinecone index '{self.index_name}'. Waiting for it to be ready...")
|
194
|
+
# Wait time might need adjustment based on index size/type and cloud provider
|
195
|
+
await asyncio.sleep(30) # Increased wait time
|
196
|
+
else:
|
197
|
+
print(f"Using existing Pinecone index '{self.index_name}'")
|
198
|
+
except Exception as e:
|
199
|
+
print(
|
200
|
+
f"Error checking or creating Pinecone index asynchronously: {e}")
|
201
|
+
raise
|
202
|
+
|
203
|
+
async def _ensure_initialized(self):
|
204
|
+
"""Ensure the async client is initialized before use."""
|
205
|
+
if not self._initialized:
|
206
|
+
await self._initialize_async()
|
207
|
+
if not self._initialized or not self.pinecone or not self.index_host:
|
208
|
+
raise RuntimeError(
|
209
|
+
"Pinecone async client failed to initialize or get index host.")
|
210
|
+
|
211
|
+
# _get_embedding method is removed as embeddings are handled externally
|
212
|
+
|
213
|
+
async def upsert_text(self, *args, **kwargs): # pragma: no cover
|
214
|
+
"""Deprecated: Embeddings should be generated externally."""
|
215
|
+
raise NotImplementedError(
|
216
|
+
"upsert_text is deprecated. Use the generic upsert method with pre-computed vectors.")
|
217
|
+
|
218
|
+
async def upsert(
|
219
|
+
self,
|
220
|
+
# Expects {"id": str, "values": List[float], "metadata": Optional[Dict]}
|
221
|
+
vectors: List[Dict[str, Any]],
|
222
|
+
namespace: Optional[str] = None
|
223
|
+
) -> None: # pragma: no cover
|
224
|
+
"""Upsert pre-embedded vectors into Pinecone asynchronously."""
|
225
|
+
await self._ensure_initialized()
|
226
|
+
if not vectors:
|
227
|
+
print("Upsert skipped: No vectors provided.")
|
228
|
+
return
|
229
|
+
try:
|
230
|
+
async with self.pinecone.IndexAsyncio(host=self.index_host) as index_instance:
|
231
|
+
upsert_params = {"vectors": vectors}
|
232
|
+
if namespace:
|
233
|
+
upsert_params["namespace"] = namespace
|
234
|
+
await index_instance.upsert(**upsert_params)
|
235
|
+
print(
|
236
|
+
f"Successfully upserted {len(vectors)} vectors into namespace '{namespace or 'default'}'.")
|
237
|
+
except PineconeApiException as e:
|
238
|
+
print(f"Pinecone API error during async upsert: {e}")
|
239
|
+
raise
|
240
|
+
except Exception as e:
|
241
|
+
print(f"Error during async upsert: {e}")
|
242
|
+
raise
|
243
|
+
|
244
|
+
async def query_text(self, *args, **kwargs): # pragma: no cover
|
245
|
+
"""Deprecated: Use query() for simple vector search or query_and_rerank() for reranking."""
|
246
|
+
raise NotImplementedError(
|
247
|
+
"query_text is deprecated. Use query() or query_and_rerank() with a pre-computed vector.")
|
248
|
+
|
249
|
+
async def query_and_rerank(
|
250
|
+
self,
|
251
|
+
vector: List[float],
|
252
|
+
query_text_for_rerank: str, # The original query text is needed for the reranker
|
253
|
+
top_k: int = 5,
|
254
|
+
namespace: Optional[str] = None,
|
255
|
+
filter: Optional[Dict[str, Any]] = None,
|
256
|
+
include_values: bool = False,
|
257
|
+
include_metadata: bool = True,
|
258
|
+
) -> List[Dict[str, Any]]: # pragma: no cover
|
259
|
+
"""
|
260
|
+
Queries Pinecone with a vector and reranks the results using Pinecone's reranker.
|
261
|
+
Requires 'use_reranking' to be True and 'rerank_model' to be set during init.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
vector: The query vector.
|
265
|
+
query_text_for_rerank: The original text query used for the reranking model.
|
266
|
+
top_k: The final number of results desired after reranking.
|
267
|
+
namespace: Optional Pinecone namespace.
|
268
|
+
filter: Optional metadata filter for the initial query.
|
269
|
+
include_values: Whether to include vector values in the results.
|
270
|
+
include_metadata: Whether to include metadata in the results.
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
A list of reranked result dictionaries.
|
274
|
+
"""
|
275
|
+
await self._ensure_initialized()
|
276
|
+
|
277
|
+
if not self.use_reranking:
|
278
|
+
print(
|
279
|
+
"Warning: query_and_rerank called but use_reranking is False. Performing standard query.")
|
280
|
+
return await self.query(vector, top_k, namespace, filter, include_values, include_metadata)
|
281
|
+
|
282
|
+
if not self.rerank_model:
|
283
|
+
raise ValueError(
|
284
|
+
"Cannot rerank: rerank_model was not specified during initialization.")
|
285
|
+
|
286
|
+
# Determine how many results to fetch initially for reranking
|
287
|
+
initial_k = top_k * self.initial_query_top_k_multiplier
|
288
|
+
|
289
|
+
try:
|
290
|
+
# 1. Initial Vector Search
|
291
|
+
initial_results = await self.query(
|
292
|
+
vector=vector,
|
293
|
+
top_k=initial_k,
|
294
|
+
namespace=namespace,
|
295
|
+
filter=filter,
|
296
|
+
include_values=include_values, # Include values if requested in final output
|
297
|
+
include_metadata=True # Always need metadata for reranking text field
|
298
|
+
)
|
299
|
+
|
300
|
+
if not initial_results:
|
301
|
+
return [] # No results from initial query
|
302
|
+
|
303
|
+
# 2. Prepare for Reranking
|
304
|
+
documents_to_rerank = []
|
305
|
+
original_results_map = {}
|
306
|
+
for match in initial_results:
|
307
|
+
# Ensure metadata exists and contains the rerank text field
|
308
|
+
doc_metadata = match.get("metadata")
|
309
|
+
if isinstance(doc_metadata, dict):
|
310
|
+
doc_text = doc_metadata.get(self.rerank_text_field)
|
311
|
+
if doc_text and isinstance(doc_text, str):
|
312
|
+
documents_to_rerank.append(doc_text)
|
313
|
+
# Store original match keyed by the text for easy lookup after reranking
|
314
|
+
original_results_map[doc_text] = match
|
315
|
+
else:
|
316
|
+
print(
|
317
|
+
f"Warning: Skipping result ID {match.get('id')} for reranking - missing or invalid text in field '{self.rerank_text_field}'.")
|
318
|
+
else:
|
319
|
+
print(
|
320
|
+
f"Warning: Skipping result ID {match.get('id')} for reranking - metadata is missing or not a dictionary.")
|
321
|
+
|
322
|
+
if not documents_to_rerank:
|
323
|
+
print(
|
324
|
+
f"⚠️ Reranking skipped: No documents found with text in the specified field ('{self.rerank_text_field}'). Returning top {top_k} initial results.")
|
325
|
+
# Return the originally requested top_k
|
326
|
+
return initial_results[:top_k]
|
327
|
+
|
328
|
+
# 3. Perform Reranking Call
|
329
|
+
if not self.pinecone:
|
330
|
+
raise RuntimeError(
|
331
|
+
"Pinecone client not initialized for reranking.")
|
332
|
+
|
333
|
+
try:
|
334
|
+
print(
|
335
|
+
f"Reranking {len(documents_to_rerank)} results using {self.rerank_model} for query: '{query_text_for_rerank[:50]}...'")
|
336
|
+
rerank_params = {} # Add model-specific params if needed
|
337
|
+
|
338
|
+
rerank_request = {
|
339
|
+
"query": query_text_for_rerank,
|
340
|
+
"documents": documents_to_rerank,
|
341
|
+
"model": self.rerank_model,
|
342
|
+
"top_n": top_k, # Request the final desired number
|
343
|
+
"parameters": rerank_params
|
344
|
+
}
|
345
|
+
|
346
|
+
rerank_response = await self.pinecone.rerank(**rerank_request)
|
347
|
+
|
348
|
+
# 4. Process Reranked Results
|
349
|
+
reranked_results = []
|
350
|
+
if rerank_response and rerank_response.results:
|
351
|
+
for result in rerank_response.results:
|
352
|
+
# Adjust based on actual rerank response structure (assuming v3+)
|
353
|
+
doc_text = result.document.text if result.document else ""
|
354
|
+
score = result.relevance_score
|
355
|
+
original_match = original_results_map.get(doc_text)
|
356
|
+
if original_match:
|
357
|
+
# Create a new dict to avoid modifying the original map values
|
358
|
+
updated_match = dict(original_match)
|
359
|
+
# Update score with relevance score
|
360
|
+
updated_match["score"] = score
|
361
|
+
reranked_results.append(updated_match)
|
362
|
+
else:
|
363
|
+
print(
|
364
|
+
f"Warning: Reranked document text not found in original results map: '{doc_text[:50]}...'")
|
365
|
+
|
366
|
+
if reranked_results:
|
367
|
+
print(
|
368
|
+
f"Reranking complete. Returning {len(reranked_results)} results.")
|
369
|
+
return reranked_results
|
370
|
+
else:
|
371
|
+
# Should not happen if rerank_response.results existed, but handle defensively
|
372
|
+
print(
|
373
|
+
"Warning: No matches found after processing reranking response. Falling back to initial vector search results.")
|
374
|
+
return initial_results[:top_k]
|
375
|
+
|
376
|
+
except Exception as rerank_error:
|
377
|
+
print(
|
378
|
+
f"Error during reranking with {self.rerank_model}: {rerank_error}. Returning initial results.")
|
379
|
+
# Fallback to top_k initial results
|
380
|
+
return initial_results[:top_k]
|
381
|
+
|
382
|
+
except Exception as e:
|
383
|
+
print(f"Failed to query or rerank: {e}")
|
384
|
+
return []
|
385
|
+
|
386
|
+
async def query(
|
387
|
+
self,
|
388
|
+
vector: List[float],
|
389
|
+
top_k: int = 5,
|
390
|
+
namespace: Optional[str] = None,
|
391
|
+
filter: Optional[Dict[str, Any]] = None,
|
392
|
+
include_values: bool = False,
|
393
|
+
include_metadata: bool = True,
|
394
|
+
) -> List[Dict[str, Any]]: # pragma: no cover
|
395
|
+
"""
|
396
|
+
Query Pinecone for similar vectors asynchronously (no reranking).
|
397
|
+
|
398
|
+
Args:
|
399
|
+
vector: The query vector.
|
400
|
+
top_k: The number of results to return.
|
401
|
+
namespace: Optional Pinecone namespace.
|
402
|
+
filter: Optional metadata filter.
|
403
|
+
include_values: Whether to include vector values in the results.
|
404
|
+
include_metadata: Whether to include metadata in the results.
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
A list of result dictionaries.
|
408
|
+
"""
|
409
|
+
await self._ensure_initialized()
|
410
|
+
try:
|
411
|
+
async with self.pinecone.IndexAsyncio(host=self.index_host) as index_instance:
|
412
|
+
query_params = {
|
413
|
+
"vector": vector,
|
414
|
+
"top_k": top_k,
|
415
|
+
"include_values": include_values,
|
416
|
+
"include_metadata": include_metadata
|
417
|
+
}
|
418
|
+
if namespace:
|
419
|
+
query_params["namespace"] = namespace
|
420
|
+
if filter:
|
421
|
+
query_params["filter"] = filter
|
422
|
+
query_response = await index_instance.query(**query_params)
|
423
|
+
|
424
|
+
# Ensure response structure is handled safely
|
425
|
+
matches = query_response.get(
|
426
|
+
"matches", []) if query_response else []
|
427
|
+
return matches
|
428
|
+
|
429
|
+
except PineconeApiException as e:
|
430
|
+
print(f"Pinecone API error during async query: {e}")
|
431
|
+
raise # Re-raise API errors
|
432
|
+
except Exception as e:
|
433
|
+
print(f"Error during async query: {e}")
|
434
|
+
return [] # Return empty list for general errors
|
435
|
+
|
436
|
+
async def delete(
|
437
|
+
self,
|
438
|
+
ids: List[str],
|
439
|
+
namespace: Optional[str] = None
|
440
|
+
) -> None: # pragma: no cover
|
441
|
+
"""Delete vectors by IDs from Pinecone asynchronously."""
|
442
|
+
await self._ensure_initialized()
|
443
|
+
if not ids:
|
444
|
+
print("Delete skipped: No IDs provided.")
|
445
|
+
return
|
446
|
+
try:
|
447
|
+
async with self.pinecone.IndexAsyncio(host=self.index_host) as index_instance:
|
448
|
+
delete_params = {"ids": ids}
|
449
|
+
if namespace:
|
450
|
+
delete_params["namespace"] = namespace
|
451
|
+
await index_instance.delete(**delete_params)
|
452
|
+
print(
|
453
|
+
f"Attempted to delete {len(ids)} vectors from namespace '{namespace or 'default'}'.")
|
454
|
+
except PineconeApiException as e:
|
455
|
+
print(f"Pinecone API error during async delete: {e}")
|
456
|
+
raise
|
457
|
+
except Exception as e:
|
458
|
+
print(f"Error during async delete: {e}")
|
459
|
+
raise
|
460
|
+
|
461
|
+
async def describe_index_stats(self) -> Dict[str, Any]: # pragma: no cover
|
462
|
+
"""Get statistics about the index asynchronously."""
|
463
|
+
print(f"describe_index_stats: Entering for host {self.index_host}")
|
464
|
+
try:
|
465
|
+
print(
|
466
|
+
f"describe_index_stats: Getting IndexAsyncio context for host {self.index_host}...")
|
467
|
+
async with self.pinecone.IndexAsyncio(host=self.index_host) as index_instance:
|
468
|
+
print(
|
469
|
+
f"describe_index_stats: Context acquired. Calling describe_index_stats on index instance...")
|
470
|
+
stats_response = await index_instance.describe_index_stats()
|
471
|
+
print(
|
472
|
+
f"describe_index_stats: Call completed. Response: {stats_response}")
|
473
|
+
|
474
|
+
# Convert response to dict if necessary (handle potential None or different types)
|
475
|
+
if hasattr(stats_response, 'to_dict'):
|
476
|
+
result_dict = stats_response.to_dict()
|
477
|
+
elif isinstance(stats_response, dict):
|
478
|
+
result_dict = stats_response
|
479
|
+
else:
|
480
|
+
# Attempt basic conversion or return empty
|
481
|
+
try:
|
482
|
+
result_dict = dict(stats_response)
|
483
|
+
except (TypeError, ValueError):
|
484
|
+
print(
|
485
|
+
f"Warning: Could not convert stats_response to dict: {stats_response}")
|
486
|
+
result_dict = {}
|
487
|
+
|
488
|
+
print(f"describe_index_stats: Returning stats dict: {result_dict}")
|
489
|
+
return result_dict
|
490
|
+
except PineconeApiException as e:
|
491
|
+
print(
|
492
|
+
f"Pinecone API error describing index stats asynchronously: {e}")
|
493
|
+
raise # Re-raise API errors
|
494
|
+
except Exception as e:
|
495
|
+
print(f"Error describing index stats asynchronously: {e}")
|
496
|
+
return {} # Return empty dict for general errors
|