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
@@ -3,15 +3,18 @@ from hashlib import md5
3
3
  from math import sqrt
4
4
  from typing import Any, Dict, List, Optional, Union, cast
5
5
 
6
+ from agno.utils.string import generate_id
7
+
6
8
  try:
7
- from sqlalchemy import update
9
+ from sqlalchemy import and_, not_, or_, update
8
10
  from sqlalchemy.dialects import postgresql
9
11
  from sqlalchemy.engine import Engine, create_engine
10
12
  from sqlalchemy.inspection import inspect
11
13
  from sqlalchemy.orm import Session, scoped_session, sessionmaker
12
14
  from sqlalchemy.schema import Column, Index, MetaData, Table
15
+ from sqlalchemy.sql.elements import ColumnElement
13
16
  from sqlalchemy.sql.expression import bindparam, desc, func, select, text
14
- from sqlalchemy.types import DateTime, String
17
+ from sqlalchemy.types import DateTime, Integer, String
15
18
 
16
19
  except ImportError:
17
20
  raise ImportError("`sqlalchemy` not installed. Please install using `pip install sqlalchemy psycopg`")
@@ -21,10 +24,11 @@ try:
21
24
  except ImportError:
22
25
  raise ImportError("`pgvector` not installed. Please install using `pip install pgvector`")
23
26
 
27
+ from agno.filters import FilterExpr
24
28
  from agno.knowledge.document import Document
25
29
  from agno.knowledge.embedder import Embedder
26
30
  from agno.knowledge.reranker.base import Reranker
27
- from agno.utils.log import log_debug, log_info, logger
31
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
28
32
  from agno.vectordb.base import VectorDb
29
33
  from agno.vectordb.distance import Distance
30
34
  from agno.vectordb.pgvector.index import HNSW, Ivfflat
@@ -43,6 +47,9 @@ class PgVector(VectorDb):
43
47
  self,
44
48
  table_name: str,
45
49
  schema: str = "ai",
50
+ name: Optional[str] = None,
51
+ description: Optional[str] = None,
52
+ id: Optional[str] = None,
46
53
  db_url: Optional[str] = None,
47
54
  db_engine: Optional[Engine] = None,
48
55
  embedder: Optional[Embedder] = None,
@@ -55,6 +62,7 @@ class PgVector(VectorDb):
55
62
  schema_version: int = 1,
56
63
  auto_upgrade_schema: bool = False,
57
64
  reranker: Optional[Reranker] = None,
65
+ create_schema: bool = True,
58
66
  ):
59
67
  """
60
68
  Initialize the PgVector instance.
@@ -62,6 +70,8 @@ class PgVector(VectorDb):
62
70
  Args:
63
71
  table_name (str): Name of the table to store vector data.
64
72
  schema (str): Database schema name.
73
+ name (Optional[str]): Name of the vector database.
74
+ description (Optional[str]): Description of the vector database.
65
75
  db_url (Optional[str]): Database connection URL.
66
76
  db_engine (Optional[Engine]): SQLAlchemy database engine.
67
77
  embedder (Optional[Embedder]): Embedder instance for creating embeddings.
@@ -73,6 +83,8 @@ class PgVector(VectorDb):
73
83
  content_language (str): Language for full-text search.
74
84
  schema_version (int): Version of the database schema.
75
85
  auto_upgrade_schema (bool): Automatically upgrade schema if True.
86
+ create_schema (bool): Whether to automatically create the database schema if it doesn't exist.
87
+ Set to False if schema is managed externally (e.g., via migrations). Defaults to True.
76
88
  """
77
89
  if not table_name:
78
90
  raise ValueError("Table name must be provided.")
@@ -80,13 +92,22 @@ class PgVector(VectorDb):
80
92
  if db_engine is None and db_url is None:
81
93
  raise ValueError("Either 'db_url' or 'db_engine' must be provided.")
82
94
 
95
+ if id is None:
96
+ base_seed = db_url or str(db_engine.url) # type: ignore
97
+ schema_suffix = table_name if table_name is not None else "ai"
98
+ seed = f"{base_seed}#{schema_suffix}"
99
+ id = generate_id(seed)
100
+
101
+ # Initialize base class with name and description
102
+ super().__init__(id=id, name=name, description=description)
103
+
83
104
  if db_engine is None:
84
105
  if db_url is None:
85
106
  raise ValueError("Must provide 'db_url' if 'db_engine' is None.")
86
107
  try:
87
108
  db_engine = create_engine(db_url)
88
109
  except Exception as e:
89
- logger.error(f"Failed to create engine from 'db_url': {e}")
110
+ log_error(f"Failed to create engine from 'db_url': {e}")
90
111
  raise
91
112
 
92
113
  # Database settings
@@ -129,6 +150,9 @@ class PgVector(VectorDb):
129
150
  # Reranker instance
130
151
  self.reranker: Optional[Reranker] = reranker
131
152
 
153
+ # Schema creation flag
154
+ self.create_schema: bool = create_schema
155
+
132
156
  # Database session
133
157
  self.Session: scoped_session = scoped_session(sessionmaker(bind=self.db_engine))
134
158
  # Database table
@@ -191,7 +215,7 @@ class PgVector(VectorDb):
191
215
  try:
192
216
  return inspect(self.db_engine).has_table(self.table_name, schema=self.schema)
193
217
  except Exception as e:
194
- logger.error(f"Error checking if table exists: {e}")
218
+ log_error(f"Error checking if table exists: {e}")
195
219
  return False
196
220
 
197
221
  def create(self) -> None:
@@ -202,7 +226,7 @@ class PgVector(VectorDb):
202
226
  with self.Session() as sess, sess.begin():
203
227
  log_debug("Creating extension: vector")
204
228
  sess.execute(text("CREATE EXTENSION IF NOT EXISTS vector;"))
205
- if self.schema is not None:
229
+ if self.create_schema and self.schema is not None:
206
230
  log_debug(f"Creating schema: {self.schema}")
207
231
  sess.execute(text(f"CREATE SCHEMA IF NOT EXISTS {self.schema};"))
208
232
  log_debug(f"Creating table: {self.table_name}")
@@ -229,7 +253,7 @@ class PgVector(VectorDb):
229
253
  result = sess.execute(stmt).first()
230
254
  return result is not None
231
255
  except Exception as e:
232
- logger.error(f"Error checking if record exists: {e}")
256
+ log_error(f"Error checking if record exists: {e}")
233
257
  return False
234
258
 
235
259
  def name_exists(self, name: str) -> bool:
@@ -306,7 +330,7 @@ class PgVector(VectorDb):
306
330
  try:
307
331
  batch_records.append(self._get_document_record(doc, filters, content_hash))
308
332
  except Exception as e:
309
- logger.error(f"Error processing document '{doc.name}': {e}")
333
+ log_error(f"Error processing document '{doc.name}': {e}")
310
334
 
311
335
  # Insert the batch of records
312
336
  insert_stmt = postgresql.insert(self.table)
@@ -314,11 +338,11 @@ class PgVector(VectorDb):
314
338
  sess.commit() # Commit batch independently
315
339
  log_info(f"Inserted batch of {len(batch_records)} documents.")
316
340
  except Exception as e:
317
- logger.error(f"Error with batch starting at index {i}: {e}")
341
+ log_error(f"Error with batch starting at index {i}: {e}")
318
342
  sess.rollback() # Rollback the current batch if there's an error
319
343
  raise
320
344
  except Exception as e:
321
- logger.error(f"Error inserting documents: {e}")
345
+ log_error(f"Error inserting documents: {e}")
322
346
  raise
323
347
 
324
348
  async def async_insert(
@@ -343,7 +367,10 @@ class PgVector(VectorDb):
343
367
  for doc in batch_docs:
344
368
  try:
345
369
  cleaned_content = self._clean_content(doc.content)
346
- record_id = doc.id or content_hash
370
+ # Include content_hash in ID to ensure uniqueness across different content hashes
371
+ # This allows the same URL/content to be inserted with different descriptions
372
+ base_id = doc.id or md5(cleaned_content.encode()).hexdigest()
373
+ record_id = md5(f"{base_id}_{content_hash}".encode()).hexdigest()
347
374
 
348
375
  meta_data = doc.meta_data or {}
349
376
  if filters:
@@ -362,7 +389,7 @@ class PgVector(VectorDb):
362
389
  }
363
390
  batch_records.append(record)
364
391
  except Exception as e:
365
- logger.error(f"Error processing document '{doc.name}': {e}")
392
+ log_error(f"Error processing document '{doc.name}': {e}")
366
393
 
367
394
  # Insert the batch of records
368
395
  if batch_records:
@@ -371,11 +398,11 @@ class PgVector(VectorDb):
371
398
  sess.commit() # Commit batch independently
372
399
  log_info(f"Inserted batch of {len(batch_records)} documents.")
373
400
  except Exception as e:
374
- logger.error(f"Error with batch starting at index {i}: {e}")
401
+ log_error(f"Error with batch starting at index {i}: {e}")
375
402
  sess.rollback() # Rollback the current batch if there's an error
376
403
  raise
377
404
  except Exception as e:
378
- logger.error(f"Error inserting documents: {e}")
405
+ log_error(f"Error inserting documents: {e}")
379
406
  raise
380
407
 
381
408
  def upsert_available(self) -> bool:
@@ -404,7 +431,7 @@ class PgVector(VectorDb):
404
431
  self._delete_by_content_hash(content_hash)
405
432
  self._upsert(content_hash, documents, filters, batch_size)
406
433
  except Exception as e:
407
- logger.error(f"Error upserting documents by content hash: {e}")
434
+ log_error(f"Error upserting documents by content hash: {e}")
408
435
  raise
409
436
 
410
437
  def _upsert(
@@ -432,9 +459,11 @@ class PgVector(VectorDb):
432
459
  batch_records_dict: Dict[str, Dict[str, Any]] = {} # Use dict to deduplicate by ID
433
460
  for doc in batch_docs:
434
461
  try:
435
- batch_records_dict[doc.id] = self._get_document_record(doc, filters, content_hash) # type: ignore
462
+ record = self._get_document_record(doc, filters, content_hash)
463
+ # Use the generated record ID (which includes content_hash) for deduplication
464
+ batch_records_dict[record["id"]] = record
436
465
  except Exception as e:
437
- logger.error(f"Error processing document '{doc.name}': {e}")
466
+ log_error(f"Error processing document '{doc.name}': {e}")
438
467
 
439
468
  # Convert dict to list for upsert
440
469
  batch_records = list(batch_records_dict.values())
@@ -461,11 +490,11 @@ class PgVector(VectorDb):
461
490
  sess.commit() # Commit batch independently
462
491
  log_info(f"Upserted batch of {len(batch_records)} documents.")
463
492
  except Exception as e:
464
- logger.error(f"Error with batch starting at index {i}: {e}")
493
+ log_error(f"Error with batch starting at index {i}: {e}")
465
494
  sess.rollback() # Rollback the current batch if there's an error
466
495
  raise
467
496
  except Exception as e:
468
- logger.error(f"Error upserting documents: {e}")
497
+ log_error(f"Error upserting documents: {e}")
469
498
  raise
470
499
 
471
500
  def _get_document_record(
@@ -473,7 +502,10 @@ class PgVector(VectorDb):
473
502
  ) -> Dict[str, Any]:
474
503
  doc.embed(embedder=self.embedder)
475
504
  cleaned_content = self._clean_content(doc.content)
476
- record_id = doc.id or content_hash
505
+ # Include content_hash in ID to ensure uniqueness across different content hashes
506
+ # This allows the same URL/content to be inserted with different descriptions
507
+ base_id = doc.id or md5(cleaned_content.encode()).hexdigest()
508
+ record_id = md5(f"{base_id}_{content_hash}".encode()).hexdigest()
477
509
 
478
510
  meta_data = doc.meta_data or {}
479
511
  if filters:
@@ -514,7 +546,7 @@ class PgVector(VectorDb):
514
546
  doc.embedding = embeddings[j]
515
547
  doc.usage = usages[j] if j < len(usages) else None
516
548
  except Exception as e:
517
- logger.error(f"Error assigning batch embedding to document '{doc.name}': {e}")
549
+ log_error(f"Error assigning batch embedding to document '{doc.name}': {e}")
518
550
 
519
551
  except Exception as e:
520
552
  # Check if this is a rate limit error - don't fall back as it would make things worse
@@ -525,17 +557,41 @@ class PgVector(VectorDb):
525
557
  )
526
558
 
527
559
  if is_rate_limit:
528
- logger.error(f"Rate limit detected during batch embedding. {e}")
560
+ log_error(f"Rate limit detected during batch embedding. {e}")
529
561
  raise e
530
562
  else:
531
- logger.warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
563
+ log_warning(f"Async batch embedding failed, falling back to individual embeddings: {e}")
532
564
  # Fall back to individual embedding
533
565
  embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in batch_docs]
534
- await asyncio.gather(*embed_tasks, return_exceptions=True)
566
+ results = await asyncio.gather(*embed_tasks, return_exceptions=True)
567
+
568
+ # Check for exceptions and handle them
569
+ for i, result in enumerate(results):
570
+ if isinstance(result, Exception):
571
+ error_msg = str(result)
572
+ # If it's an event loop closure error, log it but don't fail
573
+ if "Event loop is closed" in error_msg or "RuntimeError" in type(result).__name__:
574
+ log_warning(
575
+ f"Event loop closure during embedding for document {i}, but operation may have succeeded: {result}"
576
+ )
577
+ else:
578
+ log_error(f"Error embedding document {i}: {result}")
535
579
  else:
536
580
  # Use individual embedding
537
581
  embed_tasks = [doc.async_embed(embedder=self.embedder) for doc in batch_docs]
538
- await asyncio.gather(*embed_tasks, return_exceptions=True)
582
+ results = await asyncio.gather(*embed_tasks, return_exceptions=True)
583
+
584
+ # Check for exceptions and handle them
585
+ for i, result in enumerate(results):
586
+ if isinstance(result, Exception):
587
+ error_msg = str(result)
588
+ # If it's an event loop closure error, log it but don't fail
589
+ if "Event loop is closed" in error_msg or "RuntimeError" in type(result).__name__:
590
+ log_warning(
591
+ f"Event loop closure during embedding for document {i}, but operation may have succeeded: {result}"
592
+ )
593
+ else:
594
+ log_error(f"Error embedding document {i}: {result}")
539
595
 
540
596
  async def async_upsert(
541
597
  self,
@@ -550,7 +606,7 @@ class PgVector(VectorDb):
550
606
  self._delete_by_content_hash(content_hash)
551
607
  await self._async_upsert(content_hash, documents, filters, batch_size)
552
608
  except Exception as e:
553
- logger.error(f"Error upserting documents by content hash: {e}")
609
+ log_error(f"Error upserting documents by content hash: {e}")
554
610
  raise
555
611
 
556
612
  async def _async_upsert(
@@ -579,10 +635,20 @@ class PgVector(VectorDb):
579
635
 
580
636
  # Prepare documents for upserting
581
637
  batch_records_dict = {} # Use dict to deduplicate by ID
582
- for doc in batch_docs:
638
+ for idx, doc in enumerate(batch_docs):
583
639
  try:
584
640
  cleaned_content = self._clean_content(doc.content)
585
- record_id = md5(cleaned_content.encode()).hexdigest()
641
+ # Include content_hash in ID to ensure uniqueness across different content hashes
642
+ # This allows the same URL/content to be inserted with different descriptions
643
+ base_id = doc.id or md5(cleaned_content.encode()).hexdigest()
644
+ record_id = md5(f"{base_id}_{content_hash}".encode()).hexdigest()
645
+
646
+ if (
647
+ doc.embedding is not None
648
+ and isinstance(doc.embedding, list)
649
+ and len(doc.embedding) == 0
650
+ ):
651
+ log_warning(f"Document {idx} '{doc.name}' has empty embedding (length 0)")
586
652
 
587
653
  meta_data = doc.meta_data or {}
588
654
  if filters:
@@ -601,7 +667,7 @@ class PgVector(VectorDb):
601
667
  }
602
668
  batch_records_dict[record_id] = record # This deduplicates by ID
603
669
  except Exception as e:
604
- logger.error(f"Error processing document '{doc.name}': {e}")
670
+ log_error(f"Error processing document '{doc.name}': {e}")
605
671
 
606
672
  # Convert dict to list for upsert
607
673
  batch_records = list(batch_records_dict.values())
@@ -628,11 +694,11 @@ class PgVector(VectorDb):
628
694
  sess.commit() # Commit batch independently
629
695
  log_info(f"Upserted batch of {len(batch_records)} documents.")
630
696
  except Exception as e:
631
- logger.error(f"Error with batch starting at index {i}: {e}")
697
+ log_error(f"Error with batch starting at index {i}: {e}")
632
698
  sess.rollback() # Rollback the current batch if there's an error
633
699
  raise
634
700
  except Exception as e:
635
- logger.error(f"Error upserting documents: {e}")
701
+ log_error(f"Error upserting documents: {e}")
636
702
  raise
637
703
 
638
704
  def update_metadata(self, content_id: str, metadata: Dict[str, Any]) -> None:
@@ -640,38 +706,38 @@ class PgVector(VectorDb):
640
706
  Update the metadata for a document.
641
707
 
642
708
  Args:
643
- id (str): The ID of the document.
709
+ content_id (str): The ID of the document.
644
710
  metadata (Dict[str, Any]): The metadata to update.
645
711
  """
646
712
  try:
647
713
  with self.Session() as sess:
648
- # Merge JSONB instead of overwriting: coalesce(existing, '{}') || :new
714
+ # Merge JSONB for metadata, but replace filters entirely (absolute value)
649
715
  stmt = (
650
716
  update(self.table)
651
717
  .where(self.table.c.content_id == content_id)
652
718
  .values(
653
719
  meta_data=func.coalesce(self.table.c.meta_data, text("'{}'::jsonb")).op("||")(
654
- bindparam("md", metadata, type_=postgresql.JSONB)
655
- ),
656
- filters=func.coalesce(self.table.c.filters, text("'{}'::jsonb")).op("||")(
657
- bindparam("ft", metadata, type_=postgresql.JSONB)
720
+ bindparam("md", type_=postgresql.JSONB)
658
721
  ),
722
+ filters=bindparam("ft", type_=postgresql.JSONB),
659
723
  )
660
724
  )
661
- sess.execute(stmt)
725
+ sess.execute(stmt, {"md": metadata, "ft": metadata})
662
726
  sess.commit()
663
727
  except Exception as e:
664
- logger.error(f"Error updating metadata for document {content_id}: {e}")
728
+ log_error(f"Error updating metadata for document {content_id}: {e}")
665
729
  raise
666
730
 
667
- def search(self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None) -> List[Document]:
731
+ def search(
732
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
733
+ ) -> List[Document]:
668
734
  """
669
735
  Perform a search based on the configured search type.
670
736
 
671
737
  Args:
672
738
  query (str): The search query.
673
739
  limit (int): Maximum number of results to return.
674
- filters (Optional[Dict[str, Any]]): Filters to apply to the search.
740
+ filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
675
741
 
676
742
  Returns:
677
743
  List[Document]: List of matching documents.
@@ -683,23 +749,46 @@ class PgVector(VectorDb):
683
749
  elif self.search_type == SearchType.hybrid:
684
750
  return self.hybrid_search(query=query, limit=limit, filters=filters)
685
751
  else:
686
- logger.error(f"Invalid search type '{self.search_type}'.")
752
+ log_error(f"Invalid search type '{self.search_type}'.")
687
753
  return []
688
754
 
689
755
  async def async_search(
690
- self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None
756
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
691
757
  ) -> List[Document]:
692
758
  """Search asynchronously by running in a thread."""
693
759
  return await asyncio.to_thread(self.search, query, limit, filters)
694
760
 
695
- def vector_search(self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None) -> List[Document]:
761
+ def _dsl_to_sqlalchemy(self, filter_expr, table) -> ColumnElement[bool]:
762
+ op = filter_expr["op"]
763
+
764
+ if op == "EQ":
765
+ return table.c.meta_data[filter_expr["key"]].astext == str(filter_expr["value"])
766
+ elif op == "IN":
767
+ # Postgres JSONB array containment
768
+ return table.c.meta_data[filter_expr["key"]].astext.in_([str(v) for v in filter_expr["values"]])
769
+ elif op == "GT":
770
+ return table.c.meta_data[filter_expr["key"]].astext.cast(Integer) > filter_expr["value"]
771
+ elif op == "LT":
772
+ return table.c.meta_data[filter_expr["key"]].astext.cast(Integer) < filter_expr["value"]
773
+ elif op == "NOT":
774
+ return not_(self._dsl_to_sqlalchemy(filter_expr["condition"], table))
775
+ elif op == "AND":
776
+ return and_(*[self._dsl_to_sqlalchemy(cond, table) for cond in filter_expr["conditions"]])
777
+ elif op == "OR":
778
+ return or_(*[self._dsl_to_sqlalchemy(cond, table) for cond in filter_expr["conditions"]])
779
+ else:
780
+ raise ValueError(f"Unknown filter operator: {op}")
781
+
782
+ def vector_search(
783
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
784
+ ) -> List[Document]:
696
785
  """
697
786
  Perform a vector similarity search.
698
787
 
699
788
  Args:
700
789
  query (str): The search query.
701
790
  limit (int): Maximum number of results to return.
702
- filters (Optional[Dict[str, Any]]): Filters to apply to the search.
791
+ filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
703
792
 
704
793
  Returns:
705
794
  List[Document]: List of matching documents.
@@ -708,7 +797,7 @@ class PgVector(VectorDb):
708
797
  # Get the embedding for the query string
709
798
  query_embedding = self.embedder.get_embedding(query)
710
799
  if query_embedding is None:
711
- logger.error(f"Error getting embedding for Query: {query}")
800
+ log_error(f"Error getting embedding for Query: {query}")
712
801
  return []
713
802
 
714
803
  # Define the columns to select
@@ -726,7 +815,17 @@ class PgVector(VectorDb):
726
815
 
727
816
  # Apply filters if provided
728
817
  if filters is not None:
729
- stmt = stmt.where(self.table.c.meta_data.contains(filters))
818
+ # Handle dict filters
819
+ if isinstance(filters, dict):
820
+ stmt = stmt.where(self.table.c.meta_data.contains(filters))
821
+ # Handle FilterExpr DSL
822
+ else:
823
+ # Convert each DSL expression to SQLAlchemy and AND them together
824
+ sqlalchemy_conditions = [
825
+ self._dsl_to_sqlalchemy(f.to_dict() if hasattr(f, "to_dict") else f, self.table)
826
+ for f in filters
827
+ ]
828
+ stmt = stmt.where(and_(*sqlalchemy_conditions))
730
829
 
731
830
  # Order the results based on the distance metric
732
831
  if self.distance == Distance.l2:
@@ -736,7 +835,7 @@ class PgVector(VectorDb):
736
835
  elif self.distance == Distance.max_inner_product:
737
836
  stmt = stmt.order_by(self.table.c.embedding.max_inner_product(query_embedding))
738
837
  else:
739
- logger.error(f"Unknown distance metric: {self.distance}")
838
+ log_error(f"Unknown distance metric: {self.distance}")
740
839
  return []
741
840
 
742
841
  # Limit the number of results
@@ -755,8 +854,8 @@ class PgVector(VectorDb):
755
854
  sess.execute(text(f"SET LOCAL hnsw.ef_search = {self.vector_index.ef_search}"))
756
855
  results = sess.execute(stmt).fetchall()
757
856
  except Exception as e:
758
- logger.error(f"Error performing semantic search: {e}")
759
- logger.error("Table might not exist, creating for future use")
857
+ log_error(f"Error performing semantic search: {e}")
858
+ log_error("Table might not exist, creating for future use")
760
859
  self.create()
761
860
  return []
762
861
 
@@ -781,7 +880,7 @@ class PgVector(VectorDb):
781
880
  log_info(f"Found {len(search_results)} documents")
782
881
  return search_results
783
882
  except Exception as e:
784
- logger.error(f"Error during vector search: {e}")
883
+ log_error(f"Error during vector search: {e}")
785
884
  return []
786
885
 
787
886
  def enable_prefix_matching(self, query: str) -> str:
@@ -799,14 +898,16 @@ class PgVector(VectorDb):
799
898
  processed_words = [word + "*" for word in words]
800
899
  return " ".join(processed_words)
801
900
 
802
- def keyword_search(self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None) -> List[Document]:
901
+ def keyword_search(
902
+ self, query: str, limit: int = 5, filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None
903
+ ) -> List[Document]:
803
904
  """
804
905
  Perform a keyword search on the 'content' column.
805
906
 
806
907
  Args:
807
908
  query (str): The search query.
808
909
  limit (int): Maximum number of results to return.
809
- filters (Optional[Dict[str, Any]]): Filters to apply to the search.
910
+ filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
810
911
 
811
912
  Returns:
812
913
  List[Document]: List of matching documents.
@@ -835,8 +936,17 @@ class PgVector(VectorDb):
835
936
 
836
937
  # Apply filters if provided
837
938
  if filters is not None:
838
- # Use the contains() method for JSONB columns to check if the filters column contains the specified filters
839
- stmt = stmt.where(self.table.c.meta_data.contains(filters))
939
+ # Handle dict filters
940
+ if isinstance(filters, dict):
941
+ stmt = stmt.where(self.table.c.meta_data.contains(filters))
942
+ # Handle FilterExpr DSL
943
+ else:
944
+ # Convert each DSL expression to SQLAlchemy and AND them together
945
+ sqlalchemy_conditions = [
946
+ self._dsl_to_sqlalchemy(f.to_dict() if hasattr(f, "to_dict") else f, self.table)
947
+ for f in filters
948
+ ]
949
+ stmt = stmt.where(and_(*sqlalchemy_conditions))
840
950
 
841
951
  # Order by the relevance rank
842
952
  stmt = stmt.order_by(text_rank.desc())
@@ -852,8 +962,8 @@ class PgVector(VectorDb):
852
962
  with self.Session() as sess, sess.begin():
853
963
  results = sess.execute(stmt).fetchall()
854
964
  except Exception as e:
855
- logger.error(f"Error performing keyword search: {e}")
856
- logger.error("Table might not exist, creating for future use")
965
+ log_error(f"Error performing keyword search: {e}")
966
+ log_error("Table might not exist, creating for future use")
857
967
  self.create()
858
968
  return []
859
969
 
@@ -875,14 +985,14 @@ class PgVector(VectorDb):
875
985
  log_info(f"Found {len(search_results)} documents")
876
986
  return search_results
877
987
  except Exception as e:
878
- logger.error(f"Error during keyword search: {e}")
988
+ log_error(f"Error during keyword search: {e}")
879
989
  return []
880
990
 
881
991
  def hybrid_search(
882
992
  self,
883
993
  query: str,
884
994
  limit: int = 5,
885
- filters: Optional[Dict[str, Any]] = None,
995
+ filters: Optional[Union[Dict[str, Any], List[FilterExpr]]] = None,
886
996
  ) -> List[Document]:
887
997
  """
888
998
  Perform a hybrid search combining vector similarity and full-text search.
@@ -890,7 +1000,7 @@ class PgVector(VectorDb):
890
1000
  Args:
891
1001
  query (str): The search query.
892
1002
  limit (int): Maximum number of results to return.
893
- filters (Optional[Dict[str, Any]]): Filters to apply to the search.
1003
+ filters (Optional[Union[Dict[str, Any], List[FilterExpr]]]): Filters to apply to the search.
894
1004
 
895
1005
  Returns:
896
1006
  List[Document]: List of matching documents.
@@ -899,7 +1009,7 @@ class PgVector(VectorDb):
899
1009
  # Get the embedding for the query string
900
1010
  query_embedding = self.embedder.get_embedding(query)
901
1011
  if query_embedding is None:
902
- logger.error(f"Error getting embedding for Query: {query}")
1012
+ log_error(f"Error getting embedding for Query: {query}")
903
1013
  return []
904
1014
 
905
1015
  # Define the columns to select
@@ -937,7 +1047,7 @@ class PgVector(VectorDb):
937
1047
  # Normalize to range [0, 1]
938
1048
  vector_score = (raw_vector_score + 1) / 2
939
1049
  else:
940
- logger.error(f"Unknown distance metric: {self.distance}")
1050
+ log_error(f"Unknown distance metric: {self.distance}")
941
1051
  return []
942
1052
 
943
1053
  # Apply weights to control the influence of each score
@@ -957,7 +1067,17 @@ class PgVector(VectorDb):
957
1067
 
958
1068
  # Apply filters if provided
959
1069
  if filters is not None:
960
- stmt = stmt.where(self.table.c.meta_data.contains(filters))
1070
+ # Handle dict filters
1071
+ if isinstance(filters, dict):
1072
+ stmt = stmt.where(self.table.c.meta_data.contains(filters))
1073
+ # Handle FilterExpr DSL
1074
+ else:
1075
+ # Convert each DSL expression to SQLAlchemy and AND them together
1076
+ sqlalchemy_conditions = [
1077
+ self._dsl_to_sqlalchemy(f.to_dict() if hasattr(f, "to_dict") else f, self.table)
1078
+ for f in filters
1079
+ ]
1080
+ stmt = stmt.where(and_(*sqlalchemy_conditions))
961
1081
 
962
1082
  # Order the results by the hybrid score in descending order
963
1083
  stmt = stmt.order_by(desc("hybrid_score"))
@@ -978,7 +1098,7 @@ class PgVector(VectorDb):
978
1098
  sess.execute(text(f"SET LOCAL hnsw.ef_search = {self.vector_index.ef_search}"))
979
1099
  results = sess.execute(stmt).fetchall()
980
1100
  except Exception as e:
981
- logger.error(f"Error performing hybrid search: {e}")
1101
+ log_error(f"Error performing hybrid search: {e}")
982
1102
  return []
983
1103
 
984
1104
  # Process the results and convert to Document objects
@@ -1000,9 +1120,10 @@ class PgVector(VectorDb):
1000
1120
  search_results = self.reranker.rerank(query=query, documents=search_results)
1001
1121
 
1002
1122
  log_info(f"Found {len(search_results)} documents")
1123
+
1003
1124
  return search_results
1004
1125
  except Exception as e:
1005
- logger.error(f"Error during hybrid search: {e}")
1126
+ log_error(f"Error during hybrid search: {e}")
1006
1127
  return []
1007
1128
 
1008
1129
  def drop(self) -> None:
@@ -1015,7 +1136,7 @@ class PgVector(VectorDb):
1015
1136
  self.table.drop(self.db_engine)
1016
1137
  log_info(f"Table '{self.table.fullname}' dropped successfully.")
1017
1138
  except Exception as e:
1018
- logger.error(f"Error dropping table '{self.table.fullname}': {e}")
1139
+ log_error(f"Error dropping table '{self.table.fullname}': {e}")
1019
1140
  raise
1020
1141
  else:
1021
1142
  log_info(f"Table '{self.table.fullname}' does not exist.")
@@ -1050,7 +1171,7 @@ class PgVector(VectorDb):
1050
1171
  result = sess.execute(stmt).scalar()
1051
1172
  return int(result) if result is not None else 0
1052
1173
  except Exception as e:
1053
- logger.error(f"Error getting count from table '{self.table.fullname}': {e}")
1174
+ log_error(f"Error getting count from table '{self.table.fullname}': {e}")
1054
1175
  return 0
1055
1176
 
1056
1177
  def optimize(self, force_recreate: bool = False) -> None:
@@ -1091,7 +1212,7 @@ class PgVector(VectorDb):
1091
1212
  drop_index_sql = f'DROP INDEX IF EXISTS "{self.schema}"."{index_name}";'
1092
1213
  sess.execute(text(drop_index_sql))
1093
1214
  except Exception as e:
1094
- logger.error(f"Error dropping index '{index_name}': {e}")
1215
+ log_error(f"Error dropping index '{index_name}': {e}")
1095
1216
  raise
1096
1217
 
1097
1218
  def _create_vector_index(self, force_recreate: bool = False) -> None:
@@ -1146,10 +1267,10 @@ class PgVector(VectorDb):
1146
1267
  elif isinstance(self.vector_index, HNSW):
1147
1268
  self._create_hnsw_index(sess, table_fullname, index_distance)
1148
1269
  else:
1149
- logger.error(f"Unknown index type: {type(self.vector_index)}")
1270
+ log_error(f"Unknown index type: {type(self.vector_index)}")
1150
1271
  return
1151
1272
  except Exception as e:
1152
- logger.error(f"Error creating vector index '{self.vector_index.name}': {e}")
1273
+ log_error(f"Error creating vector index '{self.vector_index.name}': {e}")
1153
1274
  raise
1154
1275
 
1155
1276
  def _create_ivfflat_index(self, sess: Session, table_fullname: str, index_distance: str) -> None:
@@ -1248,7 +1369,7 @@ class PgVector(VectorDb):
1248
1369
  )
1249
1370
  sess.execute(create_gin_index_sql)
1250
1371
  except Exception as e:
1251
- logger.error(f"Error creating GIN index '{gin_index_name}': {e}")
1372
+ log_error(f"Error creating GIN index '{gin_index_name}': {e}")
1252
1373
  raise
1253
1374
 
1254
1375
  def delete(self) -> bool:
@@ -1267,7 +1388,7 @@ class PgVector(VectorDb):
1267
1388
  log_info(f"Deleted all records from table '{self.table.fullname}'.")
1268
1389
  return True
1269
1390
  except Exception as e:
1270
- logger.error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1391
+ log_error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1271
1392
  sess.rollback()
1272
1393
  return False
1273
1394
 
@@ -1283,7 +1404,7 @@ class PgVector(VectorDb):
1283
1404
  log_info(f"Deleted records with id '{id}' from table '{self.table.fullname}'.")
1284
1405
  return True
1285
1406
  except Exception as e:
1286
- logger.error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1407
+ log_error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1287
1408
  sess.rollback()
1288
1409
  return False
1289
1410
 
@@ -1299,7 +1420,7 @@ class PgVector(VectorDb):
1299
1420
  log_info(f"Deleted records with name '{name}' from table '{self.table.fullname}'.")
1300
1421
  return True
1301
1422
  except Exception as e:
1302
- logger.error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1423
+ log_error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1303
1424
  sess.rollback()
1304
1425
  return False
1305
1426
 
@@ -1315,7 +1436,7 @@ class PgVector(VectorDb):
1315
1436
  log_info(f"Deleted records with metadata '{metadata}' from table '{self.table.fullname}'.")
1316
1437
  return True
1317
1438
  except Exception as e:
1318
- logger.error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1439
+ log_error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1319
1440
  sess.rollback()
1320
1441
  return False
1321
1442
 
@@ -1331,7 +1452,7 @@ class PgVector(VectorDb):
1331
1452
  log_info(f"Deleted records with content ID '{content_id}' from table '{self.table.fullname}'.")
1332
1453
  return True
1333
1454
  except Exception as e:
1334
- logger.error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1455
+ log_error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1335
1456
  sess.rollback()
1336
1457
  return False
1337
1458
 
@@ -1347,7 +1468,7 @@ class PgVector(VectorDb):
1347
1468
  log_info(f"Deleted records with content hash '{content_hash}' from table '{self.table.fullname}'.")
1348
1469
  return True
1349
1470
  except Exception as e:
1350
- logger.error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1471
+ log_error(f"Error deleting rows from table '{self.table.fullname}': {e}")
1351
1472
  sess.rollback()
1352
1473
  return False
1353
1474
 
@@ -1383,3 +1504,6 @@ class PgVector(VectorDb):
1383
1504
  copied_obj.table = copied_obj.get_table()
1384
1505
 
1385
1506
  return copied_obj
1507
+
1508
+ def get_supported_search_types(self) -> List[str]:
1509
+ return [SearchType.vector, SearchType.keyword, SearchType.hybrid]