agno 2.0.0rc2__py3-none-any.whl → 2.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.
- agno/agent/agent.py +6009 -2874
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +385 -6
- agno/db/dynamo/dynamo.py +388 -81
- agno/db/dynamo/schemas.py +47 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +435 -64
- agno/db/firestore/schemas.py +11 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +384 -42
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +351 -66
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +339 -48
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +510 -37
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2036 -0
- agno/db/mongo/mongo.py +653 -76
- agno/db/mongo/schemas.py +13 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/mysql.py +687 -25
- agno/db/mysql/schemas.py +61 -37
- agno/db/mysql/utils.py +60 -2
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2001 -0
- agno/db/postgres/postgres.py +676 -57
- agno/db/postgres/schemas.py +43 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +344 -38
- agno/db/redis/schemas.py +18 -0
- agno/db/redis/utils.py +60 -2
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +26 -1
- agno/db/singlestore/singlestore.py +687 -53
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2371 -0
- agno/db/sqlite/schemas.py +24 -0
- agno/db/sqlite/sqlite.py +774 -85
- agno/db/sqlite/utils.py +168 -5
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1361 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +50 -22
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +68 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/chunking/semantic.py +40 -8
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +13 -0
- agno/knowledge/embedder/openai.py +37 -65
- agno/knowledge/embedder/sentence_transformer.py +8 -4
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +595 -187
- agno/knowledge/reader/base.py +9 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/json_reader.py +6 -5
- agno/knowledge/reader/markdown_reader.py +13 -13
- agno/knowledge/reader/pdf_reader.py +43 -68
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +51 -6
- agno/knowledge/reader/s3_reader.py +3 -15
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +13 -13
- agno/knowledge/reader/web_search_reader.py +2 -43
- agno/knowledge/reader/website_reader.py +43 -25
- agno/knowledge/reranker/__init__.py +3 -0
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +339 -266
- agno/memory/manager.py +336 -82
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/anthropic/claude.py +183 -37
- agno/models/aws/bedrock.py +52 -112
- agno/models/aws/claude.py +33 -1
- agno/models/azure/ai_foundry.py +33 -15
- agno/models/azure/openai_chat.py +25 -8
- agno/models/base.py +1011 -566
- agno/models/cerebras/cerebras.py +19 -13
- agno/models/cerebras/cerebras_openai.py +8 -5
- agno/models/cohere/chat.py +27 -1
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/google/gemini.py +110 -37
- agno/models/groq/groq.py +28 -11
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/chat.py +18 -1
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +143 -4
- agno/models/meta/llama.py +27 -10
- agno/models/meta/llama_openai.py +5 -17
- agno/models/nebius/nebius.py +6 -6
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/ollama/chat.py +60 -6
- agno/models/openai/chat.py +102 -43
- agno/models/openai/responses.py +103 -106
- agno/models/openrouter/openrouter.py +41 -3
- agno/models/perplexity/perplexity.py +4 -5
- agno/models/portkey/portkey.py +3 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +81 -5
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/together.py +2 -2
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +2 -2
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +96 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +3 -2
- agno/os/app.py +543 -175
- agno/os/auth.py +24 -14
- agno/os/config.py +1 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/agui.py +23 -7
- agno/os/interfaces/agui/router.py +27 -3
- agno/os/interfaces/agui/utils.py +242 -142
- agno/os/interfaces/base.py +6 -2
- agno/os/interfaces/slack/router.py +81 -23
- agno/os/interfaces/slack/slack.py +29 -14
- agno/os/interfaces/whatsapp/router.py +11 -4
- agno/os/interfaces/whatsapp/whatsapp.py +14 -7
- agno/os/mcp.py +111 -54
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +556 -139
- agno/os/routers/evals/evals.py +71 -34
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/evals/utils.py +6 -5
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +185 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +158 -53
- agno/os/routers/memory/schemas.py +20 -16
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +499 -38
- agno/os/schema.py +308 -198
- agno/os/utils.py +401 -41
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +7 -2
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +266 -112
- agno/run/base.py +53 -24
- agno/run/team.py +252 -111
- agno/run/workflow.py +156 -45
- agno/session/agent.py +105 -89
- agno/session/summary.py +65 -25
- agno/session/team.py +176 -96
- agno/session/workflow.py +406 -40
- agno/team/team.py +3854 -1692
- agno/tools/brightdata.py +3 -3
- agno/tools/cartesia.py +3 -5
- agno/tools/dalle.py +9 -8
- agno/tools/decorator.py +4 -2
- agno/tools/desi_vocal.py +2 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +20 -13
- agno/tools/eleven_labs.py +26 -28
- agno/tools/exa.py +21 -16
- agno/tools/fal.py +4 -4
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +257 -37
- agno/tools/giphy.py +2 -2
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +270 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/knowledge.py +3 -3
- agno/tools/lumalab.py +3 -3
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memori.py +1 -53
- agno/tools/memory.py +419 -0
- agno/tools/models/azure_openai.py +2 -2
- agno/tools/models/gemini.py +3 -3
- agno/tools/models/groq.py +3 -5
- agno/tools/models/nebius.py +7 -7
- agno/tools/models_labs.py +25 -15
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +4 -9
- agno/tools/opencv.py +3 -3
- agno/tools/parallel.py +314 -0
- agno/tools/replicate.py +7 -7
- agno/tools/scrapegraph.py +58 -31
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/slack.py +18 -3
- agno/tools/spider.py +2 -2
- agno/tools/tavily.py +146 -0
- agno/tools/whatsapp.py +1 -1
- agno/tools/workflow.py +278 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/events.py +222 -7
- agno/utils/gemini.py +181 -23
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +111 -0
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +95 -5
- agno/utils/media.py +188 -10
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +60 -0
- agno/utils/models/claude.py +40 -11
- agno/utils/models/cohere.py +1 -1
- agno/utils/models/watsonx.py +1 -1
- agno/utils/openai.py +1 -1
- agno/utils/print_response/agent.py +105 -21
- agno/utils/print_response/team.py +103 -38
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/reasoning.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +16 -10
- agno/utils/string.py +41 -0
- agno/utils/team.py +98 -9
- agno/utils/tools.py +1 -1
- agno/vectordb/base.py +23 -4
- agno/vectordb/cassandra/cassandra.py +65 -9
- agno/vectordb/chroma/chromadb.py +182 -38
- agno/vectordb/clickhouse/clickhousedb.py +64 -11
- agno/vectordb/couchbase/couchbase.py +105 -10
- agno/vectordb/lancedb/lance_db.py +183 -135
- agno/vectordb/langchaindb/langchaindb.py +25 -7
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +46 -7
- agno/vectordb/milvus/milvus.py +126 -9
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +112 -7
- agno/vectordb/pgvector/pgvector.py +142 -21
- agno/vectordb/pineconedb/pineconedb.py +80 -8
- agno/vectordb/qdrant/qdrant.py +125 -39
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/singlestore/singlestore.py +111 -25
- agno/vectordb/surrealdb/surrealdb.py +31 -5
- agno/vectordb/upstashdb/upstashdb.py +76 -8
- agno/vectordb/weaviate/weaviate.py +86 -15
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +112 -18
- agno/workflow/loop.py +69 -10
- agno/workflow/parallel.py +266 -118
- agno/workflow/router.py +110 -17
- agno/workflow/step.py +645 -136
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +71 -33
- agno/workflow/workflow.py +2113 -300
- agno-2.3.0.dist-info/METADATA +618 -0
- agno-2.3.0.dist-info/RECORD +577 -0
- agno-2.3.0.dist-info/licenses/LICENSE +201 -0
- agno/knowledge/reader/url_reader.py +0 -128
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -610
- agno/utils/models/aws_claude.py +0 -170
- agno-2.0.0rc2.dist-info/METADATA +0 -355
- agno-2.0.0rc2.dist-info/RECORD +0 -515
- agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/vectordb/mongodb/mongodb.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
-
from typing import Any, Dict, List, Optional
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
4
|
|
|
5
5
|
from bson import ObjectId
|
|
6
6
|
|
|
7
|
+
from agno.filters import FilterExpr
|
|
7
8
|
from agno.knowledge.document import Document
|
|
8
9
|
from agno.knowledge.embedder import Embedder
|
|
9
10
|
from agno.utils.log import log_debug, log_info, log_warning, logger
|
|
@@ -33,6 +34,9 @@ class MongoDb(VectorDb):
|
|
|
33
34
|
def __init__(
|
|
34
35
|
self,
|
|
35
36
|
collection_name: str,
|
|
37
|
+
name: Optional[str] = None,
|
|
38
|
+
description: Optional[str] = None,
|
|
39
|
+
id: Optional[str] = None,
|
|
36
40
|
db_url: Optional[str] = "mongodb://localhost:27017/",
|
|
37
41
|
database: str = "agno",
|
|
38
42
|
embedder: Optional[Embedder] = None,
|
|
@@ -56,6 +60,8 @@ class MongoDb(VectorDb):
|
|
|
56
60
|
|
|
57
61
|
Args:
|
|
58
62
|
collection_name (str): Name of the MongoDB collection.
|
|
63
|
+
name (Optional[str]): Name of the vector database.
|
|
64
|
+
description (Optional[str]): Description of the vector database.
|
|
59
65
|
db_url (Optional[str]): MongoDB connection string.
|
|
60
66
|
database (str): Database name.
|
|
61
67
|
embedder (Embedder): Embedder instance for generating embeddings.
|
|
@@ -74,11 +80,24 @@ class MongoDb(VectorDb):
|
|
|
74
80
|
hybrid_rank_constant (int): Default rank constant (k) for Reciprocal Rank Fusion in hybrid search. This constant is added to the rank before taking the reciprocal, helping to smooth scores. A common value is 60.
|
|
75
81
|
**kwargs: Additional arguments for MongoClient.
|
|
76
82
|
"""
|
|
83
|
+
# Validate required parameters
|
|
77
84
|
if not collection_name:
|
|
78
85
|
raise ValueError("Collection name must not be empty.")
|
|
79
86
|
if not database:
|
|
80
87
|
raise ValueError("Database name must not be empty.")
|
|
88
|
+
|
|
89
|
+
# Dynamic ID generation based on unique identifiers
|
|
90
|
+
if id is None:
|
|
91
|
+
from agno.utils.string import generate_id
|
|
92
|
+
|
|
93
|
+
connection_identifier = db_url or "mongodb://localhost:27017/"
|
|
94
|
+
seed = f"{connection_identifier}#{database}#{collection_name}"
|
|
95
|
+
id = generate_id(seed)
|
|
96
|
+
|
|
81
97
|
self.collection_name = collection_name
|
|
98
|
+
# Initialize base class with name, description, and generated ID
|
|
99
|
+
super().__init__(id=id, name=name, description=description)
|
|
100
|
+
|
|
82
101
|
self.database = database
|
|
83
102
|
self.search_index_name = search_index_name
|
|
84
103
|
self.cosmos_compatibility = cosmos_compatibility
|
|
@@ -567,9 +586,16 @@ class MongoDb(VectorDb):
|
|
|
567
586
|
return True
|
|
568
587
|
|
|
569
588
|
def search(
|
|
570
|
-
self,
|
|
589
|
+
self,
|
|
590
|
+
query: str,
|
|
591
|
+
limit: int = 5,
|
|
592
|
+
filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
593
|
+
min_score: float = 0.0,
|
|
571
594
|
) -> List[Document]:
|
|
572
595
|
"""Search for documents using vector similarity."""
|
|
596
|
+
if isinstance(filters, List):
|
|
597
|
+
log_warning("Filters Expressions are not supported in MongoDB. No filters will be applied.")
|
|
598
|
+
filters = None
|
|
573
599
|
if self.search_type == SearchType.hybrid:
|
|
574
600
|
return self.hybrid_search(query, limit=limit, filters=filters)
|
|
575
601
|
|
|
@@ -1018,8 +1044,44 @@ class MongoDb(VectorDb):
|
|
|
1018
1044
|
log_debug(f"Inserting {len(documents)} documents asynchronously")
|
|
1019
1045
|
collection = await self._get_async_collection()
|
|
1020
1046
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1047
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
1048
|
+
# Use batch embedding when enabled and supported
|
|
1049
|
+
try:
|
|
1050
|
+
# Extract content from all documents
|
|
1051
|
+
doc_contents = [doc.content for doc in documents]
|
|
1052
|
+
|
|
1053
|
+
# Get batch embeddings and usage
|
|
1054
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
1055
|
+
|
|
1056
|
+
# Process documents with pre-computed embeddings
|
|
1057
|
+
for j, doc in enumerate(documents):
|
|
1058
|
+
try:
|
|
1059
|
+
if j < len(embeddings):
|
|
1060
|
+
doc.embedding = embeddings[j]
|
|
1061
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
1062
|
+
except Exception as e:
|
|
1063
|
+
logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
|
|
1064
|
+
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
# Check if this is a rate limit error - don't fall back as it would make things worse
|
|
1067
|
+
error_str = str(e).lower()
|
|
1068
|
+
is_rate_limit = any(
|
|
1069
|
+
phrase in error_str
|
|
1070
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
if is_rate_limit:
|
|
1074
|
+
logger.error(f"Rate limit detected during batch embedding. {e}")
|
|
1075
|
+
raise e
|
|
1076
|
+
else:
|
|
1077
|
+
logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
|
|
1078
|
+
# Fall back to individual embedding
|
|
1079
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
1080
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
1081
|
+
else:
|
|
1082
|
+
# Use individual embedding
|
|
1083
|
+
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
1084
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
1023
1085
|
|
|
1024
1086
|
prepared_docs = []
|
|
1025
1087
|
for document in documents:
|
|
@@ -1047,8 +1109,44 @@ class MongoDb(VectorDb):
|
|
|
1047
1109
|
log_info(f"Upserting {len(documents)} documents asynchronously")
|
|
1048
1110
|
collection = await self._get_async_collection()
|
|
1049
1111
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1112
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
1113
|
+
# Use batch embedding when enabled and supported
|
|
1114
|
+
try:
|
|
1115
|
+
# Extract content from all documents
|
|
1116
|
+
doc_contents = [doc.content for doc in documents]
|
|
1117
|
+
|
|
1118
|
+
# Get batch embeddings and usage
|
|
1119
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
1120
|
+
|
|
1121
|
+
# Process documents with pre-computed embeddings
|
|
1122
|
+
for j, doc in enumerate(documents):
|
|
1123
|
+
try:
|
|
1124
|
+
if j < len(embeddings):
|
|
1125
|
+
doc.embedding = embeddings[j]
|
|
1126
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
1127
|
+
except Exception as e:
|
|
1128
|
+
logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
|
|
1129
|
+
|
|
1130
|
+
except Exception as e:
|
|
1131
|
+
# Check if this is a rate limit error - don't fall back as it would make things worse
|
|
1132
|
+
error_str = str(e).lower()
|
|
1133
|
+
is_rate_limit = any(
|
|
1134
|
+
phrase in error_str
|
|
1135
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
if is_rate_limit:
|
|
1139
|
+
logger.error(f"Rate limit detected during batch embedding. {e}")
|
|
1140
|
+
raise e
|
|
1141
|
+
else:
|
|
1142
|
+
logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
|
|
1143
|
+
# Fall back to individual embedding
|
|
1144
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
1145
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
1146
|
+
else:
|
|
1147
|
+
# Use individual embedding
|
|
1148
|
+
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
1149
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
1052
1150
|
|
|
1053
1151
|
for document in documents:
|
|
1054
1152
|
try:
|
|
@@ -1063,9 +1161,12 @@ class MongoDb(VectorDb):
|
|
|
1063
1161
|
logger.error(f"Error upserting document '{document.name}' asynchronously: {e}")
|
|
1064
1162
|
|
|
1065
1163
|
async def async_search(
|
|
1066
|
-
self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None
|
|
1164
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
1067
1165
|
) -> List[Document]:
|
|
1068
1166
|
"""Search for documents asynchronously."""
|
|
1167
|
+
if isinstance(filters, List):
|
|
1168
|
+
log_warning("Filters Expressions are not supported in MongoDB. No filters will be applied.")
|
|
1169
|
+
filters = None
|
|
1069
1170
|
query_embedding = self.embedder.get_embedding(query)
|
|
1070
1171
|
if query_embedding is None:
|
|
1071
1172
|
logger.error(f"Failed to generate embedding for query: {query}")
|
|
@@ -1310,3 +1411,7 @@ class MongoDb(VectorDb):
|
|
|
1310
1411
|
except Exception as e:
|
|
1311
1412
|
logger.error(f"Error updating metadata for content_id '{content_id}': {e}")
|
|
1312
1413
|
raise
|
|
1414
|
+
|
|
1415
|
+
def get_supported_search_types(self) -> List[str]:
|
|
1416
|
+
"""Get the supported search types for this vector database."""
|
|
1417
|
+
return [SearchType.vector, SearchType.hybrid]
|
|
@@ -3,15 +3,18 @@ from hashlib import md5
|
|
|
3
3
|
from math import sqrt
|
|
4
4
|
from typing import Any, Dict, List, Optional, Union, cast
|
|
5
5
|
|
|
6
|
+
from agno.utils.string import generate_id
|
|
7
|
+
|
|
6
8
|
try:
|
|
7
|
-
from sqlalchemy import update
|
|
9
|
+
from sqlalchemy import and_, not_, or_, update
|
|
8
10
|
from sqlalchemy.dialects import postgresql
|
|
9
11
|
from sqlalchemy.engine import Engine, create_engine
|
|
10
12
|
from sqlalchemy.inspection import inspect
|
|
11
13
|
from sqlalchemy.orm import Session, scoped_session, sessionmaker
|
|
12
14
|
from sqlalchemy.schema import Column, Index, MetaData, Table
|
|
15
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
13
16
|
from sqlalchemy.sql.expression import bindparam, desc, func, select, text
|
|
14
|
-
from sqlalchemy.types import DateTime, String
|
|
17
|
+
from sqlalchemy.types import DateTime, Integer, String
|
|
15
18
|
|
|
16
19
|
except ImportError:
|
|
17
20
|
raise ImportError("`sqlalchemy` not installed. Please install using `pip install sqlalchemy psycopg`")
|
|
@@ -21,6 +24,7 @@ try:
|
|
|
21
24
|
except ImportError:
|
|
22
25
|
raise ImportError("`pgvector` not installed. Please install using `pip install pgvector`")
|
|
23
26
|
|
|
27
|
+
from agno.filters import FilterExpr
|
|
24
28
|
from agno.knowledge.document import Document
|
|
25
29
|
from agno.knowledge.embedder import Embedder
|
|
26
30
|
from agno.knowledge.reranker.base import Reranker
|
|
@@ -43,6 +47,9 @@ class PgVector(VectorDb):
|
|
|
43
47
|
self,
|
|
44
48
|
table_name: str,
|
|
45
49
|
schema: str = "ai",
|
|
50
|
+
name: Optional[str] = None,
|
|
51
|
+
description: Optional[str] = None,
|
|
52
|
+
id: Optional[str] = None,
|
|
46
53
|
db_url: Optional[str] = None,
|
|
47
54
|
db_engine: Optional[Engine] = None,
|
|
48
55
|
embedder: Optional[Embedder] = None,
|
|
@@ -55,7 +62,6 @@ class PgVector(VectorDb):
|
|
|
55
62
|
schema_version: int = 1,
|
|
56
63
|
auto_upgrade_schema: bool = False,
|
|
57
64
|
reranker: Optional[Reranker] = None,
|
|
58
|
-
use_batch: bool = False,
|
|
59
65
|
):
|
|
60
66
|
"""
|
|
61
67
|
Initialize the PgVector instance.
|
|
@@ -63,6 +69,8 @@ class PgVector(VectorDb):
|
|
|
63
69
|
Args:
|
|
64
70
|
table_name (str): Name of the table to store vector data.
|
|
65
71
|
schema (str): Database schema name.
|
|
72
|
+
name (Optional[str]): Name of the vector database.
|
|
73
|
+
description (Optional[str]): Description of the vector database.
|
|
66
74
|
db_url (Optional[str]): Database connection URL.
|
|
67
75
|
db_engine (Optional[Engine]): SQLAlchemy database engine.
|
|
68
76
|
embedder (Optional[Embedder]): Embedder instance for creating embeddings.
|
|
@@ -81,6 +89,15 @@ class PgVector(VectorDb):
|
|
|
81
89
|
if db_engine is None and db_url is None:
|
|
82
90
|
raise ValueError("Either 'db_url' or 'db_engine' must be provided.")
|
|
83
91
|
|
|
92
|
+
if id is None:
|
|
93
|
+
base_seed = db_url or str(db_engine.url) # type: ignore
|
|
94
|
+
schema_suffix = table_name if table_name is not None else "ai"
|
|
95
|
+
seed = f"{base_seed}#{schema_suffix}"
|
|
96
|
+
id = generate_id(seed)
|
|
97
|
+
|
|
98
|
+
# Initialize base class with name and description
|
|
99
|
+
super().__init__(id=id, name=name, description=description)
|
|
100
|
+
|
|
84
101
|
if db_engine is None:
|
|
85
102
|
if db_url is None:
|
|
86
103
|
raise ValueError("Must provide 'db_url' if 'db_engine' is None.")
|
|
@@ -96,7 +113,6 @@ class PgVector(VectorDb):
|
|
|
96
113
|
self.db_url: Optional[str] = db_url
|
|
97
114
|
self.db_engine: Engine = db_engine
|
|
98
115
|
self.metadata: MetaData = MetaData(schema=self.schema)
|
|
99
|
-
self.use_batch: bool = use_batch
|
|
100
116
|
|
|
101
117
|
# Embedder for embedding the document contents
|
|
102
118
|
if embedder is None:
|
|
@@ -337,8 +353,8 @@ class PgVector(VectorDb):
|
|
|
337
353
|
batch_docs = documents[i : i + batch_size]
|
|
338
354
|
log_debug(f"Processing batch starting at index {i}, size: {len(batch_docs)}")
|
|
339
355
|
try:
|
|
340
|
-
|
|
341
|
-
await
|
|
356
|
+
# Embed all documents in the batch
|
|
357
|
+
await self._async_embed_documents(batch_docs)
|
|
342
358
|
|
|
343
359
|
# Prepare documents for insertion
|
|
344
360
|
batch_records = []
|
|
@@ -493,6 +509,52 @@ class PgVector(VectorDb):
|
|
|
493
509
|
"content_id": doc.content_id,
|
|
494
510
|
}
|
|
495
511
|
|
|
512
|
+
async def _async_embed_documents(self, batch_docs: List[Document]) -> None:
|
|
513
|
+
"""
|
|
514
|
+
Embed a batch of documents using either batch embedding or individual embedding.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
batch_docs: List of documents to embed
|
|
518
|
+
"""
|
|
519
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
520
|
+
# Use batch embedding when enabled and supported
|
|
521
|
+
try:
|
|
522
|
+
# Extract content from all documents
|
|
523
|
+
doc_contents = [doc.content for doc in batch_docs]
|
|
524
|
+
|
|
525
|
+
# Get batch embeddings and usage
|
|
526
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
527
|
+
|
|
528
|
+
# Process documents with pre-computed embeddings
|
|
529
|
+
for j, doc in enumerate(batch_docs):
|
|
530
|
+
try:
|
|
531
|
+
if j < len(embeddings):
|
|
532
|
+
doc.embedding = embeddings[j]
|
|
533
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
534
|
+
except Exception as e:
|
|
535
|
+
logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
# Check if this is a rate limit error - don't fall back as it would make things worse
|
|
539
|
+
error_str = str(e).lower()
|
|
540
|
+
is_rate_limit = any(
|
|
541
|
+
phrase in error_str
|
|
542
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
if is_rate_limit:
|
|
546
|
+
logger.error(f"Rate limit detected during batch embedding. {e}")
|
|
547
|
+
raise e
|
|
548
|
+
else:
|
|
549
|
+
logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
|
|
550
|
+
# Fall back to individual embedding
|
|
551
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in batch_docs]
|
|
552
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
553
|
+
else:
|
|
554
|
+
# Use individual embedding
|
|
555
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in batch_docs]
|
|
556
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
557
|
+
|
|
496
558
|
async def async_upsert(
|
|
497
559
|
self,
|
|
498
560
|
content_hash: str,
|
|
@@ -530,8 +592,8 @@ class PgVector(VectorDb):
|
|
|
530
592
|
batch_docs = documents[i : i + batch_size]
|
|
531
593
|
log_info(f"Processing batch starting at index {i}, size: {len(batch_docs)}")
|
|
532
594
|
try:
|
|
533
|
-
|
|
534
|
-
await
|
|
595
|
+
# Embed all documents in the batch
|
|
596
|
+
await self._async_embed_documents(batch_docs)
|
|
535
597
|
|
|
536
598
|
# Prepare documents for upserting
|
|
537
599
|
batch_records_dict = {} # Use dict to deduplicate by ID
|
|
@@ -620,14 +682,16 @@ class PgVector(VectorDb):
|
|
|
620
682
|
logger.error(f"Error updating metadata for document {content_id}: {e}")
|
|
621
683
|
raise
|
|
622
684
|
|
|
623
|
-
def search(
|
|
685
|
+
def search(
|
|
686
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
687
|
+
) -> List[Document]:
|
|
624
688
|
"""
|
|
625
689
|
Perform a search based on the configured search type.
|
|
626
690
|
|
|
627
691
|
Args:
|
|
628
692
|
query (str): The search query.
|
|
629
693
|
limit (int): Maximum number of results to return.
|
|
630
|
-
filters (Optional[Dict[str, Any]]): Filters to apply to the search.
|
|
694
|
+
filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
|
|
631
695
|
|
|
632
696
|
Returns:
|
|
633
697
|
List[Document]: List of matching documents.
|
|
@@ -643,19 +707,42 @@ class PgVector(VectorDb):
|
|
|
643
707
|
return []
|
|
644
708
|
|
|
645
709
|
async def async_search(
|
|
646
|
-
self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None
|
|
710
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
647
711
|
) -> List[Document]:
|
|
648
712
|
"""Search asynchronously by running in a thread."""
|
|
649
713
|
return await asyncio.to_thread(self.search, query, limit, filters)
|
|
650
714
|
|
|
651
|
-
def
|
|
715
|
+
def _dsl_to_sqlalchemy(self, filter_expr, table) -> ColumnElement[bool]:
|
|
716
|
+
op = filter_expr["op"]
|
|
717
|
+
|
|
718
|
+
if op == "EQ":
|
|
719
|
+
return table.c.meta_data[filter_expr["key"]].astext == str(filter_expr["value"])
|
|
720
|
+
elif op == "IN":
|
|
721
|
+
# Postgres JSONB array containment
|
|
722
|
+
return table.c.meta_data[filter_expr["key"]].astext.in_([str(v) for v in filter_expr["values"]])
|
|
723
|
+
elif op == "GT":
|
|
724
|
+
return table.c.meta_data[filter_expr["key"]].astext.cast(Integer) > filter_expr["value"]
|
|
725
|
+
elif op == "LT":
|
|
726
|
+
return table.c.meta_data[filter_expr["key"]].astext.cast(Integer) < filter_expr["value"]
|
|
727
|
+
elif op == "NOT":
|
|
728
|
+
return not_(self._dsl_to_sqlalchemy(filter_expr["condition"], table))
|
|
729
|
+
elif op == "AND":
|
|
730
|
+
return and_(*[self._dsl_to_sqlalchemy(cond, table) for cond in filter_expr["conditions"]])
|
|
731
|
+
elif op == "OR":
|
|
732
|
+
return or_(*[self._dsl_to_sqlalchemy(cond, table) for cond in filter_expr["conditions"]])
|
|
733
|
+
else:
|
|
734
|
+
raise ValueError(f"Unknown filter operator: {op}")
|
|
735
|
+
|
|
736
|
+
def vector_search(
|
|
737
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
738
|
+
) -> List[Document]:
|
|
652
739
|
"""
|
|
653
740
|
Perform a vector similarity search.
|
|
654
741
|
|
|
655
742
|
Args:
|
|
656
743
|
query (str): The search query.
|
|
657
744
|
limit (int): Maximum number of results to return.
|
|
658
|
-
filters (Optional[Dict[str, Any]]): Filters to apply to the search.
|
|
745
|
+
filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
|
|
659
746
|
|
|
660
747
|
Returns:
|
|
661
748
|
List[Document]: List of matching documents.
|
|
@@ -682,7 +769,17 @@ class PgVector(VectorDb):
|
|
|
682
769
|
|
|
683
770
|
# Apply filters if provided
|
|
684
771
|
if filters is not None:
|
|
685
|
-
|
|
772
|
+
# Handle dict filters
|
|
773
|
+
if isinstance(filters, dict):
|
|
774
|
+
stmt = stmt.where(self.table.c.meta_data.contains(filters))
|
|
775
|
+
# Handle FilterExpr DSL
|
|
776
|
+
else:
|
|
777
|
+
# Convert each DSL expression to SQLAlchemy and AND them together
|
|
778
|
+
sqlalchemy_conditions = [
|
|
779
|
+
self._dsl_to_sqlalchemy(f.to_dict() if hasattr(f, "to_dict") else f, self.table)
|
|
780
|
+
for f in filters
|
|
781
|
+
]
|
|
782
|
+
stmt = stmt.where(and_(*sqlalchemy_conditions))
|
|
686
783
|
|
|
687
784
|
# Order the results based on the distance metric
|
|
688
785
|
if self.distance == Distance.l2:
|
|
@@ -755,14 +852,16 @@ class PgVector(VectorDb):
|
|
|
755
852
|
processed_words = [word + "*" for word in words]
|
|
756
853
|
return " ".join(processed_words)
|
|
757
854
|
|
|
758
|
-
def keyword_search(
|
|
855
|
+
def keyword_search(
|
|
856
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
857
|
+
) -> List[Document]:
|
|
759
858
|
"""
|
|
760
859
|
Perform a keyword search on the 'content' column.
|
|
761
860
|
|
|
762
861
|
Args:
|
|
763
862
|
query (str): The search query.
|
|
764
863
|
limit (int): Maximum number of results to return.
|
|
765
|
-
filters (Optional[Dict[str, Any]]): Filters to apply to the search.
|
|
864
|
+
filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
|
|
766
865
|
|
|
767
866
|
Returns:
|
|
768
867
|
List[Document]: List of matching documents.
|
|
@@ -791,8 +890,17 @@ class PgVector(VectorDb):
|
|
|
791
890
|
|
|
792
891
|
# Apply filters if provided
|
|
793
892
|
if filters is not None:
|
|
794
|
-
#
|
|
795
|
-
|
|
893
|
+
# Handle dict filters
|
|
894
|
+
if isinstance(filters, dict):
|
|
895
|
+
stmt = stmt.where(self.table.c.meta_data.contains(filters))
|
|
896
|
+
# Handle FilterExpr DSL
|
|
897
|
+
else:
|
|
898
|
+
# Convert each DSL expression to SQLAlchemy and AND them together
|
|
899
|
+
sqlalchemy_conditions = [
|
|
900
|
+
self._dsl_to_sqlalchemy(f.to_dict() if hasattr(f, "to_dict") else f, self.table)
|
|
901
|
+
for f in filters
|
|
902
|
+
]
|
|
903
|
+
stmt = stmt.where(and_(*sqlalchemy_conditions))
|
|
796
904
|
|
|
797
905
|
# Order by the relevance rank
|
|
798
906
|
stmt = stmt.order_by(text_rank.desc())
|
|
@@ -838,7 +946,7 @@ class PgVector(VectorDb):
|
|
|
838
946
|
self,
|
|
839
947
|
query: str,
|
|
840
948
|
limit: int = 5,
|
|
841
|
-
filters: Optional[Dict[str, Any]] = None,
|
|
949
|
+
filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
842
950
|
) -> List[Document]:
|
|
843
951
|
"""
|
|
844
952
|
Perform a hybrid search combining vector similarity and full-text search.
|
|
@@ -846,7 +954,7 @@ class PgVector(VectorDb):
|
|
|
846
954
|
Args:
|
|
847
955
|
query (str): The search query.
|
|
848
956
|
limit (int): Maximum number of results to return.
|
|
849
|
-
filters (Optional[Dict[str, Any]]): Filters to apply to the search.
|
|
957
|
+
filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
|
|
850
958
|
|
|
851
959
|
Returns:
|
|
852
960
|
List[Document]: List of matching documents.
|
|
@@ -913,7 +1021,17 @@ class PgVector(VectorDb):
|
|
|
913
1021
|
|
|
914
1022
|
# Apply filters if provided
|
|
915
1023
|
if filters is not None:
|
|
916
|
-
|
|
1024
|
+
# Handle dict filters
|
|
1025
|
+
if isinstance(filters, dict):
|
|
1026
|
+
stmt = stmt.where(self.table.c.meta_data.contains(filters))
|
|
1027
|
+
# Handle FilterExpr DSL
|
|
1028
|
+
else:
|
|
1029
|
+
# Convert each DSL expression to SQLAlchemy and AND them together
|
|
1030
|
+
sqlalchemy_conditions = [
|
|
1031
|
+
self._dsl_to_sqlalchemy(f.to_dict() if hasattr(f, "to_dict") else f, self.table)
|
|
1032
|
+
for f in filters
|
|
1033
|
+
]
|
|
1034
|
+
stmt = stmt.where(and_(*sqlalchemy_conditions))
|
|
917
1035
|
|
|
918
1036
|
# Order the results by the hybrid score in descending order
|
|
919
1037
|
stmt = stmt.order_by(desc("hybrid_score"))
|
|
@@ -1339,3 +1457,6 @@ class PgVector(VectorDb):
|
|
|
1339
1457
|
copied_obj.table = copied_obj.get_table()
|
|
1340
1458
|
|
|
1341
1459
|
return copied_obj
|
|
1460
|
+
|
|
1461
|
+
def get_supported_search_types(self) -> List[str]:
|
|
1462
|
+
return [SearchType.vector, SearchType.keyword, SearchType.hybrid]
|
|
@@ -22,6 +22,7 @@ except ImportError:
|
|
|
22
22
|
raise ImportError("The `pinecone` package is not installed, please install using `pip install pinecone`.")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
from agno.filters import FilterExpr
|
|
25
26
|
from agno.knowledge.document import Document
|
|
26
27
|
from agno.knowledge.embedder import Embedder
|
|
27
28
|
from agno.knowledge.reranker.base import Reranker
|
|
@@ -66,9 +67,11 @@ class PineconeDb(VectorDb):
|
|
|
66
67
|
|
|
67
68
|
def __init__(
|
|
68
69
|
self,
|
|
69
|
-
name: str,
|
|
70
70
|
dimension: int,
|
|
71
71
|
spec: Union[Dict, ServerlessSpec, PodSpec],
|
|
72
|
+
name: Optional[str] = None,
|
|
73
|
+
description: Optional[str] = None,
|
|
74
|
+
id: Optional[str] = None,
|
|
72
75
|
embedder: Optional[Embedder] = None,
|
|
73
76
|
metric: Optional[str] = "cosine",
|
|
74
77
|
additional_headers: Optional[Dict[str, str]] = None,
|
|
@@ -84,6 +87,23 @@ class PineconeDb(VectorDb):
|
|
|
84
87
|
reranker: Optional[Reranker] = None,
|
|
85
88
|
**kwargs,
|
|
86
89
|
):
|
|
90
|
+
# Validate required parameters
|
|
91
|
+
if dimension is None or dimension <= 0:
|
|
92
|
+
raise ValueError("Dimension must be provided and greater than 0.")
|
|
93
|
+
if spec is None:
|
|
94
|
+
raise ValueError("Spec must be provided for Pinecone index.")
|
|
95
|
+
|
|
96
|
+
# Dynamic ID generation based on unique identifiers
|
|
97
|
+
if id is None:
|
|
98
|
+
from agno.utils.string import generate_id
|
|
99
|
+
|
|
100
|
+
index_name = name or "default_index"
|
|
101
|
+
seed = f"{host or 'pinecone'}#{index_name}#{dimension}"
|
|
102
|
+
id = generate_id(seed)
|
|
103
|
+
|
|
104
|
+
# Initialize base class with name, description, and generated ID
|
|
105
|
+
super().__init__(id=id, name=name, description=description)
|
|
106
|
+
|
|
87
107
|
self._client = None
|
|
88
108
|
self._index = None
|
|
89
109
|
self.api_key: Optional[str] = api_key
|
|
@@ -93,7 +113,6 @@ class PineconeDb(VectorDb):
|
|
|
93
113
|
self.pool_threads: Optional[int] = pool_threads
|
|
94
114
|
self.namespace: Optional[str] = namespace
|
|
95
115
|
self.index_api: Optional[Any] = index_api
|
|
96
|
-
self.name: str = name
|
|
97
116
|
self.dimension: Optional[int] = dimension
|
|
98
117
|
self.spec: Union[Dict, ServerlessSpec, PodSpec] = spec
|
|
99
118
|
self.metric: Optional[str] = metric
|
|
@@ -307,6 +326,8 @@ class PineconeDb(VectorDb):
|
|
|
307
326
|
show_progress: bool = False,
|
|
308
327
|
) -> None:
|
|
309
328
|
"""Upsert documents into the index asynchronously with batching."""
|
|
329
|
+
if self.content_hash_exists(content_hash):
|
|
330
|
+
await asyncio.to_thread(self._delete_by_content_hash, content_hash)
|
|
310
331
|
if not documents:
|
|
311
332
|
return
|
|
312
333
|
|
|
@@ -320,7 +341,7 @@ class PineconeDb(VectorDb):
|
|
|
320
341
|
|
|
321
342
|
# Process each batch in parallel
|
|
322
343
|
async def process_batch(batch_docs):
|
|
323
|
-
return await self._prepare_vectors(batch_docs)
|
|
344
|
+
return await self._prepare_vectors(batch_docs, content_hash, filters)
|
|
324
345
|
|
|
325
346
|
# Run all batches in parallel
|
|
326
347
|
batch_vectors = await asyncio.gather(*[process_batch(batch) for batch in batches])
|
|
@@ -335,21 +356,65 @@ class PineconeDb(VectorDb):
|
|
|
335
356
|
|
|
336
357
|
log_debug(f"Finished async upsert of {len(documents)} documents")
|
|
337
358
|
|
|
338
|
-
async def _prepare_vectors(
|
|
359
|
+
async def _prepare_vectors(
|
|
360
|
+
self, documents: List[Document], content_hash: str, filters: Optional[Dict[str, Any]] = None
|
|
361
|
+
) -> List[Dict[str, Any]]:
|
|
339
362
|
"""Prepare vectors for upsert."""
|
|
340
363
|
vectors = []
|
|
341
|
-
|
|
342
|
-
|
|
364
|
+
|
|
365
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
366
|
+
# Use batch embedding when enabled and supported
|
|
367
|
+
try:
|
|
368
|
+
# Extract content from all documents
|
|
369
|
+
doc_contents = [doc.content for doc in documents]
|
|
370
|
+
|
|
371
|
+
# Get batch embeddings and usage
|
|
372
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
373
|
+
|
|
374
|
+
# Process documents with pre-computed embeddings
|
|
375
|
+
for j, doc in enumerate(documents):
|
|
376
|
+
try:
|
|
377
|
+
if j < len(embeddings):
|
|
378
|
+
doc.embedding = embeddings[j]
|
|
379
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
# Check if this is a rate limit error - don't fall back as it would make things worse
|
|
385
|
+
error_str = str(e).lower()
|
|
386
|
+
is_rate_limit = any(
|
|
387
|
+
phrase in error_str
|
|
388
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if is_rate_limit:
|
|
392
|
+
logger.error(f"Rate limit detected during batch embedding. {e}")
|
|
393
|
+
raise e
|
|
394
|
+
else:
|
|
395
|
+
logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
|
|
396
|
+
# Fall back to individual embedding
|
|
397
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
398
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
399
|
+
else:
|
|
400
|
+
# Use individual embedding
|
|
401
|
+
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
402
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
343
403
|
|
|
344
404
|
for doc in documents:
|
|
345
405
|
doc.meta_data["text"] = doc.content
|
|
346
406
|
# Include name and content_id in metadata
|
|
347
407
|
metadata = doc.meta_data.copy()
|
|
408
|
+
if filters:
|
|
409
|
+
metadata.update(filters)
|
|
410
|
+
|
|
348
411
|
if doc.name:
|
|
349
412
|
metadata["name"] = doc.name
|
|
350
413
|
if doc.content_id:
|
|
351
414
|
metadata["content_id"] = doc.content_id
|
|
352
415
|
|
|
416
|
+
metadata["content_hash"] = content_hash
|
|
417
|
+
|
|
353
418
|
data_to_upsert = {
|
|
354
419
|
"id": doc.id,
|
|
355
420
|
"values": doc.embedding,
|
|
@@ -410,7 +475,7 @@ class PineconeDb(VectorDb):
|
|
|
410
475
|
self,
|
|
411
476
|
query: str,
|
|
412
477
|
limit: int = 5,
|
|
413
|
-
filters: Optional[Dict[str,
|
|
478
|
+
filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
414
479
|
namespace: Optional[str] = None,
|
|
415
480
|
include_values: Optional[bool] = None,
|
|
416
481
|
) -> List[Document]:
|
|
@@ -428,6 +493,9 @@ class PineconeDb(VectorDb):
|
|
|
428
493
|
List[Document]: The list of matching documents.
|
|
429
494
|
|
|
430
495
|
"""
|
|
496
|
+
if isinstance(filters, List):
|
|
497
|
+
log_warning("Filters Expressions are not supported in PineconeDB. No filters will be applied.")
|
|
498
|
+
filters = None
|
|
431
499
|
dense_embedding = self.embedder.get_embedding(query)
|
|
432
500
|
|
|
433
501
|
if self.use_hybrid_search:
|
|
@@ -476,7 +544,7 @@ class PineconeDb(VectorDb):
|
|
|
476
544
|
self,
|
|
477
545
|
query: str,
|
|
478
546
|
limit: int = 5,
|
|
479
|
-
filters: Optional[Dict[str,
|
|
547
|
+
filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
|
|
480
548
|
namespace: Optional[str] = None,
|
|
481
549
|
include_values: Optional[bool] = None,
|
|
482
550
|
) -> List[Document]:
|
|
@@ -673,3 +741,7 @@ class PineconeDb(VectorDb):
|
|
|
673
741
|
except Exception as e:
|
|
674
742
|
logger.error(f"Error updating metadata for content_id '{content_id}': {e}")
|
|
675
743
|
raise
|
|
744
|
+
|
|
745
|
+
def get_supported_search_types(self) -> List[str]:
|
|
746
|
+
"""Get the supported search types for this vector database."""
|
|
747
|
+
return [] # PineconeDb doesn't use SearchType enum
|