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.
- agno/agent/agent.py +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
+
log_error(f"Rate limit detected during batch embedding. {e}")
|
|
529
561
|
raise e
|
|
530
562
|
else:
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
728
|
+
log_error(f"Error updating metadata for document {content_id}: {e}")
|
|
665
729
|
raise
|
|
666
730
|
|
|
667
|
-
def search(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
759
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
839
|
-
|
|
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
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1270
|
+
log_error(f"Unknown index type: {type(self.vector_index)}")
|
|
1150
1271
|
return
|
|
1151
1272
|
except Exception as e:
|
|
1152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|