agno 2.1.3__py3-none-any.whl → 2.1.4__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 +39 -69
- agno/db/dynamo/dynamo.py +8 -6
- agno/db/dynamo/schemas.py +1 -10
- agno/db/dynamo/utils.py +2 -2
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/os/app.py +7 -3
- agno/os/interfaces/agui/utils.py +6 -0
- agno/os/schema.py +2 -0
- agno/os/utils.py +2 -1
- agno/session/workflow.py +69 -1
- agno/team/team.py +29 -79
- agno/tools/function.py +36 -18
- agno/tools/google_drive.py +270 -0
- agno/utils/print_response/workflow.py +112 -12
- agno/workflow/condition.py +25 -0
- agno/workflow/loop.py +25 -0
- agno/workflow/parallel.py +137 -113
- agno/workflow/router.py +25 -0
- agno/workflow/step.py +138 -25
- agno/workflow/steps.py +25 -0
- agno/workflow/types.py +26 -1
- agno/workflow/workflow.py +225 -7
- {agno-2.1.3.dist-info → agno-2.1.4.dist-info}/METADATA +1 -1
- {agno-2.1.3.dist-info → agno-2.1.4.dist-info}/RECORD +27 -26
- {agno-2.1.3.dist-info → agno-2.1.4.dist-info}/WHEEL +0 -0
- {agno-2.1.3.dist-info → agno-2.1.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.3.dist-info → agno-2.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google Drive API integration for file management and sharing.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
This module provides functions to interact with Google Drive, including listing,
|
|
6
|
+
uploading, and downloading files.
|
|
7
|
+
It uses the Google Drive API and handles authentication via OAuth2.
|
|
8
|
+
|
|
9
|
+
Required Environment Variables:
|
|
10
|
+
-----------------------------
|
|
11
|
+
- GOOGLE_CLIENT_ID: Google OAuth client ID
|
|
12
|
+
- GOOGLE_CLIENT_SECRET: Google OAuth client secret
|
|
13
|
+
- GOOGLE_PROJECT_ID: Google Cloud project ID
|
|
14
|
+
- GOOGLE_REDIRECT_URI: Google OAuth redirect URI (default: http://localhost)
|
|
15
|
+
- GOOGLE_CLOUD_QUOTA_PROJECT_ID: Google Cloud quota project ID
|
|
16
|
+
|
|
17
|
+
How to Get These Credentials:
|
|
18
|
+
---------------------------
|
|
19
|
+
1. Go to Google Cloud Console (https://console.cloud.google.com)
|
|
20
|
+
2. Create a new project or select an existing one
|
|
21
|
+
3. Enable the Google Drive API:
|
|
22
|
+
- Go to "APIs & Services" > "Enable APIs and Services"
|
|
23
|
+
- Search for "Google Drive API"
|
|
24
|
+
- Click "Enable"
|
|
25
|
+
|
|
26
|
+
4. Create OAuth 2.0 credentials:
|
|
27
|
+
- Go to "APIs & Services" > "Credentials"
|
|
28
|
+
- Click "Create Credentials" > "OAuth client ID"
|
|
29
|
+
- Enable the OAuth Consent Screen if you haven't already
|
|
30
|
+
- After enabling the Consent Screen, click on "Create Credentials" > "OAuth client ID"
|
|
31
|
+
- You'll receive:
|
|
32
|
+
* Client ID (GOOGLE_CLIENT_ID)
|
|
33
|
+
* Client Secret (GOOGLE_CLIENT_SECRET)
|
|
34
|
+
- The Project ID (GOOGLE_PROJECT_ID) is visible in the project dropdown at the top of the page
|
|
35
|
+
|
|
36
|
+
5. Add auth redirect URI:
|
|
37
|
+
- Go to https://console.cloud.google.com/auth/clients
|
|
38
|
+
- Add `http://localhost:5050` as a recognized redirect URI OR with http://localhost:{PORT_NUMBER}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
6. Set up environment variables:
|
|
42
|
+
Create a .envrc file in your project root with:
|
|
43
|
+
``
|
|
44
|
+
export GOOGLE_CLIENT_ID=your_client_id_here
|
|
45
|
+
export GOOGLE_CLIENT_SECRET=your_client_secret_here
|
|
46
|
+
export GOOGLE_PROJECT_ID=your_project_id_here
|
|
47
|
+
export GOOGLE_REDIRECT_URI=http://localhost/ # Default value
|
|
48
|
+
export GOOGLE_AUTHENTICATION_PORT=5050 # Port for OAuth redirect
|
|
49
|
+
export GOOGLE_CLOUD_QUOTA_PROJECT_ID=your_quota_project_id_here
|
|
50
|
+
``
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Remember to install the dependencies using `pip install google google-auth-oauthlib`
|
|
55
|
+
|
|
56
|
+
Important Points to Note :
|
|
57
|
+
1. The first time you run the application, it will open a browser window for OAuth authentication.
|
|
58
|
+
2. A token.json file will be created to store the authentication credentials for future use.
|
|
59
|
+
|
|
60
|
+
You can customize the authentication port by setting the `GOOGLE_AUTHENTICATION_PORT` environment variable.
|
|
61
|
+
This will be used in the `run_local_server` method for OAuth authentication.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
import mimetypes
|
|
66
|
+
from functools import wraps
|
|
67
|
+
from os import getenv
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
from typing import Any, List, Optional, Union
|
|
70
|
+
|
|
71
|
+
from agno.tools import Toolkit
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from google.auth.transport.requests import Request
|
|
75
|
+
from google.oauth2.credentials import Credentials
|
|
76
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
77
|
+
from googleapiclient.discovery import Resource, build
|
|
78
|
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
|
79
|
+
except ImportError:
|
|
80
|
+
raise ImportError(
|
|
81
|
+
"Google client library for Python not found , install it using `pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib`"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def authenticate(func):
|
|
86
|
+
"""Decorator to ensure authentication before executing a function."""
|
|
87
|
+
|
|
88
|
+
@wraps(func)
|
|
89
|
+
def wrapper(self, *args, **kwargs):
|
|
90
|
+
if not self.creds or not self.creds.valid:
|
|
91
|
+
self._auth()
|
|
92
|
+
if not self.service:
|
|
93
|
+
# Set quota project on credentials if available
|
|
94
|
+
creds_to_use = self.creds
|
|
95
|
+
if hasattr(self, "quota_project_id") and self.quota_project_id:
|
|
96
|
+
creds_to_use = self.creds.with_quota_project(self.quota_project_id)
|
|
97
|
+
self.service = build("drive", "v3", credentials=creds_to_use)
|
|
98
|
+
return func(self, *args, **kwargs)
|
|
99
|
+
|
|
100
|
+
return wrapper
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class GoogleDriveTools(Toolkit):
|
|
104
|
+
# Default scopes for Google Drive API access
|
|
105
|
+
DEFAULT_SCOPES = ["https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.readonly"]
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
auth_port: Optional[int] = 5050,
|
|
110
|
+
creds: Optional[Credentials] = None,
|
|
111
|
+
scopes: Optional[List[str]] = None,
|
|
112
|
+
creds_path: Optional[str] = None,
|
|
113
|
+
token_path: Optional[str] = None,
|
|
114
|
+
quota_project_id: Optional[str] = None,
|
|
115
|
+
**kwargs,
|
|
116
|
+
):
|
|
117
|
+
self.creds: Optional[Credentials] = creds
|
|
118
|
+
self.service: Optional[Resource] = None
|
|
119
|
+
self.credentials_path = creds_path
|
|
120
|
+
self.token_path = token_path
|
|
121
|
+
self.scopes = scopes or []
|
|
122
|
+
self.scopes.extend(self.DEFAULT_SCOPES)
|
|
123
|
+
|
|
124
|
+
self.quota_project_id = quota_project_id or getenv("GOOGLE_CLOUD_QUOTA_PROJECT_ID")
|
|
125
|
+
if not self.quota_project_id:
|
|
126
|
+
raise ValueError("GOOGLE_CLOUD_QUOTA_PROJECT_ID is not set")
|
|
127
|
+
|
|
128
|
+
self.auth_port: int = int(getenv("GOOGLE_AUTH_PORT", str(auth_port)))
|
|
129
|
+
if not self.auth_port:
|
|
130
|
+
raise ValueError("GOOGLE_AUTH_PORT is not set")
|
|
131
|
+
|
|
132
|
+
tools: List[Any] = [
|
|
133
|
+
self.list_files,
|
|
134
|
+
]
|
|
135
|
+
super().__init__(name="google_drive_tools", tools=tools, **kwargs)
|
|
136
|
+
if not self.scopes:
|
|
137
|
+
# Add read permission by default
|
|
138
|
+
self.scopes.append(self.DEFAULT_SCOPES[1]) # 'drive.readonly'
|
|
139
|
+
# Add write permission if allow_update is True
|
|
140
|
+
if getattr(self, "allow_update", False):
|
|
141
|
+
self.scopes.append(self.DEFAULT_SCOPES[0]) # 'drive.file'
|
|
142
|
+
|
|
143
|
+
def _auth(self):
|
|
144
|
+
"""
|
|
145
|
+
Authenticate and set up the Google Drive API client.
|
|
146
|
+
This method checks if credentials are valid and refreshes or requests them if needed.
|
|
147
|
+
"""
|
|
148
|
+
if self.creds and self.creds.valid:
|
|
149
|
+
# Already authenticated
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
token_file = Path(self.token_path or "token.json")
|
|
153
|
+
creds_file = Path(self.credentials_path or "credentials.json")
|
|
154
|
+
|
|
155
|
+
if token_file.exists():
|
|
156
|
+
self.creds = Credentials.from_authorized_user_file(str(token_file), self.scopes)
|
|
157
|
+
if not self.creds or not self.creds.valid:
|
|
158
|
+
if self.creds and self.creds.expired and self.creds.refresh_token:
|
|
159
|
+
self.creds.refresh(Request())
|
|
160
|
+
else:
|
|
161
|
+
client_config = {
|
|
162
|
+
"installed": {
|
|
163
|
+
"client_id": getenv("GOOGLE_CLIENT_ID"),
|
|
164
|
+
"client_secret": getenv("GOOGLE_CLIENT_SECRET"),
|
|
165
|
+
"project_id": getenv("GOOGLE_PROJECT_ID"),
|
|
166
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
167
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
168
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
169
|
+
"redirect_uris": [getenv("GOOGLE_REDIRECT_URI", "http://localhost")],
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
# File based authentication
|
|
173
|
+
if creds_file.exists():
|
|
174
|
+
flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), self.scopes)
|
|
175
|
+
else:
|
|
176
|
+
flow = InstalledAppFlow.from_client_config(client_config, self.scopes)
|
|
177
|
+
# Opens up a browser window for OAuth authentication
|
|
178
|
+
self.creds = flow.run_local_server(port=self.auth_port) # type: ignore
|
|
179
|
+
|
|
180
|
+
token_file.write_text(self.creds.to_json()) if self.creds else None
|
|
181
|
+
|
|
182
|
+
@authenticate
|
|
183
|
+
def list_files(self, query: Optional[str] = None, page_size: int = 10) -> List[dict]:
|
|
184
|
+
"""
|
|
185
|
+
List files in your Google Drive.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
query (Optional[str]): Optional search query to filter files (see Google Drive API docs).
|
|
189
|
+
page_size (int): Maximum number of files to return.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List[dict]: List of file metadata dictionaries.
|
|
193
|
+
"""
|
|
194
|
+
if not self.service:
|
|
195
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
196
|
+
try:
|
|
197
|
+
results = (
|
|
198
|
+
self.service.files() # type: ignore
|
|
199
|
+
.list(q=query, pageSize=page_size, fields="nextPageToken, files(id, name, mimeType, modifiedTime)")
|
|
200
|
+
.execute()
|
|
201
|
+
)
|
|
202
|
+
items = results.get("files", [])
|
|
203
|
+
return items
|
|
204
|
+
except Exception as error:
|
|
205
|
+
print(f"Could not list files: {error}")
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
@authenticate
|
|
209
|
+
def upload_file(self, file_path: Union[str, Path], mime_type: Optional[str] = None) -> Optional[dict]:
|
|
210
|
+
"""
|
|
211
|
+
Upload a file to your Google Drive.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
file_path (Union[str, Path]): Path to the file you want to upload.
|
|
215
|
+
mime_type (Optional[str]): MIME type of the file. If not provided, it will be guessed.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Optional[dict]: Metadata of the uploaded file, or None if upload failed.
|
|
219
|
+
"""
|
|
220
|
+
if not self.service:
|
|
221
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
222
|
+
file_path = Path(file_path)
|
|
223
|
+
if not file_path.exists() or not file_path.is_file():
|
|
224
|
+
raise ValueError(f"The file '{file_path}' does not exist or is not a file.")
|
|
225
|
+
if mime_type is None:
|
|
226
|
+
mime_type, _ = mimetypes.guess_type(file_path.as_posix())
|
|
227
|
+
if mime_type is None:
|
|
228
|
+
mime_type = "application/octet-stream" # Default MIME type
|
|
229
|
+
|
|
230
|
+
file_metadata = {"name": file_path.name}
|
|
231
|
+
media = MediaFileUpload(file_path.as_posix(), mimetype=mime_type)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
uploaded_file = (
|
|
235
|
+
self.service.files() # type: ignore
|
|
236
|
+
.create(body=file_metadata, media_body=media, fields="id, name, mimeType, modifiedTime")
|
|
237
|
+
.execute()
|
|
238
|
+
)
|
|
239
|
+
return uploaded_file
|
|
240
|
+
except Exception as error:
|
|
241
|
+
print(f"Could not upload file '{file_path}': {error}")
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
@authenticate
|
|
245
|
+
def download_file(self, file_id: str, dest_path: Union[str, Path]) -> Optional[Path]:
|
|
246
|
+
"""
|
|
247
|
+
Download a file from your Google Drive.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
file_id (str): The ID of the file you want to download.
|
|
251
|
+
dest_path (Union[str, Path]): Where to save the downloaded file.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Optional[Path]: The path to the downloaded file, or None if download failed.
|
|
255
|
+
"""
|
|
256
|
+
if not self.service:
|
|
257
|
+
raise ValueError("Google Drive service is not initialized. Please authenticate first.")
|
|
258
|
+
dest_path = Path(dest_path)
|
|
259
|
+
try:
|
|
260
|
+
request = self.service.files().get_media(fileId=file_id) # type: ignore
|
|
261
|
+
with open(dest_path, "wb") as fh:
|
|
262
|
+
downloader = MediaIoBaseDownload(fh, request)
|
|
263
|
+
done = False
|
|
264
|
+
while not done:
|
|
265
|
+
status, done = downloader.next_chunk()
|
|
266
|
+
print(f"Download progress: {int(status.progress() * 100)}%.")
|
|
267
|
+
return dest_path
|
|
268
|
+
except Exception as error:
|
|
269
|
+
print(f"Could not download file '{file_id}': {error}")
|
|
270
|
+
return None
|
|
@@ -257,6 +257,11 @@ def print_response_stream(
|
|
|
257
257
|
current_primitive_context = None # Current primitive being executed (parallel, loop, etc.)
|
|
258
258
|
step_display_cache = {} # type: ignore
|
|
259
259
|
|
|
260
|
+
# Parallel-aware tracking for simultaneous steps
|
|
261
|
+
parallel_step_states: Dict[
|
|
262
|
+
Any, Dict[str, Any]
|
|
263
|
+
] = {} # track state of each parallel step: {step_index: {"name": str, "content": str, "started": bool, "completed": bool}}
|
|
264
|
+
|
|
260
265
|
def get_step_display_number(step_index: Union[int, tuple], step_name: str = "") -> str:
|
|
261
266
|
"""Generate clean two-level step numbering: x.y format only"""
|
|
262
267
|
|
|
@@ -321,8 +326,11 @@ def print_response_stream(
|
|
|
321
326
|
live_log.update(status)
|
|
322
327
|
|
|
323
328
|
elif isinstance(response, StepStartedEvent):
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
step_name = response.step_name or "Unknown"
|
|
330
|
+
step_index = response.step_index or 0 # type: ignore
|
|
331
|
+
|
|
332
|
+
current_step_name = step_name
|
|
333
|
+
current_step_index = step_index # type: ignore
|
|
326
334
|
current_step_content = ""
|
|
327
335
|
step_started_printed = False
|
|
328
336
|
|
|
@@ -335,6 +343,14 @@ def print_response_stream(
|
|
|
335
343
|
step_name = response.step_name or "Unknown"
|
|
336
344
|
step_index = response.step_index or 0
|
|
337
345
|
|
|
346
|
+
# Skip parallel sub-step completed events - they're handled in ParallelExecutionCompletedEvent (avoid duplication)
|
|
347
|
+
if (
|
|
348
|
+
current_primitive_context
|
|
349
|
+
and current_primitive_context["type"] == "parallel"
|
|
350
|
+
and isinstance(step_index, tuple)
|
|
351
|
+
):
|
|
352
|
+
continue
|
|
353
|
+
|
|
338
354
|
# Generate smart step number for completion (will use cached value)
|
|
339
355
|
step_display = get_step_display_number(step_index, step_name)
|
|
340
356
|
status.update(f"Completed {step_display}: {step_name}")
|
|
@@ -376,7 +392,8 @@ def print_response_stream(
|
|
|
376
392
|
"max_iterations": response.max_iterations,
|
|
377
393
|
}
|
|
378
394
|
|
|
379
|
-
#
|
|
395
|
+
# Initialize parallel step tracking - clear previous states
|
|
396
|
+
parallel_step_states.clear()
|
|
380
397
|
step_display_cache.clear()
|
|
381
398
|
|
|
382
399
|
status.update(f"Starting loop: {current_step_name} (max {response.max_iterations} iterations)...")
|
|
@@ -442,7 +459,8 @@ def print_response_stream(
|
|
|
442
459
|
"total_steps": response.parallel_step_count,
|
|
443
460
|
}
|
|
444
461
|
|
|
445
|
-
#
|
|
462
|
+
# Initialize parallel step tracking - clear previous states
|
|
463
|
+
parallel_step_states.clear()
|
|
446
464
|
step_display_cache.clear()
|
|
447
465
|
|
|
448
466
|
# Print parallel execution summary panel
|
|
@@ -468,8 +486,30 @@ def print_response_stream(
|
|
|
468
486
|
|
|
469
487
|
status.update(f"Completed parallel execution: {step_name}")
|
|
470
488
|
|
|
489
|
+
# Display individual parallel step results immediately
|
|
490
|
+
if show_step_details and response.step_results:
|
|
491
|
+
live_log.update(status, refresh=True)
|
|
492
|
+
|
|
493
|
+
# Get the parallel container's display number for consistent numbering
|
|
494
|
+
parallel_step_display = get_step_display_number(step_index, step_name)
|
|
495
|
+
|
|
496
|
+
# Show each parallel step with the same number (1.1, 1.1)
|
|
497
|
+
for step_result in response.step_results:
|
|
498
|
+
if step_result.content:
|
|
499
|
+
step_result_name = step_result.step_name or "Parallel Step"
|
|
500
|
+
formatted_content = format_step_content_for_display(step_result.content) # type: ignore
|
|
501
|
+
|
|
502
|
+
# All parallel sub-steps get the same number
|
|
503
|
+
parallel_step_panel = create_panel(
|
|
504
|
+
content=Markdown(formatted_content) if markdown else formatted_content,
|
|
505
|
+
title=f"{parallel_step_display}: {step_result_name} (Completed)",
|
|
506
|
+
border_style="orange3",
|
|
507
|
+
)
|
|
508
|
+
console.print(parallel_step_panel) # type: ignore
|
|
509
|
+
|
|
471
510
|
# Reset context
|
|
472
511
|
current_primitive_context = None
|
|
512
|
+
parallel_step_states.clear()
|
|
473
513
|
step_display_cache.clear()
|
|
474
514
|
|
|
475
515
|
elif isinstance(response, ConditionExecutionStartedEvent):
|
|
@@ -486,7 +526,8 @@ def print_response_stream(
|
|
|
486
526
|
"condition_result": response.condition_result,
|
|
487
527
|
}
|
|
488
528
|
|
|
489
|
-
#
|
|
529
|
+
# Initialize parallel step tracking - clear previous states
|
|
530
|
+
parallel_step_states.clear()
|
|
490
531
|
step_display_cache.clear()
|
|
491
532
|
|
|
492
533
|
condition_text = "met" if response.condition_result else "not met"
|
|
@@ -517,7 +558,8 @@ def print_response_stream(
|
|
|
517
558
|
"selected_steps": response.selected_steps,
|
|
518
559
|
}
|
|
519
560
|
|
|
520
|
-
#
|
|
561
|
+
# Initialize parallel step tracking - clear previous states
|
|
562
|
+
parallel_step_states.clear()
|
|
521
563
|
step_display_cache.clear()
|
|
522
564
|
|
|
523
565
|
selected_steps_text = ", ".join(response.selected_steps) if response.selected_steps else "none"
|
|
@@ -667,6 +709,14 @@ def print_response_stream(
|
|
|
667
709
|
# Use the unified formatting function for consistency
|
|
668
710
|
response_str = format_step_content_for_display(response_str) # type: ignore
|
|
669
711
|
|
|
712
|
+
# Skip streaming content from parallel sub-steps - they're handled in ParallelExecutionCompletedEvent
|
|
713
|
+
if (
|
|
714
|
+
current_primitive_context
|
|
715
|
+
and current_primitive_context["type"] == "parallel"
|
|
716
|
+
and isinstance(current_step_index, tuple)
|
|
717
|
+
):
|
|
718
|
+
continue
|
|
719
|
+
|
|
670
720
|
# Filter out empty responses and add to current step content
|
|
671
721
|
if response_str and response_str.strip():
|
|
672
722
|
# If it's a structured output from a team, replace the content instead of appending
|
|
@@ -990,6 +1040,11 @@ async def aprint_response_stream(
|
|
|
990
1040
|
current_primitive_context = None # Current primitive being executed (parallel, loop, etc.)
|
|
991
1041
|
step_display_cache = {} # type: ignore
|
|
992
1042
|
|
|
1043
|
+
# Parallel-aware tracking for simultaneous steps
|
|
1044
|
+
parallel_step_states: Dict[
|
|
1045
|
+
Any, Dict[str, Any]
|
|
1046
|
+
] = {} # track state of each parallel step: {step_index: {"name": str, "content": str, "started": bool, "completed": bool}}
|
|
1047
|
+
|
|
993
1048
|
def get_step_display_number(step_index: Union[int, tuple], step_name: str = "") -> str:
|
|
994
1049
|
"""Generate clean two-level step numbering: x.y format only"""
|
|
995
1050
|
|
|
@@ -1054,8 +1109,11 @@ async def aprint_response_stream(
|
|
|
1054
1109
|
live_log.update(status)
|
|
1055
1110
|
|
|
1056
1111
|
elif isinstance(response, StepStartedEvent):
|
|
1057
|
-
|
|
1058
|
-
|
|
1112
|
+
step_name = response.step_name or "Unknown"
|
|
1113
|
+
step_index = response.step_index or 0 # type: ignore
|
|
1114
|
+
|
|
1115
|
+
current_step_name = step_name
|
|
1116
|
+
current_step_index = step_index # type: ignore
|
|
1059
1117
|
current_step_content = ""
|
|
1060
1118
|
step_started_printed = False
|
|
1061
1119
|
|
|
@@ -1068,6 +1126,14 @@ async def aprint_response_stream(
|
|
|
1068
1126
|
step_name = response.step_name or "Unknown"
|
|
1069
1127
|
step_index = response.step_index or 0
|
|
1070
1128
|
|
|
1129
|
+
# Skip parallel sub-step completed events - they're handled in ParallelExecutionCompletedEvent (avoid duplication)
|
|
1130
|
+
if (
|
|
1131
|
+
current_primitive_context
|
|
1132
|
+
and current_primitive_context["type"] == "parallel"
|
|
1133
|
+
and isinstance(step_index, tuple)
|
|
1134
|
+
):
|
|
1135
|
+
continue
|
|
1136
|
+
|
|
1071
1137
|
# Generate smart step number for completion (will use cached value)
|
|
1072
1138
|
step_display = get_step_display_number(step_index, step_name)
|
|
1073
1139
|
status.update(f"Completed {step_display}: {step_name}")
|
|
@@ -1109,7 +1175,8 @@ async def aprint_response_stream(
|
|
|
1109
1175
|
"max_iterations": response.max_iterations,
|
|
1110
1176
|
}
|
|
1111
1177
|
|
|
1112
|
-
#
|
|
1178
|
+
# Initialize parallel step tracking - clear previous states
|
|
1179
|
+
parallel_step_states.clear()
|
|
1113
1180
|
step_display_cache.clear()
|
|
1114
1181
|
|
|
1115
1182
|
status.update(f"Starting loop: {current_step_name} (max {response.max_iterations} iterations)...")
|
|
@@ -1175,7 +1242,8 @@ async def aprint_response_stream(
|
|
|
1175
1242
|
"total_steps": response.parallel_step_count,
|
|
1176
1243
|
}
|
|
1177
1244
|
|
|
1178
|
-
#
|
|
1245
|
+
# Initialize parallel step tracking - clear previous states
|
|
1246
|
+
parallel_step_states.clear()
|
|
1179
1247
|
step_display_cache.clear()
|
|
1180
1248
|
|
|
1181
1249
|
# Print parallel execution summary panel
|
|
@@ -1201,8 +1269,30 @@ async def aprint_response_stream(
|
|
|
1201
1269
|
|
|
1202
1270
|
status.update(f"Completed parallel execution: {step_name}")
|
|
1203
1271
|
|
|
1272
|
+
# Display individual parallel step results immediately
|
|
1273
|
+
if show_step_details and response.step_results:
|
|
1274
|
+
live_log.update(status, refresh=True)
|
|
1275
|
+
|
|
1276
|
+
# Get the parallel container's display number for consistent numbering
|
|
1277
|
+
parallel_step_display = get_step_display_number(step_index, step_name)
|
|
1278
|
+
|
|
1279
|
+
# Show each parallel step with the same number (1.1, 1.1)
|
|
1280
|
+
for step_result in response.step_results:
|
|
1281
|
+
if step_result.content:
|
|
1282
|
+
step_result_name = step_result.step_name or "Parallel Step"
|
|
1283
|
+
formatted_content = format_step_content_for_display(step_result.content) # type: ignore
|
|
1284
|
+
|
|
1285
|
+
# All parallel sub-steps get the same number
|
|
1286
|
+
parallel_step_panel = create_panel(
|
|
1287
|
+
content=Markdown(formatted_content) if markdown else formatted_content,
|
|
1288
|
+
title=f"{parallel_step_display}: {step_result_name} (Completed)",
|
|
1289
|
+
border_style="orange3",
|
|
1290
|
+
)
|
|
1291
|
+
console.print(parallel_step_panel) # type: ignore
|
|
1292
|
+
|
|
1204
1293
|
# Reset context
|
|
1205
1294
|
current_primitive_context = None
|
|
1295
|
+
parallel_step_states.clear()
|
|
1206
1296
|
step_display_cache.clear()
|
|
1207
1297
|
|
|
1208
1298
|
elif isinstance(response, ConditionExecutionStartedEvent):
|
|
@@ -1219,7 +1309,8 @@ async def aprint_response_stream(
|
|
|
1219
1309
|
"condition_result": response.condition_result,
|
|
1220
1310
|
}
|
|
1221
1311
|
|
|
1222
|
-
#
|
|
1312
|
+
# Initialize parallel step tracking - clear previous states
|
|
1313
|
+
parallel_step_states.clear()
|
|
1223
1314
|
step_display_cache.clear()
|
|
1224
1315
|
|
|
1225
1316
|
condition_text = "met" if response.condition_result else "not met"
|
|
@@ -1250,7 +1341,8 @@ async def aprint_response_stream(
|
|
|
1250
1341
|
"selected_steps": response.selected_steps,
|
|
1251
1342
|
}
|
|
1252
1343
|
|
|
1253
|
-
#
|
|
1344
|
+
# Initialize parallel step tracking - clear previous states
|
|
1345
|
+
parallel_step_states.clear()
|
|
1254
1346
|
step_display_cache.clear()
|
|
1255
1347
|
|
|
1256
1348
|
selected_steps_text = ", ".join(response.selected_steps) if response.selected_steps else "none"
|
|
@@ -1404,6 +1496,14 @@ async def aprint_response_stream(
|
|
|
1404
1496
|
# Use the unified formatting function for consistency
|
|
1405
1497
|
response_str = format_step_content_for_display(response_str) # type: ignore
|
|
1406
1498
|
|
|
1499
|
+
# Skip streaming content from parallel sub-steps - they're handled in ParallelExecutionCompletedEvent
|
|
1500
|
+
if (
|
|
1501
|
+
current_primitive_context
|
|
1502
|
+
and current_primitive_context["type"] == "parallel"
|
|
1503
|
+
and isinstance(current_step_index, tuple)
|
|
1504
|
+
):
|
|
1505
|
+
continue
|
|
1506
|
+
|
|
1407
1507
|
# Filter out empty responses and add to current step content
|
|
1408
1508
|
if response_str and response_str.strip():
|
|
1409
1509
|
# If it's a structured output from a team, replace the content instead of appending
|
agno/workflow/condition.py
CHANGED
|
@@ -11,6 +11,7 @@ from agno.run.workflow import (
|
|
|
11
11
|
WorkflowRunOutput,
|
|
12
12
|
WorkflowRunOutputEvent,
|
|
13
13
|
)
|
|
14
|
+
from agno.session.workflow import WorkflowSession
|
|
14
15
|
from agno.utils.log import log_debug, logger
|
|
15
16
|
from agno.workflow.step import Step
|
|
16
17
|
from agno.workflow.types import StepInput, StepOutput, StepType
|
|
@@ -153,6 +154,9 @@ class Condition:
|
|
|
153
154
|
workflow_run_response: Optional[WorkflowRunOutput] = None,
|
|
154
155
|
store_executor_outputs: bool = True,
|
|
155
156
|
session_state: Optional[Dict[str, Any]] = None,
|
|
157
|
+
workflow_session: Optional[WorkflowSession] = None,
|
|
158
|
+
add_workflow_history_to_steps: Optional[bool] = False,
|
|
159
|
+
num_history_runs: int = 3,
|
|
156
160
|
) -> StepOutput:
|
|
157
161
|
"""Execute the condition and its steps with sequential chaining if condition is true"""
|
|
158
162
|
log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
|
|
@@ -189,6 +193,9 @@ class Condition:
|
|
|
189
193
|
workflow_run_response=workflow_run_response,
|
|
190
194
|
store_executor_outputs=store_executor_outputs,
|
|
191
195
|
session_state=session_state,
|
|
196
|
+
workflow_session=workflow_session,
|
|
197
|
+
add_workflow_history_to_steps=add_workflow_history_to_steps,
|
|
198
|
+
num_history_runs=num_history_runs,
|
|
192
199
|
)
|
|
193
200
|
|
|
194
201
|
# Handle both single StepOutput and List[StepOutput] (from Loop/Condition/Router steps)
|
|
@@ -255,6 +262,9 @@ class Condition:
|
|
|
255
262
|
store_executor_outputs: bool = True,
|
|
256
263
|
session_state: Optional[Dict[str, Any]] = None,
|
|
257
264
|
parent_step_id: Optional[str] = None,
|
|
265
|
+
workflow_session: Optional[WorkflowSession] = None,
|
|
266
|
+
add_workflow_history_to_steps: Optional[bool] = False,
|
|
267
|
+
num_history_runs: int = 3,
|
|
258
268
|
) -> Iterator[Union[WorkflowRunOutputEvent, StepOutput]]:
|
|
259
269
|
"""Execute the condition with streaming support - mirrors Loop logic"""
|
|
260
270
|
log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
|
|
@@ -327,6 +337,9 @@ class Condition:
|
|
|
327
337
|
store_executor_outputs=store_executor_outputs,
|
|
328
338
|
session_state=session_state,
|
|
329
339
|
parent_step_id=conditional_step_id,
|
|
340
|
+
workflow_session=workflow_session,
|
|
341
|
+
add_workflow_history_to_steps=add_workflow_history_to_steps,
|
|
342
|
+
num_history_runs=num_history_runs,
|
|
330
343
|
):
|
|
331
344
|
if isinstance(event, StepOutput):
|
|
332
345
|
step_outputs_for_step.append(event)
|
|
@@ -407,6 +420,9 @@ class Condition:
|
|
|
407
420
|
workflow_run_response: Optional[WorkflowRunOutput] = None,
|
|
408
421
|
store_executor_outputs: bool = True,
|
|
409
422
|
session_state: Optional[Dict[str, Any]] = None,
|
|
423
|
+
workflow_session: Optional[WorkflowSession] = None,
|
|
424
|
+
add_workflow_history_to_steps: Optional[bool] = False,
|
|
425
|
+
num_history_runs: int = 3,
|
|
410
426
|
) -> StepOutput:
|
|
411
427
|
"""Async execute the condition and its steps with sequential chaining"""
|
|
412
428
|
log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
|
|
@@ -445,6 +461,9 @@ class Condition:
|
|
|
445
461
|
workflow_run_response=workflow_run_response,
|
|
446
462
|
store_executor_outputs=store_executor_outputs,
|
|
447
463
|
session_state=session_state,
|
|
464
|
+
workflow_session=workflow_session,
|
|
465
|
+
add_workflow_history_to_steps=add_workflow_history_to_steps,
|
|
466
|
+
num_history_runs=num_history_runs,
|
|
448
467
|
)
|
|
449
468
|
|
|
450
469
|
# Handle both single StepOutput and List[StepOutput]
|
|
@@ -509,6 +528,9 @@ class Condition:
|
|
|
509
528
|
store_executor_outputs: bool = True,
|
|
510
529
|
session_state: Optional[Dict[str, Any]] = None,
|
|
511
530
|
parent_step_id: Optional[str] = None,
|
|
531
|
+
workflow_session: Optional[WorkflowSession] = None,
|
|
532
|
+
add_workflow_history_to_steps: Optional[bool] = False,
|
|
533
|
+
num_history_runs: int = 3,
|
|
512
534
|
) -> AsyncIterator[Union[WorkflowRunOutputEvent, TeamRunOutputEvent, RunOutputEvent, StepOutput]]:
|
|
513
535
|
"""Async execute the condition with streaming support - mirrors Loop logic"""
|
|
514
536
|
log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
|
|
@@ -583,6 +605,9 @@ class Condition:
|
|
|
583
605
|
store_executor_outputs=store_executor_outputs,
|
|
584
606
|
session_state=session_state,
|
|
585
607
|
parent_step_id=conditional_step_id,
|
|
608
|
+
workflow_session=workflow_session,
|
|
609
|
+
add_workflow_history_to_steps=add_workflow_history_to_steps,
|
|
610
|
+
num_history_runs=num_history_runs,
|
|
586
611
|
):
|
|
587
612
|
if isinstance(event, StepOutput):
|
|
588
613
|
step_outputs_for_step.append(event)
|