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
|
@@ -3,10 +3,10 @@ import time
|
|
|
3
3
|
from datetime import timedelta
|
|
4
4
|
from typing import Any, Dict, List, Optional, Union
|
|
5
5
|
|
|
6
|
+
from agno.filters import FilterExpr
|
|
6
7
|
from agno.knowledge.document import Document
|
|
7
8
|
from agno.knowledge.embedder import Embedder
|
|
8
|
-
from agno.
|
|
9
|
-
from agno.utils.log import log_debug, log_info, logger
|
|
9
|
+
from agno.utils.log import log_debug, log_info, log_warning, logger
|
|
10
10
|
from agno.vectordb.base import VectorDb
|
|
11
11
|
|
|
12
12
|
try:
|
|
@@ -61,11 +61,13 @@ class CouchbaseSearch(VectorDb):
|
|
|
61
61
|
couchbase_connection_string: str,
|
|
62
62
|
cluster_options: ClusterOptions,
|
|
63
63
|
search_index: Union[str, SearchIndex],
|
|
64
|
-
embedder: Embedder =
|
|
64
|
+
embedder: Optional[Embedder] = None,
|
|
65
65
|
overwrite: bool = False,
|
|
66
66
|
is_global_level_index: bool = False,
|
|
67
67
|
wait_until_index_ready: float = 0,
|
|
68
68
|
batch_limit: int = 500,
|
|
69
|
+
name: Optional[str] = None,
|
|
70
|
+
description: Optional[str] = None,
|
|
69
71
|
**kwargs,
|
|
70
72
|
):
|
|
71
73
|
"""
|
|
@@ -75,6 +77,8 @@ class CouchbaseSearch(VectorDb):
|
|
|
75
77
|
bucket_name (str): Name of the Couchbase bucket.
|
|
76
78
|
scope_name (str): Name of the scope within the bucket.
|
|
77
79
|
collection_name (str): Name of the collection within the scope.
|
|
80
|
+
name (Optional[str]): Name of the vector database.
|
|
81
|
+
description (Optional[str]): Description of the vector database.
|
|
78
82
|
couchbase_connection_string (str): Couchbase connection string.
|
|
79
83
|
cluster_options (ClusterOptions): Options for configuring the Couchbase cluster connection.
|
|
80
84
|
search_index (Union[str, SearchIndex], optional): Search index configuration, either as index name or SearchIndex definition.
|
|
@@ -92,10 +96,18 @@ class CouchbaseSearch(VectorDb):
|
|
|
92
96
|
self.collection_name = collection_name
|
|
93
97
|
self.connection_string = couchbase_connection_string
|
|
94
98
|
self.cluster_options = cluster_options
|
|
99
|
+
if embedder is None:
|
|
100
|
+
from agno.knowledge.embedder.openai import OpenAIEmbedder
|
|
101
|
+
|
|
102
|
+
embedder = OpenAIEmbedder()
|
|
103
|
+
log_info("Embedder not provided, using OpenAIEmbedder as default.")
|
|
95
104
|
self.embedder = embedder
|
|
96
105
|
self.overwrite = overwrite
|
|
97
106
|
self.is_global_level_index = is_global_level_index
|
|
98
107
|
self.wait_until_index_ready = wait_until_index_ready
|
|
108
|
+
# Initialize base class with name and description
|
|
109
|
+
super().__init__(name=name, description=description)
|
|
110
|
+
|
|
99
111
|
self.kwargs = kwargs
|
|
100
112
|
self.batch_limit = batch_limit
|
|
101
113
|
if isinstance(search_index, str):
|
|
@@ -451,7 +463,12 @@ class CouchbaseSearch(VectorDb):
|
|
|
451
463
|
if errors_occurred:
|
|
452
464
|
logger.warning("Some errors occurred during the upsert operation. Please check logs for details.")
|
|
453
465
|
|
|
454
|
-
def search(
|
|
466
|
+
def search(
|
|
467
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
468
|
+
) -> List[Document]:
|
|
469
|
+
if isinstance(filters, List):
|
|
470
|
+
log_warning("Filter Expressions are not yet supported in Couchbase. No filters will be applied.")
|
|
471
|
+
filters = None
|
|
455
472
|
"""Search the Couchbase bucket for documents relevant to the query."""
|
|
456
473
|
query_embedding = self.embedder.get_embedding(query)
|
|
457
474
|
if query_embedding is None:
|
|
@@ -871,8 +888,44 @@ class CouchbaseSearch(VectorDb):
|
|
|
871
888
|
async_collection_instance = await self.get_async_collection()
|
|
872
889
|
all_docs_to_insert: Dict[str, Any] = {}
|
|
873
890
|
|
|
874
|
-
|
|
875
|
-
|
|
891
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
892
|
+
# Use batch embedding when enabled and supported
|
|
893
|
+
try:
|
|
894
|
+
# Extract content from all documents
|
|
895
|
+
doc_contents = [doc.content for doc in documents]
|
|
896
|
+
|
|
897
|
+
# Get batch embeddings and usage
|
|
898
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
899
|
+
|
|
900
|
+
# Process documents with pre-computed embeddings
|
|
901
|
+
for j, doc in enumerate(documents):
|
|
902
|
+
try:
|
|
903
|
+
if j < len(embeddings):
|
|
904
|
+
doc.embedding = embeddings[j]
|
|
905
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
906
|
+
except Exception as e:
|
|
907
|
+
logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
|
|
908
|
+
|
|
909
|
+
except Exception as e:
|
|
910
|
+
# Check if this is a rate limit error - don't fall back as it would make things worse
|
|
911
|
+
error_str = str(e).lower()
|
|
912
|
+
is_rate_limit = any(
|
|
913
|
+
phrase in error_str
|
|
914
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
if is_rate_limit:
|
|
918
|
+
logger.error(f"Rate limit detected during batch embedding. {e}")
|
|
919
|
+
raise e
|
|
920
|
+
else:
|
|
921
|
+
logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
|
|
922
|
+
# Fall back to individual embedding
|
|
923
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
924
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
925
|
+
else:
|
|
926
|
+
# Use individual embedding
|
|
927
|
+
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
928
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
876
929
|
|
|
877
930
|
for document in documents:
|
|
878
931
|
try:
|
|
@@ -937,8 +990,44 @@ class CouchbaseSearch(VectorDb):
|
|
|
937
990
|
async_collection_instance = await self.get_async_collection()
|
|
938
991
|
all_docs_to_upsert: Dict[str, Any] = {}
|
|
939
992
|
|
|
940
|
-
|
|
941
|
-
|
|
993
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
994
|
+
# Use batch embedding when enabled and supported
|
|
995
|
+
try:
|
|
996
|
+
# Extract content from all documents
|
|
997
|
+
doc_contents = [doc.content for doc in documents]
|
|
998
|
+
|
|
999
|
+
# Get batch embeddings and usage
|
|
1000
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
1001
|
+
|
|
1002
|
+
# Process documents with pre-computed embeddings
|
|
1003
|
+
for j, doc in enumerate(documents):
|
|
1004
|
+
try:
|
|
1005
|
+
if j < len(embeddings):
|
|
1006
|
+
doc.embedding = embeddings[j]
|
|
1007
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
1008
|
+
except Exception as e:
|
|
1009
|
+
logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
|
|
1010
|
+
|
|
1011
|
+
except Exception as e:
|
|
1012
|
+
# Check if this is a rate limit error - don't fall back as it would make things worse
|
|
1013
|
+
error_str = str(e).lower()
|
|
1014
|
+
is_rate_limit = any(
|
|
1015
|
+
phrase in error_str
|
|
1016
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
if is_rate_limit:
|
|
1020
|
+
logger.error(f"Rate limit detected during batch embedding. {e}")
|
|
1021
|
+
raise e
|
|
1022
|
+
else:
|
|
1023
|
+
logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
|
|
1024
|
+
# Fall back to individual embedding
|
|
1025
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
1026
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
1027
|
+
else:
|
|
1028
|
+
# Use individual embedding
|
|
1029
|
+
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
1030
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
942
1031
|
|
|
943
1032
|
for document in documents:
|
|
944
1033
|
try:
|
|
@@ -989,8 +1078,11 @@ class CouchbaseSearch(VectorDb):
|
|
|
989
1078
|
logger.info(f"[async] Total successfully upserted: {total_upserted_count}, Total failed: {total_failed_count}.")
|
|
990
1079
|
|
|
991
1080
|
async def async_search(
|
|
992
|
-
self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None
|
|
1081
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
993
1082
|
) -> List[Document]:
|
|
1083
|
+
if isinstance(filters, List):
|
|
1084
|
+
log_warning("Filter Expressions are not yet supported in Couchbase. No filters will be applied.")
|
|
1085
|
+
filters = None
|
|
994
1086
|
query_embedding = self.embedder.get_embedding(query)
|
|
995
1087
|
if query_embedding is None:
|
|
996
1088
|
logger.error(f"[async] Failed to generate embedding for query: {query}")
|
|
@@ -1225,7 +1317,6 @@ class CouchbaseSearch(VectorDb):
|
|
|
1225
1317
|
rows = list(result.rows()) # Collect once
|
|
1226
1318
|
|
|
1227
1319
|
for row in rows:
|
|
1228
|
-
print(row)
|
|
1229
1320
|
self.collection.remove(row.get("doc_id"))
|
|
1230
1321
|
log_info(f"Deleted {len(rows)} documents with metadata {metadata}")
|
|
1231
1322
|
return True
|
|
@@ -1349,3 +1440,7 @@ class CouchbaseSearch(VectorDb):
|
|
|
1349
1440
|
except Exception as e:
|
|
1350
1441
|
logger.error(f"Error updating metadata for content_id '{content_id}': {e}")
|
|
1351
1442
|
raise
|
|
1443
|
+
|
|
1444
|
+
def get_supported_search_types(self) -> List[str]:
|
|
1445
|
+
"""Get the supported search types for this vector database."""
|
|
1446
|
+
return [] # CouchbaseSearch doesn't use SearchType enum
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import json
|
|
3
3
|
from hashlib import md5
|
|
4
4
|
from os import getenv
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
try:
|
|
8
8
|
import lancedb
|
|
@@ -10,10 +10,11 @@ try:
|
|
|
10
10
|
except ImportError:
|
|
11
11
|
raise ImportError("`lancedb` not installed. Please install using `pip install lancedb`")
|
|
12
12
|
|
|
13
|
+
from agno.filters import FilterExpr
|
|
13
14
|
from agno.knowledge.document import Document
|
|
14
15
|
from agno.knowledge.embedder import Embedder
|
|
15
16
|
from agno.knowledge.reranker.base import Reranker
|
|
16
|
-
from agno.utils.log import log_debug, log_info, logger
|
|
17
|
+
from agno.utils.log import log_debug, log_info, log_warning, logger
|
|
17
18
|
from agno.vectordb.base import VectorDb
|
|
18
19
|
from agno.vectordb.distance import Distance
|
|
19
20
|
from agno.vectordb.search import SearchType
|
|
@@ -25,6 +26,8 @@ class LanceDb(VectorDb):
|
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
28
|
uri: The URI of the LanceDB database.
|
|
29
|
+
name: Name of the vector database.
|
|
30
|
+
description: Description of the vector database.
|
|
28
31
|
connection: The LanceDB connection to use.
|
|
29
32
|
table: The LanceDB table instance to use.
|
|
30
33
|
async_connection: The LanceDB async connection to use.
|
|
@@ -44,6 +47,9 @@ class LanceDb(VectorDb):
|
|
|
44
47
|
def __init__(
|
|
45
48
|
self,
|
|
46
49
|
uri: lancedb.URI = "/tmp/lancedb",
|
|
50
|
+
name: Optional[str] = None,
|
|
51
|
+
description: Optional[str] = None,
|
|
52
|
+
id: Optional[str] = None,
|
|
47
53
|
connection: Optional[lancedb.LanceDBConnection] = None,
|
|
48
54
|
table: Optional[lancedb.db.LanceTable] = None,
|
|
49
55
|
async_connection: Optional[lancedb.AsyncConnection] = None,
|
|
@@ -59,6 +65,17 @@ class LanceDb(VectorDb):
|
|
|
59
65
|
on_bad_vectors: Optional[str] = None, # One of "error", "drop", "fill", "null".
|
|
60
66
|
fill_value: Optional[float] = None, # Only used if on_bad_vectors is "fill"
|
|
61
67
|
):
|
|
68
|
+
# Dynamic ID generation based on unique identifiers
|
|
69
|
+
if id is None:
|
|
70
|
+
from agno.utils.string import generate_id
|
|
71
|
+
|
|
72
|
+
table_identifier = table_name or "default_table"
|
|
73
|
+
seed = f"{uri}#{table_identifier}"
|
|
74
|
+
id = generate_id(seed)
|
|
75
|
+
|
|
76
|
+
# Initialize base class with name, description, and generated ID
|
|
77
|
+
super().__init__(id=id, name=name, description=description)
|
|
78
|
+
|
|
62
79
|
# Embedder for embedding the document contents
|
|
63
80
|
if embedder is None:
|
|
64
81
|
from agno.knowledge.embedder.openai import OpenAIEmbedder
|
|
@@ -140,6 +157,29 @@ class LanceDb(VectorDb):
|
|
|
140
157
|
|
|
141
158
|
log_debug(f"Initialized LanceDb with table: '{self.table_name}'")
|
|
142
159
|
|
|
160
|
+
def _prepare_vector(self, embedding) -> List[float]:
|
|
161
|
+
"""Prepare vector embedding for insertion, ensuring correct dimensions and type."""
|
|
162
|
+
if embedding is not None and len(embedding) > 0:
|
|
163
|
+
# Convert to list of floats
|
|
164
|
+
vector = [float(x) for x in embedding]
|
|
165
|
+
|
|
166
|
+
# Ensure vector has correct dimensions if specified
|
|
167
|
+
if self.dimensions:
|
|
168
|
+
if len(vector) != self.dimensions:
|
|
169
|
+
if len(vector) > self.dimensions:
|
|
170
|
+
# Truncate if too long
|
|
171
|
+
vector = vector[: self.dimensions]
|
|
172
|
+
log_debug(f"Truncated vector from {len(embedding)} to {self.dimensions} dimensions")
|
|
173
|
+
else:
|
|
174
|
+
# Pad with zeros if too short
|
|
175
|
+
vector.extend([0.0] * (self.dimensions - len(vector)))
|
|
176
|
+
log_debug(f"Padded vector from {len(embedding)} to {self.dimensions} dimensions")
|
|
177
|
+
|
|
178
|
+
return vector
|
|
179
|
+
else:
|
|
180
|
+
# Fallback if embedding is None or empty
|
|
181
|
+
return [0.0] * (self.dimensions or 1536)
|
|
182
|
+
|
|
143
183
|
async def _get_async_connection(self) -> lancedb.AsyncConnection:
|
|
144
184
|
"""Get or create an async connection to LanceDB."""
|
|
145
185
|
if self.async_connection is None:
|
|
@@ -161,7 +201,6 @@ class LanceDb(VectorDb):
|
|
|
161
201
|
# Re-establish sync connection to see async changes
|
|
162
202
|
if self.connection and self.table_name in self.connection.table_names():
|
|
163
203
|
self.table = self.connection.open_table(self.table_name)
|
|
164
|
-
log_debug(f"Refreshed sync connection for table: {self.table_name}")
|
|
165
204
|
except Exception as e:
|
|
166
205
|
log_debug(f"Could not refresh sync connection: {e}")
|
|
167
206
|
# If refresh fails, we can still function but sync methods might not see async changes
|
|
@@ -174,22 +213,37 @@ class LanceDb(VectorDb):
|
|
|
174
213
|
async def async_create(self) -> None:
|
|
175
214
|
"""Create the table asynchronously if it does not exist."""
|
|
176
215
|
if not await self.async_exists():
|
|
177
|
-
|
|
178
|
-
|
|
216
|
+
try:
|
|
217
|
+
conn = await self._get_async_connection()
|
|
218
|
+
schema = self._base_schema()
|
|
179
219
|
|
|
180
|
-
|
|
181
|
-
|
|
220
|
+
log_debug(f"Creating table asynchronously: {self.table_name}")
|
|
221
|
+
self.async_table = await conn.create_table(
|
|
222
|
+
self.table_name, schema=schema, mode="overwrite", exist_ok=True
|
|
223
|
+
)
|
|
224
|
+
log_debug(f"Successfully created async table: {self.table_name}")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error creating async table: {e}")
|
|
227
|
+
# Try to fall back to sync table creation
|
|
228
|
+
try:
|
|
229
|
+
log_debug("Falling back to sync table creation")
|
|
230
|
+
self.table = self._init_table()
|
|
231
|
+
log_debug("Sync table created successfully")
|
|
232
|
+
except Exception as sync_e:
|
|
233
|
+
logger.error(f"Sync table creation also failed: {sync_e}")
|
|
234
|
+
raise
|
|
182
235
|
|
|
183
236
|
def _base_schema(self) -> pa.Schema:
|
|
237
|
+
# Use fixed-size list for vector field as required by LanceDB
|
|
238
|
+
if self.dimensions:
|
|
239
|
+
vector_field = pa.field(self._vector_col, pa.list_(pa.float32(), self.dimensions))
|
|
240
|
+
else:
|
|
241
|
+
# Fallback to dynamic list if dimensions not known (should be rare)
|
|
242
|
+
vector_field = pa.field(self._vector_col, pa.list_(pa.float32()))
|
|
243
|
+
|
|
184
244
|
return pa.schema(
|
|
185
245
|
[
|
|
186
|
-
|
|
187
|
-
self._vector_col,
|
|
188
|
-
pa.list_(
|
|
189
|
-
pa.float32(),
|
|
190
|
-
len(self.embedder.get_embedding("test")), # type: ignore
|
|
191
|
-
),
|
|
192
|
-
),
|
|
246
|
+
vector_field,
|
|
193
247
|
pa.field(self._id, pa.string()),
|
|
194
248
|
pa.field("payload", pa.string()),
|
|
195
249
|
]
|
|
@@ -278,7 +332,7 @@ class LanceDb(VectorDb):
|
|
|
278
332
|
data.append(
|
|
279
333
|
{
|
|
280
334
|
"id": doc_id,
|
|
281
|
-
"vector": document.embedding,
|
|
335
|
+
"vector": self._prepare_vector(document.embedding),
|
|
282
336
|
"payload": json.dumps(payload),
|
|
283
337
|
}
|
|
284
338
|
)
|
|
@@ -305,6 +359,9 @@ class LanceDb(VectorDb):
|
|
|
305
359
|
"""
|
|
306
360
|
Asynchronously insert documents into the database.
|
|
307
361
|
|
|
362
|
+
Note: Currently wraps sync insert method since LanceDB async insert has sync/async table
|
|
363
|
+
synchronization issues causing empty vectors. We still do async embedding for performance.
|
|
364
|
+
|
|
308
365
|
Args:
|
|
309
366
|
documents (List[Document]): List of documents to insert
|
|
310
367
|
filters (Optional[Dict[str, Any]]): Filters to apply while inserting documents
|
|
@@ -314,60 +371,36 @@ class LanceDb(VectorDb):
|
|
|
314
371
|
return
|
|
315
372
|
|
|
316
373
|
log_debug(f"Inserting {len(documents)} documents")
|
|
317
|
-
data = []
|
|
318
|
-
|
|
319
|
-
# Prepare documents for insertion.
|
|
320
|
-
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
321
|
-
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
322
374
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
)
|
|
350
|
-
log_debug(f"Parsed document: {document.name} ({document.meta_data})")
|
|
351
|
-
|
|
352
|
-
if not data:
|
|
353
|
-
log_debug("No new data to insert")
|
|
354
|
-
return
|
|
355
|
-
|
|
356
|
-
try:
|
|
357
|
-
await self._get_async_connection()
|
|
358
|
-
|
|
359
|
-
if self.on_bad_vectors is not None:
|
|
360
|
-
await self.async_table.add(data, on_bad_vectors=self.on_bad_vectors, fill_value=self.fill_value) # type: ignore
|
|
361
|
-
else:
|
|
362
|
-
await self.async_table.add(data) # type: ignore
|
|
363
|
-
|
|
364
|
-
log_debug(f"Asynchronously inserted {len(data)} documents")
|
|
375
|
+
# Still do async embedding for performance
|
|
376
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
377
|
+
try:
|
|
378
|
+
doc_contents = [doc.content for doc in documents]
|
|
379
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
380
|
+
|
|
381
|
+
for j, doc in enumerate(documents):
|
|
382
|
+
if j < len(embeddings):
|
|
383
|
+
doc.embedding = embeddings[j]
|
|
384
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
385
|
+
except Exception as e:
|
|
386
|
+
error_str = str(e).lower()
|
|
387
|
+
is_rate_limit = any(
|
|
388
|
+
phrase in error_str
|
|
389
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
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
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
397
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
398
|
+
else:
|
|
399
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
400
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
365
401
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
except Exception as e:
|
|
369
|
-
logger.error(f"Error during async document insertion: {e}")
|
|
370
|
-
raise
|
|
402
|
+
# Use sync insert to avoid sync/async table synchronization issues
|
|
403
|
+
self.insert(content_hash, documents, filters)
|
|
371
404
|
|
|
372
405
|
def upsert_available(self) -> bool:
|
|
373
406
|
"""Check if upsert is available in LanceDB."""
|
|
@@ -388,11 +421,42 @@ class LanceDb(VectorDb):
|
|
|
388
421
|
async def async_upsert(
|
|
389
422
|
self, content_hash: str, documents: List[Document], filters: Optional[Dict[str, Any]] = None
|
|
390
423
|
) -> None:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
await self.async_insert(content_hash=content_hash, documents=documents, filters=filters)
|
|
424
|
+
"""
|
|
425
|
+
Asynchronously upsert documents into the database.
|
|
394
426
|
|
|
395
|
-
|
|
427
|
+
Note: Uses async embedding for performance, then sync upsert for reliability.
|
|
428
|
+
"""
|
|
429
|
+
if len(documents) > 0:
|
|
430
|
+
# Do async embedding for performance
|
|
431
|
+
if self.embedder.enable_batch and hasattr(self.embedder, "async_get_embeddings_batch_and_usage"):
|
|
432
|
+
try:
|
|
433
|
+
doc_contents = [doc.content for doc in documents]
|
|
434
|
+
embeddings, usages = await self.embedder.async_get_embeddings_batch_and_usage(doc_contents)
|
|
435
|
+
for j, doc in enumerate(documents):
|
|
436
|
+
if j < len(embeddings):
|
|
437
|
+
doc.embedding = embeddings[j]
|
|
438
|
+
doc.usage = usages[j] if j < len(usages) else None
|
|
439
|
+
except Exception as e:
|
|
440
|
+
error_str = str(e).lower()
|
|
441
|
+
is_rate_limit = any(
|
|
442
|
+
phrase in error_str
|
|
443
|
+
for phrase in ["rate limit", "too many requests", "429", "trial key", "api calls / minute"]
|
|
444
|
+
)
|
|
445
|
+
if is_rate_limit:
|
|
446
|
+
raise e
|
|
447
|
+
else:
|
|
448
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
449
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
450
|
+
else:
|
|
451
|
+
embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in documents]
|
|
452
|
+
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
453
|
+
|
|
454
|
+
# Use sync upsert for reliability
|
|
455
|
+
self.upsert(content_hash=content_hash, documents=documents, filters=filters)
|
|
456
|
+
|
|
457
|
+
def search(
|
|
458
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
459
|
+
) -> List[Document]:
|
|
396
460
|
"""
|
|
397
461
|
Search for documents matching the query.
|
|
398
462
|
|
|
@@ -409,6 +473,10 @@ class LanceDb(VectorDb):
|
|
|
409
473
|
|
|
410
474
|
results = None
|
|
411
475
|
|
|
476
|
+
if isinstance(filters, list):
|
|
477
|
+
log_warning("Filter Expressions are not yet supported in LanceDB. No filters will be applied.")
|
|
478
|
+
filters = None
|
|
479
|
+
|
|
412
480
|
if self.search_type == SearchType.vector:
|
|
413
481
|
results = self.vector_search(query, limit)
|
|
414
482
|
elif self.search_type == SearchType.keyword:
|
|
@@ -450,11 +518,14 @@ class LanceDb(VectorDb):
|
|
|
450
518
|
return search_results
|
|
451
519
|
|
|
452
520
|
async def async_search(
|
|
453
|
-
self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None
|
|
521
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
454
522
|
) -> List[Document]:
|
|
455
523
|
"""
|
|
456
524
|
Asynchronously search for documents matching the query.
|
|
457
525
|
|
|
526
|
+
Note: Currently wraps sync search method since LanceDB async search has sync/async table
|
|
527
|
+
synchronization issues. Performance impact is minimal for search operations.
|
|
528
|
+
|
|
458
529
|
Args:
|
|
459
530
|
query (str): Query string to search for
|
|
460
531
|
limit (int): Maximum number of results to return
|
|
@@ -463,53 +534,12 @@ class LanceDb(VectorDb):
|
|
|
463
534
|
Returns:
|
|
464
535
|
List[Document]: List of matching documents
|
|
465
536
|
"""
|
|
466
|
-
#
|
|
467
|
-
|
|
468
|
-
self.table = self.connection.open_table(name=self.table_name)
|
|
469
|
-
|
|
470
|
-
results = None
|
|
471
|
-
|
|
472
|
-
if self.search_type == SearchType.vector:
|
|
473
|
-
results = self.vector_search(query, limit)
|
|
474
|
-
elif self.search_type == SearchType.keyword:
|
|
475
|
-
results = self.keyword_search(query, limit)
|
|
476
|
-
elif self.search_type == SearchType.hybrid:
|
|
477
|
-
results = self.hybrid_search(query, limit)
|
|
478
|
-
else:
|
|
479
|
-
logger.error(f"Invalid search type '{self.search_type}'.")
|
|
480
|
-
return []
|
|
481
|
-
|
|
482
|
-
if results is None:
|
|
483
|
-
return []
|
|
484
|
-
|
|
485
|
-
search_results = self._build_search_results(results)
|
|
486
|
-
|
|
487
|
-
# Filter results based on metadata if filters are provided
|
|
488
|
-
if filters and search_results:
|
|
489
|
-
filtered_results = []
|
|
490
|
-
for doc in search_results:
|
|
491
|
-
if doc.meta_data is None:
|
|
492
|
-
continue
|
|
537
|
+
# Wrap sync search method to avoid sync/async table synchronization issues
|
|
538
|
+
return self.search(query=query, limit=limit, filters=filters)
|
|
493
539
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if key not in doc.meta_data or doc.meta_data[key] != value:
|
|
498
|
-
match = False
|
|
499
|
-
break
|
|
500
|
-
|
|
501
|
-
if match:
|
|
502
|
-
filtered_results.append(doc)
|
|
503
|
-
|
|
504
|
-
search_results = filtered_results
|
|
505
|
-
|
|
506
|
-
if self.reranker and search_results:
|
|
507
|
-
search_results = self.reranker.rerank(query=query, documents=search_results)
|
|
508
|
-
|
|
509
|
-
log_info(f"Found {len(search_results)} documents")
|
|
510
|
-
return search_results
|
|
511
|
-
|
|
512
|
-
def vector_search(self, query: str, limit: int = 5) -> List[Document]:
|
|
540
|
+
def vector_search(
|
|
541
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
542
|
+
) -> List[Document]:
|
|
513
543
|
query_embedding = self.embedder.get_embedding(query)
|
|
514
544
|
if query_embedding is None:
|
|
515
545
|
logger.error(f"Error getting embedding for Query: {query}")
|
|
@@ -529,7 +559,9 @@ class LanceDb(VectorDb):
|
|
|
529
559
|
|
|
530
560
|
return results.to_pandas()
|
|
531
561
|
|
|
532
|
-
def hybrid_search(
|
|
562
|
+
def hybrid_search(
|
|
563
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
564
|
+
) -> List[Document]:
|
|
533
565
|
query_embedding = self.embedder.get_embedding(query)
|
|
534
566
|
if query_embedding is None:
|
|
535
567
|
logger.error(f"Error getting embedding for Query: {query}")
|
|
@@ -558,7 +590,9 @@ class LanceDb(VectorDb):
|
|
|
558
590
|
|
|
559
591
|
return results.to_pandas()
|
|
560
592
|
|
|
561
|
-
def keyword_search(
|
|
593
|
+
def keyword_search(
|
|
594
|
+
self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
|
|
595
|
+
) -> List[Document]:
|
|
562
596
|
if self.table is None:
|
|
563
597
|
logger.error("Table not initialized. Please create the table first")
|
|
564
598
|
return []
|
|
@@ -638,26 +672,25 @@ class LanceDb(VectorDb):
|
|
|
638
672
|
return await self.async_table.count_rows()
|
|
639
673
|
return 0
|
|
640
674
|
|
|
641
|
-
def _async_get_count_sync(self) -> int:
|
|
642
|
-
"""Helper method to run async_get_count in a new thread with its own event loop"""
|
|
643
|
-
import asyncio
|
|
644
|
-
|
|
645
|
-
return asyncio.run(self.async_get_count())
|
|
646
|
-
|
|
647
675
|
def get_count(self) -> int:
|
|
648
676
|
# If we have data in the async table but sync table isn't available, try to get count from async table
|
|
649
677
|
if self.async_table is not None:
|
|
650
678
|
try:
|
|
651
679
|
import asyncio
|
|
652
680
|
|
|
653
|
-
# Check if we're already in an
|
|
681
|
+
# Check if we're already in an event loop
|
|
654
682
|
try:
|
|
655
|
-
|
|
683
|
+
asyncio.get_running_loop()
|
|
684
|
+
# We're in an async context, can't use asyncio.run
|
|
685
|
+
log_debug("Already in async context, falling back to sync table for count")
|
|
656
686
|
except RuntimeError:
|
|
657
687
|
# No event loop running, safe to use asyncio.run
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
688
|
+
try:
|
|
689
|
+
return asyncio.run(self.async_get_count())
|
|
690
|
+
except Exception as e:
|
|
691
|
+
log_debug(f"Failed to get async count: {e}")
|
|
692
|
+
except Exception as e:
|
|
693
|
+
log_debug(f"Error in async count logic: {e}")
|
|
661
694
|
|
|
662
695
|
if self.exists() and self.table:
|
|
663
696
|
return self.table.count_rows()
|
|
@@ -893,17 +926,28 @@ class LanceDb(VectorDb):
|
|
|
893
926
|
logger.error("Table not initialized")
|
|
894
927
|
return
|
|
895
928
|
|
|
896
|
-
#
|
|
897
|
-
|
|
898
|
-
results = self.table.search().
|
|
929
|
+
# Get all documents and filter in Python (LanceDB doesn't support JSON operators)
|
|
930
|
+
total_count = self.table.count_rows()
|
|
931
|
+
results = self.table.search().select(["id", "payload"]).limit(total_count).to_pandas()
|
|
899
932
|
|
|
900
933
|
if results.empty:
|
|
934
|
+
logger.debug("No documents found")
|
|
935
|
+
return
|
|
936
|
+
|
|
937
|
+
# Find matching documents with the given content_id
|
|
938
|
+
matching_rows = []
|
|
939
|
+
for _, row in results.iterrows():
|
|
940
|
+
payload = json.loads(row["payload"])
|
|
941
|
+
if payload.get("content_id") == content_id:
|
|
942
|
+
matching_rows.append(row)
|
|
943
|
+
|
|
944
|
+
if not matching_rows:
|
|
901
945
|
logger.debug(f"No documents found with content_id: {content_id}")
|
|
902
946
|
return
|
|
903
947
|
|
|
904
948
|
# Update each matching document
|
|
905
949
|
updated_count = 0
|
|
906
|
-
for
|
|
950
|
+
for row in matching_rows:
|
|
907
951
|
row_id = row["id"]
|
|
908
952
|
current_payload = json.loads(row["payload"])
|
|
909
953
|
|
|
@@ -945,3 +989,7 @@ class LanceDb(VectorDb):
|
|
|
945
989
|
except Exception as e:
|
|
946
990
|
logger.error(f"Error updating metadata for content_id '{content_id}': {e}")
|
|
947
991
|
raise
|
|
992
|
+
|
|
993
|
+
def get_supported_search_types(self) -> List[str]:
|
|
994
|
+
"""Get the supported search types for this vector database."""
|
|
995
|
+
return [SearchType.vector, SearchType.keyword, SearchType.hybrid]
|