agno 2.1.2__py3-none-any.whl → 2.3.13__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 (314) hide show
  1. agno/agent/agent.py +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,682 @@
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.knowledge.reranker.base import Reranker
19
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
20
+ from agno.utils.string import hash_string_sha256
21
+ from agno.vectordb.base import VectorDb
22
+ from agno.vectordb.distance import Distance
23
+ from agno.vectordb.search import SearchType
24
+
25
+
26
+ class RedisDB(VectorDb):
27
+ """
28
+ Redis class for managing vector operations with Redis and RedisVL.
29
+
30
+ This class provides methods for creating, inserting, searching, and managing
31
+ vector data in a Redis database using the RedisVL library.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ index_name: str,
37
+ redis_url: Optional[str] = None,
38
+ redis_client: Optional[Redis] = None,
39
+ embedder: Optional[Embedder] = None,
40
+ search_type: SearchType = SearchType.vector,
41
+ distance: Distance = Distance.cosine,
42
+ vector_score_weight: float = 0.7,
43
+ reranker: Optional[Reranker] = None,
44
+ **redis_kwargs,
45
+ ):
46
+ """
47
+ Initialize the Redis instance.
48
+
49
+ Args:
50
+ index_name (str): Name of the Redis index to store vector data.
51
+ redis_url (Optional[str]): Redis connection URL.
52
+ redis_client (Optional[redis.Redis]): Redis client instance.
53
+ embedder (Optional[Embedder]): Embedder instance for creating embeddings.
54
+ search_type (SearchType): Type of search to perform.
55
+ distance (Distance): Distance metric for vector comparisons.
56
+ vector_score_weight (float): Weight for vector similarity in hybrid search.
57
+ reranker (Optional[Reranker]): Reranker instance.
58
+ **redis_kwargs: Additional Redis connection parameters.
59
+ """
60
+ if not index_name:
61
+ raise ValueError("Index name must be provided.")
62
+
63
+ if redis_client is None and redis_url is None:
64
+ raise ValueError("Either 'redis_url' or 'redis_client' must be provided.")
65
+
66
+ self.redis_url = redis_url
67
+
68
+ # Initialize Redis client
69
+ if redis_client is None:
70
+ assert redis_url is not None
71
+ self.redis_client = Redis.from_url(redis_url, **redis_kwargs)
72
+ else:
73
+ self.redis_client = redis_client
74
+
75
+ # Index settings
76
+ self.index_name: str = index_name
77
+
78
+ # Embedder for embedding the document contents
79
+ if embedder is None:
80
+ from agno.knowledge.embedder.openai import OpenAIEmbedder
81
+
82
+ embedder = OpenAIEmbedder()
83
+ log_info("Embedder not provided, using OpenAIEmbedder as default.")
84
+
85
+ self.embedder: Embedder = embedder
86
+ self.dimensions: Optional[int] = self.embedder.dimensions
87
+
88
+ if self.dimensions is None:
89
+ raise ValueError("Embedder.dimensions must be set.")
90
+
91
+ # Search type and distance metric
92
+ self.search_type: SearchType = search_type
93
+ self.distance: Distance = distance
94
+ self.vector_score_weight: float = vector_score_weight
95
+
96
+ # Reranker instance
97
+ self.reranker: Optional[Reranker] = reranker
98
+
99
+ # Create index schema
100
+ self.schema = self._get_schema()
101
+ self.index = self._create_index()
102
+ self.meta_data_fields: set[str] = set()
103
+
104
+ # Async components - created lazily when needed
105
+ self._async_redis_client: Optional[AsyncRedis] = None
106
+ self._async_index: Optional[AsyncSearchIndex] = None
107
+
108
+ log_debug(f"Initialized Redis with index '{self.index_name}'")
109
+
110
+ async def _get_async_index(self) -> AsyncSearchIndex:
111
+ """Get or create the async index and client."""
112
+ if self._async_index is None:
113
+ if self.redis_url is None:
114
+ raise ValueError("redis_url must be provided for async operations")
115
+ url: str = self.redis_url
116
+ self._async_redis_client = AsyncRedis.from_url(url)
117
+ self._async_index = AsyncSearchIndex(schema=self.schema, redis_client=self._async_redis_client)
118
+ return self._async_index
119
+
120
+ def _get_schema(self):
121
+ """Get default redis schema"""
122
+ distance_mapping = {
123
+ Distance.cosine: "cosine",
124
+ Distance.l2: "l2",
125
+ Distance.max_inner_product: "ip",
126
+ }
127
+
128
+ return IndexSchema.from_dict(
129
+ {
130
+ "index": {
131
+ "name": self.index_name,
132
+ "prefix": f"{self.index_name}:",
133
+ "storage_type": "hash",
134
+ },
135
+ "fields": [
136
+ {"name": "id", "type": "tag"},
137
+ {"name": "name", "type": "tag"},
138
+ {"name": "content", "type": "text"},
139
+ {"name": "content_hash", "type": "tag"},
140
+ {"name": "content_id", "type": "tag"},
141
+ # Common metadata fields used in operations/tests
142
+ {"name": "status", "type": "tag"},
143
+ {"name": "category", "type": "tag"},
144
+ {"name": "tag", "type": "tag"},
145
+ {"name": "source", "type": "tag"},
146
+ {"name": "mode", "type": "tag"},
147
+ {
148
+ "name": "embedding",
149
+ "type": "vector",
150
+ "attrs": {
151
+ "dims": self.dimensions,
152
+ "distance_metric": distance_mapping[self.distance],
153
+ "algorithm": "flat",
154
+ },
155
+ },
156
+ ],
157
+ }
158
+ )
159
+
160
+ def _create_index(self) -> SearchIndex:
161
+ """Create the RedisVL index object for this schema."""
162
+ return SearchIndex(self.schema, redis_url=self.redis_url)
163
+
164
+ def create(self) -> None:
165
+ """Create the Redis index if it does not exist."""
166
+ try:
167
+ if not self.exists():
168
+ self.index.create()
169
+ log_debug(f"Created Redis index: {self.index_name}")
170
+ else:
171
+ log_debug(f"Redis index already exists: {self.index_name}")
172
+ except Exception as e:
173
+ log_error(f"Error creating Redis index: {e}")
174
+ raise
175
+
176
+ async def async_create(self) -> None:
177
+ """Async version of create method."""
178
+ try:
179
+ async_index = await self._get_async_index()
180
+ await async_index.create(overwrite=False, drop=False)
181
+ log_debug(f"Created Redis index: {self.index_name}")
182
+ except Exception as e:
183
+ if "already exists" in str(e).lower():
184
+ log_debug(f"Redis index already exists: {self.index_name}")
185
+ else:
186
+ log_error(f"Error creating Redis index: {e}")
187
+ raise
188
+
189
+ def name_exists(self, name: str) -> bool:
190
+ """Check if a document with the given name exists."""
191
+ try:
192
+ name_filter = Tag("name") == name
193
+ query = FilterQuery(
194
+ filter_expression=name_filter,
195
+ return_fields=["id"],
196
+ num_results=1,
197
+ )
198
+ results = self.index.query(query)
199
+ return len(results) > 0
200
+ except Exception as e:
201
+ log_error(f"Error checking if name exists: {e}")
202
+ return False
203
+
204
+ async def async_name_exists(self, name: str) -> bool: # type: ignore[override]
205
+ """Async version of name_exists method."""
206
+ try:
207
+ async_index = await self._get_async_index()
208
+ name_filter = Tag("name") == name
209
+ query = FilterQuery(
210
+ filter_expression=name_filter,
211
+ return_fields=["id"],
212
+ num_results=1,
213
+ )
214
+ results = await async_index.query(query)
215
+ return len(results) > 0
216
+ except Exception as e:
217
+ log_error(f"Error checking if name exists: {e}")
218
+ return False
219
+
220
+ def id_exists(self, id: str) -> bool:
221
+ """Check if a document with the given ID exists."""
222
+ try:
223
+ id_filter = Tag("id") == id
224
+ query = FilterQuery(
225
+ filter_expression=id_filter,
226
+ return_fields=["id"],
227
+ num_results=1,
228
+ )
229
+ results = self.index.query(query)
230
+ return len(results) > 0
231
+ except Exception as e:
232
+ log_error(f"Error checking if ID exists: {e}")
233
+ return False
234
+
235
+ def content_hash_exists(self, content_hash: str) -> bool:
236
+ """Check if a document with the given content hash exists."""
237
+ try:
238
+ content_hash_filter = Tag("content_hash") == content_hash
239
+ query = FilterQuery(
240
+ filter_expression=content_hash_filter,
241
+ return_fields=["id"],
242
+ num_results=1,
243
+ )
244
+ results = self.index.query(query)
245
+ return len(results) > 0
246
+ except Exception as e:
247
+ log_error(f"Error checking if content hash exists: {e}")
248
+ return False
249
+
250
+ def _parse_redis_hash(self, doc: Document):
251
+ """
252
+ Create object serializable into Redis HASH structure
253
+ """
254
+ doc_dict = doc.to_dict()
255
+ # Ensure an ID is present; derive a deterministic one from content when missing
256
+ doc_id = doc.id or hash_string_sha256(doc.content)
257
+ doc_dict["id"] = doc_id
258
+ if not doc.embedding:
259
+ doc.embed(self.embedder)
260
+
261
+ # TODO: determine how we want to handle dtypes
262
+ doc_dict["embedding"] = array_to_buffer(doc.embedding, "float32")
263
+
264
+ # Add content_id if available
265
+ if hasattr(doc, "content_id") and doc.content_id:
266
+ doc_dict["content_id"] = doc.content_id
267
+
268
+ if "meta_data" in doc_dict:
269
+ meta_data = doc_dict.pop("meta_data", {})
270
+ for md in meta_data:
271
+ self.meta_data_fields.add(md)
272
+ doc_dict.update(meta_data)
273
+
274
+ return doc_dict
275
+
276
+ def insert(
277
+ self,
278
+ content_hash: str,
279
+ documents: List[Document],
280
+ filters: Optional[Dict[str, Any]] = None,
281
+ ) -> None:
282
+ """Insert documents into the Redis index."""
283
+ try:
284
+ # Store content hash for tracking
285
+ parsed_documents = []
286
+ for doc in documents:
287
+ parsed_doc = self._parse_redis_hash(doc)
288
+ parsed_doc["content_hash"] = content_hash
289
+ parsed_documents.append(parsed_doc)
290
+
291
+ self.index.load(parsed_documents, id_field="id")
292
+ log_debug(f"Inserted {len(documents)} documents with content_hash: {content_hash}")
293
+ except Exception as e:
294
+ log_error(f"Error inserting documents: {e}")
295
+ raise
296
+
297
+ async def async_insert(
298
+ self,
299
+ content_hash: str,
300
+ documents: List[Document],
301
+ filters: Optional[Dict[str, Any]] = None,
302
+ ) -> None:
303
+ """Async version of insert method."""
304
+ try:
305
+ async_index = await self._get_async_index()
306
+ parsed_documents = []
307
+ for doc in documents:
308
+ parsed_doc = self._parse_redis_hash(doc)
309
+ parsed_doc["content_hash"] = content_hash
310
+ parsed_documents.append(parsed_doc)
311
+ await async_index.load(parsed_documents, id_field="id")
312
+ log_debug(f"Inserted {len(documents)} documents with content_hash: {content_hash}")
313
+ except Exception as e:
314
+ log_error(f"Error inserting documents: {e}")
315
+ raise
316
+
317
+ def upsert_available(self) -> bool:
318
+ """Check if upsert is available (always True for Redis)."""
319
+ return True
320
+
321
+ def upsert(
322
+ self,
323
+ content_hash: str,
324
+ documents: List[Document],
325
+ filters: Optional[Dict[str, Any]] = None,
326
+ ) -> None:
327
+ """Upsert documents into the Redis index.
328
+ Strategy: delete existing docs with the same content_hash, then insert new docs.
329
+ """
330
+ try:
331
+ # Find existing docs for this content_hash and delete them
332
+ ch_filter = Tag("content_hash") == content_hash
333
+ query = FilterQuery(
334
+ filter_expression=ch_filter,
335
+ return_fields=["id"],
336
+ num_results=1000,
337
+ )
338
+ existing = self.index.query(query)
339
+ parsed = convert_bytes(existing)
340
+ for r in parsed:
341
+ key = r.get("id")
342
+ if key:
343
+ self.index.drop_keys(key)
344
+
345
+ # Insert new docs
346
+ self.insert(content_hash, documents, filters)
347
+ except Exception as e:
348
+ log_error(f"Error upserting documents: {e}")
349
+ raise
350
+
351
+ async def async_upsert(
352
+ self,
353
+ content_hash: str,
354
+ documents: List[Document],
355
+ filters: Optional[Dict[str, Any]] = None,
356
+ ) -> None:
357
+ """Async version of upsert method.
358
+ Strategy: delete existing docs with the same content_hash, then insert new docs.
359
+ """
360
+ try:
361
+ async_index = await self._get_async_index()
362
+
363
+ # Find existing docs for this content_hash and delete them
364
+ ch_filter = Tag("content_hash") == content_hash
365
+ query = FilterQuery(
366
+ filter_expression=ch_filter,
367
+ return_fields=["id"],
368
+ num_results=1000,
369
+ )
370
+ existing = await async_index.query(query)
371
+ parsed = convert_bytes(existing)
372
+ for r in parsed:
373
+ key = r.get("id")
374
+ if key:
375
+ await async_index.drop_keys(key)
376
+
377
+ # Insert new docs
378
+ await self.async_insert(content_hash, documents, filters)
379
+ except Exception as e:
380
+ log_error(f"Error upserting documents: {e}")
381
+ raise
382
+
383
+ def search(
384
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
385
+ ) -> List[Document]:
386
+ """Search for documents using the specified search type."""
387
+
388
+ if filters and isinstance(filters, List):
389
+ log_warning("Filters Expressions are not supported in Redis. No filters will be applied.")
390
+ filters = None
391
+ try:
392
+ if self.search_type == SearchType.vector:
393
+ return self.vector_search(query, limit)
394
+ elif self.search_type == SearchType.keyword:
395
+ return self.keyword_search(query, limit)
396
+ elif self.search_type == SearchType.hybrid:
397
+ return self.hybrid_search(query, limit)
398
+ else:
399
+ raise ValueError(f"Unsupported search type: {self.search_type}")
400
+ except Exception as e:
401
+ log_error(f"Error in search: {e}")
402
+ return []
403
+
404
+ async def async_search(
405
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
406
+ ) -> List[Document]:
407
+ """Async version of search method."""
408
+ return await asyncio.to_thread(self.search, query, limit, filters)
409
+
410
+ def vector_search(self, query: str, limit: int = 5) -> List[Document]:
411
+ """Perform vector similarity search."""
412
+ try:
413
+ # Get query embedding
414
+ query_embedding = array_to_buffer(self.embedder.get_embedding(query), "float32")
415
+
416
+ # TODO: do we want to pass back the embedding?
417
+ # Create vector query
418
+ vector_query = VectorQuery(
419
+ vector=query_embedding,
420
+ vector_field_name="embedding",
421
+ return_fields=["id", "name", "content"],
422
+ return_score=False,
423
+ num_results=limit,
424
+ )
425
+
426
+ # Execute search
427
+ results = self.index.query(vector_query)
428
+
429
+ # Convert results to documents
430
+ documents = [Document.from_dict(r) for r in results]
431
+
432
+ # Apply reranking if reranker is available
433
+ if self.reranker:
434
+ documents = self.reranker.rerank(query=query, documents=documents)
435
+
436
+ return documents
437
+ except Exception as e:
438
+ log_error(f"Error in vector search: {e}")
439
+ return []
440
+
441
+ def keyword_search(self, query: str, limit: int = 5) -> List[Document]:
442
+ """Perform keyword search using Redis text search."""
443
+ try:
444
+ # Create text query
445
+ text_query = TextQuery(
446
+ text=query,
447
+ text_field_name="content",
448
+ )
449
+
450
+ # Execute search
451
+ results = self.index.query(text_query)
452
+
453
+ # Convert results to documents
454
+ parsed = convert_bytes(results)
455
+
456
+ # Convert results to documents
457
+ documents = [Document.from_dict(p) for p in parsed]
458
+
459
+ # Apply reranking if reranker is available
460
+ if self.reranker:
461
+ documents = self.reranker.rerank(query=query, documents=documents)
462
+
463
+ return documents
464
+ except Exception as e:
465
+ log_error(f"Error in keyword search: {e}")
466
+ return []
467
+
468
+ def hybrid_search(self, query: str, limit: int = 5) -> List[Document]:
469
+ """Perform hybrid search combining vector and keyword search."""
470
+ try:
471
+ # Get query embedding
472
+ query_embedding = array_to_buffer(self.embedder.get_embedding(query), "float32")
473
+
474
+ # Create vector query
475
+ vector_query = HybridQuery(
476
+ vector=query_embedding,
477
+ vector_field_name="embedding",
478
+ text=query,
479
+ text_field_name="content",
480
+ alpha=self.vector_score_weight,
481
+ return_fields=["id", "name", "content"],
482
+ num_results=limit,
483
+ )
484
+
485
+ # Execute search
486
+ results = self.index.query(vector_query)
487
+ parsed = convert_bytes(results)
488
+
489
+ # Convert results to documents
490
+ documents = [Document.from_dict(p) for p in parsed]
491
+
492
+ # Apply reranking if reranker is available
493
+ if self.reranker:
494
+ documents = self.reranker.rerank(query=query, documents=documents)
495
+
496
+ return documents
497
+ except Exception as e:
498
+ log_error(f"Error in hybrid search: {e}")
499
+ return []
500
+
501
+ def drop(self) -> bool: # type: ignore[override]
502
+ """Drop the Redis index."""
503
+ try:
504
+ self.index.delete(drop=True)
505
+ log_debug(f"Deleted Redis index: {self.index_name}")
506
+ return True
507
+ except Exception as e:
508
+ log_error(f"Error dropping Redis index: {e}")
509
+ return False
510
+
511
+ async def async_drop(self) -> None:
512
+ """Async version of drop method."""
513
+ try:
514
+ async_index = await self._get_async_index()
515
+ await async_index.delete(drop=True)
516
+ log_debug(f"Deleted Redis index: {self.index_name}")
517
+ except Exception as e:
518
+ log_error(f"Error dropping Redis index: {e}")
519
+ raise
520
+
521
+ def exists(self) -> bool:
522
+ """Check if the Redis index exists."""
523
+ try:
524
+ return self.index.exists()
525
+ except Exception as e:
526
+ log_error(f"Error checking if index exists: {e}")
527
+ return False
528
+
529
+ async def async_exists(self) -> bool:
530
+ """Async version of exists method."""
531
+ try:
532
+ async_index = await self._get_async_index()
533
+ return await async_index.exists()
534
+ except Exception as e:
535
+ log_error(f"Error checking if index exists: {e}")
536
+ return False
537
+
538
+ def optimize(self) -> None:
539
+ """Optimize the Redis index (no-op for Redis)."""
540
+ log_debug("Redis optimization not required")
541
+ pass
542
+
543
+ def delete(self) -> bool:
544
+ """Delete the Redis index (same as drop)."""
545
+ try:
546
+ self.index.clear()
547
+ return True
548
+ except Exception as e:
549
+ log_error(f"Error deleting Redis index: {e}")
550
+ return False
551
+
552
+ def delete_by_id(self, id: str) -> bool:
553
+ """Delete documents by ID."""
554
+ try:
555
+ # Use RedisVL to drop documents by document ID
556
+ result = self.index.drop_documents(id)
557
+ log_debug(f"Deleted document with id '{id}' from Redis index")
558
+ return result > 0
559
+ except Exception as e:
560
+ log_error(f"Error deleting document by ID: {e}")
561
+ return False
562
+
563
+ def delete_by_name(self, name: str) -> bool:
564
+ """Delete documents by name."""
565
+ try:
566
+ # First find documents with the given name
567
+ name_filter = Tag("name") == name
568
+ query = FilterQuery(
569
+ filter_expression=name_filter,
570
+ return_fields=["id"],
571
+ num_results=1000, # Get all matching documents
572
+ )
573
+ results = self.index.query(query)
574
+ parsed = convert_bytes(results)
575
+
576
+ # Delete each found document by key (result['id'] is the Redis key)
577
+ deleted_count = 0
578
+ for result in parsed:
579
+ key = result.get("id")
580
+ if key:
581
+ deleted_count += self.index.drop_keys(key)
582
+
583
+ log_debug(f"Deleted {deleted_count} documents with name '{name}'")
584
+ return deleted_count > 0
585
+ except Exception as e:
586
+ log_error(f"Error deleting documents by name: {e}")
587
+ return False
588
+
589
+ def delete_by_metadata(self, metadata: Dict[str, Any]) -> bool:
590
+ """Delete documents by metadata."""
591
+ try:
592
+ # Build filter expression for metadata using Tag filters
593
+ filters = []
594
+ for key, value in metadata.items():
595
+ filters.append(Tag(key) == str(value))
596
+
597
+ # Combine filters with AND logic
598
+ if len(filters) == 1:
599
+ combined_filter = filters[0]
600
+ else:
601
+ combined_filter = filters[0]
602
+ for f in filters[1:]:
603
+ combined_filter = combined_filter & f
604
+
605
+ # Find documents with the given metadata
606
+ query = FilterQuery(
607
+ filter_expression=combined_filter,
608
+ return_fields=["id"],
609
+ num_results=1000, # Get all matching documents
610
+ )
611
+ results = self.index.query(query)
612
+ parsed = convert_bytes(results)
613
+
614
+ # Delete each found document by key (result['id'] is the Redis key)
615
+ deleted_count = 0
616
+ for result in parsed:
617
+ key = result.get("id")
618
+ if key:
619
+ deleted_count += self.index.drop_keys(key)
620
+
621
+ log_debug(f"Deleted {deleted_count} documents with metadata {metadata}")
622
+ return deleted_count > 0
623
+ except Exception as e:
624
+ log_error(f"Error deleting documents by metadata: {e}")
625
+ return False
626
+
627
+ def delete_by_content_id(self, content_id: str) -> bool:
628
+ """Delete documents by content ID."""
629
+ try:
630
+ # Find documents with the given content_id
631
+ content_id_filter = Tag("content_id") == content_id
632
+ query = FilterQuery(
633
+ filter_expression=content_id_filter,
634
+ return_fields=["id"],
635
+ num_results=1000, # Get all matching documents
636
+ )
637
+ results = self.index.query(query)
638
+ parsed = convert_bytes(results)
639
+
640
+ # Delete each found document by key (result['id'] is the Redis key)
641
+ deleted_count = 0
642
+ for result in parsed:
643
+ key = result.get("id")
644
+ if key:
645
+ deleted_count += self.index.drop_keys(key)
646
+
647
+ log_debug(f"Deleted {deleted_count} documents with content_id '{content_id}'")
648
+ return deleted_count > 0
649
+ except Exception as e:
650
+ log_error(f"Error deleting documents by content_id: {e}")
651
+ return False
652
+
653
+ def update_metadata(self, content_id: str, metadata: Dict[str, Any]) -> None:
654
+ """Update metadata for documents with the given content ID."""
655
+ try:
656
+ # Find documents with the given content_id
657
+ content_id_filter = Tag("content_id") == content_id
658
+ query = FilterQuery(
659
+ filter_expression=content_id_filter,
660
+ return_fields=["id"],
661
+ num_results=1000, # Get all matching documents
662
+ )
663
+ results = self.index.query(query)
664
+
665
+ # Update metadata for each found document
666
+ for result in results:
667
+ doc_id = result.get("id")
668
+ if doc_id:
669
+ # result['id'] is the Redis key
670
+ key = result.get("id")
671
+ # Update the hash with new metadata
672
+ if key:
673
+ self.redis_client.hset(key, mapping=metadata)
674
+
675
+ log_debug(f"Updated metadata for documents with content_id '{content_id}'")
676
+ except Exception as e:
677
+ log_error(f"Error updating metadata: {e}")
678
+ raise
679
+
680
+ def get_supported_search_types(self) -> List[str]:
681
+ """Get list of supported search types."""
682
+ return ["vector", "keyword", "hybrid"]