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
@@ -0,0 +1,694 @@
1
+ import asyncio
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ try:
5
+ from redis import Redis
6
+ from redis.asyncio import Redis as AsyncRedis
7
+ from redisvl.index import AsyncSearchIndex, SearchIndex
8
+ from redisvl.query import FilterQuery, HybridQuery, TextQuery, VectorQuery
9
+ from redisvl.query.filter import Tag
10
+ from redisvl.redis.utils import array_to_buffer, convert_bytes
11
+ from redisvl.schema import IndexSchema
12
+ except ImportError:
13
+ raise ImportError("`redis` and `redisvl` not installed. Please install using `pip install redis redisvl`")
14
+
15
+ from agno.filters import FilterExpr
16
+ from agno.knowledge.document import Document
17
+ from agno.knowledge.embedder import Embedder
18
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
19
+ from agno.utils.string import hash_string_sha256
20
+ from agno.vectordb.base import VectorDb
21
+ from agno.vectordb.distance import Distance
22
+ from agno.vectordb.search import SearchType
23
+
24
+
25
+ class RedisDB(VectorDb):
26
+ """
27
+ Redis class for managing vector operations with Redis and RedisVL.
28
+
29
+ This class provides methods for creating, inserting, searching, and managing
30
+ vector data in a Redis database using the RedisVL library.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ index_name: str,
36
+ redis_url: Optional[str] = None,
37
+ redis_client: Optional[Redis] = None,
38
+ embedder: Optional[Embedder] = None,
39
+ search_type: SearchType = SearchType.vector,
40
+ distance: Distance = Distance.cosine,
41
+ vector_score_weight: float = 0.7,
42
+ **redis_kwargs,
43
+ ):
44
+ """
45
+ Initialize the Redis instance.
46
+
47
+ Args:
48
+ index_name (str): Name of the Redis index to store vector data.
49
+ redis_url (Optional[str]): Redis connection URL.
50
+ redis_client (Optional[redis.Redis]): Redis client instance.
51
+ embedder (Optional[Embedder]): Embedder instance for creating embeddings.
52
+ search_type (SearchType): Type of search to perform.
53
+ distance (Distance): Distance metric for vector comparisons.
54
+ vector_score_weight (float): Weight for vector similarity in hybrid search.
55
+ reranker (Optional[Reranker]): Reranker instance.
56
+ **redis_kwargs: Additional Redis connection parameters.
57
+ """
58
+ if not index_name:
59
+ raise ValueError("Index name must be provided.")
60
+
61
+ if redis_client is None and redis_url is None:
62
+ raise ValueError("Either 'redis_url' or 'redis_client' must be provided.")
63
+
64
+ self.redis_url = redis_url
65
+
66
+ # Initialize Redis client
67
+ if redis_client is None:
68
+ assert redis_url is not None
69
+ self.redis_client = Redis.from_url(redis_url, **redis_kwargs)
70
+ else:
71
+ self.redis_client = redis_client
72
+
73
+ # Index settings
74
+ self.index_name: str = index_name
75
+
76
+ # Embedder for embedding the document contents
77
+ if embedder is None:
78
+ from agno.knowledge.embedder.openai import OpenAIEmbedder
79
+
80
+ embedder = OpenAIEmbedder()
81
+ log_info("Embedder not provided, using OpenAIEmbedder as default.")
82
+
83
+ self.embedder: Embedder = embedder
84
+ self.dimensions: Optional[int] = self.embedder.dimensions
85
+
86
+ if self.dimensions is None:
87
+ raise ValueError("Embedder.dimensions must be set.")
88
+
89
+ # Search type and distance metric
90
+ self.search_type: SearchType = search_type
91
+ self.distance: Distance = distance
92
+ self.vector_score_weight: float = vector_score_weight
93
+
94
+ # # Reranker instance
95
+ # self.reranker: Optional[Reranker] = reranker
96
+
97
+ # Create index schema
98
+ self.schema = self._get_schema()
99
+ self.index = self._create_index()
100
+ self.meta_data_fields: set[str] = set()
101
+
102
+ # Async components - created lazily when needed
103
+ self._async_redis_client: Optional[AsyncRedis] = None
104
+ self._async_index: Optional[AsyncSearchIndex] = None
105
+
106
+ log_debug(f"Initialized Redis with index '{self.index_name}'")
107
+
108
+ async def _get_async_index(self) -> AsyncSearchIndex:
109
+ """Get or create the async index and client."""
110
+ if self._async_index is None:
111
+ if self.redis_url is None:
112
+ raise ValueError("redis_url must be provided for async operations")
113
+ url: str = self.redis_url
114
+ self._async_redis_client = AsyncRedis.from_url(url)
115
+ self._async_index = AsyncSearchIndex(schema=self.schema, redis_client=self._async_redis_client)
116
+ return self._async_index
117
+
118
+ def _get_schema(self):
119
+ """Get default redis schema"""
120
+ distance_mapping = {
121
+ Distance.cosine: "cosine",
122
+ Distance.l2: "l2",
123
+ Distance.max_inner_product: "ip",
124
+ }
125
+
126
+ return IndexSchema.from_dict(
127
+ {
128
+ "index": {
129
+ "name": self.index_name,
130
+ "prefix": f"{self.index_name}:",
131
+ "storage_type": "hash",
132
+ },
133
+ "fields": [
134
+ {"name": "id", "type": "tag"},
135
+ {"name": "name", "type": "tag"},
136
+ {"name": "content", "type": "text"},
137
+ {"name": "content_hash", "type": "tag"},
138
+ {"name": "content_id", "type": "tag"},
139
+ # Common metadata fields used in operations/tests
140
+ {"name": "status", "type": "tag"},
141
+ {"name": "category", "type": "tag"},
142
+ {"name": "tag", "type": "tag"},
143
+ {"name": "source", "type": "tag"},
144
+ {"name": "mode", "type": "tag"},
145
+ {
146
+ "name": "embedding",
147
+ "type": "vector",
148
+ "attrs": {
149
+ "dims": self.dimensions,
150
+ "distance_metric": distance_mapping[self.distance],
151
+ "algorithm": "flat",
152
+ },
153
+ },
154
+ ],
155
+ }
156
+ )
157
+
158
+ def _create_index(self) -> SearchIndex:
159
+ """Create the RedisVL index object for this schema."""
160
+ return SearchIndex(self.schema, redis_url=self.redis_url)
161
+
162
+ def create(self) -> None:
163
+ """Create the Redis index if it does not exist."""
164
+ try:
165
+ if not self.exists():
166
+ self.index.create()
167
+ log_debug(f"Created Redis index: {self.index_name}")
168
+ else:
169
+ log_debug(f"Redis index already exists: {self.index_name}")
170
+ except Exception as e:
171
+ log_error(f"Error creating Redis index: {e}")
172
+ raise
173
+
174
+ async def async_create(self) -> None:
175
+ """Async version of create method."""
176
+ try:
177
+ async_index = await self._get_async_index()
178
+ await async_index.create(overwrite=False, drop=False)
179
+ log_debug(f"Created Redis index: {self.index_name}")
180
+ except Exception as e:
181
+ if "already exists" in str(e).lower():
182
+ log_debug(f"Redis index already exists: {self.index_name}")
183
+ else:
184
+ log_error(f"Error creating Redis index: {e}")
185
+ raise
186
+
187
+ def doc_exists(self, document: Document) -> bool:
188
+ """Check if a document exists in the index."""
189
+ try:
190
+ doc_id = document.id or hash_string_sha256(document.content)
191
+ return self.id_exists(doc_id)
192
+ except Exception as e:
193
+ log_error(f"Error checking if document exists: {e}")
194
+ return False
195
+
196
+ async def async_doc_exists(self, document: Document) -> bool:
197
+ """Async version of doc_exists method."""
198
+ try:
199
+ doc_id = document.id or hash_string_sha256(document.content)
200
+ async_index = await self._get_async_index()
201
+ id_filter = Tag("id") == doc_id
202
+ query = FilterQuery(
203
+ filter_expression=id_filter,
204
+ return_fields=["id"],
205
+ num_results=1,
206
+ )
207
+ results = await async_index.query(query)
208
+ return len(results) > 0
209
+ except Exception as e:
210
+ log_error(f"Error checking if document exists: {e}")
211
+ return False
212
+
213
+ def name_exists(self, name: str) -> bool:
214
+ """Check if a document with the given name exists."""
215
+ try:
216
+ name_filter = Tag("name") == name
217
+ query = FilterQuery(
218
+ filter_expression=name_filter,
219
+ return_fields=["id"],
220
+ num_results=1,
221
+ )
222
+ results = self.index.query(query)
223
+ return len(results) > 0
224
+ except Exception as e:
225
+ log_error(f"Error checking if name exists: {e}")
226
+ return False
227
+
228
+ async def async_name_exists(self, name: str) -> bool: # type: ignore[override]
229
+ """Async version of name_exists method."""
230
+ try:
231
+ async_index = await self._get_async_index()
232
+ name_filter = Tag("name") == name
233
+ query = FilterQuery(
234
+ filter_expression=name_filter,
235
+ return_fields=["id"],
236
+ num_results=1,
237
+ )
238
+ results = await async_index.query(query)
239
+ return len(results) > 0
240
+ except Exception as e:
241
+ log_error(f"Error checking if name exists: {e}")
242
+ return False
243
+
244
+ def id_exists(self, id: str) -> bool:
245
+ """Check if a document with the given ID exists."""
246
+ try:
247
+ id_filter = Tag("id") == id
248
+ query = FilterQuery(
249
+ filter_expression=id_filter,
250
+ return_fields=["id"],
251
+ num_results=1,
252
+ )
253
+ results = self.index.query(query)
254
+ return len(results) > 0
255
+ except Exception as e:
256
+ log_error(f"Error checking if ID exists: {e}")
257
+ return False
258
+
259
+ def content_hash_exists(self, content_hash: str) -> bool:
260
+ """Check if a document with the given content hash exists."""
261
+ try:
262
+ content_hash_filter = Tag("content_hash") == content_hash
263
+ query = FilterQuery(
264
+ filter_expression=content_hash_filter,
265
+ return_fields=["id"],
266
+ num_results=1,
267
+ )
268
+ results = self.index.query(query)
269
+ return len(results) > 0
270
+ except Exception as e:
271
+ log_error(f"Error checking if content hash exists: {e}")
272
+ return False
273
+
274
+ def _parse_redis_hash(self, doc: Document):
275
+ """
276
+ Create object serializable into Redis HASH structure
277
+ """
278
+ doc_dict = doc.to_dict()
279
+ # Ensure an ID is present; derive a deterministic one from content when missing
280
+ doc_id = doc.id or hash_string_sha256(doc.content)
281
+ doc_dict["id"] = doc_id
282
+ if not doc.embedding:
283
+ doc.embed(self.embedder)
284
+
285
+ # TODO: determine how we want to handle dtypes
286
+ doc_dict["embedding"] = array_to_buffer(doc.embedding, "float32")
287
+
288
+ # Add content_id if available
289
+ if hasattr(doc, "content_id") and doc.content_id:
290
+ doc_dict["content_id"] = doc.content_id
291
+
292
+ if "meta_data" in doc_dict:
293
+ meta_data = doc_dict.pop("meta_data", {})
294
+ for md in meta_data:
295
+ self.meta_data_fields.add(md)
296
+ doc_dict.update(meta_data)
297
+
298
+ return doc_dict
299
+
300
+ def insert(
301
+ self,
302
+ content_hash: str,
303
+ documents: List[Document],
304
+ filters: Optional[Dict[str, Any]] = None,
305
+ ) -> None:
306
+ """Insert documents into the Redis index."""
307
+ try:
308
+ # Store content hash for tracking
309
+ parsed_documents = []
310
+ for doc in documents:
311
+ parsed_doc = self._parse_redis_hash(doc)
312
+ parsed_doc["content_hash"] = content_hash
313
+ parsed_documents.append(parsed_doc)
314
+
315
+ self.index.load(parsed_documents, id_field="id")
316
+ log_debug(f"Inserted {len(documents)} documents with content_hash: {content_hash}")
317
+ except Exception as e:
318
+ log_error(f"Error inserting documents: {e}")
319
+ raise
320
+
321
+ async def async_insert(
322
+ self,
323
+ content_hash: str,
324
+ documents: List[Document],
325
+ filters: Optional[Dict[str, Any]] = None,
326
+ ) -> None:
327
+ """Async version of insert method."""
328
+ try:
329
+ async_index = await self._get_async_index()
330
+ parsed_documents = []
331
+ for doc in documents:
332
+ parsed_doc = self._parse_redis_hash(doc)
333
+ parsed_doc["content_hash"] = content_hash
334
+ parsed_documents.append(parsed_doc)
335
+ await async_index.load(parsed_documents, id_field="id")
336
+ log_debug(f"Inserted {len(documents)} documents with content_hash: {content_hash}")
337
+ except Exception as e:
338
+ log_error(f"Error inserting documents: {e}")
339
+ raise
340
+
341
+ def upsert_available(self) -> bool:
342
+ """Check if upsert is available (always True for Redis)."""
343
+ return True
344
+
345
+ def upsert(
346
+ self,
347
+ content_hash: str,
348
+ documents: List[Document],
349
+ filters: Optional[Dict[str, Any]] = None,
350
+ ) -> None:
351
+ """Upsert documents into the Redis index.
352
+ Strategy: delete existing docs with the same content_hash, then insert new docs.
353
+ """
354
+ try:
355
+ # Find existing docs for this content_hash and delete them
356
+ ch_filter = Tag("content_hash") == content_hash
357
+ query = FilterQuery(
358
+ filter_expression=ch_filter,
359
+ return_fields=["id"],
360
+ num_results=1000,
361
+ )
362
+ existing = self.index.query(query)
363
+ parsed = convert_bytes(existing)
364
+ for r in parsed:
365
+ key = r.get("id")
366
+ if key:
367
+ self.index.drop_keys(key)
368
+
369
+ # Insert new docs
370
+ self.insert(content_hash, documents, filters)
371
+ except Exception as e:
372
+ log_error(f"Error upserting documents: {e}")
373
+ raise
374
+
375
+ async def async_upsert(
376
+ self,
377
+ content_hash: str,
378
+ documents: List[Document],
379
+ filters: Optional[Dict[str, Any]] = None,
380
+ ) -> None:
381
+ """Async version of upsert method.
382
+ Strategy: delete existing docs with the same content_hash, then insert new docs.
383
+ """
384
+ try:
385
+ async_index = await self._get_async_index()
386
+
387
+ # Find existing docs for this content_hash and delete them
388
+ ch_filter = Tag("content_hash") == content_hash
389
+ query = FilterQuery(
390
+ filter_expression=ch_filter,
391
+ return_fields=["id"],
392
+ num_results=1000,
393
+ )
394
+ existing = await async_index.query(query)
395
+ parsed = convert_bytes(existing)
396
+ for r in parsed:
397
+ key = r.get("id")
398
+ if key:
399
+ await async_index.drop_keys(key)
400
+
401
+ # Insert new docs
402
+ await self.async_insert(content_hash, documents, filters)
403
+ except Exception as e:
404
+ log_error(f"Error upserting documents: {e}")
405
+ raise
406
+
407
+ def search(
408
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
409
+ ) -> List[Document]:
410
+ """Search for documents using the specified search type."""
411
+
412
+ if filters and isinstance(filters, List):
413
+ log_warning("Filters Expressions are not supported in Redis. No filters will be applied.")
414
+ filters = None
415
+ try:
416
+ if self.search_type == SearchType.vector:
417
+ return self.vector_search(query, limit)
418
+ elif self.search_type == SearchType.keyword:
419
+ return self.keyword_search(query, limit)
420
+ elif self.search_type == SearchType.hybrid:
421
+ return self.hybrid_search(query, limit)
422
+ else:
423
+ raise ValueError(f"Unsupported search type: {self.search_type}")
424
+ except Exception as e:
425
+ log_error(f"Error in search: {e}")
426
+ return []
427
+
428
+ async def async_search(
429
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
430
+ ) -> List[Document]:
431
+ """Async version of search method."""
432
+ return await asyncio.to_thread(self.search, query, limit, filters)
433
+
434
+ def vector_search(self, query: str, limit: int = 5) -> List[Document]:
435
+ """Perform vector similarity search."""
436
+ try:
437
+ # Get query embedding
438
+ query_embedding = array_to_buffer(self.embedder.get_embedding(query), "float32")
439
+
440
+ # TODO: do we want to pass back the embedding?
441
+ # Create vector query
442
+ vector_query = VectorQuery(
443
+ vector=query_embedding,
444
+ vector_field_name="embedding",
445
+ return_fields=["id", "name", "content"],
446
+ return_score=False,
447
+ num_results=limit,
448
+ )
449
+
450
+ # Execute search
451
+ results = self.index.query(vector_query)
452
+
453
+ # Convert results to documents
454
+ documents = [Document.from_dict(r) for r in results]
455
+
456
+ return documents
457
+ except Exception as e:
458
+ log_error(f"Error in vector search: {e}")
459
+ return []
460
+
461
+ def keyword_search(self, query: str, limit: int = 5) -> List[Document]:
462
+ """Perform keyword search using Redis text search."""
463
+ try:
464
+ # Create text query
465
+ text_query = TextQuery(
466
+ text=query,
467
+ text_field_name="content",
468
+ )
469
+
470
+ # Execute search
471
+ results = self.index.query(text_query)
472
+
473
+ # Convert results to documents
474
+ parsed = convert_bytes(results)
475
+
476
+ # Convert results to documents
477
+ documents = [Document.from_dict(p) for p in parsed]
478
+
479
+ return documents
480
+ except Exception as e:
481
+ log_error(f"Error in keyword search: {e}")
482
+ return []
483
+
484
+ def hybrid_search(self, query: str, limit: int = 5) -> List[Document]:
485
+ """Perform hybrid search combining vector and keyword search."""
486
+ try:
487
+ # Get query embedding
488
+ query_embedding = array_to_buffer(self.embedder.get_embedding(query), "float32")
489
+
490
+ # Create vector query
491
+ vector_query = HybridQuery(
492
+ vector=query_embedding,
493
+ vector_field_name="embedding",
494
+ text=query,
495
+ text_field_name="content",
496
+ alpha=self.vector_score_weight,
497
+ return_fields=["id", "name", "content"],
498
+ num_results=limit,
499
+ )
500
+
501
+ # Execute search
502
+ results = self.index.query(vector_query)
503
+ parsed = convert_bytes(results)
504
+
505
+ # Convert results to documents
506
+ documents = [Document.from_dict(p) for p in parsed]
507
+
508
+ return documents
509
+ except Exception as e:
510
+ log_error(f"Error in hybrid search: {e}")
511
+ return []
512
+
513
+ def drop(self) -> bool: # type: ignore[override]
514
+ """Drop the Redis index."""
515
+ try:
516
+ self.index.delete(drop=True)
517
+ log_debug(f"Deleted Redis index: {self.index_name}")
518
+ return True
519
+ except Exception as e:
520
+ log_error(f"Error dropping Redis index: {e}")
521
+ return False
522
+
523
+ async def async_drop(self) -> None:
524
+ """Async version of drop method."""
525
+ try:
526
+ async_index = await self._get_async_index()
527
+ await async_index.delete(drop=True)
528
+ log_debug(f"Deleted Redis index: {self.index_name}")
529
+ except Exception as e:
530
+ log_error(f"Error dropping Redis index: {e}")
531
+ raise
532
+
533
+ def exists(self) -> bool:
534
+ """Check if the Redis index exists."""
535
+ try:
536
+ return self.index.exists()
537
+ except Exception as e:
538
+ log_error(f"Error checking if index exists: {e}")
539
+ return False
540
+
541
+ async def async_exists(self) -> bool:
542
+ """Async version of exists method."""
543
+ try:
544
+ async_index = await self._get_async_index()
545
+ return await async_index.exists()
546
+ except Exception as e:
547
+ log_error(f"Error checking if index exists: {e}")
548
+ return False
549
+
550
+ def optimize(self) -> None:
551
+ """Optimize the Redis index (no-op for Redis)."""
552
+ log_debug("Redis optimization not required")
553
+ pass
554
+
555
+ def delete(self) -> bool:
556
+ """Delete the Redis index (same as drop)."""
557
+ try:
558
+ self.index.clear()
559
+ return True
560
+ except Exception as e:
561
+ log_error(f"Error deleting Redis index: {e}")
562
+ return False
563
+
564
+ def delete_by_id(self, id: str) -> bool:
565
+ """Delete documents by ID."""
566
+ try:
567
+ # Use RedisVL to drop documents by document ID
568
+ result = self.index.drop_documents(id)
569
+ log_debug(f"Deleted document with id '{id}' from Redis index")
570
+ return result > 0
571
+ except Exception as e:
572
+ log_error(f"Error deleting document by ID: {e}")
573
+ return False
574
+
575
+ def delete_by_name(self, name: str) -> bool:
576
+ """Delete documents by name."""
577
+ try:
578
+ # First find documents with the given name
579
+ name_filter = Tag("name") == name
580
+ query = FilterQuery(
581
+ filter_expression=name_filter,
582
+ return_fields=["id"],
583
+ num_results=1000, # Get all matching documents
584
+ )
585
+ results = self.index.query(query)
586
+ parsed = convert_bytes(results)
587
+
588
+ # Delete each found document by key (result['id'] is the Redis key)
589
+ deleted_count = 0
590
+ for result in parsed:
591
+ key = result.get("id")
592
+ if key:
593
+ deleted_count += self.index.drop_keys(key)
594
+
595
+ log_debug(f"Deleted {deleted_count} documents with name '{name}'")
596
+ return deleted_count > 0
597
+ except Exception as e:
598
+ log_error(f"Error deleting documents by name: {e}")
599
+ return False
600
+
601
+ def delete_by_metadata(self, metadata: Dict[str, Any]) -> bool:
602
+ """Delete documents by metadata."""
603
+ try:
604
+ # Build filter expression for metadata using Tag filters
605
+ filters = []
606
+ for key, value in metadata.items():
607
+ filters.append(Tag(key) == str(value))
608
+
609
+ # Combine filters with AND logic
610
+ if len(filters) == 1:
611
+ combined_filter = filters[0]
612
+ else:
613
+ combined_filter = filters[0]
614
+ for f in filters[1:]:
615
+ combined_filter = combined_filter & f
616
+
617
+ # Find documents with the given metadata
618
+ query = FilterQuery(
619
+ filter_expression=combined_filter,
620
+ return_fields=["id"],
621
+ num_results=1000, # Get all matching documents
622
+ )
623
+ results = self.index.query(query)
624
+ parsed = convert_bytes(results)
625
+
626
+ # Delete each found document by key (result['id'] is the Redis key)
627
+ deleted_count = 0
628
+ for result in parsed:
629
+ key = result.get("id")
630
+ if key:
631
+ deleted_count += self.index.drop_keys(key)
632
+
633
+ log_debug(f"Deleted {deleted_count} documents with metadata {metadata}")
634
+ return deleted_count > 0
635
+ except Exception as e:
636
+ log_error(f"Error deleting documents by metadata: {e}")
637
+ return False
638
+
639
+ def delete_by_content_id(self, content_id: str) -> bool:
640
+ """Delete documents by content ID."""
641
+ try:
642
+ # Find documents with the given content_id
643
+ content_id_filter = Tag("content_id") == content_id
644
+ query = FilterQuery(
645
+ filter_expression=content_id_filter,
646
+ return_fields=["id"],
647
+ num_results=1000, # Get all matching documents
648
+ )
649
+ results = self.index.query(query)
650
+ parsed = convert_bytes(results)
651
+
652
+ # Delete each found document by key (result['id'] is the Redis key)
653
+ deleted_count = 0
654
+ for result in parsed:
655
+ key = result.get("id")
656
+ if key:
657
+ deleted_count += self.index.drop_keys(key)
658
+
659
+ log_debug(f"Deleted {deleted_count} documents with content_id '{content_id}'")
660
+ return deleted_count > 0
661
+ except Exception as e:
662
+ log_error(f"Error deleting documents by content_id: {e}")
663
+ return False
664
+
665
+ def update_metadata(self, content_id: str, metadata: Dict[str, Any]) -> None:
666
+ """Update metadata for documents with the given content ID."""
667
+ try:
668
+ # Find documents with the given content_id
669
+ content_id_filter = Tag("content_id") == content_id
670
+ query = FilterQuery(
671
+ filter_expression=content_id_filter,
672
+ return_fields=["id"],
673
+ num_results=1000, # Get all matching documents
674
+ )
675
+ results = self.index.query(query)
676
+
677
+ # Update metadata for each found document
678
+ for result in results:
679
+ doc_id = result.get("id")
680
+ if doc_id:
681
+ # result['id'] is the Redis key
682
+ key = result.get("id")
683
+ # Update the hash with new metadata
684
+ if key:
685
+ self.redis_client.hset(key, mapping=metadata)
686
+
687
+ log_debug(f"Updated metadata for documents with content_id '{content_id}'")
688
+ except Exception as e:
689
+ log_error(f"Error updating metadata: {e}")
690
+ raise
691
+
692
+ def get_supported_search_types(self) -> List[str]:
693
+ """Get list of supported search types."""
694
+ return ["vector", "keyword", "hybrid"]