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.
Files changed (79) hide show
  1. {gumloop-0.3.0 → gumloop-0.3.2}/PKG-INFO +1 -1
  2. {gumloop-0.3.0 → gumloop-0.3.2}/pyproject.toml +1 -1
  3. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/__init__.py +1 -1
  4. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/_client.py +6 -4
  5. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/_http.py +2 -2
  6. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/mcp.py +176 -3
  7. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/sessions.py +93 -0
  8. gumloop-0.3.2/src/gumloop/resources/agents.py +183 -0
  9. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/mcp.py +78 -0
  10. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/sessions.py +62 -0
  11. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/types.py +179 -0
  12. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_mcp.py +98 -0
  13. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_sessions.py +27 -0
  14. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_agents.py +100 -0
  15. gumloop-0.3.2/tests/sdk/test_mcp.py +264 -0
  16. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_sessions.py +43 -0
  17. {gumloop-0.3.0 → gumloop-0.3.2}/uv.lock +2 -2
  18. gumloop-0.3.0/src/gumloop/resources/agents.py +0 -101
  19. gumloop-0.3.0/tests/sdk/test_mcp.py +0 -139
  20. {gumloop-0.3.0 → gumloop-0.3.2}/.env.example +0 -0
  21. {gumloop-0.3.0 → gumloop-0.3.2}/.gitignore +0 -0
  22. {gumloop-0.3.0 → gumloop-0.3.2}/.pre-commit-config.yaml +0 -0
  23. {gumloop-0.3.0 → gumloop-0.3.2}/.python-version +0 -0
  24. {gumloop-0.3.0 → gumloop-0.3.2}/LICENSE +0 -0
  25. {gumloop-0.3.0 → gumloop-0.3.2}/README.md +0 -0
  26. {gumloop-0.3.0 → gumloop-0.3.2}/scripts/check +0 -0
  27. {gumloop-0.3.0 → gumloop-0.3.2}/scripts/fmt +0 -0
  28. {gumloop-0.3.0 → gumloop-0.3.2}/scripts/sdk_examples.py +0 -0
  29. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/__init__.py +0 -0
  30. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/__init__.py +0 -0
  31. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/_args_input.py +0 -0
  32. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/_downloads.py +0 -0
  33. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/agents.py +0 -0
  34. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/artifacts.py +0 -0
  35. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/auth.py +0 -0
  36. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/chat.py +0 -0
  37. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/commands/skills.py +0 -0
  38. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/console.py +0 -0
  39. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/context.py +0 -0
  40. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/credentials.py +0 -0
  41. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/errors.py +0 -0
  42. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/main.py +0 -0
  43. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/cli/oauth.py +0 -0
  44. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/errors.py +0 -0
  45. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/oauth.py +0 -0
  46. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/py.typed +0 -0
  47. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/__init__.py +0 -0
  48. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/artifacts.py +0 -0
  49. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/chat.py +0 -0
  50. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/skills.py +0 -0
  51. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/resources/teams.py +0 -0
  52. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/spec/__init__.py +0 -0
  53. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/spec/_compat.py +0 -0
  54. {gumloop-0.3.0 → gumloop-0.3.2}/src/gumloop/spec/_extensions.py +0 -0
  55. {gumloop-0.3.0 → gumloop-0.3.2}/tests/__init__.py +0 -0
  56. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/__init__.py +0 -0
  57. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/conftest.py +0 -0
  58. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_agents.py +0 -0
  59. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_args_input.py +0 -0
  60. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_artifacts.py +0 -0
  61. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_chat.py +0 -0
  62. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_context.py +0 -0
  63. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_credentials.py +0 -0
  64. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_login.py +0 -0
  65. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_main.py +0 -0
  66. {gumloop-0.3.0 → gumloop-0.3.2}/tests/cli/test_skills.py +0 -0
  67. {gumloop-0.3.0 → gumloop-0.3.2}/tests/conftest.py +0 -0
  68. {gumloop-0.3.0 → gumloop-0.3.2}/tests/integration/conftest.py +0 -0
  69. {gumloop-0.3.0 → gumloop-0.3.2}/tests/integration/test_live.py +0 -0
  70. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/__init__.py +0 -0
  71. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/conftest.py +0 -0
  72. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/helpers.py +0 -0
  73. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_artifacts.py +0 -0
  74. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_chat.py +0 -0
  75. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_client.py +0 -0
  76. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_oauth.py +0 -0
  77. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_skills.py +0 -0
  78. {gumloop-0.3.0 → gumloop-0.3.2}/tests/sdk/test_teams.py +0 -0
  79. {gumloop-0.3.0 → gumloop-0.3.2}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gumloop
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Python client for the Gumloop API
5
5
  Project-URL: Homepage, https://github.com/gumloop/gumloop-py
6
6
  Project-URL: Bug Tracker, https://github.com/gumloop/gumloop-py/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gumloop"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Python client for the Gumloop API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -6,7 +6,7 @@ from gumloop.errors import AuthenticationError
6
6
  from gumloop.errors import GumloopError
7
7
  from gumloop.oauth import OAuth
8
8
 
9
- __version__ = "0.3.0"
9
+ __version__ = "0.3.2"
10
10
  __all__ = [
11
11
  "APIStatusError",
12
12
  "AsyncGumloop",
@@ -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 = DEFAULT_BASE_URL,
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
- self.base_url = base_url.rstrip("/")
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 = DEFAULT_BASE_URL,
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
- self.base_url = base_url.rstrip("/")
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 raw SDK response as JSON."),
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.content
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,