glaip-sdk 0.6.23__py3-none-any.whl → 0.6.25__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.
@@ -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__()
@@ -66,9 +66,16 @@ class ToolRegistry(BaseRegistry["Tool"]):
66
66
  Raises:
67
67
  ValueError: If name cannot be extracted from the reference.
68
68
  """
69
+ # Lazy import to avoid circular dependency
70
+ from glaip_sdk.tools.base import Tool # noqa: PLC0415
71
+
69
72
  if isinstance(ref, str):
70
73
  return ref
71
74
 
75
+ # Tool instance (from Tool.from_langchain() or Tool.from_native())
76
+ if isinstance(ref, Tool):
77
+ return ref.get_name()
78
+
72
79
  # Dict from API response - extract name or id
73
80
  if isinstance(ref, dict):
74
81
  return ref.get("name") or ref.get("id") or ""
@@ -101,17 +108,61 @@ class ToolRegistry(BaseRegistry["Tool"]):
101
108
  ValueError: If the tool cannot be resolved.
102
109
  """
103
110
  # Lazy imports to avoid circular dependency
111
+ from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
112
+ from glaip_sdk.tools.base import ToolType # noqa: PLC0415
104
113
  from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
105
114
  from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
106
115
 
107
- # Already deployed tool (glaip_sdk.models.Tool with ID) - just cache and return
116
+ # Tool instance from Tool.from_native() or Tool.from_langchain()
117
+ # Use try/except to handle mocked Tool class in tests
118
+ try:
119
+ is_tool_instance = isinstance(ref, ToolClass)
120
+ except TypeError:
121
+ is_tool_instance = False
122
+
123
+ if is_tool_instance:
124
+ # If Tool has an ID, it's already deployed - return as-is
125
+ if ref.id is not None:
126
+ logger.debug("Caching already deployed tool: %s", name)
127
+ self._cache[name] = ref
128
+ # Also cache by id for consistency with other resolution branches
129
+ self._cache[ref.id] = ref
130
+ return ref
131
+
132
+ # Tool.from_native() - look up on platform
133
+ if ref.tool_type == ToolType.NATIVE:
134
+ logger.info("Looking up native tool: %s", name)
135
+ tool = find_tool(name)
136
+ if tool:
137
+ self._cache[name] = tool
138
+ return tool
139
+ raise ValueError(f"Native tool not found on platform: {name}")
140
+
141
+ # Tool.from_langchain() - upload the tool_class
142
+ if ref.tool_class is not None:
143
+ logger.info("Uploading custom tool: %s", name)
144
+ tool = update_or_create_tool(ref.tool_class)
145
+ self._cache[name] = tool
146
+ if tool.id:
147
+ self._cache[tool.id] = tool
148
+ return tool
149
+
150
+ # Unresolvable Tool instance - neither native nor has tool_class
151
+ raise ValueError(
152
+ f"Cannot resolve Tool instance: {ref}. "
153
+ f"Tool has no id, is not NATIVE type, and has no tool_class. "
154
+ f"Ensure Tool is created via Tool.from_native() or Tool.from_langchain()."
155
+ )
156
+
157
+ # Already deployed tool (not a ToolClass, but has id/name) - just cache and return
158
+ # This handles API response objects and backward compatibility
108
159
  if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
109
160
  if ref.id is not None:
110
161
  logger.debug("Caching already deployed tool: %s", name)
111
162
  self._cache[name] = ref
112
163
  return ref
113
164
 
114
- # Tool without ID (e.g., Tool.from_native()) - look up on platform
165
+ # Tool without ID (backward compatibility) - look up on platform
115
166
  logger.info("Looking up native tool: %s", name)
116
167
  tool = find_tool(name)
117
168
  if tool:
@@ -132,9 +183,7 @@ class ToolRegistry(BaseRegistry["Tool"]):
132
183
  if isinstance(ref, dict):
133
184
  tool_id = ref.get("id")
134
185
  if tool_id:
135
- from glaip_sdk.tools.base import Tool # noqa: PLC0415
136
-
137
- tool = Tool(id=tool_id, name=ref.get("name", ""))
186
+ tool = ToolClass(id=tool_id, name=ref.get("name", ""))
138
187
  self._cache[name] = tool
139
188
  return tool
140
189
  raise ValueError(f"Tool dict missing 'id': {ref}")
@@ -0,0 +1,22 @@
1
+ """Schedules runtime package.
2
+
3
+ This package contains runtime schedule resource objects (class-based) that
4
+ encapsulate behavior and API interactions via attached clients.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from glaip_sdk.schedules.base import (
11
+ Schedule,
12
+ ScheduleListResult,
13
+ ScheduleRun,
14
+ ScheduleRunListResult,
15
+ )
16
+
17
+ __all__ = [
18
+ "Schedule",
19
+ "ScheduleListResult",
20
+ "ScheduleRun",
21
+ "ScheduleRunListResult",
22
+ ]
@@ -0,0 +1,291 @@
1
+ """Schedule runtime resources.
2
+
3
+ This module contains class-based runtime resources for schedules.
4
+
5
+ The runtime resources:
6
+ - Are not Pydantic models.
7
+ - Are returned from public client APIs.
8
+ - Delegate API operations to a bound ScheduleClient.
9
+
10
+ Authors:
11
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from typing import TYPE_CHECKING
19
+
20
+ from glaip_sdk.models.agent_runs import RunStatus
21
+ from glaip_sdk.models.schedule import (
22
+ ScheduleConfig,
23
+ ScheduleMetadata,
24
+ ScheduleResponse,
25
+ ScheduleRunResponse,
26
+ ScheduleRunResult,
27
+ )
28
+
29
+ if TYPE_CHECKING: # pragma: no cover
30
+ from glaip_sdk.client.schedules import ScheduleClient
31
+
32
+ _SCHEDULE_CLIENT_REQUIRED_MSG = "No client available. Use client.schedules.get() to get a client-connected schedule."
33
+ _SCHEDULE_RUN_CLIENT_REQUIRED_MSG = (
34
+ "No client available. Use client.schedules.list_runs() to get a client-connected schedule run."
35
+ )
36
+
37
+
38
+ class Schedule:
39
+ """Runtime schedule resource.
40
+
41
+ Attributes:
42
+ id (str): The schedule ID.
43
+ next_run_time (str | None): Next run time as returned by the API.
44
+ time_until_next_run (str | None): Human readable duration until next run.
45
+ metadata (ScheduleMetadata | None): Schedule metadata.
46
+ created_at (datetime | None): Creation timestamp.
47
+ updated_at (datetime | None): Update timestamp.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ *,
53
+ id: str,
54
+ next_run_time: str | None = None,
55
+ time_until_next_run: str | None = None,
56
+ metadata: ScheduleMetadata | None = None,
57
+ created_at: datetime | None = None,
58
+ updated_at: datetime | None = None,
59
+ _client: ScheduleClient | None = None,
60
+ ) -> None:
61
+ """Initialize a runtime Schedule."""
62
+ self.id = id
63
+ self.next_run_time = next_run_time
64
+ self.time_until_next_run = time_until_next_run
65
+ self.metadata = metadata
66
+ self.created_at = created_at
67
+ self.updated_at = updated_at
68
+ self._client = _client
69
+
70
+ @classmethod
71
+ def from_response(cls, response: ScheduleResponse, *, client: ScheduleClient) -> Schedule:
72
+ """Build a runtime Schedule from a DTO response.
73
+
74
+ Args:
75
+ response: Parsed schedule response DTO.
76
+ client: ScheduleClient to bind.
77
+
78
+ Returns:
79
+ Runtime Schedule.
80
+ """
81
+ return cls(
82
+ id=response.id,
83
+ next_run_time=response.next_run_time,
84
+ time_until_next_run=response.time_until_next_run,
85
+ metadata=response.metadata,
86
+ created_at=response.created_at,
87
+ updated_at=response.updated_at,
88
+ _client=client,
89
+ )
90
+
91
+ @property
92
+ def agent_id(self) -> str | None:
93
+ """Agent ID derived from metadata."""
94
+ return self.metadata.agent_id if self.metadata else None
95
+
96
+ @property
97
+ def input(self) -> str | None:
98
+ """Input text derived from metadata."""
99
+ return self.metadata.input if self.metadata else None
100
+
101
+ @property
102
+ def schedule_config(self) -> ScheduleConfig | None:
103
+ """Schedule configuration derived from metadata."""
104
+ return self.metadata.schedule if self.metadata else None
105
+
106
+ def update(
107
+ self,
108
+ *,
109
+ input: str | None = None,
110
+ schedule: ScheduleConfig | dict[str, str] | str | None = None,
111
+ ) -> Schedule:
112
+ """Update this schedule."""
113
+ if self._client is None:
114
+ raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
115
+ return self._client.update(self.id, input=input, schedule=schedule)
116
+
117
+ def delete(self) -> None:
118
+ """Delete this schedule."""
119
+ if self._client is None:
120
+ raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
121
+ self._client.delete(self.id)
122
+
123
+ def list_runs(
124
+ self,
125
+ *,
126
+ status: RunStatus | None = None,
127
+ limit: int | None = None,
128
+ page: int | None = None,
129
+ ) -> ScheduleRunListResult:
130
+ """List runs for this schedule."""
131
+ if self._client is None:
132
+ raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
133
+ if self.agent_id is None:
134
+ raise ValueError("Schedule has no agent_id")
135
+ return self._client.list_runs(
136
+ self.agent_id,
137
+ schedule_id=self.id,
138
+ status=status,
139
+ limit=limit,
140
+ page=page,
141
+ )
142
+
143
+ def __repr__(self) -> str:
144
+ """Return a developer-friendly representation."""
145
+ parts: list[str] = [f"id={self.id!r}"]
146
+ if self.agent_id is not None:
147
+ parts.append(f"agent_id={self.agent_id!r}")
148
+ if self.next_run_time is not None:
149
+ parts.append(f"next_run_time={self.next_run_time!r}")
150
+ if self.time_until_next_run is not None:
151
+ parts.append(f"time_until_next_run={self.time_until_next_run!r}")
152
+ if self.created_at is not None:
153
+ parts.append(f"created_at={self.created_at!r}")
154
+ return f"Schedule({', '.join(parts)})"
155
+
156
+ def __str__(self) -> str:
157
+ """Return a readable string representation."""
158
+ return self.__repr__()
159
+
160
+
161
+ class ScheduleRun:
162
+ """Runtime schedule run resource."""
163
+
164
+ def __init__(
165
+ self,
166
+ *,
167
+ id: str,
168
+ agent_id: str,
169
+ schedule_id: str | None = None,
170
+ status: RunStatus,
171
+ run_type: str | None = None,
172
+ started_at: datetime | None = None,
173
+ completed_at: datetime | None = None,
174
+ input: str | None = None,
175
+ config: ScheduleConfig | dict[str, str] | None = None,
176
+ created_at: datetime | None = None,
177
+ updated_at: datetime | None = None,
178
+ _client: ScheduleClient | None = None,
179
+ ) -> None:
180
+ """Initialize a runtime ScheduleRun."""
181
+ self.id = id
182
+ self.agent_id = agent_id
183
+ self.schedule_id = schedule_id
184
+ self.status = status
185
+ self.run_type = run_type
186
+ self.started_at = started_at
187
+ self.completed_at = completed_at
188
+ self.input = input
189
+ self.config = config
190
+ self.created_at = created_at
191
+ self.updated_at = updated_at
192
+ self._client = _client
193
+
194
+ @classmethod
195
+ def from_response(cls, response: ScheduleRunResponse, *, client: ScheduleClient) -> ScheduleRun:
196
+ """Build a runtime ScheduleRun from a DTO response."""
197
+ return cls(
198
+ id=response.id,
199
+ agent_id=response.agent_id,
200
+ schedule_id=response.schedule_id,
201
+ status=response.status,
202
+ run_type=response.run_type,
203
+ started_at=response.started_at,
204
+ completed_at=response.completed_at,
205
+ input=response.input,
206
+ config=response.config,
207
+ created_at=response.created_at,
208
+ updated_at=response.updated_at,
209
+ _client=client,
210
+ )
211
+
212
+ def get_result(self) -> ScheduleRunResult:
213
+ """Retrieve the full output payload for this run."""
214
+ if self._client is None:
215
+ raise RuntimeError(_SCHEDULE_RUN_CLIENT_REQUIRED_MSG)
216
+ if self.agent_id is None:
217
+ raise ValueError("Schedule run has no agent_id")
218
+ return self._client.get_run_result(self.agent_id, self.id)
219
+
220
+ @property
221
+ def duration(self) -> str | None:
222
+ """Formatted duration (HH:MM:SS) when both timestamps are available."""
223
+ if not self.started_at or not self.completed_at:
224
+ return None
225
+
226
+ total_seconds = int((self.completed_at - self.started_at).total_seconds())
227
+ minutes, seconds = divmod(total_seconds, 60)
228
+ hours, minutes = divmod(minutes, 60)
229
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
230
+
231
+ def __repr__(self) -> str:
232
+ """Return a developer-friendly representation."""
233
+ parts: list[str] = [f"id={self.id!r}", f"status={self.status!r}"]
234
+ if self.started_at is not None:
235
+ parts.append(f"started_at={self.started_at.isoformat()!r}")
236
+ duration = self.duration
237
+ if duration is not None:
238
+ parts.append(f"duration={duration!r}")
239
+ return f"ScheduleRun({', '.join(parts)})"
240
+
241
+ def __str__(self) -> str:
242
+ """Return a readable string representation."""
243
+ return self.__repr__()
244
+
245
+
246
+ @dataclass
247
+ class ScheduleListResult:
248
+ """Paginated list wrapper for runtime schedules."""
249
+
250
+ items: list[Schedule]
251
+ total: int | None = field(default=None)
252
+ page: int | None = field(default=None)
253
+ limit: int | None = field(default=None)
254
+ has_next: bool | None = field(default=None)
255
+ has_prev: bool | None = field(default=None)
256
+
257
+ def __iter__(self):
258
+ """Iterate over schedules."""
259
+ yield from self.items
260
+
261
+ def __len__(self) -> int:
262
+ """Return the number of schedules in this page."""
263
+ return self.items.__len__()
264
+
265
+ def __getitem__(self, index: int) -> Schedule:
266
+ """Return the schedule at the given index."""
267
+ return self.items[index]
268
+
269
+
270
+ @dataclass
271
+ class ScheduleRunListResult:
272
+ """Paginated list wrapper for runtime schedule runs."""
273
+
274
+ items: list[ScheduleRun]
275
+ total: int | None = field(default=None)
276
+ page: int | None = field(default=None)
277
+ limit: int | None = field(default=None)
278
+ has_next: bool | None = field(default=None)
279
+ has_prev: bool | None = field(default=None)
280
+
281
+ def __iter__(self):
282
+ """Iterate over schedule runs."""
283
+ yield from self.items
284
+
285
+ def __len__(self) -> int:
286
+ """Return the number of runs in this page."""
287
+ return self.items.__len__()
288
+
289
+ def __getitem__(self, index: int) -> ScheduleRun:
290
+ """Return the run at the given index."""
291
+ return self.items[index]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glaip-sdk
3
- Version: 0.6.23
3
+ Version: 0.6.25
4
4
  Summary: Python SDK and CLI for GL AIP (GDP Labs AI Agent Package) - Build, run, and manage AI agents
5
5
  Author-email: Raymond Christopher <raymond.christopher@gdplabs.id>
6
6
  License: MIT