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.
Files changed (59) hide show
  1. agno/agent/agent.py +113 -31
  2. agno/api/schemas/agent.py +1 -0
  3. agno/api/schemas/team.py +1 -0
  4. agno/app/fastapi/app.py +1 -1
  5. agno/app/fastapi/async_router.py +67 -16
  6. agno/app/fastapi/sync_router.py +80 -14
  7. agno/app/playground/app.py +2 -1
  8. agno/app/playground/async_router.py +97 -28
  9. agno/app/playground/operator.py +25 -19
  10. agno/app/playground/schemas.py +1 -0
  11. agno/app/playground/sync_router.py +93 -26
  12. agno/knowledge/agent.py +39 -2
  13. agno/knowledge/combined.py +1 -1
  14. agno/run/base.py +2 -0
  15. agno/run/response.py +4 -4
  16. agno/run/team.py +6 -6
  17. agno/run/v2/__init__.py +0 -0
  18. agno/run/v2/workflow.py +563 -0
  19. agno/storage/base.py +4 -4
  20. agno/storage/dynamodb.py +74 -10
  21. agno/storage/firestore.py +6 -1
  22. agno/storage/gcs_json.py +8 -2
  23. agno/storage/json.py +20 -5
  24. agno/storage/mongodb.py +14 -5
  25. agno/storage/mysql.py +56 -17
  26. agno/storage/postgres.py +55 -13
  27. agno/storage/redis.py +25 -5
  28. agno/storage/session/__init__.py +3 -1
  29. agno/storage/session/agent.py +3 -0
  30. agno/storage/session/team.py +3 -0
  31. agno/storage/session/v2/__init__.py +5 -0
  32. agno/storage/session/v2/workflow.py +89 -0
  33. agno/storage/singlestore.py +74 -12
  34. agno/storage/sqlite.py +64 -18
  35. agno/storage/yaml.py +26 -6
  36. agno/team/team.py +105 -21
  37. agno/tools/decorator.py +45 -2
  38. agno/tools/function.py +16 -12
  39. agno/utils/log.py +12 -0
  40. agno/utils/message.py +5 -1
  41. agno/utils/openai.py +20 -5
  42. agno/utils/pprint.py +34 -8
  43. agno/vectordb/surrealdb/__init__.py +3 -0
  44. agno/vectordb/surrealdb/surrealdb.py +493 -0
  45. agno/workflow/v2/__init__.py +21 -0
  46. agno/workflow/v2/condition.py +554 -0
  47. agno/workflow/v2/loop.py +602 -0
  48. agno/workflow/v2/parallel.py +659 -0
  49. agno/workflow/v2/router.py +521 -0
  50. agno/workflow/v2/step.py +861 -0
  51. agno/workflow/v2/steps.py +465 -0
  52. agno/workflow/v2/types.py +347 -0
  53. agno/workflow/v2/workflow.py +3132 -0
  54. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/METADATA +4 -1
  55. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/RECORD +59 -44
  56. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/WHEEL +0 -0
  57. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/entry_points.txt +0 -0
  58. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/licenses/LICENSE +0 -0
  59. {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
- # Assuming JPEG if type not specified, could attempt detection
100
- image_url = f"data:image/jpeg;base64,{base64_image}"
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
- image_payload = _process_bytes_image(image.content)
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
- image_payload["image_url"] = {} # Initialize if missing (though unlikely based on helper funcs)
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[RunResponse, Iterable[RunResponseEvent], TeamRunResponse, Iterable[TeamRunResponseEvent]],
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 isinstance(run_response, RunResponse) or isinstance(run_response, TeamRunResponse):
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
- streaming_response_content = JSON(resp.content.model_dump_json(exclude_none=True), indent=2) # type: ignore
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 += resp.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[RunResponse, AsyncIterable[RunResponse], TeamRunResponse, AsyncIterable[TeamRunResponse]],
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 isinstance(run_response, RunResponse) or isinstance(run_response, TeamRunResponse):
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 += resp.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,3 @@
1
+ from agno.vectordb.surrealdb.surrealdb import SurrealDb
2
+
3
+ __all__ = ["SurrealDb"]
@@ -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
+ ]