glaip-sdk 0.6.5b9__py3-none-any.whl → 0.6.6__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/agents/base.py CHANGED
@@ -50,11 +50,6 @@ from pathlib import Path
50
50
  from typing import TYPE_CHECKING, Any
51
51
 
52
52
  from glaip_sdk.registry import get_agent_registry, get_mcp_registry, get_tool_registry
53
- from glaip_sdk.runner import get_default_runner
54
- from glaip_sdk.runner.deps import (
55
- check_local_runtime_available,
56
- get_local_runtime_missing_message,
57
- )
58
53
  from glaip_sdk.utils.discovery import find_agent
59
54
  from glaip_sdk.utils.resource_refs import is_uuid
60
55
  from glaip_sdk.utils.runtime_config import normalize_runtime_config_keys
@@ -898,20 +893,13 @@ class Agent:
898
893
  message: str,
899
894
  verbose: bool = False,
900
895
  runtime_config: dict[str, Any] | None = None,
901
- chat_history: list[dict[str, str]] | None = None,
902
896
  **kwargs: Any,
903
897
  ) -> str:
904
898
  """Run the agent synchronously with a message.
905
899
 
906
- Supports two execution modes:
907
- - **Server-backed**: When the agent is deployed (has an ID), execution
908
- happens via the AIP backend server.
909
- - **Local**: When the agent is not deployed and glaip-sdk[local] is installed,
910
- execution happens locally via aip-agents (no server required).
911
-
912
900
  Args:
913
901
  message: The message to send to the agent.
914
- verbose: If True, print streaming output to console. Defaults to False.
902
+ verbose: If True, print streaming output to console.
915
903
  runtime_config: Optional runtime configuration for tools, MCPs, and agents.
916
904
  Keys can be SDK objects, UUIDs, or names. Example:
917
905
  {
@@ -919,43 +907,19 @@ class Agent:
919
907
  "mcp_configs": {"mcp-id": {"setting": "on"}},
920
908
  "agent_config": {"planning": True},
921
909
  }
922
- Defaults to None.
923
- chat_history: Optional list of prior conversation messages for context.
924
- Each message is a dict with "role" and "content" keys.
925
- Defaults to None.
926
910
  **kwargs: Additional arguments to pass to the run API.
927
911
 
928
912
  Returns:
929
913
  The agent's response as a string.
930
914
 
931
915
  Raises:
932
- ValueError: If the agent is not deployed and local runtime is not available.
933
- RuntimeError: If server-backed execution fails due to client issues.
916
+ ValueError: If the agent hasn't been deployed yet.
917
+ RuntimeError: If client is not available.
934
918
  """
935
- # Backend routing: deployed agents use server, undeployed use local (if available)
936
- if self.id:
937
- # Server-backed execution path (agent is deployed)
938
- agent_client, call_kwargs = self._prepare_run_kwargs(
939
- message, verbose, runtime_config or kwargs.get("runtime_config"), **kwargs
940
- )
941
- if chat_history is not None:
942
- call_kwargs["chat_history"] = chat_history
943
- return agent_client.run_agent(**call_kwargs)
944
-
945
- # Local execution path (agent is not deployed)
946
- if check_local_runtime_available():
947
- runner = get_default_runner()
948
- return runner.run(
949
- agent=self,
950
- message=message,
951
- verbose=verbose,
952
- runtime_config=runtime_config,
953
- chat_history=chat_history,
954
- **kwargs,
955
- )
956
-
957
- # Neither deployed nor local runtime available - provide actionable error
958
- raise ValueError(f"{_AGENT_NOT_DEPLOYED_MSG}\n\n{get_local_runtime_missing_message()}")
919
+ agent_client, call_kwargs = self._prepare_run_kwargs(
920
+ message, verbose, runtime_config or kwargs.get("runtime_config"), **kwargs
921
+ )
922
+ return agent_client.run_agent(**call_kwargs)
959
923
 
960
924
  async def arun(
961
925
  self,
@@ -151,79 +151,147 @@ class AccountsController:
151
151
 
152
152
  def _render_textual(self, rows: list[dict[str, str | bool]], store: AccountStore, env_lock: bool) -> None:
153
153
  """Launch the Textual accounts browser."""
154
- callbacks = AccountsTUICallbacks(switch_account=lambda name: self._switch_account(store, name, env_lock))
154
+ active_before = store.get_active_account()
155
+ notified = False
156
+
157
+ def _switch_in_textual(name: str) -> tuple[bool, str]:
158
+ nonlocal notified
159
+ switched, message = self._switch_account(
160
+ store,
161
+ name,
162
+ env_lock,
163
+ emit_console=False,
164
+ invalidate_session=True,
165
+ )
166
+ if switched:
167
+ notified = True
168
+ return switched, message
169
+
170
+ callbacks = AccountsTUICallbacks(switch_account=_switch_in_textual)
155
171
  active = next((row["name"] for row in rows if row.get("active")), None)
156
- run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks)
157
- # Exit snapshot: surface a success banner when a switch occurred inside the TUI
158
- active_after = store.get_active_account() or "default"
172
+ try:
173
+ run_accounts_textual(rows, active_account=active, env_lock=env_lock, callbacks=callbacks)
174
+ except Exception as exc: # pragma: no cover - defensive around Textual failures
175
+ self.console.print(f"[{WARNING_STYLE}]Accounts browser exited unexpectedly: {exc}[/]")
176
+
177
+ # Exit snapshot: surface a success banner when a switch occurred inside the TUI.
178
+ # Always notify when the active account changed, even if Textual raised.
179
+ active_after = store.get_active_account()
180
+ if active_after != active_before and not notified:
181
+ self._notify_account_switched(active_after)
159
182
  if active_after != active:
160
183
  host_after = ""
161
- account_after = store.get_account(active_after) if hasattr(store, "get_account") else None
184
+ display_account = active_after or "default"
185
+ account_after = store.get_account(display_account) if hasattr(store, "get_account") else None
162
186
  if account_after:
163
187
  host_after = account_after.get("api_url", "")
164
188
  host_suffix = f" • {host_after}" if host_after else ""
165
189
  self.console.print(
166
190
  AIPPanel(
167
- f"[{SUCCESS_STYLE}]Active account ➜ {active_after}[/]{host_suffix}",
191
+ f"[{SUCCESS_STYLE}]Active account ➜ {display_account}[/]{host_suffix}",
168
192
  title="✅ Account Switched",
169
193
  border_style=SUCCESS_STYLE,
170
194
  )
171
195
  )
172
196
 
173
- def _switch_account(self, store: AccountStore, name: str, env_lock: bool) -> tuple[bool, str]:
174
- """Validate and switch active account; returns (success, message)."""
197
+ def _format_connection_error_message(self, error_reason: str, account_name: str, api_url: str) -> str:
198
+ """Format error message for connection validation failures."""
199
+ code, detail = self._parse_error_reason(error_reason)
200
+ if code == "connection_failed":
201
+ return f"Switch aborted: cannot reach {api_url}. Check URL or network."
202
+ if code == "api_failed":
203
+ return f"Switch aborted: API error for '{account_name}'. Check credentials."
204
+ detail_suffix = f": {detail}" if detail else ""
205
+ return f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
206
+
207
+ def _emit_error_message(self, msg: str, style: str = ERROR_STYLE) -> None:
208
+ """Emit an error or warning message to the console."""
209
+ self.console.print(f"[{style}]{msg}[/]")
210
+
211
+ def _validate_account_switch(
212
+ self, store: AccountStore, name: str, env_lock: bool, emit_console: bool
213
+ ) -> tuple[bool, str, dict[str, str] | None]:
214
+ """Validate account switch prerequisites; returns (is_valid, error_msg, account_dict)."""
175
215
  if env_lock:
176
216
  msg = "Env credentials detected (AIP_API_URL/AIP_API_KEY); switching is disabled."
177
- self.console.print(f"[{WARNING_STYLE}]{msg}[/]")
178
- return False, msg
217
+ if emit_console:
218
+ self._emit_error_message(msg, WARNING_STYLE)
219
+ return False, msg, None
179
220
 
180
221
  account = store.get_account(name)
181
222
  if not account:
182
223
  msg = f"Account '{name}' not found."
183
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
184
- return False, msg
224
+ if emit_console:
225
+ self._emit_error_message(msg)
226
+ return False, msg, None
185
227
 
186
228
  api_url = account.get("api_url", "")
187
229
  api_key = account.get("api_key", "")
188
230
  if not api_url or not api_key:
189
231
  edit_cmd = f"aip accounts edit {name}"
190
232
  msg = f"Account '{name}' is missing credentials. Use `/login` or `{edit_cmd}`."
191
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
192
- return False, msg
233
+ if emit_console:
234
+ self._emit_error_message(msg)
235
+ return False, msg, None
193
236
 
194
237
  ok, error_reason = check_connection_with_reason(api_url, api_key, abort_on_error=False)
195
238
  if not ok:
196
- code, detail = self._parse_error_reason(error_reason)
197
- if code == "connection_failed":
198
- msg = f"Switch aborted: cannot reach {api_url}. Check URL or network."
199
- elif code == "api_failed":
200
- msg = f"Switch aborted: API error for '{name}'. Check credentials."
201
- else:
202
- detail_suffix = f": {detail}" if detail else ""
203
- msg = f"Switch aborted: {code or 'Validation failed'}{detail_suffix}"
204
- self.console.print(f"[{WARNING_STYLE}]{msg}[/]")
205
- return False, msg
239
+ msg = self._format_connection_error_message(error_reason, name, api_url)
240
+ if emit_console:
241
+ self._emit_error_message(msg, WARNING_STYLE)
242
+ return False, msg, None
206
243
 
244
+ return True, "", account
245
+
246
+ def _execute_account_switch(
247
+ self, store: AccountStore, name: str, account: dict[str, str], invalidate_session: bool, emit_console: bool
248
+ ) -> tuple[bool, str]:
249
+ """Execute the account switch and emit success message."""
207
250
  try:
208
251
  store.set_active_account(name)
252
+ api_url = account.get("api_url", "")
253
+ api_key = account.get("api_key", "")
209
254
  masked_key = mask_api_key_display(api_key)
210
- self.console.print(
211
- AIPPanel(
212
- f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
213
- title="✅ Account Switched",
214
- border_style=SUCCESS_STYLE,
255
+ if invalidate_session:
256
+ self._notify_account_switched(name)
257
+ if emit_console:
258
+ self.console.print(
259
+ AIPPanel(
260
+ f"[{SUCCESS_STYLE}]Active account ➜ {name}[/]\nAPI URL: {api_url}\nKey: {masked_key}",
261
+ title="✅ Account Switched",
262
+ border_style=SUCCESS_STYLE,
263
+ )
215
264
  )
216
- )
217
265
  return True, f"Switched to '{name}'."
218
266
  except AccountStoreError as exc:
219
267
  msg = f"Failed to set active account: {exc}"
220
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
268
+ if emit_console:
269
+ self._emit_error_message(msg)
221
270
  return False, msg
222
271
  except Exception as exc: # NOSONAR(S1045) - catch-all needed for unexpected errors
223
272
  msg = f"Unexpected error while switching to '{name}': {exc}"
224
- self.console.print(f"[{ERROR_STYLE}]{msg}[/]")
273
+ if emit_console:
274
+ self._emit_error_message(msg)
225
275
  return False, msg
226
276
 
277
+ def _switch_account(
278
+ self,
279
+ store: AccountStore,
280
+ name: str,
281
+ env_lock: bool,
282
+ *,
283
+ emit_console: bool = True,
284
+ invalidate_session: bool = True,
285
+ ) -> tuple[bool, str]:
286
+ """Validate and switch active account; returns (success, message)."""
287
+ is_valid, error_msg, account = self._validate_account_switch(store, name, env_lock, emit_console)
288
+ if not is_valid:
289
+ return False, error_msg
290
+
291
+ if account is None: # Defensive – should never happen, but avoid crashing in production
292
+ return False, "Unable to locate account after validation."
293
+ return self._execute_account_switch(store, name, account, invalidate_session, emit_console)
294
+
227
295
  @staticmethod
228
296
  def _parse_error_reason(reason: str | None) -> tuple[str, str]:
229
297
  """Parse error reason into (code, detail) to avoid fragile substring checks."""
@@ -404,8 +472,18 @@ class AccountsController:
404
472
  except Exception as exc:
405
473
  self.console.print(f"[{WARNING_STYLE}]Account saved but could not set active: {exc}[/]")
406
474
  else:
475
+ self._notify_account_switched(name)
407
476
  self._announce_active_change(store, name)
408
477
 
478
+ def _notify_account_switched(self, name: str | None) -> None:
479
+ """Best-effort notify the hosting session that the active account changed."""
480
+ notify = getattr(self.session, "on_account_switched", None)
481
+ if callable(notify):
482
+ try:
483
+ notify(name)
484
+ except Exception: # pragma: no cover - best-effort callback
485
+ pass
486
+
409
487
  def _confirm_delete_prompt(self, name: str) -> bool:
410
488
  """Ask for delete confirmation; return True when confirmed."""
411
489
  self.console.print(f"[{WARNING_STYLE}]Type '{name}' to confirm deletion. This cannot be undone.[/]")
@@ -38,7 +38,10 @@ class AgentRunSession:
38
38
  self.console = session.console
39
39
  self._agent_id = str(getattr(agent, "id", ""))
40
40
  self._agent_name = getattr(agent, "name", "") or self._agent_id
41
- self._prompt_placeholder: str = "Chat with this agent here; use / for shortcuts. Alt+Enter inserts a newline."
41
+ self._prompt_placeholder: str = (
42
+ "Chat with this agent here; use / for shortcuts. "
43
+ "Alt+Enter inserts a newline. Ctrl+T opens the last transcript."
44
+ )
42
45
  self._contextual_completion_help: dict[str, str] = {
43
46
  "details": "Show this agent's configuration (+ expands prompt).",
44
47
  "help": "Display this context-aware menu.",
@@ -162,6 +162,17 @@ def _create_key_bindings(_session: SlashSession) -> Any:
162
162
  if buffer.complete_state is not None:
163
163
  buffer.cancel_completion()
164
164
 
165
+ @bindings.add("c-t") # type: ignore[misc]
166
+ def _handle_ctrl_t_key(event: Any) -> None: # vulture: ignore
167
+ """Handle Ctrl+T key - open the transcript viewer (when available)."""
168
+ buffer = event.app.current_buffer
169
+ if buffer.complete_state is not None:
170
+ buffer.cancel_completion()
171
+
172
+ open_viewer = getattr(_session, "open_transcript_viewer", None)
173
+ if callable(open_viewer):
174
+ open_viewer(announce=True)
175
+
165
176
  return bindings
166
177
 
167
178
 
@@ -548,7 +548,7 @@ class SlashSession:
548
548
  try:
549
549
  # Use the modern account-aware wizard directly (bypasses legacy config gating)
550
550
  _configure_interactive(account_name=None)
551
- self._config_cache = None
551
+ self.on_account_switched()
552
552
  if self._suppress_login_layout:
553
553
  self._welcome_rendered = False
554
554
  self._default_actions_shown = False
@@ -1211,6 +1211,33 @@ class SlashSession:
1211
1211
  self._client = get_client(self.ctx)
1212
1212
  return self._client
1213
1213
 
1214
+ def on_account_switched(self, _account_name: str | None = None) -> None:
1215
+ """Reset any state that depends on the active account.
1216
+
1217
+ The active account can change via `/accounts` (or other flows that call
1218
+ AccountStore.set_active_account). The slash session caches a configured
1219
+ client instance, so we must invalidate it to avoid leaking the previous
1220
+ account's API URL/key into subsequent commands like `/agents` or `/runs`.
1221
+
1222
+ This method clears:
1223
+ - Client and config cache (account-specific credentials)
1224
+ - Current agent and recent agents (agent data is account-scoped)
1225
+ - Runs pagination state (runs are account-scoped)
1226
+ - Active renderer and transcript ready state (UI state tied to account context)
1227
+ - Contextual commands (may be account-specific)
1228
+
1229
+ These broader resets ensure a clean slate when switching accounts, preventing
1230
+ stale data from the previous account from appearing in the new account's context.
1231
+ """
1232
+ self._client = None
1233
+ self._config_cache = None
1234
+ self._current_agent = None
1235
+ self.recent_agents = []
1236
+ self._runs_pagination_state.clear()
1237
+ self.clear_active_renderer()
1238
+ self.clear_agent_transcript_ready()
1239
+ self.set_contextual_commands(None)
1240
+
1214
1241
  def set_contextual_commands(self, commands: dict[str, str] | None, *, include_global: bool = True) -> None:
1215
1242
  """Set context-specific commands that should appear in completions."""
1216
1243
  self._contextual_commands = dict(commands or {})
@@ -1340,14 +1367,16 @@ class SlashSession:
1340
1367
  f"[{ACCENT_STYLE}]{agent_info['id']}[/]"
1341
1368
  )
1342
1369
  status_line = f"[{SUCCESS_STYLE}]ready[/]"
1343
- status_line += " · transcript ready" if transcript_status["transcript_ready"] else " · transcript pending"
1370
+ if not transcript_status["has_transcript"]:
1371
+ status_line += " · no transcript"
1372
+ elif transcript_status["transcript_ready"]:
1373
+ status_line += " · transcript ready"
1374
+ else:
1375
+ status_line += " · transcript pending"
1344
1376
  header_grid.add_row(primary_line, status_line)
1345
1377
 
1346
1378
  if agent_info["description"]:
1347
- description = agent_info["description"]
1348
- if not transcript_status["transcript_ready"]:
1349
- description = f"{description} (transcript pending)"
1350
- header_grid.add_row(f"[dim]{description}[/dim]", "")
1379
+ header_grid.add_row(f"[dim]{agent_info['description']}[/dim]", "")
1351
1380
 
1352
1381
  return header_grid
1353
1382
 
@@ -684,6 +684,10 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
684
684
  return
685
685
  self.exit()
686
686
 
687
+ def action_app_exit(self) -> None:
688
+ """Exit the application regardless of focus state."""
689
+ self.exit()
690
+
687
691
  def on_button_pressed(self, event: Button.Pressed) -> None:
688
692
  """Handle filter bar buttons."""
689
693
  if event.button.id == "filter-clear":
@@ -23,6 +23,7 @@ except Exception: # pragma: no cover - optional dependency
23
23
  from glaip_sdk.cli.transcript.cache import suggest_filename
24
24
  from glaip_sdk.cli.utils import prompt_export_choice_questionary, questionary_safe_ask
25
25
  from glaip_sdk.utils.rendering.layout.progress import is_delegation_tool
26
+ from glaip_sdk.utils.rendering.layout.transcript import DEFAULT_TRANSCRIPT_THEME
26
27
  from glaip_sdk.utils.rendering.viewer import (
27
28
  ViewerContext as PresenterViewerContext,
28
29
  prepare_viewer_snapshot as presenter_prepare_viewer_snapshot,
@@ -93,8 +94,9 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
93
94
  if self._view_mode == "default":
94
95
  presenter_render_post_run_view(self.console, self.ctx)
95
96
  else:
96
- snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None)
97
- presenter_render_transcript_view(self.console, snapshot)
97
+ theme = DEFAULT_TRANSCRIPT_THEME
98
+ snapshot, state = presenter_prepare_viewer_snapshot(self.ctx, glyphs=None, theme=theme)
99
+ presenter_render_transcript_view(self.console, snapshot, theme=theme)
98
100
  presenter_render_transcript_events(self.console, state.events)
99
101
 
100
102
  # ------------------------------------------------------------------
glaip_sdk/client/tools.py CHANGED
@@ -562,12 +562,14 @@ class ToolClient(BaseClient):
562
562
  ) -> Tool:
563
563
  """Find tool by name and update, or create if not found."""
564
564
  existing = self.find_tools(name)
565
+ name_lower = name.lower()
566
+ exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
565
567
 
566
- if len(existing) == 1:
568
+ if len(exact_matches) == 1:
567
569
  logger.info("Updating existing tool: %s", name)
568
- return self._do_tool_upsert_update(existing[0].id, name, code, description, framework, **kwargs)
570
+ return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
569
571
 
570
- if len(existing) > 1:
572
+ if len(exact_matches) > 1:
571
573
  raise ValueError(f"Multiple tools found with name '{name}'")
572
574
 
573
575
  # Create new tool - code is required
@@ -160,12 +160,19 @@ class ToolRegistry(BaseRegistry["Tool"]):
160
160
  True if ref is a custom tool that needs uploading.
161
161
  """
162
162
  try:
163
- from glaip_sdk.utils.tool_detection import is_langchain_tool # noqa: PLC0415
163
+ from langchain_core.tools import BaseTool # noqa: PLC0415
164
164
 
165
- return is_langchain_tool(ref)
165
+ # LangChain BaseTool class
166
+ if isinstance(ref, type) and issubclass(ref, BaseTool):
167
+ return True
168
+
169
+ # LangChain BaseTool instance
170
+ if isinstance(ref, BaseTool):
171
+ return True
166
172
  except ImportError:
167
- # Handle case where langchain_core is not available or tool_detection import fails
168
- return False
173
+ pass
174
+
175
+ return False
169
176
 
170
177
  def resolve(self, ref: Any) -> Tool:
171
178
  """Resolve a tool reference to a platform Tool object.
@@ -304,119 +304,3 @@ def normalize_runtime_config_keys(
304
304
  logger.warning("Unknown field '%s' in runtime_config, ignoring", field)
305
305
 
306
306
  return result
307
-
308
-
309
- # =============================================================================
310
- # LOCAL MODE UTILITIES
311
- # =============================================================================
312
- # The functions below are for local execution mode where resources are NOT
313
- # deployed and have no UUIDs. They resolve keys to names (not IDs).
314
- # =============================================================================
315
-
316
-
317
- def _get_name_from_class(cls: type) -> str:
318
- """Extract name from a class, handling Pydantic models.
319
-
320
- Args:
321
- cls: The class to extract name from.
322
-
323
- Returns:
324
- The resolved name string.
325
- """
326
- # Try class-level name attribute first
327
- class_name = getattr(cls, "name", None)
328
- if class_name:
329
- return class_name
330
-
331
- # For Pydantic models, check model_fields for default value
332
- model_fields = getattr(cls, "model_fields", None)
333
- if model_fields and "name" in model_fields:
334
- field_info = model_fields["name"]
335
- default = getattr(field_info, "default", None)
336
- if default and isinstance(default, str):
337
- return default
338
-
339
- # Fallback to class __name__
340
- return cls.__name__
341
-
342
-
343
- def get_name_from_key(key: object) -> str | None:
344
- """Resolve config key to name for local mode (no registry needed).
345
-
346
- Supports instances, classes, and string names. UUID strings are not
347
- supported in local mode and return None with a warning.
348
-
349
- Args:
350
- key: Tool, MCP, or Agent instance/class/string.
351
-
352
- Returns:
353
- The resolved name string, or None if UUID (not applicable locally).
354
-
355
- Raises:
356
- ValueError: If the key cannot be resolved to a valid name.
357
- """
358
- # Instance with name attribute
359
- if hasattr(key, "name"):
360
- name = getattr(key, "name", None)
361
- if name:
362
- return name
363
- raise ValueError(f"Unable to resolve config key: {key!r}")
364
-
365
- # Class type (not instance)
366
- if isinstance(key, type):
367
- return _get_name_from_class(key)
368
-
369
- # String key
370
- if isinstance(key, str):
371
- if is_uuid(key):
372
- logger.warning("UUID '%s' not supported in local mode, skipping", key)
373
- return None
374
- return key
375
-
376
- raise ValueError(f"Unable to resolve config key: {key!r}")
377
-
378
-
379
- def normalize_local_config_keys(config: dict[object, object]) -> dict[str, object]:
380
- """Normalize all keys in a config dict to names for local mode.
381
-
382
- Converts instance/class/string keys to string names without using
383
- registry. UUID keys are skipped with a warning.
384
-
385
- Args:
386
- config: Dict with instance/class/string keys and any values.
387
-
388
- Returns:
389
- Dict with string name keys only. UUID keys are omitted.
390
- """
391
- if not config:
392
- return {}
393
-
394
- result: dict[str, object] = {}
395
- for key, value in config.items():
396
- name = get_name_from_key(key)
397
- if name is not None:
398
- result[name] = value
399
- return result
400
-
401
-
402
- def merge_configs(*configs: dict | None) -> dict:
403
- """Merge multiple config dicts with priority ordering.
404
-
405
- Later configs override earlier ones for the same key. None configs
406
- are skipped gracefully.
407
-
408
- Args:
409
- *configs: Config dicts in priority order (lowest priority first).
410
-
411
- Returns:
412
- Merged config dict with later values overriding earlier ones.
413
-
414
- Example:
415
- >>> merge_configs({"a": 1}, {"a": 2, "b": 3})
416
- {"a": 2, "b": 3}
417
- """
418
- result: dict = {}
419
- for config in configs:
420
- if config:
421
- result.update(config)
422
- return result
@@ -1,41 +1,27 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.6.5b9
3
+ Version: 0.6.6
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
7
7
  Author-email: raymond.christopher@gdplabs.id
8
- Requires-Python: >=3.11,<3.13
8
+ Requires-Python: >=3.11,<3.14
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
- Provides-Extra: dev
14
- Provides-Extra: memory
15
- Provides-Extra: privacy
16
- Requires-Dist: aip-agents-binary (==0.5.0b2)
17
- Requires-Dist: aip-agents[memory] (==0.5.0b2) ; (python_version >= "3.11" and python_version < "3.13") and (extra == "memory")
18
- Requires-Dist: aip-agents[privacy] (==0.5.0b2) ; (python_version >= "3.11" and python_version < "3.13") and (extra == "privacy")
19
13
  Requires-Dist: click (>=8.2.0,<8.3.0)
20
14
  Requires-Dist: gllm-core-binary (>=0.1.0)
21
15
  Requires-Dist: gllm-tools-binary (>=0.1.3)
22
16
  Requires-Dist: httpx (>=0.28.1)
23
17
  Requires-Dist: langchain-core (>=0.3.0)
24
18
  Requires-Dist: packaging (>=23.2)
25
- Requires-Dist: pre-commit (>=4.3.0) ; extra == "dev"
26
19
  Requires-Dist: pydantic (>=2.0.0)
27
- Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
28
- Requires-Dist: pytest-asyncio (>=0.23.6) ; extra == "dev"
29
- Requires-Dist: pytest-cov (>=4.0.0) ; extra == "dev"
30
- Requires-Dist: pytest-dotenv (>=0.5.2) ; extra == "dev"
31
- Requires-Dist: pytest-timeout (>=2.3.1) ; extra == "dev"
32
- Requires-Dist: pytest-xdist (>=3.8.0) ; extra == "dev"
33
20
  Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
34
21
  Requires-Dist: pyyaml (>=6.0.0)
35
22
  Requires-Dist: questionary (>=2.1.0,<3.0.0)
36
23
  Requires-Dist: readchar (>=4.2.1,<5.0.0)
37
24
  Requires-Dist: rich (>=13.0.0)
38
- Requires-Dist: ruff (>=0.14.0) ; extra == "dev"
39
25
  Requires-Dist: textual (>=0.52.0)
40
26
  Description-Content-Type: text/markdown
41
27