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.
@@ -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
- client = self._get_client()
318
+ return self._get_client()
302
319
  except click.ClickException as exc:
303
320
  self.console.print(f"[red]{exc}[/red]")
304
- return True
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 True
332
+ return []
311
333
 
312
- if not agents:
313
- hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
314
- if hint:
315
- self.console.print(
316
- f"[yellow]No agents available. Use `{hint}` to add one.[/yellow]"
317
- )
318
- else:
319
- self.console.print("[yellow]No agents available.[/yellow]")
320
- return True
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 True
352
+ return None
329
353
  else:
330
354
  picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
331
355
 
332
- if not picked_agent:
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 so the
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.get_welcome_banner(),
714
- "",
715
- f"[dim]API URL[/dim]: {api_url or 'Not configured'}",
716
- f"[dim]Credentials[/dim]: {status}",
717
- f"[dim]Verbose mode[/dim]: {'on' if self._verbose_enabled else 'off'}",
718
- "[dim]Tip[/dim]: Press Ctrl+T or run `/verbose` to toggle verbose streaming.",
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)
@@ -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
- merged = merge_import_with_cli_args(base_payload, cli_args)
221
+ return cli_args
183
222
 
184
- additional = {
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=known.pop("language_model_id", None),
605
- provider=known.pop("provider", None),
606
- model_name=known.pop("model_name", None),
607
- agent_type=known.pop("agent_type", known.pop("type", DEFAULT_AGENT_TYPE)),
608
- framework=known.pop("framework", None) or DEFAULT_AGENT_FRAMEWORK,
609
- version=known.pop("version", None) or DEFAULT_AGENT_VERSION,
610
- account_id=known.pop("account_id", None),
611
- description=known.pop("description", None),
612
- metadata=known.pop("metadata", None),
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=known.pop("tool_configs", None),
617
- agent_config=known.pop("agent_config", None),
618
- timeout=known.pop("timeout", None) or DEFAULT_AGENT_RUN_TIMEOUT,
619
- a2a_profile=known.pop("a2a_profile", None),
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 = {"tags", "framework", "version"}
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": DEFAULT_TOOL_TYPE, # Required by backend
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=kwargs, # Pass kwargs directly as data
525
+ data=update_payload,
495
526
  )
496
527
 
497
528
  return Tool(**response)._set_client(self)
@@ -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
- # Priority 1: CLI --model flag (highest priority)
137
+ # Apply normalization based on priority order
138
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
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
- # 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
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
- # Priority 3: Extract LM settings from agent_config
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
- # 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)
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
- # 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)
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
- normalized_data["agent_config"] = sanitized_config
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 normalized_data
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
- # Recursively process dictionary values
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
- # Recursively process list items
77
- return [redact_sensitive(item) for item in text]
62
+ return _redact_list_items(text)
78
63
  elif isinstance(text, str):
79
- # Process string - first mask secrets, then redact sensitive patterns
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: