pygeai 0.7.0b1__py3-none-any.whl → 0.7.0b4__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.
- pygeai/chat/clients.py +56 -32
- pygeai/cli/commands/base.py +167 -6
- pygeai/cli/commands/migrate.py +2 -3
- pygeai/cli/geai.py +15 -7
- pygeai/cli/texts/help.py +26 -1
- pygeai/core/handlers.py +27 -14
- pygeai/core/utils/console.py +69 -5
- pygeai/core/utils/parsers.py +33 -12
- pygeai/lab/models.py +1 -1
- pygeai/lab/processes/endpoints.py +1 -1
- pygeai/migration/strategies.py +6 -14
- pygeai/tests/chat/test_streaming_json.py +436 -0
- pygeai/tests/cli/commands/test_json_parsing.py +419 -0
- pygeai/tests/core/utils/test_parsers.py +379 -0
- {pygeai-0.7.0b1.dist-info → pygeai-0.7.0b4.dist-info}/METADATA +1 -1
- {pygeai-0.7.0b1.dist-info → pygeai-0.7.0b4.dist-info}/RECORD +20 -17
- {pygeai-0.7.0b1.dist-info → pygeai-0.7.0b4.dist-info}/WHEEL +1 -1
- {pygeai-0.7.0b1.dist-info → pygeai-0.7.0b4.dist-info}/entry_points.txt +0 -0
- {pygeai-0.7.0b1.dist-info → pygeai-0.7.0b4.dist-info}/licenses/LICENSE +0 -0
- {pygeai-0.7.0b1.dist-info → pygeai-0.7.0b4.dist-info}/top_level.txt +0 -0
pygeai/chat/clients.py
CHANGED
|
@@ -178,27 +178,39 @@ class ChatClient(BaseClient):
|
|
|
178
178
|
"""
|
|
179
179
|
Processes a streaming response and yields content strings.
|
|
180
180
|
|
|
181
|
+
Optimized for performance:
|
|
182
|
+
- Early exits to avoid unnecessary processing
|
|
183
|
+
- Reduced string operations
|
|
184
|
+
- Single dict lookups with guards
|
|
185
|
+
|
|
181
186
|
:param response: The streaming response from the API.
|
|
182
187
|
:return: Generator[str, None, None] - Yields content strings extracted from streaming chunks.
|
|
183
188
|
"""
|
|
184
189
|
try:
|
|
185
190
|
for line in response:
|
|
186
|
-
if line.startswith("data:"):
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
if not line.startswith("data:"):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
chunk = line[5:].strip()
|
|
195
|
+
if chunk == "[DONE]":
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
json_data = json.loads(chunk)
|
|
200
|
+
except JSONDecodeError:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
choices = json_data.get("choices")
|
|
204
|
+
if not choices:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
first_choice = choices[0] if choices else None
|
|
208
|
+
if not first_choice:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
delta = first_choice.get("delta")
|
|
212
|
+
if delta and "content" in delta:
|
|
213
|
+
yield delta["content"]
|
|
202
214
|
except Exception as e:
|
|
203
215
|
raise InvalidAPIResponseException(f"Unable to process streaming chat response: {e}")
|
|
204
216
|
|
|
@@ -206,27 +218,39 @@ class ChatClient(BaseClient):
|
|
|
206
218
|
"""
|
|
207
219
|
Processes a streaming response from the Responses API and yields content strings.
|
|
208
220
|
|
|
221
|
+
Optimized for performance:
|
|
222
|
+
- Early exits to avoid unnecessary processing
|
|
223
|
+
- Reduced string operations
|
|
224
|
+
- Single dict lookups with guards
|
|
225
|
+
|
|
209
226
|
:param response: The streaming response from the API.
|
|
210
227
|
:return: Generator[str, None, None] - Yields content strings extracted from streaming chunks.
|
|
211
228
|
"""
|
|
212
229
|
try:
|
|
213
230
|
for line in response:
|
|
214
|
-
if line.startswith("data:"):
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
if not line.startswith("data:"):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
chunk = line[5:].strip()
|
|
235
|
+
if chunk == "[DONE]":
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
json_data = json.loads(chunk)
|
|
240
|
+
except JSONDecodeError:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
choices = json_data.get("choices")
|
|
244
|
+
if not choices:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
first_choice = choices[0] if choices else None
|
|
248
|
+
if not first_choice:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
delta = first_choice.get("delta")
|
|
252
|
+
if delta and "content" in delta:
|
|
253
|
+
yield delta["content"]
|
|
230
254
|
except Exception as e:
|
|
231
255
|
raise InvalidAPIResponseException(f"Unable to process streaming response: {e}")
|
|
232
256
|
|
pygeai/cli/commands/base.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
1
7
|
from pygeai.cli.commands import ArgumentsEnum, Command, Option
|
|
2
8
|
from pygeai.cli.commands.admin import admin_commands
|
|
3
9
|
from pygeai.cli.commands.analytics import analytics_commands
|
|
@@ -53,12 +59,142 @@ def check_for_updates():
|
|
|
53
59
|
Console.write_stdout(f"{version_status}")
|
|
54
60
|
|
|
55
61
|
|
|
56
|
-
def check_api_status():
|
|
62
|
+
def check_api_status(option_list: list = None):
|
|
57
63
|
"""
|
|
58
|
-
Checks API status
|
|
64
|
+
Checks API status with optional monitoring capabilities
|
|
65
|
+
|
|
66
|
+
:param option_list: List of tuples (option, value) including monitor, file, interval, and count
|
|
59
67
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
if option_list is None or not any(option_list):
|
|
69
|
+
api_status = HealthClient().check_api_status()
|
|
70
|
+
Console.write_stdout(f"API Status: {api_status}")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
options_dict = {opt.name: arg for opt, arg in option_list}
|
|
74
|
+
|
|
75
|
+
monitor = 'monitor' in options_dict
|
|
76
|
+
file_path = options_dict.get('file')
|
|
77
|
+
interval = int(options_dict.get('interval', 5)) if 'interval' in options_dict else 5
|
|
78
|
+
count = int(options_dict.get('count', 0)) if 'count' in options_dict else 0
|
|
79
|
+
|
|
80
|
+
if not monitor:
|
|
81
|
+
api_status = HealthClient().check_api_status()
|
|
82
|
+
Console.write_stdout(f"API Status: {api_status}")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
health_client = HealthClient()
|
|
86
|
+
check_count = 0
|
|
87
|
+
success_count = 0
|
|
88
|
+
failure_count = 0
|
|
89
|
+
downtime_periods = []
|
|
90
|
+
current_downtime_start = None
|
|
91
|
+
interrupted = False
|
|
92
|
+
|
|
93
|
+
file_handle = None
|
|
94
|
+
if file_path:
|
|
95
|
+
try:
|
|
96
|
+
file_handle = open(file_path, 'w')
|
|
97
|
+
file_handle.write("Timestamp,Status,Running,Version,Error\n")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
Console.write_stdout(f"Warning: Could not open file {file_path}: {e}")
|
|
100
|
+
file_handle = None
|
|
101
|
+
|
|
102
|
+
def signal_handler(sig, frame):
|
|
103
|
+
nonlocal interrupted
|
|
104
|
+
interrupted = True
|
|
105
|
+
|
|
106
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
107
|
+
|
|
108
|
+
Console.write_stdout(f"Monitoring API status every {interval} seconds. Press Ctrl+C to stop...")
|
|
109
|
+
Console.write_stdout("")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
while True:
|
|
113
|
+
timestamp = datetime.now()
|
|
114
|
+
timestamp_str = timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
api_status = health_client.check_api_status()
|
|
118
|
+
check_count += 1
|
|
119
|
+
|
|
120
|
+
running = api_status.get('running', False)
|
|
121
|
+
version = api_status.get('version', 'N/A')
|
|
122
|
+
|
|
123
|
+
if running:
|
|
124
|
+
success_count += 1
|
|
125
|
+
status = "✓ UP"
|
|
126
|
+
if current_downtime_start:
|
|
127
|
+
downtime_end = timestamp
|
|
128
|
+
downtime_periods.append((current_downtime_start, downtime_end))
|
|
129
|
+
current_downtime_start = None
|
|
130
|
+
else:
|
|
131
|
+
failure_count += 1
|
|
132
|
+
status = "✗ DOWN"
|
|
133
|
+
if not current_downtime_start:
|
|
134
|
+
current_downtime_start = timestamp
|
|
135
|
+
|
|
136
|
+
Console.write_stdout(f"[{timestamp_str}] {status} - Version: {version}")
|
|
137
|
+
|
|
138
|
+
if file_handle:
|
|
139
|
+
file_handle.write(f"{timestamp_str},{'UP' if running else 'DOWN'},{running},{version},\n")
|
|
140
|
+
file_handle.flush()
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
check_count += 1
|
|
144
|
+
failure_count += 1
|
|
145
|
+
status = "✗ ERROR"
|
|
146
|
+
error_msg = str(e)
|
|
147
|
+
|
|
148
|
+
Console.write_stdout(f"[{timestamp_str}] {status} - {error_msg}")
|
|
149
|
+
|
|
150
|
+
if not current_downtime_start:
|
|
151
|
+
current_downtime_start = timestamp
|
|
152
|
+
|
|
153
|
+
if file_handle:
|
|
154
|
+
file_handle.write(f"{timestamp_str},ERROR,False,N/A,{error_msg}\n")
|
|
155
|
+
file_handle.flush()
|
|
156
|
+
|
|
157
|
+
if count > 0 and check_count >= count:
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
if interrupted:
|
|
161
|
+
Console.write_stdout("\nMonitoring stopped by user.")
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
time.sleep(interval)
|
|
165
|
+
|
|
166
|
+
finally:
|
|
167
|
+
if current_downtime_start:
|
|
168
|
+
downtime_periods.append((current_downtime_start, datetime.now()))
|
|
169
|
+
|
|
170
|
+
if file_handle:
|
|
171
|
+
file_handle.close()
|
|
172
|
+
|
|
173
|
+
Console.write_stdout("")
|
|
174
|
+
Console.write_stdout("=" * 60)
|
|
175
|
+
Console.write_stdout("MONITORING SUMMARY")
|
|
176
|
+
Console.write_stdout("=" * 60)
|
|
177
|
+
Console.write_stdout(f"Total checks performed: {check_count}")
|
|
178
|
+
Console.write_stdout(f"Successful checks: {success_count}")
|
|
179
|
+
Console.write_stdout(f"Failed checks: {failure_count}")
|
|
180
|
+
|
|
181
|
+
if success_count > 0:
|
|
182
|
+
uptime_percentage = (success_count / check_count) * 100
|
|
183
|
+
Console.write_stdout(f"Uptime: {uptime_percentage:.2f}%")
|
|
184
|
+
|
|
185
|
+
if downtime_periods:
|
|
186
|
+
total_downtime = sum(
|
|
187
|
+
(end - start).total_seconds()
|
|
188
|
+
for start, end in downtime_periods
|
|
189
|
+
)
|
|
190
|
+
Console.write_stdout(f"Estimated total downtime: {total_downtime:.2f} seconds ({total_downtime/60:.2f} minutes)")
|
|
191
|
+
Console.write_stdout(f"Number of downtime incidents: {len(downtime_periods)}")
|
|
192
|
+
else:
|
|
193
|
+
Console.write_stdout("No downtime detected during monitoring period")
|
|
194
|
+
|
|
195
|
+
if file_path:
|
|
196
|
+
Console.write_stdout(f"Results saved to: {file_path}")
|
|
197
|
+
Console.write_stdout("=" * 60)
|
|
62
198
|
|
|
63
199
|
|
|
64
200
|
"""
|
|
@@ -99,9 +235,34 @@ base_commands = [
|
|
|
99
235
|
["status", "s"],
|
|
100
236
|
"Check API status for Globant Enterprise AI instance",
|
|
101
237
|
check_api_status,
|
|
102
|
-
ArgumentsEnum.
|
|
238
|
+
ArgumentsEnum.OPTIONAL,
|
|
103
239
|
[],
|
|
104
|
-
[
|
|
240
|
+
[
|
|
241
|
+
Option(
|
|
242
|
+
"monitor",
|
|
243
|
+
["--monitor", "-m"],
|
|
244
|
+
"Enable continuous monitoring mode",
|
|
245
|
+
False
|
|
246
|
+
),
|
|
247
|
+
Option(
|
|
248
|
+
"file",
|
|
249
|
+
["--file", "-f"],
|
|
250
|
+
"Log status checks to specified file",
|
|
251
|
+
True
|
|
252
|
+
),
|
|
253
|
+
Option(
|
|
254
|
+
"interval",
|
|
255
|
+
["--interval", "-i"],
|
|
256
|
+
"Interval in seconds between checks (default: 5)",
|
|
257
|
+
True
|
|
258
|
+
),
|
|
259
|
+
Option(
|
|
260
|
+
"count",
|
|
261
|
+
["--count", "-c"],
|
|
262
|
+
"Number of times to check before stopping (default: infinite)",
|
|
263
|
+
True
|
|
264
|
+
),
|
|
265
|
+
]
|
|
105
266
|
),
|
|
106
267
|
Command(
|
|
107
268
|
"configure",
|
pygeai/cli/commands/migrate.py
CHANGED
|
@@ -703,7 +703,6 @@ def clone_project(option_list: list) -> None:
|
|
|
703
703
|
logger.debug(f" - to_organization_api_key exists: {to_organization_api_key is not None}")
|
|
704
704
|
logger.debug(f" - from_organization_api_key exists: {from_organization_api_key is not None}")
|
|
705
705
|
logger.debug(f" - Using key (first 20 chars): {org_key_to_use[:20] if org_key_to_use else 'None'}...")
|
|
706
|
-
Console.write_stderr(f"DEBUG: Using org key for project creation (first 20 chars): {org_key_to_use[:20] if org_key_to_use else 'None'}...")
|
|
707
706
|
|
|
708
707
|
project_strategy = ProjectMigrationStrategy(
|
|
709
708
|
from_api_key=from_organization_api_key,
|
|
@@ -993,7 +992,7 @@ clone_project_options = [
|
|
|
993
992
|
),
|
|
994
993
|
Option(
|
|
995
994
|
"from_organization_api_key",
|
|
996
|
-
["--from-org-key", "--from-organization-key"],
|
|
995
|
+
["--from-org-key", "--from-organization-key", "--from-organization-api-key"],
|
|
997
996
|
"Source instance organization scope API key (optional, for project creation)",
|
|
998
997
|
True
|
|
999
998
|
),
|
|
@@ -1023,7 +1022,7 @@ clone_project_options = [
|
|
|
1023
1022
|
),
|
|
1024
1023
|
Option(
|
|
1025
1024
|
"to_organization_api_key",
|
|
1026
|
-
["--to-org-key", "--to-organization-key"],
|
|
1025
|
+
["--to-org-key", "--to-organization-key", "--to-organization-api-key"],
|
|
1027
1026
|
"Destination instance organization scope API key (optional, for project creation)",
|
|
1028
1027
|
True
|
|
1029
1028
|
),
|
pygeai/cli/geai.py
CHANGED
|
@@ -107,10 +107,14 @@ class CLIDriver:
|
|
|
107
107
|
elif "--alias" in arguments:
|
|
108
108
|
alias_index = arguments.index("--alias")
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
try:
|
|
111
|
+
_ = arguments.pop(alias_index)
|
|
112
|
+
alias = arguments.pop(alias_index)
|
|
113
|
+
return alias
|
|
114
|
+
except IndexError as e:
|
|
115
|
+
Console.write_stderr("-a/--alias option requires an alias. Please provide a valid alias after the option")
|
|
116
|
+
raise MissingRequirementException("Couldn't find a valid alias in parameter list.")
|
|
117
|
+
|
|
114
118
|
def _get_credentials_file(self, arguments: List[str]) -> str:
|
|
115
119
|
"""
|
|
116
120
|
Retrieves and removes credentials file path and flag from argument list.
|
|
@@ -126,9 +130,13 @@ class CLIDriver:
|
|
|
126
130
|
elif "--creds" in arguments:
|
|
127
131
|
creds_index = arguments.index("--creds")
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
try:
|
|
134
|
+
_ = arguments.pop(creds_index)
|
|
135
|
+
credentials_file = arguments.pop(creds_index)
|
|
136
|
+
return credentials_file
|
|
137
|
+
except IndexError as e:
|
|
138
|
+
Console.write_stderr("--creds/--credentials option requires a file path. Please provide a valid path after the option.")
|
|
139
|
+
raise MissingRequirementException("Couldn't find a valid path in parameter list.")
|
|
132
140
|
|
|
133
141
|
def main(self, args: Optional[List[str]] = None) -> int:
|
|
134
142
|
"""
|
pygeai/cli/texts/help.py
CHANGED
|
@@ -107,7 +107,32 @@ EXAMPLES
|
|
|
107
107
|
will help you setup the required environment variables to work with GEAI.
|
|
108
108
|
|
|
109
109
|
The command:
|
|
110
|
-
|
|
110
|
+
geai version
|
|
111
|
+
displays the current version of the GEAI CLI utility.
|
|
112
|
+
|
|
113
|
+
The command:
|
|
114
|
+
geai check-updates
|
|
115
|
+
checks if there are new versions of the GEAI package available.
|
|
116
|
+
|
|
117
|
+
The command:
|
|
118
|
+
geai status
|
|
119
|
+
checks the current API status of the Globant Enterprise AI instance.
|
|
120
|
+
|
|
121
|
+
The command:
|
|
122
|
+
geai status -m -i 10
|
|
123
|
+
monitors the API status every 10 seconds until stopped with Ctrl+C.
|
|
124
|
+
|
|
125
|
+
The command:
|
|
126
|
+
geai admin validate-token
|
|
127
|
+
validates your API token and displays organization and project information.
|
|
128
|
+
|
|
129
|
+
The command:
|
|
130
|
+
geai llm list-providers
|
|
131
|
+
lists all available LLM providers in GEAI.
|
|
132
|
+
|
|
133
|
+
The command:
|
|
134
|
+
geai emb generate -i "Hello world" -m "openai/text-embedding-3-small"
|
|
135
|
+
generates embeddings for the provided text using the specified model
|
|
111
136
|
|
|
112
137
|
INSTALL MAN PAGES
|
|
113
138
|
To install the manual pages, run:
|
pygeai/core/handlers.py
CHANGED
|
@@ -5,23 +5,36 @@ class ErrorHandler:
|
|
|
5
5
|
|
|
6
6
|
@classmethod
|
|
7
7
|
def has_errors(cls, response):
|
|
8
|
-
|
|
9
|
-
if
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
"""
|
|
9
|
+
Check if the response contains errors.
|
|
10
|
+
|
|
11
|
+
Handles both dict and string responses for backward compatibility.
|
|
12
|
+
Optimized to reduce redundant dictionary lookups when response is a dict.
|
|
13
|
+
|
|
14
|
+
:param response: Response (dict or string) to check
|
|
15
|
+
:return: True if errors found, False otherwise
|
|
16
|
+
"""
|
|
17
|
+
if "errors" in response or "error" in response:
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
if not isinstance(response, dict):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
message = response.get("message")
|
|
24
|
+
return (
|
|
25
|
+
isinstance(message, list) and
|
|
26
|
+
message and
|
|
27
|
+
message[0].get("type") == "error"
|
|
28
|
+
)
|
|
22
29
|
|
|
23
30
|
@classmethod
|
|
24
31
|
def extract_error(cls, response):
|
|
32
|
+
"""
|
|
33
|
+
Extract and map error information from response.
|
|
34
|
+
|
|
35
|
+
:param response: Response dictionary containing error
|
|
36
|
+
:return: Mapped error object
|
|
37
|
+
"""
|
|
25
38
|
if "errors" in response:
|
|
26
39
|
result = ErrorMapper.map_to_error_list_response(response)
|
|
27
40
|
elif "error" in response:
|
pygeai/core/utils/console.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import sys
|
|
2
3
|
from abc import ABC, abstractmethod
|
|
3
4
|
|
|
@@ -11,10 +12,19 @@ class StreamWriter(ABC):
|
|
|
11
12
|
def write_stdout(self, message: str = "", end: str = "\n"):
|
|
12
13
|
pass
|
|
13
14
|
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def write_success(self, message: str = "", end: str = "\n"):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def write_warning(self, message: str = "", end: str = "\n"):
|
|
21
|
+
pass
|
|
22
|
+
|
|
14
23
|
@abstractmethod
|
|
15
24
|
def write_stderr(self, message: str = "", end: str = "\n"):
|
|
16
25
|
pass
|
|
17
26
|
|
|
27
|
+
|
|
18
28
|
class ConsoleMeta(type):
|
|
19
29
|
def __getattr__(cls, name):
|
|
20
30
|
writer = cls._writer
|
|
@@ -26,6 +36,7 @@ class ConsoleMeta(type):
|
|
|
26
36
|
pass
|
|
27
37
|
return noop
|
|
28
38
|
|
|
39
|
+
|
|
29
40
|
class Console(metaclass=ConsoleMeta):
|
|
30
41
|
"""
|
|
31
42
|
A utility class for writing messages to standard output and standard error streams.
|
|
@@ -39,14 +50,45 @@ class Console(metaclass=ConsoleMeta):
|
|
|
39
50
|
"""
|
|
40
51
|
class DefaultStreamWriter(StreamWriter):
|
|
41
52
|
"""
|
|
42
|
-
Default StreamWriter that writes to sys.stdout and sys.stderr.
|
|
53
|
+
Default StreamWriter that writes to sys.stdout and sys.stderr with color support.
|
|
43
54
|
"""
|
|
55
|
+
|
|
56
|
+
# ANSI color codes
|
|
57
|
+
GREEN = "\033[32m"
|
|
58
|
+
YELLOW = "\033[33m"
|
|
59
|
+
RED = "\033[31m"
|
|
60
|
+
RESET = "\033[0m"
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _should_use_color(stream) -> bool:
|
|
64
|
+
"""Check if colors should be used based on terminal support and environment."""
|
|
65
|
+
if os.getenv("NO_COLOR"):
|
|
66
|
+
return False
|
|
67
|
+
return hasattr(stream, "isatty") and stream.isatty()
|
|
68
|
+
|
|
44
69
|
def write_stdout(self, message: str = "", end: str = "\n"):
|
|
45
70
|
sys.stdout.write(f"{message}{end}")
|
|
46
71
|
sys.stdout.flush()
|
|
47
72
|
|
|
73
|
+
def write_success(self, message: str = "", end: str = "\n"):
|
|
74
|
+
if self._should_use_color(sys.stdout):
|
|
75
|
+
sys.stdout.write(f"{self.GREEN}{message}{self.RESET}{end}")
|
|
76
|
+
else:
|
|
77
|
+
sys.stdout.write(f"{message}{end}")
|
|
78
|
+
sys.stdout.flush()
|
|
79
|
+
|
|
80
|
+
def write_warning(self, message: str = "", end: str = "\n"):
|
|
81
|
+
if self._should_use_color(sys.stderr):
|
|
82
|
+
sys.stderr.write(f"{self.YELLOW}{message}{self.RESET}{end}")
|
|
83
|
+
else:
|
|
84
|
+
sys.stderr.write(f"{message}{end}")
|
|
85
|
+
sys.stderr.flush()
|
|
86
|
+
|
|
48
87
|
def write_stderr(self, message: str = "", end: str = "\n"):
|
|
49
|
-
sys.stderr
|
|
88
|
+
if self._should_use_color(sys.stderr):
|
|
89
|
+
sys.stderr.write(f"{self.RED}{message}{self.RESET}{end}")
|
|
90
|
+
else:
|
|
91
|
+
sys.stderr.write(f"{message}{end}")
|
|
50
92
|
sys.stderr.flush()
|
|
51
93
|
|
|
52
94
|
_writer: StreamWriter = DefaultStreamWriter()
|
|
@@ -57,18 +99,40 @@ class Console(metaclass=ConsoleMeta):
|
|
|
57
99
|
Writes a message to the standard output stream (sys.stdout).
|
|
58
100
|
|
|
59
101
|
:param message: str - The message to write. Defaults to an empty string.
|
|
60
|
-
:param end: str - The string to append after the message. Defaults to a newline ('
|
|
102
|
+
:param end: str - The string to append after the message. Defaults to a newline ('\\n').
|
|
61
103
|
:return: None - No return value; output is written to sys.stdout.
|
|
62
104
|
"""
|
|
63
105
|
Console._writer.write_stdout(message, end)
|
|
64
106
|
|
|
107
|
+
@staticmethod
|
|
108
|
+
def write_success(message: str = "", end: str = "\n"):
|
|
109
|
+
"""
|
|
110
|
+
Writes a success message to the standard output stream (sys.stdout) in green.
|
|
111
|
+
|
|
112
|
+
:param message: str - The message to write. Defaults to an empty string.
|
|
113
|
+
:param end: str - The string to append after the message. Defaults to a newline ('\\n').
|
|
114
|
+
:return: None - No return value; output is written to sys.stdout.
|
|
115
|
+
"""
|
|
116
|
+
Console._writer.write_success(message, end)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def write_warning(message: str = "", end: str = "\n"):
|
|
120
|
+
"""
|
|
121
|
+
Writes a warning message to the standard error stream (sys.stderr) in yellow.
|
|
122
|
+
|
|
123
|
+
:param message: str - The message to write. Defaults to an empty string.
|
|
124
|
+
:param end: str - The string to append after the message. Defaults to a newline ('\\n').
|
|
125
|
+
:return: None - No return value; output is written to sys.stderr.
|
|
126
|
+
"""
|
|
127
|
+
Console._writer.write_warning(message, end)
|
|
128
|
+
|
|
65
129
|
@staticmethod
|
|
66
130
|
def write_stderr(message: str = "", end: str = "\n"):
|
|
67
131
|
"""
|
|
68
|
-
Writes a message to the standard error stream (sys.stderr).
|
|
132
|
+
Writes a message to the standard error stream (sys.stderr) in red.
|
|
69
133
|
|
|
70
134
|
:param message: str - The message to write. Defaults to an empty string.
|
|
71
|
-
:param end: str - The string to append after the message. Defaults to a newline ('
|
|
135
|
+
:param end: str - The string to append after the message. Defaults to a newline ('\\n').
|
|
72
136
|
:return: None - No return value; output is written to sys.stderr.
|
|
73
137
|
"""
|
|
74
138
|
Console._writer.write_stderr(message, end)
|
pygeai/core/utils/parsers.py
CHANGED
|
@@ -8,6 +8,9 @@ def parse_json_response(response, operation: str, **context):
|
|
|
8
8
|
"""
|
|
9
9
|
Parse JSON response with standardized error handling.
|
|
10
10
|
|
|
11
|
+
Optimized to keep the hot path (success case) fast by separating
|
|
12
|
+
error handling logic into a dedicated function.
|
|
13
|
+
|
|
11
14
|
:param response: HTTP response object
|
|
12
15
|
:param operation: Description of operation (e.g., "get project API token")
|
|
13
16
|
:param context: Additional context (e.g., api_token_id="123")
|
|
@@ -17,16 +20,34 @@ def parse_json_response(response, operation: str, **context):
|
|
|
17
20
|
try:
|
|
18
21
|
return response.json()
|
|
19
22
|
except JSONDecodeError as e:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if len(context) == 1:
|
|
23
|
-
# Single context value: append as 'value'
|
|
24
|
-
value = list(context.values())[0]
|
|
25
|
-
full_msg += f" '{value}'"
|
|
26
|
-
else:
|
|
27
|
-
# Multiple context values: format as (key1='value1', key2='value2')
|
|
28
|
-
context_str = ", ".join([f"{k}='{v}'" for k, v in context.items()])
|
|
29
|
-
full_msg += f" ({context_str})"
|
|
23
|
+
_handle_json_parse_error(response, operation, context, e)
|
|
24
|
+
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
def _handle_json_parse_error(response, operation: str, context: dict, error: JSONDecodeError):
|
|
27
|
+
"""
|
|
28
|
+
Handle JSON parsing errors with detailed error messages.
|
|
29
|
+
|
|
30
|
+
Separated from main parsing function to optimize the hot path.
|
|
31
|
+
This function is only called when parsing fails (rare case).
|
|
32
|
+
|
|
33
|
+
:param response: HTTP response object
|
|
34
|
+
:param operation: Description of operation
|
|
35
|
+
:param context: Additional context parameters
|
|
36
|
+
:param error: The JSONDecodeError that occurred
|
|
37
|
+
:raises InvalidAPIResponseException: Always raises with detailed message
|
|
38
|
+
"""
|
|
39
|
+
full_msg = f"Unable to {operation}"
|
|
40
|
+
|
|
41
|
+
if context:
|
|
42
|
+
if len(context) == 1:
|
|
43
|
+
value = next(iter(context.values()))
|
|
44
|
+
full_msg += f" '{value}'"
|
|
45
|
+
else:
|
|
46
|
+
context_str = ", ".join(f"{k}='{v}'" for k, v in context.items())
|
|
47
|
+
full_msg += f" ({context_str})"
|
|
48
|
+
|
|
49
|
+
logger.error(
|
|
50
|
+
f"{full_msg}: JSON parsing error (status {response.status_code}): {error}. "
|
|
51
|
+
f"Response: {response.text}"
|
|
52
|
+
)
|
|
53
|
+
raise InvalidAPIResponseException(f"{full_msg}: {response.text}")
|
pygeai/lab/models.py
CHANGED
|
@@ -427,7 +427,7 @@ class AgentData(CustomBaseModel):
|
|
|
427
427
|
:param resource_pools: Optional[List[ResourcePool]] - List of resource pools organizing tools and helper agents.
|
|
428
428
|
"""
|
|
429
429
|
prompt: Prompt = Field(..., alias="prompt")
|
|
430
|
-
llm_config: LlmConfig = Field(
|
|
430
|
+
llm_config: LlmConfig = Field(None, alias="llmConfig")
|
|
431
431
|
strategy_name: Optional[str] = Field("Dynamic Prompting", alias="strategyName")
|
|
432
432
|
models: Optional[Union[ModelList, List[Model]]] = Field(None, alias="models")
|
|
433
433
|
resource_pools: Optional[ResourcePoolList] = Field(None, alias="resourcePools", description="List of resource pools organizing tools and helper agents")
|
|
@@ -2,7 +2,7 @@ CREATE_PROCESS_V2 = "v2/processes" # POST -> Create process
|
|
|
2
2
|
UPDATE_PROCESS_V2 = "v2/processes/{processId}" # PUT -> Update process
|
|
3
3
|
UPSERT_PROCESS_V2 = "v2/processes/{processId}/upsert" # PUT -> Update or insert process
|
|
4
4
|
GET_PROCESS_V2 = "v2/processes/{processId}" # GET -> Get process
|
|
5
|
-
LIST_PROCESSES_V2 = "v2/processes" # GET -> Get process
|
|
5
|
+
LIST_PROCESSES_V2 = "v2/processes" # GET -> Get process list
|
|
6
6
|
LIST_PROCESS_INSTANCES_V2 = "v2/processes/{processId}/instances" # GET -> Get process instances
|
|
7
7
|
DELETE_PROCESS_V2 = "v2/processes/{processId}" # DELETE -> Get process
|
|
8
8
|
PUBLISH_PROCESS_REVISION_V2 = "v2/processes/{processId}/publish-revision" # POST -> Publish process revision
|