glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/__init__.py +42 -5
- glaip_sdk/agents/base.py +156 -32
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +2 -3
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +112 -32
- glaip_sdk/cli/slash/agent_session.py +5 -2
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
- glaip_sdk/cli/slash/session.py +58 -13
- glaip_sdk/cli/slash/tui/__init__.py +26 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -0
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +50 -8
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -1
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +367 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +17 -0
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/registry/tool.py +273 -59
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +317 -42
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +44 -11
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +43 -11
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +15 -12
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +47 -37
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
- glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
glaip_sdk/models/__init__.py
CHANGED
|
@@ -27,6 +27,16 @@ from glaip_sdk.models.agent_runs import (
|
|
|
27
27
|
)
|
|
28
28
|
from glaip_sdk.models.common import LanguageModelResponse, TTYRenderer
|
|
29
29
|
from glaip_sdk.models.mcp import MCPResponse
|
|
30
|
+
|
|
31
|
+
# Export schedule models
|
|
32
|
+
from glaip_sdk.models.schedule import ( # noqa: F401
|
|
33
|
+
ScheduleConfig,
|
|
34
|
+
ScheduleMetadata,
|
|
35
|
+
ScheduleResponse,
|
|
36
|
+
ScheduleRunOutputChunk,
|
|
37
|
+
ScheduleRunResponse,
|
|
38
|
+
ScheduleRunResult,
|
|
39
|
+
)
|
|
30
40
|
from glaip_sdk.models.tool import ToolResponse
|
|
31
41
|
|
|
32
42
|
|
|
@@ -87,4 +97,11 @@ __all__ = [
|
|
|
87
97
|
"RunsPage",
|
|
88
98
|
"RunWithOutput",
|
|
89
99
|
"RunOutputChunk",
|
|
100
|
+
# Schedule models
|
|
101
|
+
"ScheduleConfig",
|
|
102
|
+
"ScheduleMetadata",
|
|
103
|
+
"ScheduleResponse",
|
|
104
|
+
"ScheduleRunResponse",
|
|
105
|
+
"ScheduleRunOutputChunk",
|
|
106
|
+
"ScheduleRunResult",
|
|
90
107
|
]
|
glaip_sdk/models/agent_runs.py
CHANGED
|
@@ -13,6 +13,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
|
|
|
13
13
|
|
|
14
14
|
# Type alias for SSE event dictionaries
|
|
15
15
|
RunOutputChunk = dict[str, Any]
|
|
16
|
+
RunStatus = Literal["started", "success", "failed", "cancelled", "aborted", "unavailable"]
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class RunSummary(BaseModel):
|
|
@@ -22,7 +23,7 @@ class RunSummary(BaseModel):
|
|
|
22
23
|
agent_id: UUID
|
|
23
24
|
run_type: Literal["manual", "schedule"]
|
|
24
25
|
schedule_id: UUID | None = None
|
|
25
|
-
status:
|
|
26
|
+
status: RunStatus
|
|
26
27
|
started_at: datetime
|
|
27
28
|
completed_at: datetime | None = None
|
|
28
29
|
input: str | None = None
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Schedule DTO models for AIP SDK.
|
|
3
|
+
|
|
4
|
+
These models represent API payloads and responses. They are intentionally DTO-only
|
|
5
|
+
and do not contain runtime behavior.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict
|
|
16
|
+
|
|
17
|
+
from glaip_sdk.models.agent_runs import RunStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ScheduleConfig(BaseModel):
|
|
21
|
+
"""Cron-like schedule configuration matching backend ScheduleConfig.
|
|
22
|
+
|
|
23
|
+
All fields accept cron-style values:
|
|
24
|
+
- Specific values: "0", "9", "1"
|
|
25
|
+
- Wildcards: "*"
|
|
26
|
+
- Intervals: "*/5", "*/2"
|
|
27
|
+
- Ranges: "1-5", "9-17"
|
|
28
|
+
- Lists: "1,3,5"
|
|
29
|
+
|
|
30
|
+
Note: day_of_week uses 0-6 where 0=Monday.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
minute: str = "*"
|
|
34
|
+
hour: str = "*"
|
|
35
|
+
day_of_month: str = "*"
|
|
36
|
+
month: str = "*"
|
|
37
|
+
day_of_week: str = "*"
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(from_attributes=True)
|
|
40
|
+
|
|
41
|
+
def to_cron_string(self) -> str:
|
|
42
|
+
"""Convert to standard cron string format.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Cron string in format "minute hour day_of_month month day_of_week"
|
|
46
|
+
"""
|
|
47
|
+
return f"{self.minute} {self.hour} {self.day_of_month} {self.month} {self.day_of_week}"
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_cron_string(cls, cron: str) -> "ScheduleConfig":
|
|
51
|
+
"""Parse a cron string into ScheduleConfig.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
cron: Cron string in format "minute hour day_of_month month day_of_week"
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
ScheduleConfig instance
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If cron string doesn't have exactly 5 fields
|
|
61
|
+
"""
|
|
62
|
+
parts = cron.split()
|
|
63
|
+
if len(parts) != 5:
|
|
64
|
+
raise ValueError(f"Invalid cron string: expected 5 fields, got {len(parts)}")
|
|
65
|
+
return cls(
|
|
66
|
+
minute=parts[0],
|
|
67
|
+
hour=parts[1],
|
|
68
|
+
day_of_month=parts[2],
|
|
69
|
+
month=parts[3],
|
|
70
|
+
day_of_week=parts[4],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ScheduleMetadata(BaseModel):
|
|
75
|
+
"""Metadata embedded in schedule responses.
|
|
76
|
+
|
|
77
|
+
Contains the agent association, input text, and cron configuration.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
agent_id: str
|
|
81
|
+
input: str
|
|
82
|
+
schedule: ScheduleConfig
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(from_attributes=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ScheduleResponse(BaseModel):
|
|
88
|
+
"""Schedule response DTO.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
id: Schedule ID.
|
|
92
|
+
next_run_time: Next run time as returned by the API.
|
|
93
|
+
time_until_next_run: Human-readable duration until next run.
|
|
94
|
+
metadata: Schedule metadata.
|
|
95
|
+
created_at: Creation timestamp.
|
|
96
|
+
updated_at: Update timestamp.
|
|
97
|
+
agent_id: Agent ID derived from metadata.
|
|
98
|
+
input: Input text derived from metadata.
|
|
99
|
+
schedule_config: ScheduleConfig derived from metadata.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
id: str
|
|
103
|
+
next_run_time: str | None = None
|
|
104
|
+
time_until_next_run: str | None = None
|
|
105
|
+
metadata: ScheduleMetadata | None = None
|
|
106
|
+
created_at: datetime | None = None
|
|
107
|
+
updated_at: datetime | None = None
|
|
108
|
+
|
|
109
|
+
model_config = ConfigDict(from_attributes=True)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def agent_id(self) -> str | None:
|
|
113
|
+
"""Get the agent ID from metadata."""
|
|
114
|
+
return self.metadata.agent_id if self.metadata else None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def input(self) -> str | None:
|
|
118
|
+
"""Get the scheduled input text from metadata."""
|
|
119
|
+
return self.metadata.input if self.metadata else None
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def schedule_config(self) -> ScheduleConfig | None:
|
|
123
|
+
"""Get the schedule configuration from metadata."""
|
|
124
|
+
return self.metadata.schedule if self.metadata else None
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
"""Return a readable representation of the schedule."""
|
|
128
|
+
parts = [f"ScheduleResponse(id={self.id!r}"]
|
|
129
|
+
if self.next_run_time:
|
|
130
|
+
parts.append(f"next_run_time={self.next_run_time!r}")
|
|
131
|
+
if self.time_until_next_run:
|
|
132
|
+
parts.append(f"time_until_next_run={self.time_until_next_run!r}")
|
|
133
|
+
if self.agent_id:
|
|
134
|
+
parts.append(f"agent_id={self.agent_id!r}")
|
|
135
|
+
if self.created_at:
|
|
136
|
+
parts.append(f"created_at={self.created_at!r}")
|
|
137
|
+
return ", ".join(parts) + ")"
|
|
138
|
+
|
|
139
|
+
def __str__(self) -> str:
|
|
140
|
+
"""Return a readable string representation."""
|
|
141
|
+
return self.__repr__()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Type alias for SSE event dictionaries
|
|
145
|
+
ScheduleRunOutputChunk = dict[str, Any]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ScheduleRunResponse(BaseModel):
|
|
149
|
+
"""Schedule run response DTO."""
|
|
150
|
+
|
|
151
|
+
id: str
|
|
152
|
+
agent_id: str
|
|
153
|
+
schedule_id: str | None = None # May be None for non-scheduled runs
|
|
154
|
+
status: RunStatus # Backend uses lowercase.
|
|
155
|
+
run_type: str | None = None # "schedule" for scheduled runs
|
|
156
|
+
started_at: datetime | None = None
|
|
157
|
+
completed_at: datetime | None = None
|
|
158
|
+
input: str | None = None # Input used for the execution.
|
|
159
|
+
config: ScheduleConfig | dict[str, str] | None = None # Schedule config used.
|
|
160
|
+
created_at: datetime | None = None
|
|
161
|
+
updated_at: datetime | None = None
|
|
162
|
+
|
|
163
|
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def duration(self) -> str | None:
|
|
167
|
+
"""Calculate the duration of the run.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Formatted duration string (HH:MM:SS) or None if incomplete
|
|
171
|
+
"""
|
|
172
|
+
if self.started_at and self.completed_at:
|
|
173
|
+
delta = self.completed_at - self.started_at
|
|
174
|
+
total_seconds = int(delta.total_seconds())
|
|
175
|
+
hours, remainder = divmod(total_seconds, 3600)
|
|
176
|
+
minutes, seconds = divmod(remainder, 60)
|
|
177
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def __repr__(self) -> str:
|
|
181
|
+
"""Return a readable representation of the run."""
|
|
182
|
+
parts = [f"ScheduleRunResponse(id={self.id!r}"]
|
|
183
|
+
parts.append(f"status={self.status!r}")
|
|
184
|
+
if self.started_at:
|
|
185
|
+
parts.append(f"started_at={self.started_at.isoformat()!r}")
|
|
186
|
+
if self.duration:
|
|
187
|
+
parts.append(f"duration={self.duration!r}")
|
|
188
|
+
return ", ".join(parts) + ")"
|
|
189
|
+
|
|
190
|
+
def __str__(self) -> str:
|
|
191
|
+
"""Return a readable string representation."""
|
|
192
|
+
return self.__repr__()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ScheduleRunResult(BaseModel):
|
|
196
|
+
"""Full output payload for a schedule run.
|
|
197
|
+
|
|
198
|
+
Maps to the backend's AgentRunWithOutputResponse which includes
|
|
199
|
+
run metadata plus the output stream.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
id: str
|
|
203
|
+
agent_id: str
|
|
204
|
+
schedule_id: str | None = None
|
|
205
|
+
status: RunStatus
|
|
206
|
+
run_type: str | None = None
|
|
207
|
+
started_at: datetime | None = None
|
|
208
|
+
completed_at: datetime | None = None
|
|
209
|
+
input: str | None = None # Input used for the execution.
|
|
210
|
+
config: ScheduleConfig | dict[str, str] | None = None # Schedule config used.
|
|
211
|
+
output: list[ScheduleRunOutputChunk] | None = None
|
|
212
|
+
created_at: datetime | None = None
|
|
213
|
+
updated_at: datetime | None = None
|
|
214
|
+
|
|
215
|
+
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
216
|
+
|
|
217
|
+
def __repr__(self) -> str:
|
|
218
|
+
"""Return a readable representation of the result."""
|
|
219
|
+
output_count = len(self.output) if self.output else 0
|
|
220
|
+
return f"ScheduleRunResult(id={self.id!r}, status={self.status!r}, output_chunks={output_count})"
|
|
221
|
+
|
|
222
|
+
def __str__(self) -> str:
|
|
223
|
+
"""Return a readable string representation."""
|
|
224
|
+
return self.__repr__()
|
glaip_sdk/registry/tool.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tool registry for glaip_sdk.
|
|
2
2
|
|
|
3
|
-
This module provides
|
|
3
|
+
This module provides a ToolRegistry that caches deployed tools
|
|
4
4
|
to avoid redundant API calls when deploying agents with tools.
|
|
5
5
|
|
|
6
6
|
Authors:
|
|
@@ -54,6 +54,32 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
54
54
|
value = getattr(obj, attr, None)
|
|
55
55
|
return value if isinstance(value, str) else None
|
|
56
56
|
|
|
57
|
+
def _extract_name_from_instance(self, ref: Any) -> str | None:
|
|
58
|
+
"""Extract name from a non-type instance.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
ref: The instance to extract name from.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The extracted name, or None if not found.
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(ref, type):
|
|
67
|
+
return None
|
|
68
|
+
return self._get_string_attr(ref, "name")
|
|
69
|
+
|
|
70
|
+
def _extract_name_from_class(self, ref: Any) -> str | None:
|
|
71
|
+
"""Extract name from a class.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
ref: The class to extract name from.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The extracted name, or None if not found.
|
|
78
|
+
"""
|
|
79
|
+
if not isinstance(ref, type):
|
|
80
|
+
return None
|
|
81
|
+
return self._get_string_attr(ref, "name") or self._get_name_from_model_fields(ref)
|
|
82
|
+
|
|
57
83
|
def _extract_name(self, ref: Any) -> str:
|
|
58
84
|
"""Extract tool name from a reference.
|
|
59
85
|
|
|
@@ -66,90 +92,269 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
66
92
|
Raises:
|
|
67
93
|
ValueError: If name cannot be extracted from the reference.
|
|
68
94
|
"""
|
|
95
|
+
# Lazy import to avoid circular dependency: Tool -> ToolRegistry -> Tool
|
|
96
|
+
from glaip_sdk.tools.base import Tool # noqa: PLC0415
|
|
97
|
+
|
|
69
98
|
if isinstance(ref, str):
|
|
70
99
|
return ref
|
|
71
100
|
|
|
101
|
+
# Tool instance (from Tool.from_langchain() or Tool.from_native())
|
|
102
|
+
if isinstance(ref, Tool):
|
|
103
|
+
return ref.get_name()
|
|
104
|
+
|
|
72
105
|
# Dict from API response - extract name or id
|
|
73
106
|
if isinstance(ref, dict):
|
|
74
107
|
return ref.get("name") or ref.get("id") or ""
|
|
75
108
|
|
|
76
109
|
# Tool instance (not a class) with name attribute
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return name
|
|
110
|
+
name = self._extract_name_from_instance(ref)
|
|
111
|
+
if name:
|
|
112
|
+
return name
|
|
81
113
|
|
|
82
114
|
# Tool class - try direct attribute first, then model_fields
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return name
|
|
115
|
+
name = self._extract_name_from_class(ref)
|
|
116
|
+
if name:
|
|
117
|
+
return name
|
|
87
118
|
|
|
88
119
|
raise ValueError(f"Cannot extract name from: {ref}")
|
|
89
120
|
|
|
90
|
-
def
|
|
91
|
-
"""
|
|
121
|
+
def _cache_tool(self, tool: Tool, name: str) -> None:
|
|
122
|
+
"""Cache a tool by name and ID if available.
|
|
92
123
|
|
|
93
124
|
Args:
|
|
94
|
-
|
|
95
|
-
name: The
|
|
125
|
+
tool: The tool to cache.
|
|
126
|
+
name: The tool name.
|
|
127
|
+
"""
|
|
128
|
+
self._cache[name] = tool
|
|
129
|
+
if hasattr(tool, "id") and tool.id:
|
|
130
|
+
self._cache[tool.id] = tool
|
|
131
|
+
|
|
132
|
+
def _resolve_native_platform_tool(self, name: str, tool_class: type | None = None) -> Tool:
|
|
133
|
+
"""Find a native tool on the platform and cache it.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: The tool name to look up.
|
|
137
|
+
tool_class: Optional local implementation to preserve.
|
|
96
138
|
|
|
97
139
|
Returns:
|
|
98
|
-
The resolved
|
|
140
|
+
The resolved Tool object.
|
|
99
141
|
|
|
100
142
|
Raises:
|
|
101
|
-
ValueError: If the tool
|
|
143
|
+
ValueError: If the tool is not found on the platform.
|
|
102
144
|
"""
|
|
103
|
-
# Lazy imports to avoid circular dependency
|
|
104
145
|
from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
|
|
146
|
+
|
|
147
|
+
logger.info("Looking up native tool: %s", name)
|
|
148
|
+
tool = find_tool(name)
|
|
149
|
+
if tool:
|
|
150
|
+
# Preserve local implementation if provided
|
|
151
|
+
if tool_class:
|
|
152
|
+
tool.tool_class = tool_class
|
|
153
|
+
self._cache_tool(tool, name)
|
|
154
|
+
return tool
|
|
155
|
+
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Native tool '{name}' not found on platform. Ensure the tool is deployed or check for name mismatches."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _resolve_tool_instance(self, ref: Any, name: str) -> Tool | None:
|
|
161
|
+
"""Resolve a ToolClass instance.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
ref: The ToolClass instance to resolve.
|
|
165
|
+
name: The extracted tool name.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The resolved tool, or None if not a ToolClass instance.
|
|
169
|
+
"""
|
|
170
|
+
# Lazy imports to avoid circular dependency: Tool -> ToolRegistry -> Tool
|
|
171
|
+
from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
|
|
172
|
+
from glaip_sdk.tools.base import ToolType # noqa: PLC0415
|
|
173
|
+
|
|
174
|
+
# Use try/except to handle mocked Tool class in tests
|
|
175
|
+
try:
|
|
176
|
+
is_tool_instance = isinstance(ref, ToolClass)
|
|
177
|
+
except TypeError:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
if not is_tool_instance:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
# If Tool has an ID, it's already deployed - return as-is
|
|
184
|
+
if ref.id is not None:
|
|
185
|
+
logger.debug("Caching already deployed tool: %s", name)
|
|
186
|
+
self._cache_tool(ref, name)
|
|
187
|
+
return ref
|
|
188
|
+
|
|
189
|
+
# Tool.from_native() - look up on platform
|
|
190
|
+
if ref.tool_type == ToolType.NATIVE:
|
|
191
|
+
return self._resolve_native_platform_tool(name, tool_class=getattr(ref, "tool_class", None))
|
|
192
|
+
|
|
193
|
+
# Tool.from_langchain() - resolve the inner tool_class (promoted or uploaded)
|
|
194
|
+
if ref.tool_class is not None:
|
|
195
|
+
return self._resolve_custom_tool(ref.tool_class, name)
|
|
196
|
+
|
|
197
|
+
# Unresolvable Tool instance - neither native nor has tool_class
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"Cannot resolve Tool instance: {ref}. "
|
|
200
|
+
f"Tool has no id, is not NATIVE type, and has no tool_class. "
|
|
201
|
+
f"Ensure Tool is created via Tool.from_native() or Tool.from_langchain()."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _resolve_deployed_tool(self, ref: Any, name: str) -> Tool | None:
|
|
205
|
+
"""Resolve an already deployed tool (has id/name attributes).
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
ref: The tool reference to resolve.
|
|
209
|
+
name: The extracted tool name.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
The resolved tool, or None if not a deployed tool.
|
|
213
|
+
"""
|
|
214
|
+
# Already deployed tool (not a ToolClass, but has id/name)
|
|
215
|
+
# This handles API response objects and backward compatibility
|
|
216
|
+
if not (hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type)):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
if ref.id is not None:
|
|
220
|
+
logger.debug("Caching already deployed tool: %s", name)
|
|
221
|
+
# Use _cache_tool to cache by both name and ID for consistency
|
|
222
|
+
self._cache_tool(ref, name)
|
|
223
|
+
return ref
|
|
224
|
+
|
|
225
|
+
# Tool without ID (backward compatibility) - look up on platform
|
|
226
|
+
return self._resolve_native_platform_tool(name)
|
|
227
|
+
|
|
228
|
+
def _resolve_custom_tool(self, ref: Any, name: str) -> Tool | None:
|
|
229
|
+
"""Resolve a custom tool class, promoting aip_agents.tools classes to NATIVE.
|
|
230
|
+
|
|
231
|
+
This method handles two main paths:
|
|
232
|
+
1. **Promotion**: If the tool class is from `aip_agents.tools`, it is automatically
|
|
233
|
+
promoted to a `NATIVE` tool type. It then performs a platform lookup to link it
|
|
234
|
+
with the deployed native tool while preserving the local `tool_class` for local execution.
|
|
235
|
+
2. **Upload**: If it is a standard LangChain tool, it is uploaded to the platform
|
|
236
|
+
as a custom tool.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
ref: The tool reference (usually a class) to resolve.
|
|
240
|
+
name: The extracted tool name.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
The resolved tool, or None if not a custom tool.
|
|
244
|
+
"""
|
|
245
|
+
# aip_agents tools are automatically promoted to NATIVE
|
|
246
|
+
if self._is_aip_agents_tool(ref):
|
|
247
|
+
from glaip_sdk.utils.tool_detection import get_tool_name # noqa: PLC0415
|
|
248
|
+
|
|
249
|
+
# Get name from class attribute or field
|
|
250
|
+
tool_name = get_tool_name(ref)
|
|
251
|
+
if tool_name is None:
|
|
252
|
+
raise ValueError(f"Tool class {ref.__name__} has no 'name' attribute")
|
|
253
|
+
|
|
254
|
+
return self._resolve_native_platform_tool(tool_name, tool_class=ref)
|
|
255
|
+
|
|
256
|
+
if not self._is_custom_tool(ref):
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Regular custom tools - upload to platform
|
|
105
260
|
from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
|
|
106
261
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
262
|
+
logger.info("Uploading custom tool: %s", name)
|
|
263
|
+
tool = update_or_create_tool(ref)
|
|
264
|
+
|
|
265
|
+
# Cache the resolved tool
|
|
266
|
+
self._cache_tool(tool, name)
|
|
267
|
+
if hasattr(tool, "id") and tool.id:
|
|
268
|
+
self._cache[tool.id] = tool
|
|
113
269
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
270
|
+
return tool
|
|
271
|
+
|
|
272
|
+
def _resolve_dict_tool(self, ref: Any, name: str) -> Tool | None:
|
|
273
|
+
"""Resolve a tool from a dict (API response).
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
ref: The dict to resolve.
|
|
277
|
+
name: The extracted tool name.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The resolved tool, or None if not a dict.
|
|
281
|
+
"""
|
|
282
|
+
# Lazy imports to avoid circular dependency
|
|
283
|
+
from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
|
|
284
|
+
|
|
285
|
+
if not isinstance(ref, dict):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
tool_id = ref.get("id")
|
|
289
|
+
if tool_id:
|
|
290
|
+
tool = ToolClass(id=tool_id, name=ref.get("name", ""))
|
|
291
|
+
# Use _cache_tool to cache by both name and ID for consistency
|
|
292
|
+
self._cache_tool(tool, name)
|
|
129
293
|
return tool
|
|
294
|
+
raise ValueError(f"Tool dict missing 'id': {ref}")
|
|
130
295
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
tool_id = ref.get("id")
|
|
134
|
-
if tool_id:
|
|
135
|
-
from glaip_sdk.tools.base import Tool # noqa: PLC0415
|
|
296
|
+
def _resolve_string_tool(self, ref: Any, name: str) -> Tool | None:
|
|
297
|
+
"""Resolve a tool from a string name.
|
|
136
298
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
raise ValueError(f"Tool dict missing 'id': {ref}")
|
|
299
|
+
Args:
|
|
300
|
+
ref: The string to resolve.
|
|
301
|
+
name: The extracted tool name.
|
|
141
302
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
303
|
+
Returns:
|
|
304
|
+
The resolved tool, or None if not a string.
|
|
305
|
+
"""
|
|
306
|
+
if not isinstance(ref, str):
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
return self._resolve_native_platform_tool(name)
|
|
310
|
+
|
|
311
|
+
def _resolve_and_cache(self, ref: Any, name: str) -> Tool:
|
|
312
|
+
"""Resolve tool reference - upload if class, find if string/native.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
ref: The tool reference to resolve.
|
|
316
|
+
name: The extracted tool name.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The resolved glaip_sdk.models.Tool object.
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
ValueError: If tool cannot be resolved.
|
|
323
|
+
"""
|
|
324
|
+
# Try each resolution strategy in order
|
|
325
|
+
resolvers = [
|
|
326
|
+
self._resolve_tool_instance,
|
|
327
|
+
self._resolve_deployed_tool,
|
|
328
|
+
self._resolve_custom_tool,
|
|
329
|
+
self._resolve_dict_tool,
|
|
330
|
+
self._resolve_string_tool,
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
for resolver in resolvers:
|
|
334
|
+
result = resolver(ref, name)
|
|
335
|
+
if result is not None:
|
|
336
|
+
return result
|
|
150
337
|
|
|
151
338
|
raise ValueError(f"Could not resolve tool reference: {ref}")
|
|
152
339
|
|
|
340
|
+
def _is_aip_agents_tool(self, ref: Any) -> bool:
|
|
341
|
+
"""Check if reference is an aip-agents tool.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
ref: The reference to check.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if ref is from aip_agents.tools package.
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
351
|
+
is_aip_agents_tool,
|
|
352
|
+
)
|
|
353
|
+
except ImportError:
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
return is_aip_agents_tool(ref)
|
|
357
|
+
|
|
153
358
|
def _is_custom_tool(self, ref: Any) -> bool:
|
|
154
359
|
"""Check if reference is a custom tool class/instance.
|
|
155
360
|
|
|
@@ -160,13 +365,22 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
160
365
|
True if ref is a custom tool that needs uploading.
|
|
161
366
|
"""
|
|
162
367
|
try:
|
|
163
|
-
from glaip_sdk.utils.tool_detection import
|
|
368
|
+
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
369
|
+
is_langchain_tool,
|
|
370
|
+
)
|
|
164
371
|
|
|
165
|
-
|
|
372
|
+
is_tool = is_langchain_tool(ref)
|
|
166
373
|
except ImportError:
|
|
167
|
-
|
|
374
|
+
is_tool = hasattr(ref, "args_schema") or hasattr(ref, "_run")
|
|
375
|
+
if is_tool:
|
|
376
|
+
logger.warning("tool_detection module missing; identifying tool via fallback attributes.")
|
|
377
|
+
|
|
378
|
+
# aip_agents tools are NOT custom - they're native
|
|
379
|
+
if is_tool and self._is_aip_agents_tool(ref):
|
|
168
380
|
return False
|
|
169
381
|
|
|
382
|
+
return is_tool
|
|
383
|
+
|
|
170
384
|
def resolve(self, ref: Any) -> Tool:
|
|
171
385
|
"""Resolve a tool reference to a platform Tool object.
|
|
172
386
|
|
|
@@ -184,9 +398,9 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
184
398
|
if ref.id is not None:
|
|
185
399
|
name = self._extract_name(ref)
|
|
186
400
|
if name not in self._cache:
|
|
187
|
-
|
|
401
|
+
# Use _cache_tool to cache by both name and ID for consistency
|
|
402
|
+
self._cache_tool(ref, name)
|
|
188
403
|
return ref
|
|
189
|
-
|
|
190
404
|
# Tool without ID (e.g., from Tool.from_native()) - needs platform lookup
|
|
191
405
|
# Fall through to normal resolution
|
|
192
406
|
|