gumloop 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {gumloop-0.3.0 → gumloop-0.3.2}/PKG-INFO +1 -1
- {gumloop-0.3.0 → gumloop-0.3.2}/pyproject.toml +1 -1
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/__init__.py +1 -1
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/_client.py +6 -4
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/_http.py +2 -2
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/mcp.py +176 -3
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/sessions.py +93 -0
- gumloop-0.3.2/src/gumloop/resources/agents.py +183 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/mcp.py +78 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/sessions.py +62 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/types.py +179 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_mcp.py +98 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_sessions.py +27 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_agents.py +100 -0
- gumloop-0.3.2/tests/sdk/test_mcp.py +264 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_sessions.py +43 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/uv.lock +2 -2
- gumloop-0.3.0/src/gumloop/resources/agents.py +0 -101
- gumloop-0.3.0/tests/sdk/test_mcp.py +0 -139
- {gumloop-0.3.0 → gumloop-0.3.2}/.env.example +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/.gitignore +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/.pre-commit-config.yaml +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/.python-version +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/LICENSE +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/README.md +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/scripts/check +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/scripts/fmt +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/scripts/sdk_examples.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/agents.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/main.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/errors.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/oauth.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/py.typed +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/skills.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/conftest.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_agents.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_chat.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_context.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_login.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_main.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_skills.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/conftest.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/integration/conftest.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/integration/test_live.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/__init__.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/conftest.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/helpers.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_client.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.3.0 → gumloop-0.3.2}/tests/test_client.py +0 -0
|
@@ -51,7 +51,7 @@ class Gumloop:
|
|
|
51
51
|
*,
|
|
52
52
|
access_token: str | None = None,
|
|
53
53
|
user_id: str | None = None,
|
|
54
|
-
base_url: str =
|
|
54
|
+
base_url: str | None = None,
|
|
55
55
|
stream_base_url: str | None = None,
|
|
56
56
|
timeout: float = DEFAULT_TIMEOUT,
|
|
57
57
|
stream_timeout: float | None = DEFAULT_STREAM_TIMEOUT,
|
|
@@ -62,7 +62,8 @@ class Gumloop:
|
|
|
62
62
|
# x-auth-key header to look up the owner's stored secret. OAuth
|
|
63
63
|
# callers omit it because the bearer token already identifies them.
|
|
64
64
|
self.user_id = user_id or os.environ.get("GUMLOOP_USER_ID")
|
|
65
|
-
|
|
65
|
+
# Defaults to production; GUMLOOP_BASE_URL is an optional override.
|
|
66
|
+
self.base_url = (base_url or os.environ.get("GUMLOOP_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
66
67
|
self.stream_base_url = (stream_base_url or _derive_stream_base_url(self.base_url)).rstrip("/")
|
|
67
68
|
self.timeout = timeout
|
|
68
69
|
self.stream_timeout = stream_timeout
|
|
@@ -105,7 +106,7 @@ class AsyncGumloop:
|
|
|
105
106
|
*,
|
|
106
107
|
access_token: str | None = None,
|
|
107
108
|
user_id: str | None = None,
|
|
108
|
-
base_url: str =
|
|
109
|
+
base_url: str | None = None,
|
|
109
110
|
stream_base_url: str | None = None,
|
|
110
111
|
timeout: float = DEFAULT_TIMEOUT,
|
|
111
112
|
stream_timeout: float | None = DEFAULT_STREAM_TIMEOUT,
|
|
@@ -113,7 +114,8 @@ class AsyncGumloop:
|
|
|
113
114
|
self.api_key = api_key
|
|
114
115
|
self.access_token = access_token or api_key or os.environ.get("GUMLOOP_ACCESS_TOKEN")
|
|
115
116
|
self.user_id = user_id or os.environ.get("GUMLOOP_USER_ID")
|
|
116
|
-
|
|
117
|
+
# Defaults to production; GUMLOOP_BASE_URL is an optional override.
|
|
118
|
+
self.base_url = (base_url or os.environ.get("GUMLOOP_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
117
119
|
self.stream_base_url = (stream_base_url or _derive_stream_base_url(self.base_url)).rstrip("/")
|
|
118
120
|
self.timeout = timeout
|
|
119
121
|
self.stream_timeout = stream_timeout
|
|
@@ -184,7 +184,7 @@ class HttpClient:
|
|
|
184
184
|
yield response_model.model_validate_json(event.data)
|
|
185
185
|
except ValidationError:
|
|
186
186
|
# Server-side mid-stream error frames or schema-drift events
|
|
187
|
-
# land here.
|
|
187
|
+
# land here.
|
|
188
188
|
logger.debug("dropped non-%s SSE: %s", response_model.__name__, event.data)
|
|
189
189
|
continue
|
|
190
190
|
|
|
@@ -320,7 +320,7 @@ class AsyncHttpClient:
|
|
|
320
320
|
yield response_model.model_validate_json(event.data)
|
|
321
321
|
except ValidationError:
|
|
322
322
|
# Server-side mid-stream error frames or schema-drift events
|
|
323
|
-
# land here.
|
|
323
|
+
# land here.
|
|
324
324
|
logger.debug("dropped non-%s SSE: %s", response_model.__name__, event.data)
|
|
325
325
|
continue
|
|
326
326
|
|
|
@@ -140,7 +140,7 @@ def call_tool(
|
|
|
140
140
|
] = None,
|
|
141
141
|
json_output: Annotated[
|
|
142
142
|
bool,
|
|
143
|
-
typer.Option("--json", help="Print the
|
|
143
|
+
typer.Option("--json", help="Print the tool response as JSON, decoding JSON text content when possible."),
|
|
144
144
|
] = False,
|
|
145
145
|
) -> None:
|
|
146
146
|
"""Execute an MCP tool on a server."""
|
|
@@ -160,7 +160,7 @@ def call_tool(
|
|
|
160
160
|
exit_with_error(error, json_output=json_output)
|
|
161
161
|
|
|
162
162
|
if json_output:
|
|
163
|
-
print_json(response)
|
|
163
|
+
print_json(response.model_dump_decoded_content())
|
|
164
164
|
return
|
|
165
165
|
|
|
166
166
|
# markup=False on remote fields stops MCP server output from rendering
|
|
@@ -176,10 +176,183 @@ def call_tool(
|
|
|
176
176
|
console.print(f" status: {result.status}", markup=False, highlight=False)
|
|
177
177
|
if result.error:
|
|
178
178
|
console.print(f" error: {result.error}", markup=False, highlight=False)
|
|
179
|
-
content = result.
|
|
179
|
+
content = result.decoded_content
|
|
180
180
|
if not content:
|
|
181
181
|
continue
|
|
182
182
|
if isinstance(content, str):
|
|
183
183
|
console.print(content, markup=False, highlight=False)
|
|
184
184
|
else:
|
|
185
185
|
print_json(content)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@mcp_app.command("resources", epilog="Example:\n gumloop mcp resources gmail")
|
|
189
|
+
def list_resources(
|
|
190
|
+
ctx: typer.Context,
|
|
191
|
+
server: Annotated[str, typer.Argument(help="MCP server id whose resources should be listed.")],
|
|
192
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor from a previous page.")] = None,
|
|
193
|
+
json_output: Annotated[bool, typer.Option("--json", help="Print the raw SDK response as JSON.")] = False,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""List the resources a connected MCP server exposes."""
|
|
196
|
+
cli: CliContext = ctx.obj
|
|
197
|
+
try:
|
|
198
|
+
response = cli.call_with_refresh(
|
|
199
|
+
lambda client: client.mcp.list_resources(server, cursor=cursor, team_id=cli.effective_team_id)
|
|
200
|
+
)
|
|
201
|
+
except GumloopError as error:
|
|
202
|
+
exit_with_error(error, json_output=json_output)
|
|
203
|
+
|
|
204
|
+
if json_output:
|
|
205
|
+
print_json(response)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if response.status and response.status != "connected":
|
|
209
|
+
status_text = escape_markup(response.status)
|
|
210
|
+
console.print(f"Server [bold]{escape_markup(server)}[/bold] is not connected ({status_text}).")
|
|
211
|
+
if response.gumloop_auth_url:
|
|
212
|
+
console.print(f"Connect here: {response.gumloop_auth_url}", markup=False, highlight=False)
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if not response.resources:
|
|
216
|
+
console.print("No resources found.")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
console.print("URI", "NAME", "MIME_TYPE", "DESCRIPTION", sep="\t", soft_wrap=True)
|
|
220
|
+
for resource in response.resources:
|
|
221
|
+
console.print(
|
|
222
|
+
resource.uri,
|
|
223
|
+
resource.name or "",
|
|
224
|
+
resource.mime_type or "",
|
|
225
|
+
resource.description or "",
|
|
226
|
+
sep="\t",
|
|
227
|
+
soft_wrap=True,
|
|
228
|
+
)
|
|
229
|
+
if response.next_cursor:
|
|
230
|
+
console.print(
|
|
231
|
+
f"\nMore results — next page: --cursor {response.next_cursor}",
|
|
232
|
+
markup=False,
|
|
233
|
+
highlight=False,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@mcp_app.command("resource", epilog='Example:\n gumloop mcp resource gmail "gmail://label/INBOX"')
|
|
238
|
+
def read_resource(
|
|
239
|
+
ctx: typer.Context,
|
|
240
|
+
server: Annotated[str, typer.Argument(help="MCP server id (for example 'gmail').")],
|
|
241
|
+
uri: Annotated[str, typer.Argument(help="Resource URI to read (for example 'gmail://label/INBOX').")],
|
|
242
|
+
json_output: Annotated[bool, typer.Option("--json", help="Print the raw SDK response as JSON.")] = False,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Read a single MCP resource by URI."""
|
|
245
|
+
cli: CliContext = ctx.obj
|
|
246
|
+
try:
|
|
247
|
+
response = cli.call_with_refresh(
|
|
248
|
+
lambda client: client.mcp.get_resource(server, uri, team_id=cli.effective_team_id)
|
|
249
|
+
)
|
|
250
|
+
except GumloopError as error:
|
|
251
|
+
exit_with_error(error, json_output=json_output)
|
|
252
|
+
|
|
253
|
+
if json_output:
|
|
254
|
+
print_json(response)
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
text = response.text
|
|
258
|
+
if text is not None:
|
|
259
|
+
console.print(text, markup=False, highlight=False)
|
|
260
|
+
return
|
|
261
|
+
# Binary or empty contents: fall back to the structured payload.
|
|
262
|
+
if not response.contents:
|
|
263
|
+
console.print("(no content)")
|
|
264
|
+
return
|
|
265
|
+
print_json(response)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@mcp_app.command("prompts", epilog="Example:\n gumloop mcp prompts gmail")
|
|
269
|
+
def list_prompts(
|
|
270
|
+
ctx: typer.Context,
|
|
271
|
+
server: Annotated[str, typer.Argument(help="MCP server id whose prompts should be listed.")],
|
|
272
|
+
json_output: Annotated[bool, typer.Option("--json", help="Print the raw SDK response as JSON.")] = False,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""List the prompts a connected MCP server exposes."""
|
|
275
|
+
cli: CliContext = ctx.obj
|
|
276
|
+
try:
|
|
277
|
+
response = cli.call_with_refresh(lambda client: client.mcp.list_prompts(server, team_id=cli.effective_team_id))
|
|
278
|
+
except GumloopError as error:
|
|
279
|
+
exit_with_error(error, json_output=json_output)
|
|
280
|
+
|
|
281
|
+
if json_output:
|
|
282
|
+
print_json(response)
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
if response.status and response.status != "connected":
|
|
286
|
+
status_text = escape_markup(response.status)
|
|
287
|
+
console.print(f"Server [bold]{escape_markup(server)}[/bold] is not connected ({status_text}).")
|
|
288
|
+
if response.gumloop_auth_url:
|
|
289
|
+
console.print(f"Connect here: {response.gumloop_auth_url}", markup=False, highlight=False)
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
if not response.prompts:
|
|
293
|
+
console.print("No prompts found.")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
console.print("NAME", "ARGUMENTS", "DESCRIPTION", sep="\t", soft_wrap=True)
|
|
297
|
+
for prompt in response.prompts:
|
|
298
|
+
# A trailing * marks a required argument.
|
|
299
|
+
args = ", ".join(f"{arg.name}*" if arg.required else arg.name for arg in prompt.arguments)
|
|
300
|
+
console.print(prompt.name, args, prompt.description or "", sep="\t", soft_wrap=True)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@mcp_app.command(
|
|
304
|
+
"prompt",
|
|
305
|
+
epilog=(
|
|
306
|
+
"Examples:\n"
|
|
307
|
+
' gumloop mcp prompt gmail summarize_thread --args-json \'{"thread_id": "abc"}\'\n'
|
|
308
|
+
" gumloop mcp prompt gmail summarize_thread --args-file ./args.json"
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
def get_prompt(
|
|
312
|
+
ctx: typer.Context,
|
|
313
|
+
server: Annotated[str, typer.Argument(help="MCP server id (for example 'gmail').")],
|
|
314
|
+
name: Annotated[str, typer.Argument(help="Prompt name on that server.")],
|
|
315
|
+
args_json: Annotated[
|
|
316
|
+
str | None,
|
|
317
|
+
typer.Option("--args-json", help="Inline JSON object of prompt arguments."),
|
|
318
|
+
] = None,
|
|
319
|
+
args_file: Annotated[
|
|
320
|
+
str | None,
|
|
321
|
+
typer.Option("--args-file", help="Path to a file containing the JSON arguments."),
|
|
322
|
+
] = None,
|
|
323
|
+
args_stdin: Annotated[
|
|
324
|
+
str | None,
|
|
325
|
+
typer.Option("--args", help="Use '-' to read JSON arguments from stdin.", metavar="-"),
|
|
326
|
+
] = None,
|
|
327
|
+
json_output: Annotated[
|
|
328
|
+
bool,
|
|
329
|
+
typer.Option("--json", help="Print the prompt response as JSON."),
|
|
330
|
+
] = False,
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Get (render) an MCP prompt with arguments."""
|
|
333
|
+
cli: CliContext = ctx.obj
|
|
334
|
+
try:
|
|
335
|
+
arguments = resolve_json_args(inline=args_json, file_path=args_file, stdin_marker=args_stdin)
|
|
336
|
+
response = cli.call_with_refresh(
|
|
337
|
+
lambda client: client.mcp.get_prompt(server, name, arguments, team_id=cli.effective_team_id)
|
|
338
|
+
)
|
|
339
|
+
except GumloopError as error:
|
|
340
|
+
exit_with_error(error, json_output=json_output)
|
|
341
|
+
|
|
342
|
+
if json_output:
|
|
343
|
+
print_json(response)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
if response.description:
|
|
347
|
+
console.print(response.description, markup=False, highlight=False)
|
|
348
|
+
if not response.messages:
|
|
349
|
+
console.print("(no messages)")
|
|
350
|
+
return
|
|
351
|
+
for message in response.messages:
|
|
352
|
+
role = message.role or ""
|
|
353
|
+
console.print(f"[bold]{escape_markup(role)}[/bold]", markup=True, highlight=False)
|
|
354
|
+
text = message.content.get("text") if isinstance(message.content, dict) else None
|
|
355
|
+
if text:
|
|
356
|
+
console.print(text, markup=False, highlight=False)
|
|
357
|
+
else:
|
|
358
|
+
print_json(message.content)
|
|
@@ -93,6 +93,99 @@ def create_session(
|
|
|
93
93
|
console.print(f" [{m.role or ''}] {content[:200]}", markup=False, highlight=False)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
@sessions_app.command(
|
|
97
|
+
"list",
|
|
98
|
+
epilog=(
|
|
99
|
+
"Examples:\n"
|
|
100
|
+
" gumloop sessions list agent_abc --state completed --limit 50 --json\n"
|
|
101
|
+
" gumloop sessions list agent_abc --search 'invoice' --sort oldest\n"
|
|
102
|
+
" gumloop sessions list agent_abc --type api --creator user_123"
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
def list_sessions(
|
|
106
|
+
ctx: typer.Context,
|
|
107
|
+
agent_id: Annotated[str, typer.Argument(help="Agent id whose sessions should be listed.")],
|
|
108
|
+
search: Annotated[
|
|
109
|
+
str | None,
|
|
110
|
+
typer.Option("--search", help="Search session name and content."),
|
|
111
|
+
] = None,
|
|
112
|
+
state: Annotated[
|
|
113
|
+
str | None,
|
|
114
|
+
typer.Option("--state", help="Filter by state (e.g. completed, failed, processing, idle)."),
|
|
115
|
+
] = None,
|
|
116
|
+
type_filter: Annotated[
|
|
117
|
+
str | None,
|
|
118
|
+
typer.Option("--type", help="Filter by session type (e.g. api, chat, slack)."),
|
|
119
|
+
] = None,
|
|
120
|
+
creator: Annotated[
|
|
121
|
+
str | None,
|
|
122
|
+
typer.Option("--creator", help="Filter by creator user id."),
|
|
123
|
+
] = None,
|
|
124
|
+
trigger_id: Annotated[
|
|
125
|
+
str | None,
|
|
126
|
+
typer.Option("--trigger-id", help="Filter by originating trigger id."),
|
|
127
|
+
] = None,
|
|
128
|
+
sort_order: Annotated[
|
|
129
|
+
str | None,
|
|
130
|
+
typer.Option("--sort", help="Sort order (e.g. newest, oldest, credits_desc, name_asc)."),
|
|
131
|
+
] = None,
|
|
132
|
+
limit: Annotated[
|
|
133
|
+
int | None,
|
|
134
|
+
typer.Option("--limit", help="Maximum number of sessions to return."),
|
|
135
|
+
] = None,
|
|
136
|
+
cursor: Annotated[
|
|
137
|
+
str | None,
|
|
138
|
+
typer.Option("--cursor", help="Pagination cursor from a previous list call."),
|
|
139
|
+
] = None,
|
|
140
|
+
json_output: Annotated[
|
|
141
|
+
bool,
|
|
142
|
+
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
143
|
+
] = False,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""List an agent's sessions with search, filters, and sort."""
|
|
146
|
+
cli: CliContext = ctx.obj
|
|
147
|
+
try:
|
|
148
|
+
response = cli.call_with_refresh(
|
|
149
|
+
lambda client: client.sessions.list(
|
|
150
|
+
agent_id,
|
|
151
|
+
search=search,
|
|
152
|
+
state=state,
|
|
153
|
+
type=type_filter,
|
|
154
|
+
creator_user_id=creator,
|
|
155
|
+
trigger_id=trigger_id,
|
|
156
|
+
sort_order=sort_order,
|
|
157
|
+
page_size=limit,
|
|
158
|
+
cursor=cursor,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
except GumloopError as error:
|
|
162
|
+
exit_with_error(error, json_output=json_output)
|
|
163
|
+
|
|
164
|
+
if json_output:
|
|
165
|
+
print_json(response)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
if not response.sessions:
|
|
169
|
+
console.print("No sessions found.")
|
|
170
|
+
else:
|
|
171
|
+
console.print("ID", "STATE", "TYPE", "NAME", "CREATED", sep="\t", soft_wrap=True)
|
|
172
|
+
for session in response.sessions:
|
|
173
|
+
console.print(
|
|
174
|
+
session.id,
|
|
175
|
+
session.state or "",
|
|
176
|
+
session.type or "",
|
|
177
|
+
session.name or "",
|
|
178
|
+
session.created_at or "",
|
|
179
|
+
sep="\t",
|
|
180
|
+
soft_wrap=True,
|
|
181
|
+
markup=False,
|
|
182
|
+
highlight=False,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if response.next_cursor:
|
|
186
|
+
console.print(f"\n[dim]Next cursor:[/dim] {escape_markup(response.next_cursor)}")
|
|
187
|
+
|
|
188
|
+
|
|
96
189
|
@sessions_app.command("get", epilog="Example:\n gumloop sessions get session_abc --json")
|
|
97
190
|
def get_session(
|
|
98
191
|
ctx: typer.Context,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from gumloop._http import AsyncHttpClient
|
|
7
|
+
from gumloop._http import HttpClient
|
|
8
|
+
from gumloop.types import AgentCreateRequest
|
|
9
|
+
from gumloop.types import AgentListResponse
|
|
10
|
+
from gumloop.types import AgentResponse
|
|
11
|
+
from gumloop.types import AgentUpdateRequest
|
|
12
|
+
from gumloop.types import EvaluationConfigResponse
|
|
13
|
+
from gumloop.types import EvaluationConfigUpdateRequest
|
|
14
|
+
from gumloop.types import EvaluationResultListResponse
|
|
15
|
+
from gumloop.types import EvaluationResultResponse
|
|
16
|
+
from gumloop.types import ModelListResponse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Agents:
|
|
20
|
+
def __init__(self, client: HttpClient) -> None:
|
|
21
|
+
self._client = client
|
|
22
|
+
|
|
23
|
+
def list(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
search: str | None = None,
|
|
27
|
+
team_id: str | None = None,
|
|
28
|
+
**kwargs: Any,
|
|
29
|
+
) -> AgentListResponse:
|
|
30
|
+
return AgentListResponse.model_validate(
|
|
31
|
+
self._client.get("agents", params={"search": search, "team_id": team_id, **kwargs})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def create(
|
|
35
|
+
self,
|
|
36
|
+
request: AgentCreateRequest | Mapping[str, Any] | None = None,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> AgentResponse:
|
|
39
|
+
return AgentResponse.model_validate(
|
|
40
|
+
self._client.post("agents", json=AgentCreateRequest.build(request, **kwargs))
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def retrieve(self, agent_id: str) -> AgentResponse:
|
|
44
|
+
return AgentResponse.model_validate(self._client.get(f"agents/{agent_id}"))
|
|
45
|
+
|
|
46
|
+
def update(
|
|
47
|
+
self,
|
|
48
|
+
agent_id: str,
|
|
49
|
+
request: AgentUpdateRequest | Mapping[str, Any] | None = None,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> AgentResponse:
|
|
52
|
+
return AgentResponse.model_validate(
|
|
53
|
+
self._client.patch(f"agents/{agent_id}", json=AgentUpdateRequest.build(request, **kwargs))
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def get_evaluation_config(self, agent_id: str) -> EvaluationConfigResponse:
|
|
57
|
+
return EvaluationConfigResponse.model_validate(self._client.get(f"agents/{agent_id}/evaluation-config"))
|
|
58
|
+
|
|
59
|
+
def update_evaluation_config(
|
|
60
|
+
self,
|
|
61
|
+
agent_id: str,
|
|
62
|
+
request: EvaluationConfigUpdateRequest | Mapping[str, Any] | None = None,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> EvaluationConfigResponse:
|
|
65
|
+
"""Partially update the evaluation config. Only the fields you send are changed;
|
|
66
|
+
omitted fields keep their current value. A provided list (criteria/tags/
|
|
67
|
+
data_points) replaces that list wholesale."""
|
|
68
|
+
return EvaluationConfigResponse.model_validate(
|
|
69
|
+
self._client.patch(
|
|
70
|
+
f"agents/{agent_id}/evaluation-config",
|
|
71
|
+
json=EvaluationConfigUpdateRequest.build(request, **kwargs),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def list_evaluations(
|
|
76
|
+
self,
|
|
77
|
+
agent_id: str,
|
|
78
|
+
*,
|
|
79
|
+
grade: str | None = None,
|
|
80
|
+
page_size: int | None = None,
|
|
81
|
+
cursor: str | None = None,
|
|
82
|
+
**kwargs: Any,
|
|
83
|
+
) -> EvaluationResultListResponse:
|
|
84
|
+
return EvaluationResultListResponse.model_validate(
|
|
85
|
+
self._client.get(
|
|
86
|
+
f"agents/{agent_id}/evaluations",
|
|
87
|
+
params={"grade": grade, "page_size": page_size, "cursor": cursor, **kwargs},
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def get_evaluation(self, agent_id: str, evaluation_id: str) -> EvaluationResultResponse:
|
|
92
|
+
return EvaluationResultResponse.model_validate(
|
|
93
|
+
self._client.get(f"agents/{agent_id}/evaluations/{evaluation_id}")
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Models:
|
|
98
|
+
def __init__(self, client: HttpClient) -> None:
|
|
99
|
+
self._client = client
|
|
100
|
+
|
|
101
|
+
def list(self, **kwargs: Any) -> ModelListResponse:
|
|
102
|
+
return ModelListResponse.model_validate(self._client.get("models", params=kwargs))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AsyncAgents:
|
|
106
|
+
def __init__(self, client: AsyncHttpClient) -> None:
|
|
107
|
+
self._client = client
|
|
108
|
+
|
|
109
|
+
async def list(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
search: str | None = None,
|
|
113
|
+
team_id: str | None = None,
|
|
114
|
+
**kwargs: Any,
|
|
115
|
+
) -> AgentListResponse:
|
|
116
|
+
data = await self._client.get("agents", params={"search": search, "team_id": team_id, **kwargs})
|
|
117
|
+
return AgentListResponse.model_validate(data)
|
|
118
|
+
|
|
119
|
+
async def create(
|
|
120
|
+
self,
|
|
121
|
+
request: AgentCreateRequest | Mapping[str, Any] | None = None,
|
|
122
|
+
**kwargs: Any,
|
|
123
|
+
) -> AgentResponse:
|
|
124
|
+
data = await self._client.post("agents", json=AgentCreateRequest.build(request, **kwargs))
|
|
125
|
+
return AgentResponse.model_validate(data)
|
|
126
|
+
|
|
127
|
+
async def retrieve(self, agent_id: str) -> AgentResponse:
|
|
128
|
+
return AgentResponse.model_validate(await self._client.get(f"agents/{agent_id}"))
|
|
129
|
+
|
|
130
|
+
async def update(
|
|
131
|
+
self,
|
|
132
|
+
agent_id: str,
|
|
133
|
+
request: AgentUpdateRequest | Mapping[str, Any] | None = None,
|
|
134
|
+
**kwargs: Any,
|
|
135
|
+
) -> AgentResponse:
|
|
136
|
+
data = await self._client.patch(f"agents/{agent_id}", json=AgentUpdateRequest.build(request, **kwargs))
|
|
137
|
+
return AgentResponse.model_validate(data)
|
|
138
|
+
|
|
139
|
+
async def get_evaluation_config(self, agent_id: str) -> EvaluationConfigResponse:
|
|
140
|
+
data = await self._client.get(f"agents/{agent_id}/evaluation-config")
|
|
141
|
+
return EvaluationConfigResponse.model_validate(data)
|
|
142
|
+
|
|
143
|
+
async def update_evaluation_config(
|
|
144
|
+
self,
|
|
145
|
+
agent_id: str,
|
|
146
|
+
request: EvaluationConfigUpdateRequest | Mapping[str, Any] | None = None,
|
|
147
|
+
**kwargs: Any,
|
|
148
|
+
) -> EvaluationConfigResponse:
|
|
149
|
+
"""Partially update the evaluation config. Only the fields you send are changed;
|
|
150
|
+
omitted fields keep their current value. A provided list (criteria/tags/
|
|
151
|
+
data_points) replaces that list wholesale."""
|
|
152
|
+
data = await self._client.patch(
|
|
153
|
+
f"agents/{agent_id}/evaluation-config",
|
|
154
|
+
json=EvaluationConfigUpdateRequest.build(request, **kwargs),
|
|
155
|
+
)
|
|
156
|
+
return EvaluationConfigResponse.model_validate(data)
|
|
157
|
+
|
|
158
|
+
async def list_evaluations(
|
|
159
|
+
self,
|
|
160
|
+
agent_id: str,
|
|
161
|
+
*,
|
|
162
|
+
grade: str | None = None,
|
|
163
|
+
page_size: int | None = None,
|
|
164
|
+
cursor: str | None = None,
|
|
165
|
+
**kwargs: Any,
|
|
166
|
+
) -> EvaluationResultListResponse:
|
|
167
|
+
data = await self._client.get(
|
|
168
|
+
f"agents/{agent_id}/evaluations",
|
|
169
|
+
params={"grade": grade, "page_size": page_size, "cursor": cursor, **kwargs},
|
|
170
|
+
)
|
|
171
|
+
return EvaluationResultListResponse.model_validate(data)
|
|
172
|
+
|
|
173
|
+
async def get_evaluation(self, agent_id: str, evaluation_id: str) -> EvaluationResultResponse:
|
|
174
|
+
data = await self._client.get(f"agents/{agent_id}/evaluations/{evaluation_id}")
|
|
175
|
+
return EvaluationResultResponse.model_validate(data)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class AsyncModels:
|
|
179
|
+
def __init__(self, client: AsyncHttpClient) -> None:
|
|
180
|
+
self._client = client
|
|
181
|
+
|
|
182
|
+
async def list(self, **kwargs: Any) -> ModelListResponse:
|
|
183
|
+
return ModelListResponse.model_validate(await self._client.get("models", params=kwargs))
|
|
@@ -6,6 +6,10 @@ from typing import Any
|
|
|
6
6
|
from gumloop._http import AsyncHttpClient
|
|
7
7
|
from gumloop._http import HttpClient
|
|
8
8
|
from gumloop.types import McpExecuteResponse
|
|
9
|
+
from gumloop.types import McpPromptResponse
|
|
10
|
+
from gumloop.types import McpPromptsResponse
|
|
11
|
+
from gumloop.types import McpResourceReadResponse
|
|
12
|
+
from gumloop.types import McpResourcesResponse
|
|
9
13
|
from gumloop.types import McpServerResponse
|
|
10
14
|
from gumloop.types import McpServersResponse
|
|
11
15
|
from gumloop.types import McpToolCallRequest
|
|
@@ -42,6 +46,44 @@ class MCP:
|
|
|
42
46
|
self._client.get(f"mcp/servers/{server_id}/tools", params={"team_id": team_id, **kwargs})
|
|
43
47
|
)
|
|
44
48
|
|
|
49
|
+
def list_resources(
|
|
50
|
+
self, server_id: str, *, cursor: str | None = None, team_id: str | None = None, **kwargs: Any
|
|
51
|
+
) -> McpResourcesResponse:
|
|
52
|
+
return McpResourcesResponse.model_validate(
|
|
53
|
+
self._client.get(
|
|
54
|
+
f"mcp/servers/{server_id}/resources",
|
|
55
|
+
params={"cursor": cursor, "team_id": team_id, **kwargs},
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def get_resource(
|
|
60
|
+
self, server_id: str, uri: str, *, team_id: str | None = None, **kwargs: Any
|
|
61
|
+
) -> McpResourceReadResponse:
|
|
62
|
+
return McpResourceReadResponse.model_validate(
|
|
63
|
+
self._client.get(
|
|
64
|
+
f"mcp/servers/{server_id}/resources/read",
|
|
65
|
+
params={"uri": uri, "team_id": team_id, **kwargs},
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def list_prompts(self, server_id: str, *, team_id: str | None = None, **kwargs: Any) -> McpPromptsResponse:
|
|
70
|
+
return McpPromptsResponse.model_validate(
|
|
71
|
+
self._client.get(f"mcp/servers/{server_id}/prompts", params={"team_id": team_id, **kwargs})
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def get_prompt(
|
|
75
|
+
self,
|
|
76
|
+
server_id: str,
|
|
77
|
+
name: str,
|
|
78
|
+
arguments: dict[str, Any] | None = None,
|
|
79
|
+
*,
|
|
80
|
+
team_id: str | None = None,
|
|
81
|
+
) -> McpPromptResponse:
|
|
82
|
+
body: dict[str, Any] = {"name": name, "arguments": arguments or {}}
|
|
83
|
+
if team_id is not None:
|
|
84
|
+
body["team_id"] = team_id
|
|
85
|
+
return McpPromptResponse.model_validate(self._client.post(f"mcp/servers/{server_id}/prompts/get", json=body))
|
|
86
|
+
|
|
45
87
|
def execute(
|
|
46
88
|
self,
|
|
47
89
|
server_id: str,
|
|
@@ -84,6 +126,42 @@ class AsyncMCP:
|
|
|
84
126
|
data = await self._client.get(f"mcp/servers/{server_id}/tools", params={"team_id": team_id, **kwargs})
|
|
85
127
|
return McpToolsResponse.model_validate(data)
|
|
86
128
|
|
|
129
|
+
async def list_resources(
|
|
130
|
+
self, server_id: str, *, cursor: str | None = None, team_id: str | None = None, **kwargs: Any
|
|
131
|
+
) -> McpResourcesResponse:
|
|
132
|
+
data = await self._client.get(
|
|
133
|
+
f"mcp/servers/{server_id}/resources",
|
|
134
|
+
params={"cursor": cursor, "team_id": team_id, **kwargs},
|
|
135
|
+
)
|
|
136
|
+
return McpResourcesResponse.model_validate(data)
|
|
137
|
+
|
|
138
|
+
async def get_resource(
|
|
139
|
+
self, server_id: str, uri: str, *, team_id: str | None = None, **kwargs: Any
|
|
140
|
+
) -> McpResourceReadResponse:
|
|
141
|
+
data = await self._client.get(
|
|
142
|
+
f"mcp/servers/{server_id}/resources/read",
|
|
143
|
+
params={"uri": uri, "team_id": team_id, **kwargs},
|
|
144
|
+
)
|
|
145
|
+
return McpResourceReadResponse.model_validate(data)
|
|
146
|
+
|
|
147
|
+
async def list_prompts(self, server_id: str, *, team_id: str | None = None, **kwargs: Any) -> McpPromptsResponse:
|
|
148
|
+
data = await self._client.get(f"mcp/servers/{server_id}/prompts", params={"team_id": team_id, **kwargs})
|
|
149
|
+
return McpPromptsResponse.model_validate(data)
|
|
150
|
+
|
|
151
|
+
async def get_prompt(
|
|
152
|
+
self,
|
|
153
|
+
server_id: str,
|
|
154
|
+
name: str,
|
|
155
|
+
arguments: dict[str, Any] | None = None,
|
|
156
|
+
*,
|
|
157
|
+
team_id: str | None = None,
|
|
158
|
+
) -> McpPromptResponse:
|
|
159
|
+
body: dict[str, Any] = {"name": name, "arguments": arguments or {}}
|
|
160
|
+
if team_id is not None:
|
|
161
|
+
body["team_id"] = team_id
|
|
162
|
+
data = await self._client.post(f"mcp/servers/{server_id}/prompts/get", json=body)
|
|
163
|
+
return McpPromptResponse.model_validate(data)
|
|
164
|
+
|
|
87
165
|
async def execute(
|
|
88
166
|
self,
|
|
89
167
|
server_id: str,
|