glaip-sdk 0.0.17__py3-none-any.whl → 0.0.19__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/cli/commands/mcps.py +304 -33
- glaip_sdk/cli/parsers/json_input.py +62 -14
- glaip_sdk/cli/slash/prompt.py +32 -17
- glaip_sdk/cli/slash/session.py +54 -25
- glaip_sdk/client/agents.py +90 -28
- glaip_sdk/client/main.py +2 -2
- glaip_sdk/client/tools.py +34 -3
- glaip_sdk/utils/agent_config.py +49 -26
- glaip_sdk/utils/rendering/formatting.py +50 -29
- glaip_sdk/utils/rendering/renderer/base.py +156 -70
- {glaip_sdk-0.0.17.dist-info → glaip_sdk-0.0.19.dist-info}/METADATA +1 -1
- {glaip_sdk-0.0.17.dist-info → glaip_sdk-0.0.19.dist-info}/RECORD +14 -14
- {glaip_sdk-0.0.17.dist-info → glaip_sdk-0.0.19.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.17.dist-info → glaip_sdk-0.0.19.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -297,51 +297,80 @@ class SlashSession:
|
|
|
297
297
|
return True
|
|
298
298
|
|
|
299
299
|
def _cmd_agents(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
300
|
+
client = self._get_client_or_fail()
|
|
301
|
+
if not client:
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
agents = self._get_agents_or_fail(client)
|
|
305
|
+
if not agents:
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
picked_agent = self._resolve_or_pick_agent(client, agents, args)
|
|
309
|
+
|
|
310
|
+
if not picked_agent:
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
return self._run_agent_session(picked_agent)
|
|
314
|
+
|
|
315
|
+
def _get_client_or_fail(self) -> Any:
|
|
316
|
+
"""Get client or handle failure and return None."""
|
|
300
317
|
try:
|
|
301
|
-
|
|
318
|
+
return self._get_client()
|
|
302
319
|
except click.ClickException as exc:
|
|
303
320
|
self.console.print(f"[red]{exc}[/red]")
|
|
304
|
-
return
|
|
321
|
+
return None
|
|
305
322
|
|
|
323
|
+
def _get_agents_or_fail(self, client: Any) -> list:
|
|
324
|
+
"""Get agents list or handle failure and return empty list."""
|
|
306
325
|
try:
|
|
307
326
|
agents = client.list_agents()
|
|
327
|
+
if not agents:
|
|
328
|
+
self._handle_no_agents()
|
|
329
|
+
return agents
|
|
308
330
|
except Exception as exc: # pragma: no cover - API failures
|
|
309
331
|
self.console.print(f"[red]Failed to load agents: {exc}[/red]")
|
|
310
|
-
return
|
|
332
|
+
return []
|
|
311
333
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
def _handle_no_agents(self) -> None:
|
|
335
|
+
"""Handle case when no agents are available."""
|
|
336
|
+
hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
|
|
337
|
+
if hint:
|
|
338
|
+
self.console.print(
|
|
339
|
+
f"[yellow]No agents available. Use `{hint}` to add one.[/yellow]"
|
|
340
|
+
)
|
|
341
|
+
else:
|
|
342
|
+
self.console.print("[yellow]No agents available.[/yellow]")
|
|
321
343
|
|
|
344
|
+
def _resolve_or_pick_agent(self, client: Any, agents: list, args: list[str]) -> Any:
|
|
345
|
+
"""Resolve agent from args or pick interactively."""
|
|
322
346
|
if args:
|
|
323
347
|
picked_agent = self._resolve_agent_from_ref(client, agents, args[0])
|
|
324
348
|
if picked_agent is None:
|
|
325
349
|
self.console.print(
|
|
326
350
|
f"[yellow]Could not resolve agent '{args[0]}'. Try `/agents` to browse interactively.[/yellow]"
|
|
327
351
|
)
|
|
328
|
-
return
|
|
352
|
+
return None
|
|
329
353
|
else:
|
|
330
354
|
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
331
355
|
|
|
332
|
-
|
|
333
|
-
return True
|
|
356
|
+
return picked_agent
|
|
334
357
|
|
|
358
|
+
def _run_agent_session(self, picked_agent: Any) -> bool:
|
|
359
|
+
"""Run agent session and show follow-up actions."""
|
|
335
360
|
self._remember_agent(picked_agent)
|
|
336
|
-
|
|
337
361
|
AgentRunSession(self, picked_agent).run()
|
|
338
362
|
|
|
339
|
-
# Refresh the main palette header and surface follow-up actions
|
|
340
|
-
# user has immediate cues after leaving the agent context.
|
|
363
|
+
# Refresh the main palette header and surface follow-up actions
|
|
341
364
|
self._render_header()
|
|
342
365
|
|
|
366
|
+
self._show_agent_followup_actions(picked_agent)
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
def _show_agent_followup_actions(self, picked_agent: Any) -> None:
|
|
370
|
+
"""Show follow-up action hints after agent session."""
|
|
343
371
|
agent_id = str(getattr(picked_agent, "id", ""))
|
|
344
372
|
agent_label = getattr(picked_agent, "name", "") or agent_id or "this agent"
|
|
373
|
+
|
|
345
374
|
hints: list[tuple[str, str]] = []
|
|
346
375
|
if agent_id:
|
|
347
376
|
hints.append((f"/agents {agent_id}", f"Reopen {agent_label}"))
|
|
@@ -351,8 +380,8 @@ class SlashSession:
|
|
|
351
380
|
(self.STATUS_COMMAND, "Check connection"),
|
|
352
381
|
]
|
|
353
382
|
)
|
|
383
|
+
|
|
354
384
|
self._show_quick_actions(hints, title="Next actions")
|
|
355
|
-
return True
|
|
356
385
|
|
|
357
386
|
def _cmd_exit(self, _args: list[str], invoked_from_agent: bool) -> bool:
|
|
358
387
|
if invoked_from_agent:
|
|
@@ -710,12 +739,12 @@ class SlashSession:
|
|
|
710
739
|
|
|
711
740
|
if full:
|
|
712
741
|
lines = [
|
|
713
|
-
self._branding.
|
|
714
|
-
"",
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
742
|
+
f"GL AIP v{self._branding.version} · GDP Labs AI Agents Package",
|
|
743
|
+
f"API: {api_url or 'Not configured'} · Credentials: {status}",
|
|
744
|
+
(
|
|
745
|
+
f"Verbose: {'on' if self._verbose_enabled else 'off'} "
|
|
746
|
+
"(Ctrl+T toggles verbose streaming)"
|
|
747
|
+
),
|
|
719
748
|
]
|
|
720
749
|
extra: list[str] = []
|
|
721
750
|
self._add_agent_info_to_header(extra, active_agent)
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -64,6 +64,7 @@ logger = logging.getLogger("glaip_sdk.agents")
|
|
|
64
64
|
|
|
65
65
|
_SERVER_ONLY_IMPORT_FIELDS = set(list_server_only_fields()) | {"success", "message"}
|
|
66
66
|
_MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
|
|
67
|
+
_DEFAULT_METADATA_TYPE = "custom"
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def _normalise_sequence(value: Any) -> list[Any] | None:
|
|
@@ -119,6 +120,20 @@ def _split_known_and_extra(
|
|
|
119
120
|
return known, extras
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
def _prepare_agent_metadata(value: Any) -> dict[str, Any]:
|
|
124
|
+
"""Ensure agent metadata contains ``type: custom`` by default."""
|
|
125
|
+
if value is None:
|
|
126
|
+
return {"type": _DEFAULT_METADATA_TYPE}
|
|
127
|
+
if not isinstance(value, Mapping):
|
|
128
|
+
return {"type": _DEFAULT_METADATA_TYPE}
|
|
129
|
+
|
|
130
|
+
prepared = dict(value)
|
|
131
|
+
metadata_type = prepared.get("type")
|
|
132
|
+
if not metadata_type:
|
|
133
|
+
prepared["type"] = _DEFAULT_METADATA_TYPE
|
|
134
|
+
return prepared
|
|
135
|
+
|
|
136
|
+
|
|
122
137
|
def _load_agent_file_payload(
|
|
123
138
|
file_path: Path, *, model_override: str | None
|
|
124
139
|
) -> dict[str, Any]:
|
|
@@ -151,16 +166,39 @@ def _prepare_import_payload(
|
|
|
151
166
|
overrides_dict = dict(overrides)
|
|
152
167
|
|
|
153
168
|
raw_definition = load_resource_from_file(file_path)
|
|
154
|
-
original_refs =
|
|
169
|
+
original_refs = _extract_original_refs(raw_definition)
|
|
170
|
+
|
|
171
|
+
base_payload = _load_agent_file_payload(
|
|
172
|
+
file_path, model_override=overrides_dict.get("model")
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
cli_args = _build_cli_args(overrides_dict)
|
|
176
|
+
|
|
177
|
+
merged = merge_import_with_cli_args(base_payload, cli_args)
|
|
178
|
+
|
|
179
|
+
additional = _build_additional_args(overrides_dict, cli_args)
|
|
180
|
+
merged.update(additional)
|
|
181
|
+
|
|
182
|
+
if drop_model_fields:
|
|
183
|
+
_remove_model_fields_if_needed(merged, overrides_dict)
|
|
184
|
+
|
|
185
|
+
_set_default_refs(merged, original_refs)
|
|
186
|
+
|
|
187
|
+
_normalise_sequence_fields(merged)
|
|
188
|
+
return merged
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
|
|
192
|
+
"""Extract original tool/agent/mcp references from raw definition."""
|
|
193
|
+
return {
|
|
155
194
|
"tools": list(raw_definition.get("tools") or []),
|
|
156
195
|
"agents": list(raw_definition.get("agents") or []),
|
|
157
196
|
"mcps": list(raw_definition.get("mcps") or []),
|
|
158
197
|
}
|
|
159
198
|
|
|
160
|
-
base_payload = _load_agent_file_payload(
|
|
161
|
-
file_path, model_override=overrides_dict.get("model")
|
|
162
|
-
)
|
|
163
199
|
|
|
200
|
+
def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
|
|
201
|
+
"""Build CLI args from overrides, filtering out None values."""
|
|
164
202
|
cli_args = {
|
|
165
203
|
key: overrides_dict.get(key)
|
|
166
204
|
for key in (
|
|
@@ -175,32 +213,37 @@ def _prepare_import_payload(
|
|
|
175
213
|
if overrides_dict.get(key) is not None
|
|
176
214
|
}
|
|
177
215
|
|
|
216
|
+
# Normalize sequence fields
|
|
178
217
|
for field in _MERGED_SEQUENCE_FIELDS:
|
|
179
218
|
if field in cli_args:
|
|
180
219
|
cli_args[field] = tuple(_normalise_sequence(cli_args[field]) or [])
|
|
181
220
|
|
|
182
|
-
|
|
221
|
+
return cli_args
|
|
183
222
|
|
|
184
|
-
|
|
223
|
+
|
|
224
|
+
def _build_additional_args(overrides_dict: dict, cli_args: dict) -> dict[str, Any]:
|
|
225
|
+
"""Build additional args not already in CLI args."""
|
|
226
|
+
return {
|
|
185
227
|
key: value
|
|
186
228
|
for key, value in overrides_dict.items()
|
|
187
229
|
if value is not None and key not in cli_args
|
|
188
230
|
}
|
|
189
|
-
merged.update(additional)
|
|
190
231
|
|
|
191
|
-
if drop_model_fields:
|
|
192
|
-
if overrides_dict.get("language_model_id") is None:
|
|
193
|
-
merged.pop("language_model_id", None)
|
|
194
|
-
if overrides_dict.get("provider") is None:
|
|
195
|
-
merged.pop("provider", None)
|
|
196
232
|
|
|
233
|
+
def _remove_model_fields_if_needed(merged: dict, overrides_dict: dict) -> None:
|
|
234
|
+
"""Remove model fields if not explicitly overridden."""
|
|
235
|
+
if overrides_dict.get("language_model_id") is None:
|
|
236
|
+
merged.pop("language_model_id", None)
|
|
237
|
+
if overrides_dict.get("provider") is None:
|
|
238
|
+
merged.pop("provider", None)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _set_default_refs(merged: dict, original_refs: dict) -> None:
|
|
242
|
+
"""Set default references if not already present."""
|
|
197
243
|
merged.setdefault("_tool_refs", original_refs["tools"])
|
|
198
244
|
merged.setdefault("_agent_refs", original_refs["agents"])
|
|
199
245
|
merged.setdefault("_mcp_refs", original_refs["mcps"])
|
|
200
246
|
|
|
201
|
-
_normalise_sequence_fields(merged)
|
|
202
|
-
return merged
|
|
203
|
-
|
|
204
247
|
|
|
205
248
|
class AgentClient(BaseClient):
|
|
206
249
|
"""Client for agent operations."""
|
|
@@ -594,6 +637,25 @@ class AgentClient(BaseClient):
|
|
|
594
637
|
resolved_agents = self._resolve_agent_ids(agents_raw, agent_refs)
|
|
595
638
|
resolved_mcps = self._resolve_mcp_ids(mcps_raw, mcp_refs)
|
|
596
639
|
|
|
640
|
+
language_model_id = known.pop("language_model_id", None)
|
|
641
|
+
provider = known.pop("provider", None)
|
|
642
|
+
model_name = known.pop("model_name", None)
|
|
643
|
+
|
|
644
|
+
agent_type_value = known.pop("agent_type", None)
|
|
645
|
+
fallback_type_value = known.pop("type", None)
|
|
646
|
+
if agent_type_value is None:
|
|
647
|
+
agent_type_value = fallback_type_value or DEFAULT_AGENT_TYPE
|
|
648
|
+
|
|
649
|
+
framework_value = known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK
|
|
650
|
+
version_value = known.pop("version", None) or DEFAULT_AGENT_VERSION
|
|
651
|
+
account_id = known.pop("account_id", None)
|
|
652
|
+
description = known.pop("description", None)
|
|
653
|
+
metadata = _prepare_agent_metadata(known.pop("metadata", None))
|
|
654
|
+
tool_configs = known.pop("tool_configs", None)
|
|
655
|
+
agent_config = known.pop("agent_config", None)
|
|
656
|
+
timeout_value = known.pop("timeout", None)
|
|
657
|
+
a2a_profile = known.pop("a2a_profile", None)
|
|
658
|
+
|
|
597
659
|
final_extras = {**known, **extras}
|
|
598
660
|
final_extras.setdefault("model", resolved_model)
|
|
599
661
|
|
|
@@ -601,22 +663,22 @@ class AgentClient(BaseClient):
|
|
|
601
663
|
name=str(name).strip(),
|
|
602
664
|
instruction=validated_instruction,
|
|
603
665
|
model=resolved_model,
|
|
604
|
-
language_model_id=
|
|
605
|
-
provider=
|
|
606
|
-
model_name=
|
|
607
|
-
agent_type=
|
|
608
|
-
framework=
|
|
609
|
-
version=
|
|
610
|
-
account_id=
|
|
611
|
-
description=
|
|
612
|
-
metadata=
|
|
666
|
+
language_model_id=language_model_id,
|
|
667
|
+
provider=provider,
|
|
668
|
+
model_name=model_name,
|
|
669
|
+
agent_type=agent_type_value,
|
|
670
|
+
framework=framework_value,
|
|
671
|
+
version=version_value,
|
|
672
|
+
account_id=account_id,
|
|
673
|
+
description=description,
|
|
674
|
+
metadata=metadata,
|
|
613
675
|
tools=resolved_tools,
|
|
614
676
|
agents=resolved_agents,
|
|
615
677
|
mcps=resolved_mcps,
|
|
616
|
-
tool_configs=
|
|
617
|
-
agent_config=
|
|
618
|
-
timeout=
|
|
619
|
-
a2a_profile=
|
|
678
|
+
tool_configs=tool_configs,
|
|
679
|
+
agent_config=agent_config,
|
|
680
|
+
timeout=timeout_value or DEFAULT_AGENT_RUN_TIMEOUT,
|
|
681
|
+
a2a_profile=a2a_profile,
|
|
620
682
|
extras=final_extras,
|
|
621
683
|
)
|
|
622
684
|
|
glaip_sdk/client/main.py
CHANGED
|
@@ -158,9 +158,9 @@ class Client(BaseClient):
|
|
|
158
158
|
"""Get tool script content."""
|
|
159
159
|
return self.tools.get_tool_script(tool_id)
|
|
160
160
|
|
|
161
|
-
def update_tool_via_file(self, tool_id: str, file_path: str) -> Tool:
|
|
161
|
+
def update_tool_via_file(self, tool_id: str, file_path: str, **kwargs) -> Tool:
|
|
162
162
|
"""Update tool via file."""
|
|
163
|
-
return self.tools.update_tool_via_file(tool_id, file_path)
|
|
163
|
+
return self.tools.update_tool_via_file(tool_id, file_path, **kwargs)
|
|
164
164
|
|
|
165
165
|
# MCPs
|
|
166
166
|
def create_mcp(self, **kwargs) -> MCP:
|
glaip_sdk/client/tools.py
CHANGED
|
@@ -241,7 +241,15 @@ class ToolClient(BaseClient):
|
|
|
241
241
|
self, update_data: dict[str, Any], kwargs: dict[str, Any]
|
|
242
242
|
) -> None:
|
|
243
243
|
"""Handle additional kwargs in update payload."""
|
|
244
|
-
excluded_keys = {
|
|
244
|
+
excluded_keys = {
|
|
245
|
+
"tags",
|
|
246
|
+
"framework",
|
|
247
|
+
"version",
|
|
248
|
+
"type",
|
|
249
|
+
"tool_type",
|
|
250
|
+
"name",
|
|
251
|
+
"description",
|
|
252
|
+
}
|
|
245
253
|
for key, value in kwargs.items():
|
|
246
254
|
if key not in excluded_keys:
|
|
247
255
|
update_data[key] = value
|
|
@@ -269,9 +277,19 @@ class ToolClient(BaseClient):
|
|
|
269
277
|
- Handles metadata updates properly
|
|
270
278
|
"""
|
|
271
279
|
# Prepare the update payload with current values as defaults
|
|
280
|
+
type_override = kwargs.pop("type", None)
|
|
281
|
+
if type_override is None:
|
|
282
|
+
type_override = kwargs.pop("tool_type", None)
|
|
283
|
+
current_type = (
|
|
284
|
+
type_override
|
|
285
|
+
or getattr(current_tool, "tool_type", None)
|
|
286
|
+
or getattr(current_tool, "type", None)
|
|
287
|
+
or DEFAULT_TOOL_TYPE
|
|
288
|
+
)
|
|
289
|
+
|
|
272
290
|
update_data = {
|
|
273
291
|
"name": name if name is not None else current_tool.name,
|
|
274
|
-
"type":
|
|
292
|
+
"type": current_type,
|
|
275
293
|
"framework": kwargs.get(
|
|
276
294
|
"framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)
|
|
277
295
|
),
|
|
@@ -476,6 +494,19 @@ class ToolClient(BaseClient):
|
|
|
476
494
|
# Validate file exists
|
|
477
495
|
self._validate_and_read_file(file_path)
|
|
478
496
|
|
|
497
|
+
# Fetch current metadata to ensure required fields are preserved
|
|
498
|
+
current_tool = self.get_tool_by_id(tool_id)
|
|
499
|
+
|
|
500
|
+
payload_kwargs = kwargs.copy()
|
|
501
|
+
name = payload_kwargs.pop("name", None)
|
|
502
|
+
description = payload_kwargs.pop("description", None)
|
|
503
|
+
update_payload = self._build_update_payload(
|
|
504
|
+
current_tool=current_tool,
|
|
505
|
+
name=name,
|
|
506
|
+
description=description,
|
|
507
|
+
**payload_kwargs,
|
|
508
|
+
)
|
|
509
|
+
|
|
479
510
|
try:
|
|
480
511
|
# Prepare multipart upload
|
|
481
512
|
with open(file_path, "rb") as fb:
|
|
@@ -491,7 +522,7 @@ class ToolClient(BaseClient):
|
|
|
491
522
|
"PUT",
|
|
492
523
|
TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT.format(tool_id=tool_id),
|
|
493
524
|
files=files,
|
|
494
|
-
data=
|
|
525
|
+
data=update_payload,
|
|
495
526
|
)
|
|
496
527
|
|
|
497
528
|
return Tool(**response)._set_client(self)
|
glaip_sdk/utils/agent_config.py
CHANGED
|
@@ -134,25 +134,51 @@ def normalize_agent_config_for_import(
|
|
|
134
134
|
if not isinstance(agent_config, dict):
|
|
135
135
|
return normalized_data
|
|
136
136
|
|
|
137
|
-
#
|
|
137
|
+
# Apply normalization based on priority order
|
|
138
138
|
if cli_model:
|
|
139
|
-
|
|
140
|
-
normalized_data["model"] = cli_model
|
|
141
|
-
return normalized_data
|
|
139
|
+
return _apply_cli_model_override(normalized_data, cli_model)
|
|
142
140
|
|
|
143
|
-
# Priority 2: language_model_id already exists - clean up agent_config
|
|
144
141
|
if normalized_data.get("language_model_id"):
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
return _cleanup_existing_language_model(normalized_data, agent_config)
|
|
143
|
+
|
|
144
|
+
return _extract_lm_from_agent_config(normalized_data, agent_config)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _apply_cli_model_override(normalized_data: dict, cli_model: str) -> dict:
|
|
148
|
+
"""Apply CLI model override (highest priority)."""
|
|
149
|
+
normalized_data["model"] = cli_model
|
|
150
|
+
return normalized_data
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cleanup_existing_language_model(normalized_data: dict, agent_config: dict) -> dict:
|
|
154
|
+
"""Clean up agent_config when language_model_id already exists."""
|
|
155
|
+
# Remove LM identity keys from agent_config since language_model_id takes precedence
|
|
156
|
+
lm_keys_to_remove = {"lm_provider", "lm_name", "lm_base_url"}
|
|
157
|
+
for key in lm_keys_to_remove:
|
|
158
|
+
agent_config.pop(key, None)
|
|
159
|
+
normalized_data["agent_config"] = agent_config
|
|
160
|
+
return normalized_data
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _extract_lm_from_agent_config(normalized_data: dict, agent_config: dict) -> dict:
|
|
164
|
+
"""Extract LM settings from agent_config (lowest priority)."""
|
|
165
|
+
extracted_lm = _extract_lm_settings(agent_config)
|
|
166
|
+
|
|
167
|
+
if not extracted_lm:
|
|
153
168
|
return normalized_data
|
|
154
169
|
|
|
155
|
-
#
|
|
170
|
+
# Add extracted LM settings to top level
|
|
171
|
+
normalized_data.update(extracted_lm)
|
|
172
|
+
|
|
173
|
+
# Create sanitized agent_config (remove extracted LM settings but keep memory)
|
|
174
|
+
sanitized_config = _sanitize_agent_config(agent_config)
|
|
175
|
+
normalized_data["agent_config"] = sanitized_config
|
|
176
|
+
|
|
177
|
+
return normalized_data
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _extract_lm_settings(agent_config: dict) -> dict[str, Any]:
|
|
181
|
+
"""Extract LM settings from agent_config."""
|
|
156
182
|
extracted_lm = {}
|
|
157
183
|
|
|
158
184
|
# Extract lm_name if present
|
|
@@ -163,19 +189,16 @@ def normalize_agent_config_for_import(
|
|
|
163
189
|
if "lm_provider" in agent_config:
|
|
164
190
|
extracted_lm["lm_provider"] = agent_config["lm_provider"]
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
if extracted_lm:
|
|
168
|
-
# Add extracted LM settings to top level
|
|
169
|
-
normalized_data.update(extracted_lm)
|
|
192
|
+
return extracted_lm
|
|
170
193
|
|
|
171
|
-
# Create sanitized agent_config (remove extracted LM settings but keep memory)
|
|
172
|
-
sanitized_config = agent_config.copy()
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
sanitized_config.pop(key, None)
|
|
195
|
+
def _sanitize_agent_config(agent_config: dict) -> dict:
|
|
196
|
+
"""Create sanitized agent_config by removing LM identity keys."""
|
|
197
|
+
sanitized_config = agent_config.copy()
|
|
178
198
|
|
|
179
|
-
|
|
199
|
+
# Remove LM identity keys but preserve memory and other settings
|
|
200
|
+
lm_keys_to_remove = {"lm_provider", "lm_name", "lm_base_url"}
|
|
201
|
+
for key in lm_keys_to_remove:
|
|
202
|
+
sanitized_config.pop(key, None)
|
|
180
203
|
|
|
181
|
-
return
|
|
204
|
+
return sanitized_config
|
|
@@ -57,41 +57,62 @@ def mask_secrets_in_string(text: str) -> str:
|
|
|
57
57
|
def redact_sensitive(text: str | dict | list) -> str | dict | list:
|
|
58
58
|
"""Redact sensitive information in a string, dict, or list."""
|
|
59
59
|
if isinstance(text, dict):
|
|
60
|
-
|
|
61
|
-
result = {}
|
|
62
|
-
for key, value in text.items():
|
|
63
|
-
# Check if the key itself is sensitive
|
|
64
|
-
key_lower = key.lower()
|
|
65
|
-
if any(
|
|
66
|
-
sensitive in key_lower
|
|
67
|
-
for sensitive in ["password", "secret", "token", "key", "api_key"]
|
|
68
|
-
):
|
|
69
|
-
result[key] = "••••••"
|
|
70
|
-
elif isinstance(value, dict | list) or isinstance(value, str):
|
|
71
|
-
result[key] = redact_sensitive(value)
|
|
72
|
-
else:
|
|
73
|
-
result[key] = value
|
|
74
|
-
return result
|
|
60
|
+
return _redact_dict_values(text)
|
|
75
61
|
elif isinstance(text, list):
|
|
76
|
-
|
|
77
|
-
return [redact_sensitive(item) for item in text]
|
|
62
|
+
return _redact_list_items(text)
|
|
78
63
|
elif isinstance(text, str):
|
|
79
|
-
|
|
80
|
-
result = text
|
|
81
|
-
# First mask secrets
|
|
82
|
-
for pattern in SECRET_VALUE_PATTERNS:
|
|
83
|
-
result = re.sub(pattern, "••••••", result)
|
|
84
|
-
# Then redact sensitive patterns
|
|
85
|
-
result = re.sub(
|
|
86
|
-
SENSITIVE_PATTERNS,
|
|
87
|
-
lambda m: m.group(0).split("=")[0] + "=••••••",
|
|
88
|
-
result,
|
|
89
|
-
)
|
|
90
|
-
return result
|
|
64
|
+
return _redact_string_content(text)
|
|
91
65
|
else:
|
|
92
66
|
return text
|
|
93
67
|
|
|
94
68
|
|
|
69
|
+
def _redact_dict_values(text: dict) -> dict:
|
|
70
|
+
"""Recursively process dictionary values and redact sensitive keys."""
|
|
71
|
+
result = {}
|
|
72
|
+
for key, value in text.items():
|
|
73
|
+
if _is_sensitive_key(key):
|
|
74
|
+
result[key] = "••••••"
|
|
75
|
+
elif _should_recurse_redaction(value):
|
|
76
|
+
result[key] = redact_sensitive(value)
|
|
77
|
+
else:
|
|
78
|
+
result[key] = value
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _redact_list_items(text: list) -> list:
|
|
83
|
+
"""Recursively process list items."""
|
|
84
|
+
return [redact_sensitive(item) for item in text]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _redact_string_content(text: str) -> str:
|
|
88
|
+
"""Process string - first mask secrets, then redact sensitive patterns."""
|
|
89
|
+
result = text
|
|
90
|
+
# First mask secrets
|
|
91
|
+
for pattern in SECRET_VALUE_PATTERNS:
|
|
92
|
+
result = re.sub(pattern, "••••••", result)
|
|
93
|
+
# Then redact sensitive patterns
|
|
94
|
+
result = re.sub(
|
|
95
|
+
SENSITIVE_PATTERNS,
|
|
96
|
+
lambda m: m.group(0).split("=")[0] + "=••••••",
|
|
97
|
+
result,
|
|
98
|
+
)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_sensitive_key(key: str) -> bool:
|
|
103
|
+
"""Check if a key contains sensitive information."""
|
|
104
|
+
key_lower = key.lower()
|
|
105
|
+
return any(
|
|
106
|
+
sensitive in key_lower
|
|
107
|
+
for sensitive in ["password", "secret", "token", "key", "api_key"]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _should_recurse_redaction(value: Any) -> bool:
|
|
112
|
+
"""Check if a value should be recursively processed."""
|
|
113
|
+
return isinstance(value, dict | list) or isinstance(value, str)
|
|
114
|
+
|
|
115
|
+
|
|
95
116
|
def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
96
117
|
"""Format arguments in a pretty way."""
|
|
97
118
|
if not args:
|