glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5b1__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.
- glaip_sdk/__init__.py +5 -5
- glaip_sdk/branding.py +18 -17
- glaip_sdk/cli/__init__.py +1 -1
- glaip_sdk/cli/agent_config.py +82 -0
- glaip_sdk/cli/commands/__init__.py +3 -3
- glaip_sdk/cli/commands/agents.py +570 -673
- glaip_sdk/cli/commands/configure.py +2 -2
- glaip_sdk/cli/commands/mcps.py +148 -143
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +250 -179
- glaip_sdk/cli/display.py +244 -0
- glaip_sdk/cli/io.py +106 -0
- glaip_sdk/cli/main.py +14 -18
- glaip_sdk/cli/resolution.py +59 -0
- glaip_sdk/cli/utils.py +305 -264
- glaip_sdk/cli/validators.py +235 -0
- glaip_sdk/client/__init__.py +3 -224
- glaip_sdk/client/agents.py +631 -191
- glaip_sdk/client/base.py +66 -4
- glaip_sdk/client/main.py +226 -0
- glaip_sdk/client/mcps.py +143 -18
- glaip_sdk/client/tools.py +146 -11
- glaip_sdk/config/constants.py +10 -1
- glaip_sdk/models.py +42 -2
- glaip_sdk/rich_components.py +29 -0
- glaip_sdk/utils/__init__.py +18 -171
- glaip_sdk/utils/agent_config.py +181 -0
- glaip_sdk/utils/client_utils.py +159 -79
- glaip_sdk/utils/display.py +100 -0
- glaip_sdk/utils/general.py +94 -0
- glaip_sdk/utils/import_export.py +140 -0
- glaip_sdk/utils/rendering/formatting.py +6 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
- glaip_sdk/utils/rendering/renderer/base.py +340 -247
- glaip_sdk/utils/rendering/renderer/debug.py +3 -2
- glaip_sdk/utils/rendering/renderer/panels.py +11 -10
- glaip_sdk/utils/rendering/steps.py +1 -1
- glaip_sdk/utils/resource_refs.py +192 -0
- glaip_sdk/utils/rich_utils.py +29 -0
- glaip_sdk/utils/serialization.py +285 -0
- glaip_sdk/utils/validation.py +273 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/METADATA +22 -21
- glaip_sdk-0.0.5b1.dist-info/RECORD +55 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/WHEEL +1 -1
- glaip_sdk-0.0.5b1.dist-info/entry_points.txt +3 -0
- glaip_sdk/cli/commands/init.py +0 -93
- glaip_sdk-0.0.4.dist-info/RECORD +0 -41
- glaip_sdk-0.0.4.dist-info/entry_points.txt +0 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Agent configuration utilities for import/export normalization and LM selection.
|
|
2
|
+
|
|
3
|
+
This module consolidates language model selection logic and agent configuration
|
|
4
|
+
sanitization that was previously split between CLI and SDK layers.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _strip_agent_config_credentials(agent_config: dict | None) -> dict:
|
|
14
|
+
"""Return agent_config without sensitive credentials; keep all other keys.
|
|
15
|
+
|
|
16
|
+
We intentionally keep keys like memory and agent_id so that backends supporting
|
|
17
|
+
mem0 memory (gllm-agents-binary>=0.4.6) receive them under agent_config.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
agent_config: The agent configuration dictionary
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Agent config with credentials stripped but mem0 keys preserved
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(agent_config, dict):
|
|
26
|
+
return {}
|
|
27
|
+
return {k: v for k, v in agent_config.items() if k != "lm_credentials"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sanitize_agent_config(
|
|
31
|
+
agent_config: dict | None,
|
|
32
|
+
*,
|
|
33
|
+
strip_credentials: bool = True,
|
|
34
|
+
strip_lm_identity: bool = False,
|
|
35
|
+
) -> dict:
|
|
36
|
+
"""Sanitize agent_config based on chosen LM selection method.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
agent_config: The agent configuration to sanitize
|
|
40
|
+
strip_credentials: Always drop lm_credentials (default: True)
|
|
41
|
+
strip_lm_identity: Also drop lm_provider/lm_name/lm_base_url when True
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Sanitized agent configuration
|
|
45
|
+
|
|
46
|
+
Notes:
|
|
47
|
+
- Always drops lm_credentials to prevent credential leakage
|
|
48
|
+
- When strip_lm_identity=True, also drops LM identity keys to avoid conflicts
|
|
49
|
+
- Never drops mem0 keys (memory, agent_id) as they're needed by backends
|
|
50
|
+
"""
|
|
51
|
+
if strip_credentials:
|
|
52
|
+
cfg = _strip_agent_config_credentials(agent_config)
|
|
53
|
+
else:
|
|
54
|
+
cfg = agent_config or {}
|
|
55
|
+
|
|
56
|
+
if strip_lm_identity and isinstance(cfg, dict):
|
|
57
|
+
cfg = {
|
|
58
|
+
k: v
|
|
59
|
+
for k, v in cfg.items()
|
|
60
|
+
if k not in {"lm_provider", "lm_name", "lm_base_url"}
|
|
61
|
+
}
|
|
62
|
+
return cfg
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_language_model_selection(
|
|
66
|
+
merged_data: dict[str, Any], cli_model: str | None
|
|
67
|
+
) -> tuple[dict[str, Any], bool]:
|
|
68
|
+
"""Resolve language model selection from merged data and CLI args.
|
|
69
|
+
|
|
70
|
+
Implements the LM selection priority:
|
|
71
|
+
1. CLI --model (maps to provider/model_name)
|
|
72
|
+
2. language_model_id from import
|
|
73
|
+
3. agent_config.lm_name from import (legacy)
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
merged_data: Merged import data and CLI args
|
|
77
|
+
cli_model: Model specified via CLI --model flag
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple of (lm_selection_dict, should_strip_lm_identity)
|
|
81
|
+
- lm_selection_dict: Dict with exactly one LM method: {"language_model_id": "..."} OR {"model": "..."}
|
|
82
|
+
- should_strip_lm_identity: True when LM identity keys should be stripped from agent_config
|
|
83
|
+
|
|
84
|
+
Notes:
|
|
85
|
+
- Returns exactly one LM selection method to avoid conflicts
|
|
86
|
+
- CLI model takes highest priority
|
|
87
|
+
- language_model_id is preferred over legacy lm_name
|
|
88
|
+
- When extracting from agent_config, signals that LM identity should be stripped
|
|
89
|
+
"""
|
|
90
|
+
# Priority 1: CLI --model flag
|
|
91
|
+
if cli_model:
|
|
92
|
+
return {"model": cli_model}, False
|
|
93
|
+
|
|
94
|
+
# Priority 2: language_model_id from import
|
|
95
|
+
if merged_data.get("language_model_id"):
|
|
96
|
+
return {"language_model_id": merged_data["language_model_id"]}, True
|
|
97
|
+
|
|
98
|
+
# Priority 3: Legacy lm_name from agent_config
|
|
99
|
+
agent_config = merged_data.get("agent_config") or {}
|
|
100
|
+
if isinstance(agent_config, dict) and agent_config.get("lm_name"):
|
|
101
|
+
return {
|
|
102
|
+
"model": agent_config["lm_name"]
|
|
103
|
+
}, True # Strip LM identity when extracting from agent_config
|
|
104
|
+
|
|
105
|
+
# No LM selection found
|
|
106
|
+
return {}, False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def normalize_agent_config_for_import(
|
|
110
|
+
agent_data: dict[str, Any], cli_model: str | None = None
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""Automatically normalize agent configuration by extracting LM settings from agent_config.
|
|
113
|
+
|
|
114
|
+
This function addresses the common issue where exported agent configurations contain
|
|
115
|
+
language model settings in agent_config, but the backend expects them at the top level
|
|
116
|
+
to avoid conflicts.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
agent_data: Raw agent configuration data (from import file)
|
|
120
|
+
cli_model: CLI model override (highest priority)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Normalized agent configuration with LM settings properly positioned
|
|
124
|
+
|
|
125
|
+
Notes:
|
|
126
|
+
- Automatically extracts lm_provider/lm_name from agent_config to top level
|
|
127
|
+
- Preserves memory settings (memory, agent_id) in agent_config
|
|
128
|
+
- Handles conflicts by prioritizing CLI model > existing language_model_id > extracted lm_name
|
|
129
|
+
- Strips redundant LM settings from agent_config after extraction
|
|
130
|
+
"""
|
|
131
|
+
normalized_data = agent_data.copy()
|
|
132
|
+
agent_config = normalized_data.get("agent_config", {})
|
|
133
|
+
|
|
134
|
+
if not isinstance(agent_config, dict):
|
|
135
|
+
return normalized_data
|
|
136
|
+
|
|
137
|
+
# Priority 1: CLI --model flag (highest priority)
|
|
138
|
+
if cli_model:
|
|
139
|
+
# When CLI model is specified, set it and don't extract from agent_config
|
|
140
|
+
normalized_data["model"] = cli_model
|
|
141
|
+
return normalized_data
|
|
142
|
+
|
|
143
|
+
# Priority 2: language_model_id already exists - clean up agent_config
|
|
144
|
+
if normalized_data.get("language_model_id"):
|
|
145
|
+
# If language_model_id exists, we should still clean up any conflicting
|
|
146
|
+
# LM settings from agent_config to prevent backend validation errors
|
|
147
|
+
if isinstance(agent_config, dict):
|
|
148
|
+
# Remove LM identity keys from agent_config since language_model_id takes precedence
|
|
149
|
+
lm_keys_to_remove = {"lm_provider", "lm_name", "lm_base_url"}
|
|
150
|
+
for key in lm_keys_to_remove:
|
|
151
|
+
agent_config.pop(key, None)
|
|
152
|
+
normalized_data["agent_config"] = agent_config
|
|
153
|
+
return normalized_data
|
|
154
|
+
|
|
155
|
+
# Priority 3: Extract LM settings from agent_config
|
|
156
|
+
extracted_lm = {}
|
|
157
|
+
|
|
158
|
+
# Extract lm_name if present
|
|
159
|
+
if "lm_name" in agent_config:
|
|
160
|
+
extracted_lm["model"] = agent_config["lm_name"]
|
|
161
|
+
|
|
162
|
+
# Extract lm_provider if present (for completeness)
|
|
163
|
+
if "lm_provider" in agent_config:
|
|
164
|
+
extracted_lm["lm_provider"] = agent_config["lm_provider"]
|
|
165
|
+
|
|
166
|
+
# If we extracted LM settings, update the normalized data
|
|
167
|
+
if extracted_lm:
|
|
168
|
+
# Add extracted LM settings to top level
|
|
169
|
+
normalized_data.update(extracted_lm)
|
|
170
|
+
|
|
171
|
+
# Create sanitized agent_config (remove extracted LM settings but keep memory)
|
|
172
|
+
sanitized_config = agent_config.copy()
|
|
173
|
+
|
|
174
|
+
# Remove LM identity keys but preserve memory and other settings
|
|
175
|
+
lm_keys_to_remove = {"lm_provider", "lm_name", "lm_base_url"}
|
|
176
|
+
for key in lm_keys_to_remove:
|
|
177
|
+
sanitized_config.pop(key, None)
|
|
178
|
+
|
|
179
|
+
normalized_data["agent_config"] = sanitized_config
|
|
180
|
+
|
|
181
|
+
return normalized_data
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -9,6 +9,7 @@ Authors:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
+
from collections.abc import AsyncGenerator
|
|
12
13
|
from contextlib import ExitStack
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, BinaryIO
|
|
@@ -42,7 +43,7 @@ class MultipartData:
|
|
|
42
43
|
def __enter__(self):
|
|
43
44
|
return self
|
|
44
45
|
|
|
45
|
-
def __exit__(self,
|
|
46
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb):
|
|
46
47
|
self.close()
|
|
47
48
|
|
|
48
49
|
|
|
@@ -54,21 +55,18 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
|
|
|
54
55
|
|
|
55
56
|
Returns:
|
|
56
57
|
List of extracted IDs, or None if items is empty/None
|
|
58
|
+
|
|
59
|
+
Note:
|
|
60
|
+
This function maintains backward compatibility by returning None for empty input.
|
|
61
|
+
New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
|
|
57
62
|
"""
|
|
63
|
+
from .resource_refs import extract_ids as extract_ids_new
|
|
64
|
+
|
|
58
65
|
if not items:
|
|
59
66
|
return None
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if isinstance(item, str):
|
|
64
|
-
ids.append(item)
|
|
65
|
-
elif hasattr(item, "id"):
|
|
66
|
-
ids.append(item.id)
|
|
67
|
-
else:
|
|
68
|
-
# Fallback: convert to string
|
|
69
|
-
ids.append(str(item))
|
|
70
|
-
|
|
71
|
-
return ids
|
|
68
|
+
result = extract_ids_new(items)
|
|
69
|
+
return result if result else None
|
|
72
70
|
|
|
73
71
|
|
|
74
72
|
def create_model_instances(
|
|
@@ -108,14 +106,96 @@ def find_by_name(
|
|
|
108
106
|
|
|
109
107
|
Returns:
|
|
110
108
|
Filtered list of items matching the name
|
|
109
|
+
|
|
110
|
+
Note:
|
|
111
|
+
This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
|
|
111
112
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
from .resource_refs import find_by_name as find_by_name_new
|
|
114
|
+
|
|
115
|
+
return find_by_name_new(items, name, case_sensitive)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _parse_sse_line(line: str, buf: list, event_type: str = None, event_id: str = None):
|
|
119
|
+
"""Parse a single SSE line and return updated buffer and event metadata."""
|
|
120
|
+
# Normalize CRLF and treat whitespace-only as blank
|
|
121
|
+
line = line.rstrip("\r")
|
|
122
|
+
|
|
123
|
+
if not line.strip(): # blank line
|
|
124
|
+
if buf:
|
|
125
|
+
data = "\n".join(buf)
|
|
126
|
+
return (
|
|
127
|
+
[],
|
|
128
|
+
None,
|
|
129
|
+
None,
|
|
130
|
+
{
|
|
131
|
+
"event": event_type or "message",
|
|
132
|
+
"id": event_id,
|
|
133
|
+
"data": data,
|
|
134
|
+
},
|
|
135
|
+
False,
|
|
136
|
+
) # no completion
|
|
137
|
+
return buf, event_type, event_id, None, False
|
|
138
|
+
|
|
139
|
+
if line.startswith(":"): # comment
|
|
140
|
+
return buf, event_type, event_id, None, False
|
|
141
|
+
|
|
142
|
+
if line.startswith("data:"):
|
|
143
|
+
data_line = line[5:].lstrip()
|
|
144
|
+
if data_line.strip() == "[DONE]": # sentinel end marker
|
|
145
|
+
if buf:
|
|
146
|
+
data = "\n".join(buf)
|
|
147
|
+
return (
|
|
148
|
+
[],
|
|
149
|
+
None,
|
|
150
|
+
None,
|
|
151
|
+
{
|
|
152
|
+
"event": event_type or "message",
|
|
153
|
+
"id": event_id,
|
|
154
|
+
"data": data,
|
|
155
|
+
},
|
|
156
|
+
True,
|
|
157
|
+
) # signal completion
|
|
158
|
+
return buf, event_type, event_id, None, True
|
|
159
|
+
buf.append(data_line)
|
|
160
|
+
elif line.startswith("event:"):
|
|
161
|
+
event_type = line[6:].strip() or None
|
|
162
|
+
elif line.startswith("id:"):
|
|
163
|
+
event_id = line[3:].strip() or None
|
|
164
|
+
|
|
165
|
+
return buf, event_type, event_id, None, False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _handle_streaming_error(
|
|
169
|
+
e: Exception, timeout_seconds: float = None, agent_name: str = None
|
|
170
|
+
):
|
|
171
|
+
"""Handle different types of streaming errors with appropriate logging and exceptions."""
|
|
172
|
+
if isinstance(e, httpx.ReadTimeout):
|
|
173
|
+
logger.error(f"Read timeout during streaming: {e}")
|
|
174
|
+
logger.error("This usually indicates the backend is taking too long to respond")
|
|
175
|
+
logger.error(
|
|
176
|
+
"Consider increasing the timeout value or checking backend performance"
|
|
177
|
+
)
|
|
178
|
+
raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
|
|
179
|
+
|
|
180
|
+
elif isinstance(e, httpx.TimeoutException):
|
|
181
|
+
logger.error(f"General timeout during streaming: {e}")
|
|
182
|
+
raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
|
|
183
|
+
|
|
184
|
+
elif isinstance(e, httpx.StreamClosed):
|
|
185
|
+
logger.error(f"Stream closed unexpectedly during streaming: {e}")
|
|
186
|
+
logger.error("This may indicate a backend issue or network problem")
|
|
187
|
+
logger.error("The response stream was closed before all data could be read")
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
elif isinstance(e, httpx.ConnectError):
|
|
191
|
+
logger.error(f"Connection error during streaming: {e}")
|
|
192
|
+
logger.error("Check your network connection and backend availability")
|
|
193
|
+
raise
|
|
114
194
|
|
|
115
|
-
if case_sensitive:
|
|
116
|
-
return [item for item in items if name in item.name]
|
|
117
195
|
else:
|
|
118
|
-
|
|
196
|
+
logger.error(f"Unexpected error during streaming: {e}")
|
|
197
|
+
logger.error(f"Error type: {type(e).__name__}")
|
|
198
|
+
raise
|
|
119
199
|
|
|
120
200
|
|
|
121
201
|
def iter_sse_events(
|
|
@@ -146,41 +226,69 @@ def iter_sse_events(
|
|
|
146
226
|
if line is None:
|
|
147
227
|
continue
|
|
148
228
|
|
|
149
|
-
|
|
150
|
-
|
|
229
|
+
result = _parse_sse_line(line, buf, event_type, event_id)
|
|
230
|
+
if len(result) == 5: # completion signal included
|
|
231
|
+
buf, event_type, event_id, event_data, completed = result
|
|
232
|
+
else: # normal case
|
|
233
|
+
buf, event_type, event_id, event_data = result
|
|
234
|
+
completed = False
|
|
151
235
|
|
|
152
|
-
if
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
236
|
+
if event_data:
|
|
237
|
+
yield event_data
|
|
238
|
+
if completed:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# Flush any remaining data
|
|
242
|
+
if buf:
|
|
243
|
+
yield {
|
|
244
|
+
"event": event_type or "message",
|
|
245
|
+
"id": event_id,
|
|
246
|
+
"data": "\n".join(buf),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
_handle_streaming_error(e, timeout_seconds, agent_name)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def aiter_sse_events(
|
|
254
|
+
response: httpx.Response, timeout_seconds: float = None, agent_name: str = None
|
|
255
|
+
) -> AsyncGenerator[dict, None]:
|
|
256
|
+
"""Async iterate over Server-Sent Events with proper parsing.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
response: HTTP response object with streaming content
|
|
260
|
+
timeout_seconds: Timeout duration in seconds (for error messages)
|
|
261
|
+
agent_name: Agent name (for error messages)
|
|
262
|
+
|
|
263
|
+
Yields:
|
|
264
|
+
Dictionary with event data, type, and ID
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
AgentTimeoutError: When agent execution times out
|
|
268
|
+
httpx.TimeoutException: When general timeout occurs
|
|
269
|
+
Exception: For other unexpected errors
|
|
270
|
+
"""
|
|
271
|
+
buf = []
|
|
272
|
+
event_type = None
|
|
273
|
+
event_id = None
|
|
162
274
|
|
|
163
|
-
|
|
275
|
+
try:
|
|
276
|
+
async for raw in response.aiter_lines():
|
|
277
|
+
line = raw
|
|
278
|
+
if line is None:
|
|
164
279
|
continue
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
buf.append(data_line)
|
|
180
|
-
elif line.startswith("event:"):
|
|
181
|
-
event_type = line[6:].strip() or None
|
|
182
|
-
elif line.startswith("id:"):
|
|
183
|
-
event_id = line[3:].strip() or None
|
|
280
|
+
|
|
281
|
+
result = _parse_sse_line(line, buf, event_type, event_id)
|
|
282
|
+
if len(result) == 5: # completion signal included
|
|
283
|
+
buf, event_type, event_id, event_data, completed = result
|
|
284
|
+
else: # normal case
|
|
285
|
+
buf, event_type, event_id, event_data = result
|
|
286
|
+
completed = False
|
|
287
|
+
|
|
288
|
+
if event_data:
|
|
289
|
+
yield event_data
|
|
290
|
+
if completed:
|
|
291
|
+
return
|
|
184
292
|
|
|
185
293
|
# Flush any remaining data
|
|
186
294
|
if buf:
|
|
@@ -189,37 +297,9 @@ def iter_sse_events(
|
|
|
189
297
|
"id": event_id,
|
|
190
298
|
"data": "\n".join(buf),
|
|
191
299
|
}
|
|
192
|
-
|
|
193
|
-
logger.error(f"Read timeout during streaming: {e}")
|
|
194
|
-
logger.error("This usually indicates the backend is taking too long to respond")
|
|
195
|
-
logger.error(
|
|
196
|
-
"Consider increasing the timeout value or checking backend performance"
|
|
197
|
-
)
|
|
198
|
-
# Raise a more user-friendly timeout error
|
|
199
|
-
raise AgentTimeoutError(
|
|
200
|
-
timeout_seconds or 30.0, # Default to 30s if not provided
|
|
201
|
-
agent_name,
|
|
202
|
-
)
|
|
203
|
-
except httpx.TimeoutException as e:
|
|
204
|
-
logger.error(f"General timeout during streaming: {e}")
|
|
205
|
-
# Also convert general timeout to agent timeout for consistency
|
|
206
|
-
raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
|
|
207
|
-
except httpx.StreamClosed as e:
|
|
208
|
-
logger.error(f"Stream closed unexpectedly during streaming: {e}")
|
|
209
|
-
logger.error("This may indicate a backend issue or network problem")
|
|
210
|
-
logger.error("The response stream was closed before all data could be read")
|
|
211
|
-
raise
|
|
212
|
-
except httpx.ConnectError as e:
|
|
213
|
-
logger.error(f"Connection error during streaming: {e}")
|
|
214
|
-
logger.error("Check your network connection and backend availability")
|
|
215
|
-
raise
|
|
300
|
+
|
|
216
301
|
except Exception as e:
|
|
217
|
-
|
|
218
|
-
logger.error(f"Error type: {type(e).__name__}")
|
|
219
|
-
# Log additional context if available
|
|
220
|
-
if hasattr(e, "__cause__") and e.__cause__:
|
|
221
|
-
logger.error(f"Caused by: {e.__cause__}")
|
|
222
|
-
raise
|
|
302
|
+
_handle_streaming_error(e, timeout_seconds, agent_name)
|
|
223
303
|
|
|
224
304
|
|
|
225
305
|
def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Rich display utilities for enhanced output.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
11
|
+
"""Print agent output with rich formatting.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
output: The agent's response text
|
|
15
|
+
title: Title for the output panel
|
|
16
|
+
"""
|
|
17
|
+
if RICH_AVAILABLE:
|
|
18
|
+
# Lazy import Rich components
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
panel = AIPPanel(
|
|
26
|
+
Text(output, style="green"),
|
|
27
|
+
title=title,
|
|
28
|
+
border_style="green",
|
|
29
|
+
)
|
|
30
|
+
console.print(panel)
|
|
31
|
+
else:
|
|
32
|
+
print(f"\n=== {title} ===")
|
|
33
|
+
print(output)
|
|
34
|
+
print("=" * (len(title) + 8))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_agent_created(agent, title: str = "🤖 Agent Created") -> None:
|
|
38
|
+
"""Print agent creation success with rich formatting.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
agent: The created agent object
|
|
42
|
+
title: Title for the output panel
|
|
43
|
+
"""
|
|
44
|
+
if RICH_AVAILABLE:
|
|
45
|
+
# Lazy import Rich components
|
|
46
|
+
from rich.console import Console
|
|
47
|
+
|
|
48
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
49
|
+
|
|
50
|
+
console = Console()
|
|
51
|
+
panel = AIPPanel(
|
|
52
|
+
f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
|
|
53
|
+
f"ID: {agent.id}\n"
|
|
54
|
+
f"Model: {getattr(agent, 'model', 'N/A')}\n"
|
|
55
|
+
f"Type: {getattr(agent, 'type', 'config')}\n"
|
|
56
|
+
f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
|
|
57
|
+
f"Version: {getattr(agent, 'version', '1.0')}",
|
|
58
|
+
title=title,
|
|
59
|
+
border_style="green",
|
|
60
|
+
)
|
|
61
|
+
console.print(panel)
|
|
62
|
+
else:
|
|
63
|
+
print(f"✅ Agent '{agent.name}' created successfully!")
|
|
64
|
+
print(f"ID: {agent.id}")
|
|
65
|
+
print(f"Model: {getattr(agent, 'model', 'N/A')}")
|
|
66
|
+
print(f"Type: {getattr(agent, 'type', 'config')}")
|
|
67
|
+
print(f"Framework: {getattr(agent, 'framework', 'langchain')}")
|
|
68
|
+
print(f"Version: {getattr(agent, 'version', '1.0')}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def print_agent_updated(agent) -> None:
|
|
72
|
+
"""Print agent update success with rich formatting.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
agent: The updated agent object
|
|
76
|
+
"""
|
|
77
|
+
if RICH_AVAILABLE:
|
|
78
|
+
# Lazy import Rich components
|
|
79
|
+
from rich.console import Console
|
|
80
|
+
|
|
81
|
+
console = Console()
|
|
82
|
+
console.print(f"[green]✅ Agent '{agent.name}' updated successfully[/green]")
|
|
83
|
+
else:
|
|
84
|
+
print(f"✅ Agent '{agent.name}' updated successfully")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def print_agent_deleted(agent_id: str) -> None:
|
|
88
|
+
"""Print agent deletion success with rich formatting.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agent_id: The deleted agent's ID
|
|
92
|
+
"""
|
|
93
|
+
if RICH_AVAILABLE:
|
|
94
|
+
# Lazy import Rich components
|
|
95
|
+
from rich.console import Console
|
|
96
|
+
|
|
97
|
+
console = Console()
|
|
98
|
+
console.print(f"[green]✅ Agent deleted successfully (ID: {agent_id})[/green]")
|
|
99
|
+
else:
|
|
100
|
+
print(f"✅ Agent deleted successfully (ID: {agent_id})")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""General utility functions for AIP SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_uuid(value: str) -> bool:
|
|
15
|
+
"""Check if a string is a valid UUID.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
value: String to check
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
True if value is a valid UUID, False otherwise
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
UUID(value)
|
|
25
|
+
return True
|
|
26
|
+
except (ValueError, TypeError):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sanitize_name(name: str) -> str:
|
|
31
|
+
"""Sanitize a name for resource creation.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Raw name input
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Sanitized name suitable for resource creation
|
|
38
|
+
"""
|
|
39
|
+
# Remove special characters and normalize
|
|
40
|
+
sanitized = re.sub(r"[^a-zA-Z0-9\-_]", "-", name.strip())
|
|
41
|
+
sanitized = re.sub(r"-+", "-", sanitized) # Collapse multiple dashes
|
|
42
|
+
return sanitized.lower().strip("-")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_file_size(size_bytes: int) -> str:
|
|
46
|
+
"""Format file size in human readable format.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
size_bytes: Size in bytes
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Human readable size string (e.g., "1.5 MB")
|
|
53
|
+
"""
|
|
54
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
55
|
+
if size_bytes < 1024.0:
|
|
56
|
+
return f"{size_bytes:.1f} {unit}"
|
|
57
|
+
size_bytes /= 1024.0
|
|
58
|
+
return f"{size_bytes:.1f} TB"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_datetime(dt):
|
|
62
|
+
"""Format datetime object to readable string.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
dt: Datetime object or string to format
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Formatted datetime string or "N/A" if None
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(dt, datetime):
|
|
71
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
72
|
+
elif dt is None:
|
|
73
|
+
return "N/A"
|
|
74
|
+
return str(dt)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def progress_bar(iterable, description: str = "Processing"):
|
|
78
|
+
"""Simple progress bar using click.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
iterable: Iterable to process
|
|
82
|
+
description: Progress description
|
|
83
|
+
|
|
84
|
+
Yields:
|
|
85
|
+
Items from iterable with progress display
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
with click.progressbar(iterable, label=description) as bar:
|
|
89
|
+
for item in bar:
|
|
90
|
+
yield item
|
|
91
|
+
except ImportError:
|
|
92
|
+
# Fallback if click not available
|
|
93
|
+
for item in iterable:
|
|
94
|
+
yield item
|