gumloop 0.3.1__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 (78) hide show
  1. {gumloop-0.3.1 → gumloop-0.3.2}/PKG-INFO +1 -1
  2. {gumloop-0.3.1 → gumloop-0.3.2}/pyproject.toml +1 -1
  3. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/__init__.py +1 -1
  4. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/_http.py +2 -2
  5. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/sessions.py +93 -0
  6. gumloop-0.3.2/src/gumloop/resources/agents.py +183 -0
  7. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/sessions.py +62 -0
  8. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/types.py +71 -0
  9. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_sessions.py +27 -0
  10. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_agents.py +100 -0
  11. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_sessions.py +43 -0
  12. {gumloop-0.3.1 → gumloop-0.3.2}/uv.lock +2 -2
  13. gumloop-0.3.1/src/gumloop/resources/agents.py +0 -101
  14. {gumloop-0.3.1 → gumloop-0.3.2}/.env.example +0 -0
  15. {gumloop-0.3.1 → gumloop-0.3.2}/.gitignore +0 -0
  16. {gumloop-0.3.1 → gumloop-0.3.2}/.pre-commit-config.yaml +0 -0
  17. {gumloop-0.3.1 → gumloop-0.3.2}/.python-version +0 -0
  18. {gumloop-0.3.1 → gumloop-0.3.2}/LICENSE +0 -0
  19. {gumloop-0.3.1 → gumloop-0.3.2}/README.md +0 -0
  20. {gumloop-0.3.1 → gumloop-0.3.2}/scripts/check +0 -0
  21. {gumloop-0.3.1 → gumloop-0.3.2}/scripts/fmt +0 -0
  22. {gumloop-0.3.1 → gumloop-0.3.2}/scripts/sdk_examples.py +0 -0
  23. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/_client.py +0 -0
  24. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/__init__.py +0 -0
  25. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/__init__.py +0 -0
  26. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/_args_input.py +0 -0
  27. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/_downloads.py +0 -0
  28. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/agents.py +0 -0
  29. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/artifacts.py +0 -0
  30. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/auth.py +0 -0
  31. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/chat.py +0 -0
  32. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/mcp.py +0 -0
  33. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/commands/skills.py +0 -0
  34. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/console.py +0 -0
  35. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/context.py +0 -0
  36. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/credentials.py +0 -0
  37. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/errors.py +0 -0
  38. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/main.py +0 -0
  39. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/cli/oauth.py +0 -0
  40. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/errors.py +0 -0
  41. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/oauth.py +0 -0
  42. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/py.typed +0 -0
  43. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/__init__.py +0 -0
  44. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/artifacts.py +0 -0
  45. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/chat.py +0 -0
  46. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/mcp.py +0 -0
  47. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/skills.py +0 -0
  48. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/resources/teams.py +0 -0
  49. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/spec/__init__.py +0 -0
  50. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/spec/_compat.py +0 -0
  51. {gumloop-0.3.1 → gumloop-0.3.2}/src/gumloop/spec/_extensions.py +0 -0
  52. {gumloop-0.3.1 → gumloop-0.3.2}/tests/__init__.py +0 -0
  53. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/__init__.py +0 -0
  54. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/conftest.py +0 -0
  55. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_agents.py +0 -0
  56. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_args_input.py +0 -0
  57. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_artifacts.py +0 -0
  58. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_chat.py +0 -0
  59. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_context.py +0 -0
  60. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_credentials.py +0 -0
  61. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_login.py +0 -0
  62. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_main.py +0 -0
  63. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_mcp.py +0 -0
  64. {gumloop-0.3.1 → gumloop-0.3.2}/tests/cli/test_skills.py +0 -0
  65. {gumloop-0.3.1 → gumloop-0.3.2}/tests/conftest.py +0 -0
  66. {gumloop-0.3.1 → gumloop-0.3.2}/tests/integration/conftest.py +0 -0
  67. {gumloop-0.3.1 → gumloop-0.3.2}/tests/integration/test_live.py +0 -0
  68. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/__init__.py +0 -0
  69. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/conftest.py +0 -0
  70. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/helpers.py +0 -0
  71. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_artifacts.py +0 -0
  72. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_chat.py +0 -0
  73. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_client.py +0 -0
  74. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_mcp.py +0 -0
  75. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_oauth.py +0 -0
  76. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_skills.py +0 -0
  77. {gumloop-0.3.1 → gumloop-0.3.2}/tests/sdk/test_teams.py +0 -0
  78. {gumloop-0.3.1 → 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.1
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.1"
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.1"
9
+ __version__ = "0.3.2"
10
10
  __all__ = [
11
11
  "APIStatusError",
12
12
  "AsyncGumloop",
@@ -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
 
@@ -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))
@@ -11,6 +11,7 @@ from gumloop._http import AsyncHttpClient
11
11
  from gumloop._http import HttpClient
12
12
  from gumloop.types import SessionContinueRequest
13
13
  from gumloop.types import SessionCreateRequest
14
+ from gumloop.types import SessionListResponse
14
15
  from gumloop.types import SessionResponse
15
16
  from gumloop.types import StreamEvent
16
17
 
@@ -19,6 +20,37 @@ class Sessions:
19
20
  def __init__(self, client: HttpClient) -> None:
20
21
  self._client = client
21
22
 
23
+ def list(
24
+ self,
25
+ agent_id: str,
26
+ *,
27
+ search: str | None = None,
28
+ state: str | None = None,
29
+ type: str | None = None,
30
+ creator_user_id: str | None = None,
31
+ trigger_id: str | None = None,
32
+ sort_order: str | None = None,
33
+ page_size: int | None = None,
34
+ cursor: str | None = None,
35
+ **kwargs: Any,
36
+ ) -> SessionListResponse:
37
+ return SessionListResponse.model_validate(
38
+ self._client.get(
39
+ f"agents/{agent_id}/sessions",
40
+ params={
41
+ "search": search,
42
+ "state": state,
43
+ "type": type,
44
+ "creator_user_id": creator_user_id,
45
+ "trigger_id": trigger_id,
46
+ "sort_order": sort_order,
47
+ "page_size": page_size,
48
+ "cursor": cursor,
49
+ **kwargs,
50
+ },
51
+ )
52
+ )
53
+
22
54
  @overload
23
55
  def create(
24
56
  self,
@@ -117,6 +149,36 @@ class AsyncSessions:
117
149
  def __init__(self, client: AsyncHttpClient) -> None:
118
150
  self._client = client
119
151
 
152
+ async def list(
153
+ self,
154
+ agent_id: str,
155
+ *,
156
+ search: str | None = None,
157
+ state: str | None = None,
158
+ type: str | None = None,
159
+ creator_user_id: str | None = None,
160
+ trigger_id: str | None = None,
161
+ sort_order: str | None = None,
162
+ page_size: int | None = None,
163
+ cursor: str | None = None,
164
+ **kwargs: Any,
165
+ ) -> SessionListResponse:
166
+ data = await self._client.get(
167
+ f"agents/{agent_id}/sessions",
168
+ params={
169
+ "search": search,
170
+ "state": state,
171
+ "type": type,
172
+ "creator_user_id": creator_user_id,
173
+ "trigger_id": trigger_id,
174
+ "sort_order": sort_order,
175
+ "page_size": page_size,
176
+ "cursor": cursor,
177
+ **kwargs,
178
+ },
179
+ )
180
+ return SessionListResponse.model_validate(data)
181
+
120
182
  @overload
121
183
  async def create(
122
184
  self,
@@ -4,6 +4,7 @@ import json
4
4
  from typing import Any
5
5
  from typing import Literal
6
6
 
7
+ from pydantic import AliasChoices
7
8
  from pydantic import BaseModel
8
9
  from pydantic import ConfigDict
9
10
  from pydantic import Field
@@ -152,6 +153,8 @@ class MessagePayload(_Model):
152
153
  class Session(_Model):
153
154
  id: str
154
155
  agent_id: str
156
+ name: str | None = None
157
+ type: str | None = None
155
158
  state: str | None = None
156
159
  messages: list[MessagePayload] = Field(default_factory=list)
157
160
  created_at: str | None = None
@@ -169,6 +172,11 @@ class SessionResponse(_Model):
169
172
  queue_position: int | None = None
170
173
 
171
174
 
175
+ class SessionListResponse(_Model):
176
+ sessions: list[Session] = Field(default_factory=list)
177
+ next_cursor: str | None = None
178
+
179
+
172
180
  class StreamEvent(_Model):
173
181
  type: str | None = None
174
182
  data: Any = None
@@ -426,3 +434,66 @@ class McpPromptResponse(_Model):
426
434
  name: str | None = None
427
435
  description: str | None = None
428
436
  messages: list[McpPromptMessage] = Field(default_factory=list)
437
+
438
+
439
+ # ---------------------------------------------------------------------------
440
+ # Evaluation types
441
+ # ---------------------------------------------------------------------------
442
+
443
+
444
+ class EvaluationConfig(_Model):
445
+ agent_id: str
446
+ enabled: bool = False
447
+ is_active: bool = True
448
+ model_name: str | None = None
449
+ frequency: str | None = None
450
+ language: str | None = None
451
+ include_auto_tags: bool = False
452
+ interaction_types: list[Any] = Field(default_factory=list)
453
+ criteria: list[Any] = Field(default_factory=list)
454
+ tags: list[Any] = Field(default_factory=list)
455
+ data_points: list[Any] = Field(default_factory=list)
456
+ sentiment: dict[str, Any] | None = None
457
+ updated_ts: str | None = None
458
+
459
+
460
+ class EvaluationConfigResponse(_Model):
461
+ config: EvaluationConfig
462
+
463
+
464
+ class EvaluationConfigUpdateRequest(_Model):
465
+ enabled: bool | None = None
466
+ model_name: str | None = None
467
+ frequency: str | None = None
468
+ language: str | None = None
469
+ include_auto_tags: bool | None = None
470
+ interaction_types: list[Any] | None = None
471
+ criteria: list[Any] | None = None
472
+ tags: list[Any] | None = None
473
+ data_points: list[Any] | None = None
474
+ sentiment: dict[str, Any] | None = None
475
+
476
+
477
+ class EvaluationResult(_Model):
478
+ evaluation_id: str
479
+ session_id: str = Field(validation_alias=AliasChoices("interaction_id", "session_id"))
480
+ agent_id: str
481
+ created_ts: str | None = None
482
+ # "completed" | "failed"; grade/call_successful are null when failed.
483
+ status: str | None = None
484
+ grade: str | None = None
485
+ call_successful: str | None = None
486
+ sentiment: str | None = None
487
+ summary: str | None = None
488
+ criteria_results: list[Any] = Field(default_factory=list)
489
+ data_results: list[Any] = Field(default_factory=list)
490
+ applied_tags: list[Any] = Field(default_factory=list)
491
+
492
+
493
+ class EvaluationResultResponse(_Model):
494
+ evaluation: EvaluationResult | None = None
495
+
496
+
497
+ class EvaluationResultListResponse(_Model):
498
+ evaluations: list[EvaluationResult] = Field(default_factory=list)
499
+ next_cursor: str | None = None
@@ -59,6 +59,33 @@ def test_sessions_create_rejects_both_input_and_input_stdin(cli_runner: CliRunne
59
59
  assert result.exit_code != 0
60
60
 
61
61
 
62
+ @respx.mock
63
+ def test_sessions_list_forwards_filters(cli_runner: CliRunner) -> None:
64
+ route = respx.get(f"{API_BASE}/agents/agent_abc/sessions").mock(
65
+ return_value=httpx.Response(
66
+ 200,
67
+ json={
68
+ "sessions": [{"id": "session_xyz", "agent_id": "agent_abc", "state": "completed"}],
69
+ "next_cursor": None,
70
+ },
71
+ )
72
+ )
73
+ save_credentials(Credentials(api_key="key"))
74
+
75
+ result = cli_runner.invoke(
76
+ app,
77
+ ["sessions", "list", "agent_abc", "--state", "completed", "--search", "x", "--sort", "oldest", "--limit", "5"],
78
+ )
79
+
80
+ assert result.exit_code == 0, result.output
81
+ assert "session_xyz" in result.output
82
+ params = route.calls[0].request.url.params
83
+ assert params["state"] == "completed"
84
+ assert params["search"] == "x"
85
+ assert params["sort_order"] == "oldest"
86
+ assert params["page_size"] == "5"
87
+
88
+
62
89
  @respx.mock
63
90
  def test_sessions_get_calls_per_session_endpoint(cli_runner: CliRunner) -> None:
64
91
  respx.get(f"{API_BASE}/sessions/session_xyz").mock(
@@ -100,6 +100,106 @@ def test_models_list(client: Gumloop) -> None:
100
100
  assert client.models.list().model_groups[0]["id"] == "auto"
101
101
 
102
102
 
103
+ @respx.mock
104
+ def test_agents_get_evaluation_config(client: Gumloop) -> None:
105
+ respx.get(f"{API_BASE}/agents/agent_123/evaluation-config").mock(
106
+ return_value=httpx.Response(200, json={"config": {"agent_id": "agent_123", "enabled": False}})
107
+ )
108
+
109
+ result = client.agents.get_evaluation_config("agent_123")
110
+
111
+ assert result.config.agent_id == "agent_123"
112
+ assert result.config.enabled is False
113
+
114
+
115
+ @respx.mock
116
+ def test_agents_update_evaluation_config_patches_only_sent_fields(client: Gumloop) -> None:
117
+ route = respx.patch(f"{API_BASE}/agents/agent_123/evaluation-config").mock(
118
+ return_value=httpx.Response(200, json={"config": {"agent_id": "agent_123", "enabled": True}})
119
+ )
120
+
121
+ result = client.agents.update_evaluation_config("agent_123", enabled=True)
122
+
123
+ assert result.config.enabled is True
124
+ # PATCH merge: only the field we set goes on the wire — omitted fields aren't
125
+ # cleared client-side (the backend preserves them).
126
+ assert request_json(route.calls[0].request) == {"enabled": True}
127
+
128
+
129
+ @respx.mock
130
+ def test_agents_list_evaluations_sends_filters(client: Gumloop) -> None:
131
+ route = respx.get(f"{API_BASE}/agents/agent_123/evaluations").mock(
132
+ return_value=httpx.Response(
133
+ 200,
134
+ json={
135
+ "evaluations": [{"evaluation_id": "eval_1", "interaction_id": "i1", "agent_id": "agent_123"}],
136
+ "next_cursor": "cursor_2",
137
+ },
138
+ )
139
+ )
140
+
141
+ result = client.agents.list_evaluations("agent_123", grade="needs_review", page_size=10, cursor="cursor_1")
142
+
143
+ assert result.evaluations[0].evaluation_id == "eval_1"
144
+ assert result.next_cursor == "cursor_2"
145
+ params = route.calls[0].request.url.params
146
+ assert params["grade"] == "needs_review"
147
+ assert params["page_size"] == "10"
148
+ assert params["cursor"] == "cursor_1"
149
+
150
+
151
+ @respx.mock
152
+ def test_agents_get_evaluation(client: Gumloop) -> None:
153
+ respx.get(f"{API_BASE}/agents/agent_123/evaluations/eval_1").mock(
154
+ return_value=httpx.Response(
155
+ 200,
156
+ json={
157
+ "evaluation": {
158
+ "evaluation_id": "eval_1",
159
+ "interaction_id": "i1",
160
+ "agent_id": "agent_123",
161
+ "status": "completed",
162
+ "grade": "pass",
163
+ }
164
+ },
165
+ )
166
+ )
167
+
168
+ result = client.agents.get_evaluation("agent_123", "eval_1")
169
+
170
+ assert result.evaluation is not None
171
+ assert result.evaluation.evaluation_id == "eval_1"
172
+ assert result.evaluation.status == "completed"
173
+ assert result.evaluation.grade == "pass"
174
+
175
+
176
+ @respx.mock
177
+ def test_async_agents_evaluation_methods() -> None:
178
+ respx.get(f"{API_BASE}/agents/agent_123/evaluation-config").mock(
179
+ return_value=httpx.Response(200, json={"config": {"agent_id": "agent_123"}})
180
+ )
181
+ respx.patch(f"{API_BASE}/agents/agent_123/evaluation-config").mock(
182
+ return_value=httpx.Response(200, json={"config": {"agent_id": "agent_123", "enabled": True}})
183
+ )
184
+ respx.get(f"{API_BASE}/agents/agent_123/evaluations").mock(
185
+ return_value=httpx.Response(200, json={"evaluations": []})
186
+ )
187
+ respx.get(f"{API_BASE}/agents/agent_123/evaluations/eval_1").mock(
188
+ return_value=httpx.Response(
189
+ 200, json={"evaluation": {"evaluation_id": "eval_1", "interaction_id": "i1", "agent_id": "agent_123"}}
190
+ )
191
+ )
192
+
193
+ async def run() -> None:
194
+ async with AsyncGumloop(access_token="token") as client:
195
+ assert (await client.agents.get_evaluation_config("agent_123")).config.agent_id == "agent_123"
196
+ assert (await client.agents.update_evaluation_config("agent_123", enabled=True)).config.enabled is True
197
+ assert (await client.agents.list_evaluations("agent_123")).evaluations == []
198
+ assert (await client.agents.get_evaluation("agent_123", "eval_1")).evaluation.evaluation_id == "eval_1"
199
+
200
+ asyncio.run(run())
201
+
202
+
103
203
  @respx.mock
104
204
  def test_async_agents_models_and_user_methods() -> None:
105
205
  respx.get(f"{API_BASE}/agents").mock(return_value=httpx.Response(200, json={"agents": []}))
@@ -82,6 +82,44 @@ def test_sessions_retrieve_send_and_cancel_routes(client: Gumloop) -> None:
82
82
  assert request_json(cancel_route.calls[0].request) == {}
83
83
 
84
84
 
85
+ @respx.mock
86
+ def test_sessions_list_forwards_filters_and_returns_envelope(client: Gumloop) -> None:
87
+ route = respx.get(f"{API_BASE}/agents/agent_123/sessions").mock(
88
+ return_value=httpx.Response(
89
+ 200,
90
+ json={
91
+ "sessions": [
92
+ {"id": "session_123", "agent_id": "agent_123", "name": "Run", "type": "api", "state": "completed"}
93
+ ],
94
+ "next_cursor": "next",
95
+ },
96
+ )
97
+ )
98
+
99
+ result = client.sessions.list(
100
+ "agent_123",
101
+ search="invoice",
102
+ state="completed",
103
+ type="api",
104
+ sort_order="oldest",
105
+ page_size=10,
106
+ cursor="prev",
107
+ )
108
+
109
+ assert [s.id for s in result.sessions] == ["session_123"]
110
+ assert result.sessions[0].type == "api"
111
+ assert result.next_cursor == "next"
112
+ params = route.calls[0].request.url.params
113
+ assert params["search"] == "invoice"
114
+ assert params["state"] == "completed"
115
+ assert params["type"] == "api"
116
+ assert params["sort_order"] == "oldest"
117
+ assert params["page_size"] == "10"
118
+ assert params["cursor"] == "prev"
119
+ assert "creator_user_id" not in params
120
+ assert "trigger_id" not in params
121
+
122
+
85
123
  def test_sessions_send_requires_input_or_message() -> None:
86
124
  client = Gumloop(access_token="token")
87
125
 
@@ -163,6 +201,9 @@ def test_async_sessions_methods() -> None:
163
201
  200, json={"session": {"id": "session_123", "agent_id": "agent_123", "state": "cancelled"}}
164
202
  )
165
203
  )
204
+ respx.get(f"{API_BASE}/agents/agent_123/sessions").mock(
205
+ return_value=httpx.Response(200, json={"sessions": [{"id": "session_123", "agent_id": "agent_123"}]})
206
+ )
166
207
 
167
208
  async def run() -> None:
168
209
  async with AsyncGumloop(access_token="token") as client:
@@ -170,6 +211,8 @@ def test_async_sessions_methods() -> None:
170
211
  assert (await client.sessions.retrieve("session_123")).session.id == "session_123"
171
212
  assert (await client.sessions.send("session_123", input="Hello")).session.id == "session_123"
172
213
  assert (await client.sessions.cancel("session_123")).session.id == "session_123"
214
+ listed = await client.sessions.list("agent_123", state="completed")
215
+ assert [s.id for s in listed.sessions] == ["session_123"]
173
216
 
174
217
  asyncio.run(run())
175
218
 
@@ -328,7 +328,7 @@ name = "exceptiongroup"
328
328
  version = "1.3.1"
329
329
  source = { registry = "https://pypi.org/simple" }
330
330
  dependencies = [
331
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
331
+ { name = "typing-extensions" },
332
332
  ]
333
333
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
334
334
  wheels = [
@@ -355,7 +355,7 @@ wheels = [
355
355
 
356
356
  [[package]]
357
357
  name = "gumloop"
358
- version = "0.3.1"
358
+ version = "0.3.2"
359
359
  source = { editable = "." }
360
360
  dependencies = [
361
361
  { name = "httpx" },
@@ -1,101 +0,0 @@
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 ModelListResponse
13
-
14
-
15
- class Agents:
16
- def __init__(self, client: HttpClient) -> None:
17
- self._client = client
18
-
19
- def list(
20
- self,
21
- *,
22
- search: str | None = None,
23
- team_id: str | None = None,
24
- **kwargs: Any,
25
- ) -> AgentListResponse:
26
- return AgentListResponse.model_validate(
27
- self._client.get("agents", params={"search": search, "team_id": team_id, **kwargs})
28
- )
29
-
30
- def create(
31
- self,
32
- request: AgentCreateRequest | Mapping[str, Any] | None = None,
33
- **kwargs: Any,
34
- ) -> AgentResponse:
35
- return AgentResponse.model_validate(
36
- self._client.post("agents", json=AgentCreateRequest.build(request, **kwargs))
37
- )
38
-
39
- def retrieve(self, agent_id: str) -> AgentResponse:
40
- return AgentResponse.model_validate(self._client.get(f"agents/{agent_id}"))
41
-
42
- def update(
43
- self,
44
- agent_id: str,
45
- request: AgentUpdateRequest | Mapping[str, Any] | None = None,
46
- **kwargs: Any,
47
- ) -> AgentResponse:
48
- return AgentResponse.model_validate(
49
- self._client.patch(f"agents/{agent_id}", json=AgentUpdateRequest.build(request, **kwargs))
50
- )
51
-
52
-
53
- class Models:
54
- def __init__(self, client: HttpClient) -> None:
55
- self._client = client
56
-
57
- def list(self, **kwargs: Any) -> ModelListResponse:
58
- return ModelListResponse.model_validate(self._client.get("models", params=kwargs))
59
-
60
-
61
- class AsyncAgents:
62
- def __init__(self, client: AsyncHttpClient) -> None:
63
- self._client = client
64
-
65
- async def list(
66
- self,
67
- *,
68
- search: str | None = None,
69
- team_id: str | None = None,
70
- **kwargs: Any,
71
- ) -> AgentListResponse:
72
- data = await self._client.get("agents", params={"search": search, "team_id": team_id, **kwargs})
73
- return AgentListResponse.model_validate(data)
74
-
75
- async def create(
76
- self,
77
- request: AgentCreateRequest | Mapping[str, Any] | None = None,
78
- **kwargs: Any,
79
- ) -> AgentResponse:
80
- data = await self._client.post("agents", json=AgentCreateRequest.build(request, **kwargs))
81
- return AgentResponse.model_validate(data)
82
-
83
- async def retrieve(self, agent_id: str) -> AgentResponse:
84
- return AgentResponse.model_validate(await self._client.get(f"agents/{agent_id}"))
85
-
86
- async def update(
87
- self,
88
- agent_id: str,
89
- request: AgentUpdateRequest | Mapping[str, Any] | None = None,
90
- **kwargs: Any,
91
- ) -> AgentResponse:
92
- data = await self._client.patch(f"agents/{agent_id}", json=AgentUpdateRequest.build(request, **kwargs))
93
- return AgentResponse.model_validate(data)
94
-
95
-
96
- class AsyncModels:
97
- def __init__(self, client: AsyncHttpClient) -> None:
98
- self._client = client
99
-
100
- async def list(self, **kwargs: Any) -> ModelListResponse:
101
- return ModelListResponse.model_validate(await self._client.get("models", params=kwargs))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes