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.
Files changed (331) hide show
  1. agno/agent/agent.py +6009 -2874
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +595 -187
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +3 -0
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +339 -266
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +1011 -566
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +110 -37
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +143 -4
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +60 -6
  142. agno/models/openai/chat.py +102 -43
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +81 -5
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -175
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +266 -112
  205. agno/run/base.py +53 -24
  206. agno/run/team.py +252 -111
  207. agno/run/workflow.py +156 -45
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1692
  213. agno/tools/brightdata.py +3 -3
  214. agno/tools/cartesia.py +3 -5
  215. agno/tools/dalle.py +9 -8
  216. agno/tools/decorator.py +4 -2
  217. agno/tools/desi_vocal.py +2 -2
  218. agno/tools/duckduckgo.py +15 -11
  219. agno/tools/e2b.py +20 -13
  220. agno/tools/eleven_labs.py +26 -28
  221. agno/tools/exa.py +21 -16
  222. agno/tools/fal.py +4 -4
  223. agno/tools/file.py +153 -23
  224. agno/tools/file_generation.py +350 -0
  225. agno/tools/firecrawl.py +4 -4
  226. agno/tools/function.py +257 -37
  227. agno/tools/giphy.py +2 -2
  228. agno/tools/gmail.py +238 -14
  229. agno/tools/google_drive.py +270 -0
  230. agno/tools/googlecalendar.py +36 -8
  231. agno/tools/googlesheets.py +20 -5
  232. agno/tools/jira.py +20 -0
  233. agno/tools/knowledge.py +3 -3
  234. agno/tools/lumalab.py +3 -3
  235. agno/tools/mcp/__init__.py +10 -0
  236. agno/tools/mcp/mcp.py +331 -0
  237. agno/tools/mcp/multi_mcp.py +347 -0
  238. agno/tools/mcp/params.py +24 -0
  239. agno/tools/mcp_toolbox.py +284 -0
  240. agno/tools/mem0.py +11 -17
  241. agno/tools/memori.py +1 -53
  242. agno/tools/memory.py +419 -0
  243. agno/tools/models/azure_openai.py +2 -2
  244. agno/tools/models/gemini.py +3 -3
  245. agno/tools/models/groq.py +3 -5
  246. agno/tools/models/nebius.py +7 -7
  247. agno/tools/models_labs.py +25 -15
  248. agno/tools/notion.py +204 -0
  249. agno/tools/openai.py +4 -9
  250. agno/tools/opencv.py +3 -3
  251. agno/tools/parallel.py +314 -0
  252. agno/tools/replicate.py +7 -7
  253. agno/tools/scrapegraph.py +58 -31
  254. agno/tools/searxng.py +2 -2
  255. agno/tools/serper.py +2 -2
  256. agno/tools/slack.py +18 -3
  257. agno/tools/spider.py +2 -2
  258. agno/tools/tavily.py +146 -0
  259. agno/tools/whatsapp.py +1 -1
  260. agno/tools/workflow.py +278 -0
  261. agno/tools/yfinance.py +12 -11
  262. agno/utils/agent.py +820 -0
  263. agno/utils/audio.py +27 -0
  264. agno/utils/common.py +90 -1
  265. agno/utils/events.py +222 -7
  266. agno/utils/gemini.py +181 -23
  267. agno/utils/hooks.py +57 -0
  268. agno/utils/http.py +111 -0
  269. agno/utils/knowledge.py +12 -5
  270. agno/utils/log.py +1 -0
  271. agno/utils/mcp.py +95 -5
  272. agno/utils/media.py +188 -10
  273. agno/utils/merge_dict.py +22 -1
  274. agno/utils/message.py +60 -0
  275. agno/utils/models/claude.py +40 -11
  276. agno/utils/models/cohere.py +1 -1
  277. agno/utils/models/watsonx.py +1 -1
  278. agno/utils/openai.py +1 -1
  279. agno/utils/print_response/agent.py +105 -21
  280. agno/utils/print_response/team.py +103 -38
  281. agno/utils/print_response/workflow.py +251 -34
  282. agno/utils/reasoning.py +22 -1
  283. agno/utils/serialize.py +32 -0
  284. agno/utils/streamlit.py +16 -10
  285. agno/utils/string.py +41 -0
  286. agno/utils/team.py +98 -9
  287. agno/utils/tools.py +1 -1
  288. agno/vectordb/base.py +23 -4
  289. agno/vectordb/cassandra/cassandra.py +65 -9
  290. agno/vectordb/chroma/chromadb.py +182 -38
  291. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  292. agno/vectordb/couchbase/couchbase.py +105 -10
  293. agno/vectordb/lancedb/lance_db.py +183 -135
  294. agno/vectordb/langchaindb/langchaindb.py +25 -7
  295. agno/vectordb/lightrag/lightrag.py +17 -3
  296. agno/vectordb/llamaindex/__init__.py +3 -0
  297. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  298. agno/vectordb/milvus/milvus.py +126 -9
  299. agno/vectordb/mongodb/__init__.py +7 -1
  300. agno/vectordb/mongodb/mongodb.py +112 -7
  301. agno/vectordb/pgvector/pgvector.py +142 -21
  302. agno/vectordb/pineconedb/pineconedb.py +80 -8
  303. agno/vectordb/qdrant/qdrant.py +125 -39
  304. agno/vectordb/redis/__init__.py +9 -0
  305. agno/vectordb/redis/redisdb.py +694 -0
  306. agno/vectordb/singlestore/singlestore.py +111 -25
  307. agno/vectordb/surrealdb/surrealdb.py +31 -5
  308. agno/vectordb/upstashdb/upstashdb.py +76 -8
  309. agno/vectordb/weaviate/weaviate.py +86 -15
  310. agno/workflow/__init__.py +2 -0
  311. agno/workflow/agent.py +299 -0
  312. agno/workflow/condition.py +112 -18
  313. agno/workflow/loop.py +69 -10
  314. agno/workflow/parallel.py +266 -118
  315. agno/workflow/router.py +110 -17
  316. agno/workflow/step.py +645 -136
  317. agno/workflow/steps.py +65 -6
  318. agno/workflow/types.py +71 -33
  319. agno/workflow/workflow.py +2113 -300
  320. agno-2.3.0.dist-info/METADATA +618 -0
  321. agno-2.3.0.dist-info/RECORD +577 -0
  322. agno-2.3.0.dist-info/licenses/LICENSE +201 -0
  323. agno/knowledge/reader/url_reader.py +0 -128
  324. agno/tools/googlesearch.py +0 -98
  325. agno/tools/mcp.py +0 -610
  326. agno/utils/models/aws_claude.py +0 -170
  327. agno-2.0.0rc2.dist-info/METADATA +0 -355
  328. agno-2.0.0rc2.dist-info/RECORD +0 -515
  329. agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
  330. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  331. {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.knowledge.embedder.openai import OpenAIEmbedder
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 = OpenAIEmbedder(),
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(self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None) -> List[Document]:
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
- embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
875
- await asyncio.gather(*embed_tasks, return_exceptions=True)
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
- embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
941
- await asyncio.gather(*embed_tasks, return_exceptions=True)
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
- conn = await self._get_async_connection()
178
- schema = self._base_schema()
216
+ try:
217
+ conn = await self._get_async_connection()
218
+ schema = self._base_schema()
179
219
 
180
- log_debug(f"Creating table asynchronously: {self.table_name}")
181
- self.async_table = await conn.create_table(self.table_name, schema=schema, mode="overwrite", exist_ok=True)
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
- pa.field(
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
- for document in documents:
324
- if await self.async_doc_exists(document):
325
- continue
326
-
327
- # Add filters to document metadata if provided
328
- if filters:
329
- meta_data = document.meta_data.copy() if document.meta_data else {}
330
- meta_data.update(filters)
331
- document.meta_data = meta_data
332
-
333
- cleaned_content = document.content.replace("\x00", "\ufffd")
334
- doc_id = str(md5(cleaned_content.encode()).hexdigest())
335
- payload = {
336
- "name": document.name,
337
- "meta_data": document.meta_data,
338
- "content": cleaned_content,
339
- "usage": document.usage,
340
- "content_id": document.content_id,
341
- "content_hash": content_hash,
342
- }
343
- data.append(
344
- {
345
- "id": doc_id,
346
- "vector": document.embedding,
347
- "payload": json.dumps(payload),
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
- # Refresh sync connection to see async changes
367
- self._refresh_sync_connection()
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
- if self.content_hash_exists(content_hash):
392
- self._delete_by_content_hash(content_hash)
393
- await self.async_insert(content_hash=content_hash, documents=documents, filters=filters)
424
+ """
425
+ Asynchronously upsert documents into the database.
394
426
 
395
- def search(self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None) -> List[Document]:
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
- # TODO: Search is not yet supported in async (https://github.com/lancedb/lancedb/pull/2049)
467
- if self.connection:
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
- # Check if all filter criteria match
495
- match = True
496
- for key, value in filters.items():
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(self, query: str, limit: int = 5) -> List[Document]:
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(self, query: str, limit: int = 5) -> List[Document]:
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 async context
681
+ # Check if we're already in an event loop
654
682
  try:
655
- return self._async_get_count_sync()
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
- return asyncio.run(self.async_get_count())
659
- except Exception:
660
- pass
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
- # Search for documents with the given content_id
897
- query_filter = f"payload->>'content_id' = '{content_id}'"
898
- results = self.table.search().where(query_filter).to_pandas()
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 _, row in results.iterrows():
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]