glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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.
Files changed (156) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +265 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +251 -173
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +735 -143
  14. glaip_sdk/cli/commands/mcps.py +266 -134
  15. glaip_sdk/cli/commands/models.py +13 -9
  16. glaip_sdk/cli/commands/tools.py +67 -88
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +3 -8
  19. glaip_sdk/cli/config.py +49 -7
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +8 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +45 -32
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +14 -17
  30. glaip_sdk/cli/main.py +232 -143
  31. glaip_sdk/cli/masking.py +21 -33
  32. glaip_sdk/cli/mcp_validators.py +5 -15
  33. glaip_sdk/cli/pager.py +12 -19
  34. glaip_sdk/cli/parsers/__init__.py +1 -3
  35. glaip_sdk/cli/parsers/json_input.py +11 -22
  36. glaip_sdk/cli/resolution.py +3 -9
  37. glaip_sdk/cli/rich_helpers.py +1 -3
  38. glaip_sdk/cli/slash/__init__.py +0 -9
  39. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +65 -29
  42. glaip_sdk/cli/slash/prompt.py +24 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +807 -225
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +12 -52
  52. glaip_sdk/cli/transcript/cache.py +258 -60
  53. glaip_sdk/cli/transcript/capture.py +72 -21
  54. glaip_sdk/cli/transcript/history.py +815 -0
  55. glaip_sdk/cli/transcript/launcher.py +1 -3
  56. glaip_sdk/cli/transcript/viewer.py +79 -499
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1308
  59. glaip_sdk/cli/validators.py +16 -18
  60. glaip_sdk/client/__init__.py +2 -1
  61. glaip_sdk/client/_agent_payloads.py +53 -37
  62. glaip_sdk/client/agent_runs.py +147 -0
  63. glaip_sdk/client/agents.py +320 -92
  64. glaip_sdk/client/base.py +78 -35
  65. glaip_sdk/client/main.py +19 -10
  66. glaip_sdk/client/mcps.py +123 -15
  67. glaip_sdk/client/run_rendering.py +136 -101
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +163 -34
  70. glaip_sdk/client/validators.py +20 -48
  71. glaip_sdk/config/constants.py +11 -0
  72. glaip_sdk/exceptions.py +1 -3
  73. glaip_sdk/mcps/__init__.py +21 -0
  74. glaip_sdk/mcps/base.py +345 -0
  75. glaip_sdk/models/__init__.py +90 -0
  76. glaip_sdk/models/agent.py +47 -0
  77. glaip_sdk/models/agent_runs.py +116 -0
  78. glaip_sdk/models/common.py +42 -0
  79. glaip_sdk/models/mcp.py +33 -0
  80. glaip_sdk/models/tool.py +33 -0
  81. glaip_sdk/payload_schemas/__init__.py +1 -13
  82. glaip_sdk/payload_schemas/agent.py +1 -3
  83. glaip_sdk/registry/__init__.py +55 -0
  84. glaip_sdk/registry/agent.py +164 -0
  85. glaip_sdk/registry/base.py +139 -0
  86. glaip_sdk/registry/mcp.py +253 -0
  87. glaip_sdk/registry/tool.py +232 -0
  88. glaip_sdk/rich_components.py +58 -2
  89. glaip_sdk/runner/__init__.py +59 -0
  90. glaip_sdk/runner/base.py +84 -0
  91. glaip_sdk/runner/deps.py +115 -0
  92. glaip_sdk/runner/langgraph.py +706 -0
  93. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  94. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  95. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  96. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  97. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  98. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  99. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  100. glaip_sdk/tools/__init__.py +22 -0
  101. glaip_sdk/tools/base.py +435 -0
  102. glaip_sdk/utils/__init__.py +58 -12
  103. glaip_sdk/utils/a2a/__init__.py +34 -0
  104. glaip_sdk/utils/a2a/event_processor.py +188 -0
  105. glaip_sdk/utils/agent_config.py +4 -14
  106. glaip_sdk/utils/bundler.py +267 -0
  107. glaip_sdk/utils/client.py +111 -0
  108. glaip_sdk/utils/client_utils.py +46 -28
  109. glaip_sdk/utils/datetime_helpers.py +58 -0
  110. glaip_sdk/utils/discovery.py +78 -0
  111. glaip_sdk/utils/display.py +25 -21
  112. glaip_sdk/utils/export.py +143 -0
  113. glaip_sdk/utils/general.py +1 -36
  114. glaip_sdk/utils/import_export.py +15 -16
  115. glaip_sdk/utils/import_resolver.py +492 -0
  116. glaip_sdk/utils/instructions.py +101 -0
  117. glaip_sdk/utils/rendering/__init__.py +115 -1
  118. glaip_sdk/utils/rendering/formatting.py +7 -35
  119. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  120. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  121. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  122. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  123. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  124. glaip_sdk/utils/rendering/models.py +3 -6
  125. glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
  126. glaip_sdk/utils/rendering/renderer/base.py +258 -1577
  127. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  128. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  129. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  130. glaip_sdk/utils/rendering/renderer/stream.py +10 -51
  131. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  132. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  133. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  134. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  135. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  136. glaip_sdk/utils/rendering/state.py +204 -0
  137. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  138. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  139. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
  140. glaip_sdk/utils/rendering/steps/format.py +176 -0
  141. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  142. glaip_sdk/utils/rendering/timing.py +36 -0
  143. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  144. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  145. glaip_sdk/utils/resource_refs.py +29 -26
  146. glaip_sdk/utils/runtime_config.py +425 -0
  147. glaip_sdk/utils/serialization.py +32 -46
  148. glaip_sdk/utils/sync.py +142 -0
  149. glaip_sdk/utils/tool_detection.py +33 -0
  150. glaip_sdk/utils/validation.py +20 -28
  151. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  152. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  153. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  154. glaip_sdk/models.py +0 -259
  155. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  156. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Agent run models for AIP SDK.
3
+
4
+ Authors:
5
+ Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ """
7
+
8
+ from datetime import datetime, timedelta
9
+ from typing import Any, Literal
10
+ from uuid import UUID
11
+
12
+ from pydantic import BaseModel, Field, field_validator, model_validator
13
+
14
+ # Type alias for SSE event dictionaries
15
+ RunOutputChunk = dict[str, Any]
16
+
17
+
18
+ class RunSummary(BaseModel):
19
+ """Represents a single agent run in list/table views with metadata only."""
20
+
21
+ id: UUID
22
+ agent_id: UUID
23
+ run_type: Literal["manual", "schedule"]
24
+ schedule_id: UUID | None = None
25
+ status: Literal["started", "success", "failed", "cancelled", "aborted", "unavailable"]
26
+ started_at: datetime
27
+ completed_at: datetime | None = None
28
+ input: str | None = None
29
+ config: dict[str, Any] | None = None
30
+ created_at: datetime
31
+ updated_at: datetime
32
+
33
+ @field_validator("completed_at")
34
+ @classmethod
35
+ def validate_completed_after_started(cls, v: datetime | None, info) -> datetime | None:
36
+ """Validate that completed_at is after started_at if present."""
37
+ if v is not None and "started_at" in info.data:
38
+ started_at = info.data["started_at"]
39
+ if v < started_at:
40
+ raise ValueError("completed_at must be after started_at")
41
+ return v
42
+
43
+ def duration(self) -> timedelta | None:
44
+ """Calculate duration from started_at to completed_at.
45
+
46
+ Returns:
47
+ Duration as timedelta if completed_at exists, None otherwise
48
+ """
49
+ if self.completed_at is not None:
50
+ return self.completed_at - self.started_at
51
+ return None
52
+
53
+ def duration_formatted(self) -> str:
54
+ """Format duration as HH:MM:SS string.
55
+
56
+ Returns:
57
+ Formatted duration string or "—" if not completed
58
+ """
59
+ duration = self.duration()
60
+ if duration is None:
61
+ return "—"
62
+ total_seconds = int(duration.total_seconds())
63
+ hours = total_seconds // 3600
64
+ minutes = (total_seconds % 3600) // 60
65
+ seconds = total_seconds % 60
66
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
67
+
68
+ def input_preview(self, max_length: int = 120) -> str:
69
+ """Generate truncated input preview for table display.
70
+
71
+ Args:
72
+ max_length: Maximum length of preview string
73
+
74
+ Returns:
75
+ Truncated input string or "—" if input is None or empty
76
+ """
77
+ if not self.input:
78
+ return "—"
79
+ # Strip newlines and collapse whitespace
80
+ preview = " ".join(self.input.split())
81
+ if len(preview) > max_length:
82
+ return preview[:max_length] + "…"
83
+ return preview
84
+
85
+
86
+ class RunsPage(BaseModel):
87
+ """Represents a paginated collection of run summaries from the list endpoint."""
88
+
89
+ data: list[RunSummary]
90
+ total: int = Field(ge=0)
91
+ page: int = Field(ge=1)
92
+ limit: int = Field(ge=1, le=100)
93
+ has_next: bool
94
+ has_prev: bool
95
+
96
+ @model_validator(mode="after")
97
+ def validate_pagination_consistency(self) -> "RunsPage":
98
+ """Validate pagination consistency."""
99
+ # If has_next is True, then page * limit < total
100
+ if self.has_next and self.page * self.limit >= self.total:
101
+ raise ValueError("has_next inconsistency: page * limit must be < total when has_next is True")
102
+ return self
103
+
104
+
105
+ class RunWithOutput(RunSummary):
106
+ """Extends RunSummary with the complete SSE event stream for detailed viewing."""
107
+
108
+ output: list[RunOutputChunk] = Field(default_factory=list)
109
+
110
+ @field_validator("output", mode="before")
111
+ @classmethod
112
+ def normalize_output(cls, v: Any) -> list[RunOutputChunk]:
113
+ """Normalize output field to empty list when null."""
114
+ if v is None:
115
+ return []
116
+ return v
@@ -0,0 +1,42 @@
1
+ """Common models for AIP SDK.
2
+
3
+ This module contains common models that don't fit into specific categories.
4
+
5
+ Authors:
6
+ Raymond Christopher (raymond.christopher@gdplabs.id)
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from pydantic import BaseModel
11
+
12
+
13
+ class LanguageModelResponse(BaseModel):
14
+ """Language model response model."""
15
+
16
+ name: str
17
+ provider: str
18
+ description: str | None = None
19
+ capabilities: list[str] | None = None
20
+ max_tokens: int | None = None
21
+ supports_streaming: bool = False
22
+
23
+
24
+ class TTYRenderer:
25
+ """Simple TTY renderer for non-Rich environments."""
26
+
27
+ def __init__(self, use_color: bool = True):
28
+ """Initialize the TTY renderer.
29
+
30
+ Args:
31
+ use_color: Whether to use color output
32
+ """
33
+ self.use_color = use_color
34
+
35
+ def render_message(self, message: str, event_type: str = "message") -> None:
36
+ """Render a message with optional color."""
37
+ if event_type == "error":
38
+ print(f"ERROR: {message}", flush=True)
39
+ elif event_type == "done":
40
+ print(f"\n✅ {message}", flush=True)
41
+ else:
42
+ print(message, flush=True)
@@ -0,0 +1,33 @@
1
+ """MCP response model for AIP SDK.
2
+
3
+ This module contains the Pydantic model for MCP API responses.
4
+ This is a pure data model with no runtime behavior.
5
+
6
+ For the runtime MCP class with update/delete methods, use glaip_sdk.mcps.MCP.
7
+
8
+ Authors:
9
+ Raymond Christopher (raymond.christopher@gdplabs.id)
10
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
11
+ """
12
+
13
+ from typing import Any
14
+
15
+ from pydantic import BaseModel
16
+
17
+
18
+ class MCPResponse(BaseModel):
19
+ """Pydantic model for MCP API responses.
20
+
21
+ This is a pure data model for deserializing API responses.
22
+ It does NOT have runtime methods (update, delete, get_tools).
23
+
24
+ For the runtime MCP class, use glaip_sdk.mcps.MCP.
25
+ """
26
+
27
+ id: str
28
+ name: str
29
+ description: str | None = None
30
+ config: dict[str, Any] | None = None
31
+ transport: str | None = None # "sse" or "http"
32
+ authentication: dict[str, Any] | None = None
33
+ metadata: dict[str, Any] | None = None
@@ -0,0 +1,33 @@
1
+ """Tool response model for AIP SDK.
2
+
3
+ This module contains the Pydantic model for Tool API responses.
4
+ This is a pure data model with no runtime behavior.
5
+
6
+ For the runtime Tool class with update/delete methods, use glaip_sdk.tools.Tool.
7
+
8
+ Authors:
9
+ Raymond Christopher (raymond.christopher@gdplabs.id)
10
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
11
+ """
12
+
13
+ from pydantic import BaseModel
14
+
15
+
16
+ class ToolResponse(BaseModel):
17
+ """Pydantic model for Tool API responses.
18
+
19
+ This is a pure data model for deserializing API responses.
20
+ It does NOT have runtime methods (update, delete, get_script).
21
+
22
+ For the runtime Tool class, use glaip_sdk.tools.Tool.
23
+ """
24
+
25
+ id: str
26
+ name: str
27
+ tool_type: str | None = None
28
+ description: str | None = None
29
+ framework: str | None = None
30
+ version: str | None = None
31
+ tool_script: str | None = None
32
+ tool_file: str | None = None
33
+ tags: str | list[str] | None = None
@@ -4,16 +4,4 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
- from glaip_sdk.payload_schemas.agent import (
8
- AgentImportOperation,
9
- ImportFieldPlan,
10
- get_import_field_plan,
11
- list_server_only_fields,
12
- )
13
-
14
- __all__ = [
15
- "AgentImportOperation",
16
- "ImportFieldPlan",
17
- "get_import_field_plan",
18
- "list_server_only_fields",
19
- ]
7
+ __all__: list[str] = []
@@ -60,9 +60,7 @@ AGENT_FIELD_RULES: Mapping[str, FieldRule] = {
60
60
  }
61
61
 
62
62
 
63
- def get_import_field_plan(
64
- field_name: str, operation: AgentImportOperation
65
- ) -> ImportFieldPlan:
63
+ def get_import_field_plan(field_name: str, operation: AgentImportOperation) -> ImportFieldPlan:
66
64
  """Return the import handling plan for ``field_name`` under ``operation``.
67
65
 
68
66
  Unknown fields default to being copied as-is so new API fields propagate
@@ -0,0 +1,55 @@
1
+ """Registry package for GL AIP platform.
2
+
3
+ This package provides registries that cache platform objects to avoid
4
+ redundant API calls when deploying multi-agent systems.
5
+
6
+ Example:
7
+ >>> from glaip_sdk.registry import get_agent_registry, get_tool_registry
8
+ >>> agent_registry = get_agent_registry()
9
+ >>> tool_registry = get_tool_registry()
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib
15
+ from typing import TYPE_CHECKING
16
+
17
+ from glaip_sdk.registry.base import BaseRegistry
18
+
19
+ # Lazy imports to avoid circular dependencies
20
+ if TYPE_CHECKING: # pragma: no cover
21
+ from glaip_sdk.registry.agent import AgentRegistry, get_agent_registry
22
+ from glaip_sdk.registry.mcp import MCPRegistry, get_mcp_registry
23
+ from glaip_sdk.registry.tool import ToolRegistry, get_tool_registry
24
+
25
+ __all__ = [
26
+ "BaseRegistry",
27
+ "AgentRegistry",
28
+ "get_agent_registry",
29
+ "ToolRegistry",
30
+ "get_tool_registry",
31
+ "MCPRegistry",
32
+ "get_mcp_registry",
33
+ ]
34
+
35
+
36
+ def __getattr__(name: str) -> type:
37
+ """Lazy import to avoid circular dependencies."""
38
+ _agent_module = "glaip_sdk.registry.agent"
39
+ _tool_module = "glaip_sdk.registry.tool"
40
+ _mcp_module = "glaip_sdk.registry.mcp"
41
+
42
+ lazy_imports = {
43
+ "AgentRegistry": _agent_module,
44
+ "get_agent_registry": _agent_module,
45
+ "ToolRegistry": _tool_module,
46
+ "get_tool_registry": _tool_module,
47
+ "MCPRegistry": _mcp_module,
48
+ "get_mcp_registry": _mcp_module,
49
+ }
50
+
51
+ if name in lazy_imports:
52
+ module = importlib.import_module(lazy_imports[name])
53
+ return getattr(module, name)
54
+
55
+ raise AttributeError(f"module 'glaip_sdk.registry' has no attribute '{name}'")
@@ -0,0 +1,164 @@
1
+ """Agent registry for glaip_sdk.
2
+
3
+ This module provides the AgentRegistry that caches deployed agents
4
+ to avoid redundant API calls when deploying multi-agent systems.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from glaip_sdk.registry.base import BaseRegistry
16
+
17
+ if TYPE_CHECKING:
18
+ from glaip_sdk.agents import Agent
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class AgentRegistry(BaseRegistry["Agent"]):
24
+ """Registry for agents.
25
+
26
+ Resolves agent references to glaip_sdk.models.Agent objects.
27
+ Caches results to avoid redundant API calls and duplicate deployments.
28
+
29
+ Handles:
30
+ - glaip_sdk.agents.Agent classes → deploy, cache, return Agent
31
+ - glaip_sdk.agents.Agent instances → deploy, cache, return Agent
32
+ - glaip_sdk.models.Agent → return as-is (uses agent.id)
33
+ - String names → lookup on platform, cache, return Agent
34
+
35
+ Attributes:
36
+ _cache: Internal cache mapping names to Agent objects.
37
+
38
+ Example:
39
+ >>> registry = get_agent_registry()
40
+ >>> agent = registry.resolve(GreeterAgent) # Returns deployed Agent
41
+ >>> print(agent.id) # "uuid-123"
42
+ >>> print(agent.name) # "greeter_agent"
43
+ """
44
+
45
+ def _extract_name(self, ref: Any) -> str:
46
+ """Extract agent name from a reference.
47
+
48
+ Args:
49
+ ref: An agent class, instance, or string name.
50
+
51
+ Returns:
52
+ The extracted agent name.
53
+
54
+ Raises:
55
+ ValueError: If name cannot be extracted from the reference.
56
+ """
57
+ # Lazy import to avoid circular dependency
58
+ from glaip_sdk.agents.base import Agent # noqa: PLC0415
59
+
60
+ # Agent class
61
+ if isinstance(ref, type) and issubclass(ref, Agent):
62
+ return ref().name
63
+
64
+ # Agent instance
65
+ if isinstance(ref, Agent):
66
+ return ref.name
67
+
68
+ # Already deployed agent (glaip_sdk.models.Agent)
69
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
70
+ return ref.name
71
+
72
+ # String name
73
+ if isinstance(ref, str):
74
+ return ref
75
+
76
+ raise ValueError(f"Cannot extract name from: {ref}")
77
+
78
+ def _resolve_and_cache(self, ref: Any, name: str) -> Agent:
79
+ """Resolve agent reference - deploy if class/instance, find if string.
80
+
81
+ Args:
82
+ ref: The agent reference to resolve.
83
+ name: The extracted agent name.
84
+
85
+ Returns:
86
+ The resolved glaip_sdk.models.Agent object.
87
+
88
+ Raises:
89
+ ValueError: If the agent cannot be resolved.
90
+ """
91
+ # Lazy imports to avoid circular dependency
92
+ from glaip_sdk.agents.base import Agent # noqa: PLC0415
93
+ from glaip_sdk.utils.discovery import find_agent # noqa: PLC0415
94
+
95
+ # Agent class
96
+ if isinstance(ref, type) and issubclass(ref, Agent):
97
+ logger.info("Deploying Agent class: %s", name)
98
+ deployed = ref().deploy()
99
+ self._cache[name] = deployed
100
+ return deployed
101
+
102
+ # Agent instance
103
+ if isinstance(ref, Agent):
104
+ logger.info("Deploying Agent instance: %s", name)
105
+ deployed = ref.deploy()
106
+ self._cache[name] = deployed
107
+ return deployed
108
+
109
+ # Already deployed agent (glaip_sdk.models.Agent) - just cache and return
110
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
111
+ logger.debug("Caching already deployed agent: %s", name)
112
+ self._cache[name] = ref
113
+ return ref
114
+
115
+ # String name - look up on platform
116
+ if isinstance(ref, str):
117
+ logger.info("Looking up agent by name: %s", name)
118
+ agent = find_agent(name)
119
+ if agent:
120
+ self._cache[name] = agent
121
+ return agent
122
+ raise ValueError(f"Agent not found on platform: {name}")
123
+
124
+ raise ValueError(f"Could not resolve agent reference: {ref}")
125
+
126
+
127
+ class _AgentRegistrySingleton:
128
+ """Singleton holder for AgentRegistry to avoid global statement."""
129
+
130
+ _instance: AgentRegistry | None = None
131
+
132
+ @classmethod
133
+ def get_instance(cls) -> AgentRegistry:
134
+ """Get or create the singleton instance.
135
+
136
+ Returns:
137
+ The global AgentRegistry instance.
138
+ """
139
+ if cls._instance is None:
140
+ cls._instance = AgentRegistry()
141
+ return cls._instance
142
+
143
+ @classmethod
144
+ def reset(cls) -> None:
145
+ """Reset the singleton instance (for testing)."""
146
+ cls._instance = None
147
+
148
+
149
+ def get_agent_registry() -> AgentRegistry:
150
+ """Get the singleton AgentRegistry instance.
151
+
152
+ Returns a global AgentRegistry that caches agents across the session.
153
+ Use this function to get the registry instead of creating instances directly.
154
+
155
+ Returns:
156
+ The global AgentRegistry instance.
157
+
158
+ Example:
159
+ >>> from glaip_sdk.registry import get_agent_registry
160
+ >>> registry = get_agent_registry()
161
+ >>> agent = registry.resolve("weather_agent")
162
+ >>> print(agent.name)
163
+ """
164
+ return _AgentRegistrySingleton.get_instance()
@@ -0,0 +1,139 @@
1
+ """Abstract base registry for caching platform objects.
2
+
3
+ This module provides the BaseRegistry abstract class that serves as the
4
+ foundation for type-specific registries (AgentRegistry, ToolRegistry, MCPRegistry).
5
+
6
+ The registry pattern provides:
7
+ - In-memory caching to avoid redundant API calls
8
+ - Transparent resolution of various reference types
9
+ - Simple invalidation and cache management
10
+
11
+ Authors:
12
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from abc import ABC, abstractmethod
19
+ from typing import Any, Generic, TypeVar
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class BaseRegistry(ABC, Generic[T]):
28
+ """Abstract base registry for caching platform objects.
29
+
30
+ Provides a caching layer between local code and the AIP platform.
31
+ Subclasses implement type-specific resolution logic.
32
+
33
+ The registry follows a simple flow:
34
+ 1. Check if reference is already a platform object → return as-is
35
+ 2. Extract name from reference
36
+ 3. Check cache → return if found
37
+ 4. Resolve via subclass logic → cache and return
38
+
39
+ Attributes:
40
+ _cache: Internal cache mapping names to objects.
41
+
42
+ Example:
43
+ >>> class MyRegistry(BaseRegistry):
44
+ ... def _extract_name(self, ref: Any) -> str:
45
+ ... return ref.name if hasattr(ref, 'name') else str(ref)
46
+ ...
47
+ ... def _resolve_and_cache(self, ref: Any, name: str) -> MyType:
48
+ ... obj = fetch_from_platform(name)
49
+ ... self._cache[name] = obj
50
+ ... return obj
51
+ """
52
+
53
+ def __init__(self) -> None:
54
+ """Initialize the registry with an empty cache."""
55
+ self._cache: dict[str, T] = {}
56
+
57
+ def resolve(self, ref: Any) -> T:
58
+ """Resolve a reference to a platform object.
59
+
60
+ This is the main entry point for the registry. It handles:
61
+ - Cached references (returned from cache)
62
+ - New references (resolved via subclass, then cached)
63
+
64
+ Args:
65
+ ref: A reference to resolve. Can be a class, string name,
66
+ or platform object depending on the registry type.
67
+
68
+ Returns:
69
+ The resolved platform object.
70
+
71
+ Raises:
72
+ ValueError: If the reference cannot be resolved.
73
+ """
74
+ name = self._extract_name(ref)
75
+
76
+ if name in self._cache:
77
+ logger.debug("Cache hit: %s", name)
78
+ return self._cache[name]
79
+
80
+ return self._resolve_and_cache(ref, name)
81
+
82
+ def get(self, name: str) -> T | None:
83
+ """Get a cached object by name.
84
+
85
+ Args:
86
+ name: The name of the object to retrieve.
87
+
88
+ Returns:
89
+ The cached object, or None if not found.
90
+ """
91
+ return self._cache.get(name)
92
+
93
+ def invalidate(self, name: str) -> None:
94
+ """Remove an object from the cache.
95
+
96
+ Use this to force a re-fetch on the next resolve call.
97
+
98
+ Args:
99
+ name: The name of the object to invalidate.
100
+ """
101
+ self._cache.pop(name, None)
102
+ logger.debug("Invalidated cache entry: %s", name)
103
+
104
+ def clear(self) -> None:
105
+ """Clear all cached entries."""
106
+ self._cache.clear()
107
+ logger.debug("Cleared registry cache")
108
+
109
+ @abstractmethod
110
+ def _extract_name(self, ref: Any) -> str:
111
+ """Extract the name from a reference.
112
+
113
+ Args:
114
+ ref: The reference to extract a name from.
115
+
116
+ Returns:
117
+ The extracted name string.
118
+
119
+ Raises:
120
+ ValueError: If name cannot be extracted.
121
+ """
122
+
123
+ @abstractmethod
124
+ def _resolve_and_cache(self, ref: Any, name: str) -> T:
125
+ """Resolve the reference and cache the result.
126
+
127
+ Subclasses implement type-specific resolution logic here.
128
+ This method MUST cache the result in self._cache[name].
129
+
130
+ Args:
131
+ ref: The reference to resolve.
132
+ name: The extracted name for caching.
133
+
134
+ Returns:
135
+ The resolved platform object.
136
+
137
+ Raises:
138
+ ValueError: If resolution fails.
139
+ """