agno 1.7.3__py3-none-any.whl → 1.7.5__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 +113 -31
- agno/api/schemas/agent.py +1 -0
- agno/api/schemas/team.py +1 -0
- agno/app/fastapi/app.py +1 -1
- agno/app/fastapi/async_router.py +67 -16
- agno/app/fastapi/sync_router.py +80 -14
- agno/app/playground/app.py +2 -1
- agno/app/playground/async_router.py +97 -28
- agno/app/playground/operator.py +25 -19
- agno/app/playground/schemas.py +1 -0
- agno/app/playground/sync_router.py +93 -26
- agno/knowledge/agent.py +39 -2
- agno/knowledge/combined.py +1 -1
- agno/run/base.py +2 -0
- agno/run/response.py +4 -4
- agno/run/team.py +6 -6
- agno/run/v2/__init__.py +0 -0
- agno/run/v2/workflow.py +563 -0
- agno/storage/base.py +4 -4
- agno/storage/dynamodb.py +74 -10
- agno/storage/firestore.py +6 -1
- agno/storage/gcs_json.py +8 -2
- agno/storage/json.py +20 -5
- agno/storage/mongodb.py +14 -5
- agno/storage/mysql.py +56 -17
- agno/storage/postgres.py +55 -13
- agno/storage/redis.py +25 -5
- agno/storage/session/__init__.py +3 -1
- agno/storage/session/agent.py +3 -0
- agno/storage/session/team.py +3 -0
- agno/storage/session/v2/__init__.py +5 -0
- agno/storage/session/v2/workflow.py +89 -0
- agno/storage/singlestore.py +74 -12
- agno/storage/sqlite.py +64 -18
- agno/storage/yaml.py +26 -6
- agno/team/team.py +105 -21
- agno/tools/decorator.py +45 -2
- agno/tools/function.py +16 -12
- agno/utils/log.py +12 -0
- agno/utils/message.py +5 -1
- agno/utils/openai.py +20 -5
- agno/utils/pprint.py +34 -8
- agno/vectordb/surrealdb/__init__.py +3 -0
- agno/vectordb/surrealdb/surrealdb.py +493 -0
- agno/workflow/v2/__init__.py +21 -0
- agno/workflow/v2/condition.py +554 -0
- agno/workflow/v2/loop.py +602 -0
- agno/workflow/v2/parallel.py +659 -0
- agno/workflow/v2/router.py +521 -0
- agno/workflow/v2/step.py +861 -0
- agno/workflow/v2/steps.py +465 -0
- agno/workflow/v2/types.py +347 -0
- agno/workflow/v2/workflow.py +3132 -0
- {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/METADATA +4 -1
- {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/RECORD +59 -44
- {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/WHEEL +0 -0
- {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/entry_points.txt +0 -0
- {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/licenses/LICENSE +0 -0
- {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/top_level.txt +0 -0
agno/utils/openai.py
CHANGED
|
@@ -93,11 +93,24 @@ def audio_to_message(audio: Sequence[Audio]) -> List[Dict[str, Any]]:
|
|
|
93
93
|
return audio_messages
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def _process_bytes_image(image: bytes) -> Dict[str, Any]:
|
|
96
|
+
def _process_bytes_image(image: bytes, image_format: Optional[str] = None) -> Dict[str, Any]:
|
|
97
97
|
"""Process bytes image data."""
|
|
98
98
|
base64_image = base64.b64encode(image).decode("utf-8")
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
|
|
100
|
+
# Use provided format or attempt detection, defaulting to JPEG
|
|
101
|
+
if image_format:
|
|
102
|
+
mime_type = f"image/{image_format.lower()}"
|
|
103
|
+
else:
|
|
104
|
+
# Try to detect the image format from the bytes
|
|
105
|
+
try:
|
|
106
|
+
import imghdr
|
|
107
|
+
|
|
108
|
+
detected_format = imghdr.what(None, h=image)
|
|
109
|
+
mime_type = f"image/{detected_format}" if detected_format else "image/jpeg"
|
|
110
|
+
except Exception:
|
|
111
|
+
mime_type = "image/jpeg"
|
|
112
|
+
|
|
113
|
+
image_url = f"data:{mime_type};base64,{base64_image}"
|
|
101
114
|
return {"type": "image_url", "image_url": {"url": image_url}}
|
|
102
115
|
|
|
103
116
|
|
|
@@ -141,7 +154,8 @@ def process_image(image: Image) -> Optional[Dict[str, Any]]:
|
|
|
141
154
|
image_payload = _process_image_path(image.filepath)
|
|
142
155
|
|
|
143
156
|
elif image.content is not None:
|
|
144
|
-
|
|
157
|
+
# Pass the format from the Image object
|
|
158
|
+
image_payload = _process_bytes_image(image.content, image.format)
|
|
145
159
|
|
|
146
160
|
else:
|
|
147
161
|
log_warning(f"Unsupported image format or no data provided: {image}")
|
|
@@ -150,7 +164,8 @@ def process_image(image: Image) -> Optional[Dict[str, Any]]:
|
|
|
150
164
|
if image_payload and image.detail: # Check if payload was created before adding detail
|
|
151
165
|
# Ensure image_url key exists before trying to access its sub-dictionary
|
|
152
166
|
if "image_url" not in image_payload:
|
|
153
|
-
|
|
167
|
+
# Initialize if missing (though unlikely based on helper funcs)
|
|
168
|
+
image_payload["image_url"] = {}
|
|
154
169
|
image_payload["image_url"]["detail"] = image.detail
|
|
155
170
|
|
|
156
171
|
return image_payload
|
agno/utils/pprint.py
CHANGED
|
@@ -5,13 +5,20 @@ from pydantic import BaseModel
|
|
|
5
5
|
|
|
6
6
|
from agno.run.response import RunResponse, RunResponseEvent
|
|
7
7
|
from agno.run.team import TeamRunResponse, TeamRunResponseEvent
|
|
8
|
-
from agno.run.workflow import WorkflowRunResponseEvent
|
|
8
|
+
from agno.run.v2.workflow import WorkflowRunResponse, WorkflowRunResponseEvent
|
|
9
9
|
from agno.utils.log import logger
|
|
10
10
|
from agno.utils.timer import Timer
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def pprint_run_response(
|
|
14
|
-
run_response: Union[
|
|
14
|
+
run_response: Union[
|
|
15
|
+
RunResponse,
|
|
16
|
+
Iterable[RunResponseEvent],
|
|
17
|
+
TeamRunResponse,
|
|
18
|
+
Iterable[TeamRunResponseEvent],
|
|
19
|
+
WorkflowRunResponse,
|
|
20
|
+
Iterable[WorkflowRunResponseEvent],
|
|
21
|
+
],
|
|
15
22
|
markdown: bool = False,
|
|
16
23
|
show_time: bool = False,
|
|
17
24
|
) -> None:
|
|
@@ -25,7 +32,11 @@ def pprint_run_response(
|
|
|
25
32
|
from agno.cli.console import console
|
|
26
33
|
|
|
27
34
|
# If run_response is a single RunResponse, wrap it in a list to make it iterable
|
|
28
|
-
if
|
|
35
|
+
if (
|
|
36
|
+
isinstance(run_response, RunResponse)
|
|
37
|
+
or isinstance(run_response, TeamRunResponse)
|
|
38
|
+
or isinstance(run_response, WorkflowRunResponse)
|
|
39
|
+
):
|
|
29
40
|
single_response_content: Union[str, JSON, Markdown] = ""
|
|
30
41
|
if isinstance(run_response.content, str):
|
|
31
42
|
single_response_content = (
|
|
@@ -64,11 +75,13 @@ def pprint_run_response(
|
|
|
64
75
|
):
|
|
65
76
|
if isinstance(resp.content, BaseModel):
|
|
66
77
|
try:
|
|
67
|
-
|
|
78
|
+
JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
|
|
68
79
|
except Exception as e:
|
|
69
80
|
logger.warning(f"Failed to convert response to Markdown: {e}")
|
|
70
81
|
else:
|
|
71
|
-
streaming_response_content
|
|
82
|
+
if isinstance(streaming_response_content, JSON):
|
|
83
|
+
streaming_response_content = streaming_response_content.text + "\n" # type: ignore
|
|
84
|
+
streaming_response_content += resp.content # type: ignore
|
|
72
85
|
|
|
73
86
|
formatted_response = Markdown(streaming_response_content) if markdown else streaming_response_content # type: ignore
|
|
74
87
|
table = Table(box=ROUNDED, border_style="blue", show_header=False)
|
|
@@ -81,7 +94,14 @@ def pprint_run_response(
|
|
|
81
94
|
|
|
82
95
|
|
|
83
96
|
async def apprint_run_response(
|
|
84
|
-
run_response: Union[
|
|
97
|
+
run_response: Union[
|
|
98
|
+
RunResponse,
|
|
99
|
+
AsyncIterable[RunResponse],
|
|
100
|
+
TeamRunResponse,
|
|
101
|
+
AsyncIterable[TeamRunResponse],
|
|
102
|
+
WorkflowRunResponse,
|
|
103
|
+
AsyncIterable[WorkflowRunResponseEvent],
|
|
104
|
+
],
|
|
85
105
|
markdown: bool = False,
|
|
86
106
|
show_time: bool = False,
|
|
87
107
|
) -> None:
|
|
@@ -95,7 +115,11 @@ async def apprint_run_response(
|
|
|
95
115
|
from agno.cli.console import console
|
|
96
116
|
|
|
97
117
|
# If run_response is a single RunResponse, wrap it in a list to make it iterable
|
|
98
|
-
if
|
|
118
|
+
if (
|
|
119
|
+
isinstance(run_response, RunResponse)
|
|
120
|
+
or isinstance(run_response, TeamRunResponse)
|
|
121
|
+
or isinstance(run_response, WorkflowRunResponse)
|
|
122
|
+
):
|
|
99
123
|
single_response_content: Union[str, JSON, Markdown] = ""
|
|
100
124
|
if isinstance(run_response.content, str):
|
|
101
125
|
single_response_content = (
|
|
@@ -139,7 +163,9 @@ async def apprint_run_response(
|
|
|
139
163
|
except Exception as e:
|
|
140
164
|
logger.warning(f"Failed to convert response to Markdown: {e}")
|
|
141
165
|
else:
|
|
142
|
-
streaming_response_content
|
|
166
|
+
if isinstance(streaming_response_content, JSON):
|
|
167
|
+
streaming_response_content = streaming_response_content.text + "\n" # type: ignore
|
|
168
|
+
streaming_response_content += resp.content # type: ignore
|
|
143
169
|
|
|
144
170
|
formatted_response = Markdown(streaming_response_content) if markdown else streaming_response_content # type: ignore
|
|
145
171
|
table = Table(box=ROUNDED, border_style="blue", show_header=False)
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
from typing import Any, Dict, Final, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from surrealdb import (
|
|
5
|
+
AsyncHttpSurrealConnection,
|
|
6
|
+
AsyncWsSurrealConnection,
|
|
7
|
+
BlockingHttpSurrealConnection,
|
|
8
|
+
BlockingWsSurrealConnection,
|
|
9
|
+
)
|
|
10
|
+
except ImportError as e:
|
|
11
|
+
msg = "The `surrealdb` package is not installed. Please install it via `pip install surrealdb`."
|
|
12
|
+
raise ImportError(msg) from e
|
|
13
|
+
|
|
14
|
+
from agno.document import Document
|
|
15
|
+
from agno.embedder import Embedder
|
|
16
|
+
from agno.utils.log import log_debug, log_error, log_info
|
|
17
|
+
from agno.vectordb.base import VectorDb
|
|
18
|
+
from agno.vectordb.distance import Distance
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SurrealDb(VectorDb):
|
|
22
|
+
"""SurrealDB Vector Database implementation supporting both sync and async operations."""
|
|
23
|
+
|
|
24
|
+
# SQL Query Constants
|
|
25
|
+
CREATE_TABLE_QUERY: Final[str] = """
|
|
26
|
+
DEFINE TABLE IF NOT EXISTS {collection} SCHEMAFUL;
|
|
27
|
+
DEFINE FIELD IF NOT EXISTS content ON {collection} TYPE string;
|
|
28
|
+
DEFINE FIELD IF NOT EXISTS embedding ON {collection} TYPE array<float>;
|
|
29
|
+
DEFINE FIELD IF NOT EXISTS meta_data ON {collection} FLEXIBLE TYPE object;
|
|
30
|
+
DEFINE INDEX IF NOT EXISTS vector_idx ON {collection} FIELDS embedding HNSW DIMENSION {dimensions} DIST {distance};
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
DOC_EXISTS_QUERY: Final[str] = """
|
|
34
|
+
SELECT * FROM {collection}
|
|
35
|
+
WHERE content = $content
|
|
36
|
+
LIMIT 1
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
NAME_EXISTS_QUERY: Final[str] = """
|
|
40
|
+
SELECT * FROM {collection}
|
|
41
|
+
WHERE meta_data.name = $name
|
|
42
|
+
LIMIT 1
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
ID_EXISTS_QUERY: Final[str] = """
|
|
46
|
+
SELECT * FROM {collection}
|
|
47
|
+
WHERE id = $id
|
|
48
|
+
LIMIT 1
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
UPSERT_QUERY: Final[str] = """
|
|
52
|
+
UPSERT {thing}
|
|
53
|
+
SET content = $content,
|
|
54
|
+
embedding = $embedding,
|
|
55
|
+
meta_data = $meta_data
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
SEARCH_QUERY: Final[str] = """
|
|
59
|
+
SELECT
|
|
60
|
+
content,
|
|
61
|
+
meta_data,
|
|
62
|
+
vector::distance::knn() as distance
|
|
63
|
+
FROM {collection}
|
|
64
|
+
WHERE embedding <|{limit}, {search_ef}|> $query_embedding
|
|
65
|
+
{filter_condition}
|
|
66
|
+
ORDER BY distance ASC
|
|
67
|
+
LIMIT {limit};
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
INFO_DB_QUERY: Final[str] = "INFO FOR DB;"
|
|
71
|
+
DROP_TABLE_QUERY: Final[str] = "REMOVE TABLE {collection}"
|
|
72
|
+
DELETE_ALL_QUERY: Final[str] = "DELETE {collection}"
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
client: Optional[Union[BlockingWsSurrealConnection, BlockingHttpSurrealConnection]] = None,
|
|
77
|
+
async_client: Optional[Union[AsyncWsSurrealConnection, AsyncHttpSurrealConnection]] = None,
|
|
78
|
+
collection: str = "documents",
|
|
79
|
+
distance: Distance = Distance.cosine,
|
|
80
|
+
efc: int = 150,
|
|
81
|
+
m: int = 12,
|
|
82
|
+
search_ef: int = 40,
|
|
83
|
+
embedder: Optional[Embedder] = None,
|
|
84
|
+
):
|
|
85
|
+
"""Initialize SurrealDB connection.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
url: SurrealDB server URL (e.g. ws://localhost:8000/rpc)
|
|
89
|
+
client: A blocking connection, either HTTP or WS
|
|
90
|
+
async_client: An async connection, either HTTP or WS (default: None)
|
|
91
|
+
collection: Collection name to store documents (default: documents)
|
|
92
|
+
distance: Distance metric to use (default: cosine)
|
|
93
|
+
efc: HNSW construction time/accuracy trade-off (default: 150)
|
|
94
|
+
m: HNSW max number of connections per element (default: 12)
|
|
95
|
+
search_ef: HNSW search time/accuracy trade-off (default: 40)
|
|
96
|
+
embedder: Embedder instance for creating embeddings (default: OpenAIEmbedder)
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
# Embedder for embedding the document contents
|
|
100
|
+
if embedder is None:
|
|
101
|
+
from agno.embedder.openai import OpenAIEmbedder
|
|
102
|
+
|
|
103
|
+
embedder = OpenAIEmbedder()
|
|
104
|
+
log_info("Embedder not provided, using OpenAIEmbedder as default.")
|
|
105
|
+
self.embedder: Embedder = embedder
|
|
106
|
+
self.dimensions = self.embedder.dimensions
|
|
107
|
+
self.collection = collection
|
|
108
|
+
|
|
109
|
+
# Convert Distance enum to SurrealDB distance type
|
|
110
|
+
self.distance = {Distance.cosine: "COSINE", Distance.l2: "EUCLIDEAN", Distance.max_inner_product: "DOT"}[
|
|
111
|
+
distance
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
self._client: Optional[Union[BlockingHttpSurrealConnection, BlockingWsSurrealConnection]] = client
|
|
115
|
+
self._async_client: Optional[Union[AsyncWsSurrealConnection, AsyncHttpSurrealConnection]] = async_client
|
|
116
|
+
|
|
117
|
+
if self._client is None and self._async_client is None:
|
|
118
|
+
msg = "Client and async client are not provided. Please provide one of them."
|
|
119
|
+
raise RuntimeError(msg)
|
|
120
|
+
|
|
121
|
+
# HNSW index parameters
|
|
122
|
+
self.efc = efc
|
|
123
|
+
self.m = m
|
|
124
|
+
self.search_ef = search_ef
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def async_client(self) -> Union[AsyncWsSurrealConnection, AsyncHttpSurrealConnection]:
|
|
128
|
+
"""Check if the async client is initialized.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
RuntimeError: If the async client is not initialized.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The async client.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
if self._async_client is None:
|
|
138
|
+
msg = "Async client is not initialized"
|
|
139
|
+
raise RuntimeError(msg)
|
|
140
|
+
return self._async_client
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def client(self) -> Union[BlockingHttpSurrealConnection, BlockingWsSurrealConnection]:
|
|
144
|
+
"""Check if the client is initialized.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The client.
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
if self._client is None:
|
|
151
|
+
msg = "Client is not initialized"
|
|
152
|
+
raise RuntimeError(msg)
|
|
153
|
+
return self._client
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _build_filter_condition(filters: Optional[Dict[str, Any]] = None) -> str:
|
|
157
|
+
"""Build filter condition for queries.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
filters: A dictionary of filters to apply to the query.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
A string representing the filter condition.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
if not filters:
|
|
167
|
+
return ""
|
|
168
|
+
conditions = [f"meta_data.{key} = ${key}" for key in filters]
|
|
169
|
+
return "AND " + " AND ".join(conditions)
|
|
170
|
+
|
|
171
|
+
# Synchronous methods
|
|
172
|
+
def create(self) -> None:
|
|
173
|
+
"""Create the vector collection and index."""
|
|
174
|
+
if not self.exists():
|
|
175
|
+
log_debug(f"Creating collection: {self.collection}")
|
|
176
|
+
query = self.CREATE_TABLE_QUERY.format(
|
|
177
|
+
collection=self.collection,
|
|
178
|
+
distance=self.distance,
|
|
179
|
+
dimensions=self.dimensions,
|
|
180
|
+
efc=self.efc,
|
|
181
|
+
m=self.m,
|
|
182
|
+
)
|
|
183
|
+
self.client.query(query)
|
|
184
|
+
|
|
185
|
+
def doc_exists(self, document: Document) -> bool:
|
|
186
|
+
"""Check if a document exists by its content.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
document: The document to check.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if the document exists, False otherwise.
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
log_debug(f"Checking if document exists: {document.content}")
|
|
196
|
+
result = self.client.query(
|
|
197
|
+
self.DOC_EXISTS_QUERY.format(collection=self.collection),
|
|
198
|
+
{"content": document.content},
|
|
199
|
+
)
|
|
200
|
+
return bool(self._extract_result(result))
|
|
201
|
+
|
|
202
|
+
def name_exists(self, name: str) -> bool:
|
|
203
|
+
"""Check if a document exists by its name.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
name: The name of the document to check.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if the document exists, False otherwise.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
log_debug(f"Checking if document exists: {name}")
|
|
213
|
+
result = self.client.query(self.NAME_EXISTS_QUERY.format(collection=self.collection), {"name": name})
|
|
214
|
+
return bool(self._extract_result(result))
|
|
215
|
+
|
|
216
|
+
def insert(self, documents: List[Document], filters: Optional[Dict[str, Any]] = None) -> None:
|
|
217
|
+
"""Insert documents into the vector store.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
documents: A list of documents to insert.
|
|
221
|
+
filters: A dictionary of filters to apply to the query.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
for doc in documents:
|
|
225
|
+
doc.embed(embedder=self.embedder)
|
|
226
|
+
meta_data: Dict[str, Any] = doc.meta_data if isinstance(doc.meta_data, dict) else {}
|
|
227
|
+
data: Dict[str, Any] = {"content": doc.content, "embedding": doc.embedding, "meta_data": meta_data}
|
|
228
|
+
if filters:
|
|
229
|
+
data["meta_data"].update(filters)
|
|
230
|
+
self.client.create(self.collection, data)
|
|
231
|
+
|
|
232
|
+
def upsert(self, documents: List[Document], filters: Optional[Dict[str, Any]] = None) -> None:
|
|
233
|
+
"""Upsert documents into the vector store.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
documents: A list of documents to upsert.
|
|
237
|
+
filters: A dictionary of filters to apply to the query.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
for doc in documents:
|
|
241
|
+
doc.embed(embedder=self.embedder)
|
|
242
|
+
meta_data: Dict[str, Any] = doc.meta_data if isinstance(doc.meta_data, dict) else {}
|
|
243
|
+
data: Dict[str, Any] = {"content": doc.content, "embedding": doc.embedding, "meta_data": meta_data}
|
|
244
|
+
if filters:
|
|
245
|
+
data["meta_data"].update(filters)
|
|
246
|
+
thing = f"{self.collection}:{doc.id}" if doc.id else self.collection
|
|
247
|
+
self.client.query(self.UPSERT_QUERY.format(thing=thing), data)
|
|
248
|
+
|
|
249
|
+
def search(self, query: str, limit: int = 5, filters: Optional[Dict[str, Any]] = None) -> List[Document]:
|
|
250
|
+
"""Search for similar documents.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
query: The query to search for.
|
|
254
|
+
limit: The maximum number of documents to return.
|
|
255
|
+
filters: A dictionary of filters to apply to the query.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A list of documents that are similar to the query.
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
query_embedding = self.embedder.get_embedding(query)
|
|
262
|
+
if query_embedding is None:
|
|
263
|
+
log_error(f"Error getting embedding for Query: {query}")
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
filter_condition = self._build_filter_condition(filters)
|
|
267
|
+
log_debug(f"Filter condition: {filter_condition}")
|
|
268
|
+
search_query = self.SEARCH_QUERY.format(
|
|
269
|
+
collection=self.collection,
|
|
270
|
+
limit=limit,
|
|
271
|
+
search_ef=self.search_ef,
|
|
272
|
+
filter_condition=filter_condition,
|
|
273
|
+
distance=self.distance,
|
|
274
|
+
)
|
|
275
|
+
log_debug(f"Search query: {search_query}")
|
|
276
|
+
response = self.client.query(
|
|
277
|
+
search_query,
|
|
278
|
+
{"query_embedding": query_embedding, **filters} if filters else {"query_embedding": query_embedding},
|
|
279
|
+
)
|
|
280
|
+
log_debug(f"Search response: {response}")
|
|
281
|
+
|
|
282
|
+
documents = []
|
|
283
|
+
for item in response:
|
|
284
|
+
if isinstance(item, dict):
|
|
285
|
+
doc = Document(
|
|
286
|
+
content=item.get("content", ""),
|
|
287
|
+
embedding=item.get("embedding", []),
|
|
288
|
+
meta_data=item.get("meta_data", {}),
|
|
289
|
+
embedder=self.embedder,
|
|
290
|
+
)
|
|
291
|
+
documents.append(doc)
|
|
292
|
+
log_debug(f"Found {len(documents)} documents")
|
|
293
|
+
return documents
|
|
294
|
+
|
|
295
|
+
def drop(self) -> None:
|
|
296
|
+
"""Drop the vector collection."""
|
|
297
|
+
log_debug(f"Dropping collection: {self.collection}")
|
|
298
|
+
self.client.query(self.DROP_TABLE_QUERY.format(collection=self.collection))
|
|
299
|
+
|
|
300
|
+
def exists(self) -> bool:
|
|
301
|
+
"""Check if the vector collection exists.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
True if the collection exists, False otherwise.
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
log_debug(f"Checking if collection exists: {self.collection}")
|
|
308
|
+
response = self.client.query(self.INFO_DB_QUERY)
|
|
309
|
+
result = self._extract_result(response)
|
|
310
|
+
if isinstance(result, dict) and "tables" in result:
|
|
311
|
+
return self.collection in result["tables"]
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def delete(self) -> bool:
|
|
315
|
+
"""Delete all documents from the vector store.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
True if the collection was deleted, False otherwise.
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
self.client.query(self.DELETE_ALL_QUERY.format(collection=self.collection))
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def _extract_result(query_result: Union[List[Dict[str, Any]], Dict[str, Any]]) -> Union[List[Any], Dict[str, Any]]:
|
|
326
|
+
"""Extract the actual result from SurrealDB query response.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
query_result: The query result from SurrealDB.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
The actual result from SurrealDB query response.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
log_debug(f"Query result: {query_result}")
|
|
336
|
+
if isinstance(query_result, dict):
|
|
337
|
+
return query_result
|
|
338
|
+
if isinstance(query_result, list):
|
|
339
|
+
if len(query_result) > 0:
|
|
340
|
+
return query_result[0].get("result", {})
|
|
341
|
+
return []
|
|
342
|
+
return []
|
|
343
|
+
|
|
344
|
+
async def async_create(self) -> None:
|
|
345
|
+
"""Create the vector collection and index asynchronously."""
|
|
346
|
+
log_debug(f"Creating collection: {self.collection}")
|
|
347
|
+
await self.async_client.query(
|
|
348
|
+
self.CREATE_TABLE_QUERY.format(
|
|
349
|
+
collection=self.collection,
|
|
350
|
+
distance=self.distance,
|
|
351
|
+
dimensions=self.dimensions,
|
|
352
|
+
efc=self.efc,
|
|
353
|
+
m=self.m,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
async def async_doc_exists(self, document: Document) -> bool:
|
|
358
|
+
"""Check if a document exists by its content asynchronously.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
True if the document exists, False otherwise.
|
|
362
|
+
|
|
363
|
+
"""
|
|
364
|
+
response = await self.async_client.query(
|
|
365
|
+
self.DOC_EXISTS_QUERY.format(collection=self.collection),
|
|
366
|
+
{"content": document.content},
|
|
367
|
+
)
|
|
368
|
+
return bool(self._extract_result(response))
|
|
369
|
+
|
|
370
|
+
async def async_name_exists(self, name: str) -> bool:
|
|
371
|
+
"""Check if a document exists by its name asynchronously.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
True if the document exists, False otherwise.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
response = await self.async_client.query(
|
|
378
|
+
self.NAME_EXISTS_QUERY.format(collection=self.collection),
|
|
379
|
+
{"name": name},
|
|
380
|
+
)
|
|
381
|
+
return bool(self._extract_result(response))
|
|
382
|
+
|
|
383
|
+
async def async_insert(self, documents: List[Document], filters: Optional[Dict[str, Any]] = None) -> None:
|
|
384
|
+
"""Insert documents into the vector store asynchronously.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
documents: A list of documents to insert.
|
|
388
|
+
filters: A dictionary of filters to apply to the query.
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
for doc in documents:
|
|
392
|
+
doc.embed(embedder=self.embedder)
|
|
393
|
+
meta_data: Dict[str, Any] = doc.meta_data if isinstance(doc.meta_data, dict) else {}
|
|
394
|
+
data: Dict[str, Any] = {"content": doc.content, "embedding": doc.embedding, "meta_data": meta_data}
|
|
395
|
+
if filters:
|
|
396
|
+
data["meta_data"].update(filters)
|
|
397
|
+
log_debug(f"Inserting document asynchronously: {doc.name} ({doc.meta_data})")
|
|
398
|
+
await self.async_client.create(self.collection, data)
|
|
399
|
+
|
|
400
|
+
async def async_upsert(self, documents: List[Document], filters: Optional[Dict[str, Any]] = None) -> None:
|
|
401
|
+
"""Upsert documents into the vector store asynchronously.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
documents: A list of documents to upsert.
|
|
405
|
+
filters: A dictionary of filters to apply to the query.
|
|
406
|
+
|
|
407
|
+
"""
|
|
408
|
+
for doc in documents:
|
|
409
|
+
doc.embed(embedder=self.embedder)
|
|
410
|
+
meta_data: Dict[str, Any] = doc.meta_data if isinstance(doc.meta_data, dict) else {}
|
|
411
|
+
data: Dict[str, Any] = {"content": doc.content, "embedding": doc.embedding, "meta_data": meta_data}
|
|
412
|
+
if filters:
|
|
413
|
+
data["meta_data"].update(filters)
|
|
414
|
+
log_debug(f"Upserting document asynchronously: {doc.name} ({doc.meta_data})")
|
|
415
|
+
thing = f"{self.collection}:{doc.id}" if doc.id else self.collection
|
|
416
|
+
await self.async_client.query(self.UPSERT_QUERY.format(thing=thing), data)
|
|
417
|
+
|
|
418
|
+
async def async_search(
|
|
419
|
+
self,
|
|
420
|
+
query: str,
|
|
421
|
+
limit: int = 5,
|
|
422
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
423
|
+
) -> List[Document]:
|
|
424
|
+
"""Search for similar documents asynchronously.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
query: The query to search for.
|
|
428
|
+
limit: The maximum number of documents to return.
|
|
429
|
+
filters: A dictionary of filters to apply to the query.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
A list of documents that are similar to the query.
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
query_embedding = self.embedder.get_embedding(query)
|
|
436
|
+
if query_embedding is None:
|
|
437
|
+
log_error(f"Error getting embedding for Query: {query}")
|
|
438
|
+
return []
|
|
439
|
+
|
|
440
|
+
filter_condition = self._build_filter_condition(filters)
|
|
441
|
+
search_query = self.SEARCH_QUERY.format(
|
|
442
|
+
collection=self.collection,
|
|
443
|
+
limit=limit,
|
|
444
|
+
search_ef=self.search_ef,
|
|
445
|
+
filter_condition=filter_condition,
|
|
446
|
+
distance=self.distance,
|
|
447
|
+
)
|
|
448
|
+
response = await self.async_client.query(
|
|
449
|
+
search_query,
|
|
450
|
+
{"query_embedding": query_embedding, **filters} if filters else {"query_embedding": query_embedding},
|
|
451
|
+
)
|
|
452
|
+
log_debug(f"Search response: {response}")
|
|
453
|
+
documents = []
|
|
454
|
+
for item in response:
|
|
455
|
+
if isinstance(item, dict):
|
|
456
|
+
doc = Document(
|
|
457
|
+
content=item.get("content", ""),
|
|
458
|
+
embedding=item.get("embedding", []),
|
|
459
|
+
meta_data=item.get("meta_data", {}),
|
|
460
|
+
embedder=self.embedder,
|
|
461
|
+
)
|
|
462
|
+
documents.append(doc)
|
|
463
|
+
log_debug(f"Found {len(documents)} documents asynchronously")
|
|
464
|
+
return documents
|
|
465
|
+
|
|
466
|
+
async def async_drop(self) -> None:
|
|
467
|
+
"""Drop the vector collection asynchronously."""
|
|
468
|
+
log_debug(f"Dropping collection: {self.collection}")
|
|
469
|
+
await self.async_client.query(self.DROP_TABLE_QUERY.format(collection=self.collection))
|
|
470
|
+
|
|
471
|
+
async def async_exists(self) -> bool:
|
|
472
|
+
"""Check if the vector collection exists asynchronously.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
True if the collection exists, False otherwise.
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
log_debug(f"Checking if collection exists: {self.collection}")
|
|
479
|
+
response = await self.async_client.query(self.INFO_DB_QUERY)
|
|
480
|
+
result = self._extract_result(response)
|
|
481
|
+
if isinstance(result, dict) and "tables" in result:
|
|
482
|
+
return self.collection in result["tables"]
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
@staticmethod
|
|
486
|
+
def upsert_available() -> bool:
|
|
487
|
+
"""Check if upsert is available.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
True if upsert is available, False otherwise.
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
return True
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from agno.workflow.v2.condition import Condition
|
|
2
|
+
from agno.workflow.v2.loop import Loop
|
|
3
|
+
from agno.workflow.v2.parallel import Parallel
|
|
4
|
+
from agno.workflow.v2.router import Router
|
|
5
|
+
from agno.workflow.v2.step import Step
|
|
6
|
+
from agno.workflow.v2.steps import Steps
|
|
7
|
+
from agno.workflow.v2.types import StepInput, StepOutput, WorkflowExecutionInput
|
|
8
|
+
from agno.workflow.v2.workflow import Workflow
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Workflow",
|
|
12
|
+
"Steps",
|
|
13
|
+
"Step",
|
|
14
|
+
"Loop",
|
|
15
|
+
"Parallel",
|
|
16
|
+
"Condition",
|
|
17
|
+
"Router",
|
|
18
|
+
"WorkflowExecutionInput",
|
|
19
|
+
"StepInput",
|
|
20
|
+
"StepOutput",
|
|
21
|
+
]
|