gumloop 0.3.2__tar.gz → 0.3.4__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.2 → gumloop-0.3.4}/PKG-INFO +2 -1
- {gumloop-0.3.2 → gumloop-0.3.4}/pyproject.toml +2 -1
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/__init__.py +1 -1
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/_client.py +7 -3
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/_http.py +6 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/agents.py +147 -2
- gumloop-0.3.4/src/gumloop/cli/commands/brain.py +66 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/main.py +2 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/__init__.py +4 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/agents.py +82 -0
- gumloop-0.3.4/src/gumloop/resources/brain.py +47 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/types.py +58 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_agents.py +67 -0
- gumloop-0.3.4/tests/cli/test_brain.py +81 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/integration/conftest.py +35 -0
- gumloop-0.3.4/tests/integration/test_live.py +170 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_agents.py +126 -0
- gumloop-0.3.4/tests/sdk/test_brain.py +75 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/uv.lock +3 -1
- gumloop-0.3.2/tests/integration/test_live.py +0 -64
- {gumloop-0.3.2 → gumloop-0.3.4}/.env.example +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/.gitignore +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/.pre-commit-config.yaml +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/.python-version +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/LICENSE +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/README.md +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/scripts/check +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/scripts/fmt +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/scripts/sdk_examples.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/errors.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/oauth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/py.typed +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_context.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_login.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_main.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/cli/test_skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/helpers.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_client.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.4}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gumloop
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: click>=8.0
|
|
15
16
|
Requires-Dist: httpx-sse>=0.4.0
|
|
16
17
|
Requires-Dist: httpx>=0.24.1
|
|
17
18
|
Requires-Dist: keyring>=24
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gumloop"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "Python client for the Gumloop API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -28,6 +28,7 @@ dependencies = [
|
|
|
28
28
|
"pydantic>=2.0.0",
|
|
29
29
|
"questionary>=2.0",
|
|
30
30
|
"rich",
|
|
31
|
+
"click>=8.0",
|
|
31
32
|
"typer>=0.25.1",
|
|
32
33
|
]
|
|
33
34
|
|
|
@@ -15,12 +15,14 @@ from gumloop.resources import Agents
|
|
|
15
15
|
from gumloop.resources import Artifacts
|
|
16
16
|
from gumloop.resources import AsyncAgents
|
|
17
17
|
from gumloop.resources import AsyncArtifacts
|
|
18
|
+
from gumloop.resources import AsyncBrain
|
|
18
19
|
from gumloop.resources import AsyncChat
|
|
19
20
|
from gumloop.resources import AsyncMCP
|
|
20
21
|
from gumloop.resources import AsyncModels
|
|
21
22
|
from gumloop.resources import AsyncSessions
|
|
22
23
|
from gumloop.resources import AsyncSkills
|
|
23
24
|
from gumloop.resources import AsyncTeams
|
|
25
|
+
from gumloop.resources import Brain
|
|
24
26
|
from gumloop.resources import Chat
|
|
25
27
|
from gumloop.resources import Models
|
|
26
28
|
from gumloop.resources import Sessions
|
|
@@ -41,9 +43,9 @@ def _derive_stream_base_url(base_url: str) -> str:
|
|
|
41
43
|
class Gumloop:
|
|
42
44
|
"""Sync Gumloop SDK client. Composes an :class:`HttpClient` with the
|
|
43
45
|
resource classes (``agents``, ``sessions``, ``mcp``, ``teams``,
|
|
44
|
-
``skills``, ``artifacts``, ``models``). The :attr:`oauth`
|
|
45
|
-
holds OAuth helpers — kept off the transport because it
|
|
46
|
-
bearer token rather than consuming one."""
|
|
46
|
+
``skills``, ``artifacts``, ``brain``, ``models``). The :attr:`oauth`
|
|
47
|
+
attribute holds OAuth helpers — kept off the transport because it
|
|
48
|
+
bootstraps the bearer token rather than consuming one."""
|
|
47
49
|
|
|
48
50
|
def __init__(
|
|
49
51
|
self,
|
|
@@ -85,6 +87,7 @@ class Gumloop:
|
|
|
85
87
|
self.teams = Teams(self._http)
|
|
86
88
|
self.skills = Skills(self._http)
|
|
87
89
|
self.artifacts = Artifacts(self._http)
|
|
90
|
+
self.brain = Brain(self._http)
|
|
88
91
|
self.oauth = OAuth(base_url=self.base_url, timeout=self.timeout)
|
|
89
92
|
|
|
90
93
|
def close(self) -> None:
|
|
@@ -137,6 +140,7 @@ class AsyncGumloop:
|
|
|
137
140
|
self.teams = AsyncTeams(self._http)
|
|
138
141
|
self.skills = AsyncSkills(self._http)
|
|
139
142
|
self.artifacts = AsyncArtifacts(self._http)
|
|
143
|
+
self.brain = AsyncBrain(self._http)
|
|
140
144
|
self.oauth = OAuth(base_url=self.base_url, timeout=self.timeout)
|
|
141
145
|
|
|
142
146
|
async def __aenter__(self) -> AsyncGumloop:
|
|
@@ -110,6 +110,9 @@ class HttpClient:
|
|
|
110
110
|
) -> Any:
|
|
111
111
|
return self._request("PATCH", path, json=json, data=_omit_none_params(data), files=files)
|
|
112
112
|
|
|
113
|
+
def put(self, path: str, *, json: Any = None) -> Any:
|
|
114
|
+
return self._request("PUT", path, json=json)
|
|
115
|
+
|
|
113
116
|
def delete(self, path: str) -> Any:
|
|
114
117
|
return self._request("DELETE", path)
|
|
115
118
|
|
|
@@ -251,6 +254,9 @@ class AsyncHttpClient:
|
|
|
251
254
|
) -> Any:
|
|
252
255
|
return await self._request("PATCH", path, json=json, data=_omit_none_params(data), files=files)
|
|
253
256
|
|
|
257
|
+
async def put(self, path: str, *, json: Any = None) -> Any:
|
|
258
|
+
return await self._request("PUT", path, json=json)
|
|
259
|
+
|
|
254
260
|
async def delete(self, path: str) -> Any:
|
|
255
261
|
return await self._request("DELETE", path)
|
|
256
262
|
|
|
@@ -144,6 +144,10 @@ def create_agent(
|
|
|
144
144
|
str | None,
|
|
145
145
|
typer.Option("--tools-file", help="Path to a JSON file containing the tools array."),
|
|
146
146
|
] = None,
|
|
147
|
+
skill_ids: Annotated[
|
|
148
|
+
str | None,
|
|
149
|
+
typer.Option("--skill-ids", help="Comma-separated ids of existing skills to attach."),
|
|
150
|
+
] = None,
|
|
147
151
|
json_output: Annotated[
|
|
148
152
|
bool,
|
|
149
153
|
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
@@ -186,6 +190,12 @@ def create_agent(
|
|
|
186
190
|
raise GumloopError("Tools JSON must be an array at the top level.")
|
|
187
191
|
tools = parsed
|
|
188
192
|
|
|
193
|
+
parsed_skill_ids = (
|
|
194
|
+
[skill_id.strip() for skill_id in skill_ids.split(",") if skill_id.strip()]
|
|
195
|
+
if skill_ids is not None
|
|
196
|
+
else None
|
|
197
|
+
)
|
|
198
|
+
|
|
189
199
|
response = cli.call_with_refresh(
|
|
190
200
|
lambda client: client.agents.create(
|
|
191
201
|
name=name,
|
|
@@ -193,6 +203,7 @@ def create_agent(
|
|
|
193
203
|
description=description,
|
|
194
204
|
system_prompt=resolved_prompt,
|
|
195
205
|
tools=tools,
|
|
206
|
+
skill_ids=parsed_skill_ids,
|
|
196
207
|
team_id=cli.effective_team_id,
|
|
197
208
|
)
|
|
198
209
|
)
|
|
@@ -233,11 +244,23 @@ def update_agent(
|
|
|
233
244
|
] = None,
|
|
234
245
|
tools_json: Annotated[
|
|
235
246
|
str | None,
|
|
236
|
-
typer.Option(
|
|
247
|
+
typer.Option(
|
|
248
|
+
"--tools-json",
|
|
249
|
+
help=(
|
|
250
|
+
"Inline JSON array of tool config objects."
|
|
251
|
+
" (legacy: replaces the entire tools list; prefer attach-mcp-server/detach-mcp-server)"
|
|
252
|
+
),
|
|
253
|
+
),
|
|
237
254
|
] = None,
|
|
238
255
|
tools_file: Annotated[
|
|
239
256
|
str | None,
|
|
240
|
-
typer.Option(
|
|
257
|
+
typer.Option(
|
|
258
|
+
"--tools-file",
|
|
259
|
+
help=(
|
|
260
|
+
"Path to a JSON file containing the tools array."
|
|
261
|
+
" (legacy: replaces the entire tools list; prefer attach-mcp-server/detach-mcp-server)"
|
|
262
|
+
),
|
|
263
|
+
),
|
|
241
264
|
] = None,
|
|
242
265
|
is_active: Annotated[
|
|
243
266
|
bool | None,
|
|
@@ -303,3 +326,125 @@ def update_agent(
|
|
|
303
326
|
return
|
|
304
327
|
|
|
305
328
|
console.print(f"[green]Updated agent[/green] {agent_id}")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@agents_app.command(
|
|
332
|
+
"attach-skills",
|
|
333
|
+
epilog="Example:\n gumloop agents attach-skills agent_abc skill_1 skill_2",
|
|
334
|
+
)
|
|
335
|
+
def attach_skills(
|
|
336
|
+
ctx: typer.Context,
|
|
337
|
+
agent_id: Annotated[str, typer.Argument(help="ID of the agent to attach skills to.")],
|
|
338
|
+
skill_ids: Annotated[list[str], typer.Argument(help="One or more skill ids to attach.")],
|
|
339
|
+
json_output: Annotated[
|
|
340
|
+
bool,
|
|
341
|
+
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
342
|
+
] = False,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Attach one or more skills to an agent (idempotent)."""
|
|
345
|
+
cli: CliContext = ctx.obj
|
|
346
|
+
try:
|
|
347
|
+
response = cli.call_with_refresh(lambda client: client.agents.attach_skills(agent_id, skill_ids))
|
|
348
|
+
except GumloopError as error:
|
|
349
|
+
exit_with_error(error, json_output=json_output)
|
|
350
|
+
|
|
351
|
+
if json_output:
|
|
352
|
+
print_json(response)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
console.print(f"[green]Attached[/green] {len(response.attached)} skill(s) to {agent_id}")
|
|
356
|
+
if response.already_attached:
|
|
357
|
+
console.print(f" Already attached: {', '.join(response.already_attached)}", markup=False, highlight=False)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@agents_app.command(
|
|
361
|
+
"detach-skills",
|
|
362
|
+
epilog="Example:\n gumloop agents detach-skills agent_abc skill_1 skill_2",
|
|
363
|
+
)
|
|
364
|
+
def detach_skills(
|
|
365
|
+
ctx: typer.Context,
|
|
366
|
+
agent_id: Annotated[str, typer.Argument(help="ID of the agent to detach skills from.")],
|
|
367
|
+
skill_ids: Annotated[list[str], typer.Argument(help="One or more skill ids to detach.")],
|
|
368
|
+
json_output: Annotated[
|
|
369
|
+
bool,
|
|
370
|
+
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
371
|
+
] = False,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Detach one or more skills from an agent (idempotent)."""
|
|
374
|
+
cli: CliContext = ctx.obj
|
|
375
|
+
try:
|
|
376
|
+
response = cli.call_with_refresh(lambda client: client.agents.detach_skills(agent_id, skill_ids))
|
|
377
|
+
except GumloopError as error:
|
|
378
|
+
exit_with_error(error, json_output=json_output)
|
|
379
|
+
|
|
380
|
+
if json_output:
|
|
381
|
+
print_json(response)
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
console.print(f"[green]Detached[/green] {len(response.detached)} skill(s) from {agent_id}")
|
|
385
|
+
if response.already_detached:
|
|
386
|
+
console.print(f" Already detached: {', '.join(response.already_detached)}", markup=False, highlight=False)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@agents_app.command(
|
|
390
|
+
"attach-mcp-server",
|
|
391
|
+
epilog="Example:\n gumloop agents attach-mcp-server agent_abc gmail --approval-mode off",
|
|
392
|
+
)
|
|
393
|
+
def attach_mcp_server(
|
|
394
|
+
ctx: typer.Context,
|
|
395
|
+
agent_id: Annotated[str, typer.Argument(help="ID of the agent to attach the MCP server to.")],
|
|
396
|
+
server_id: Annotated[str, typer.Argument(help="ID of the MCP server to attach.")],
|
|
397
|
+
approval_mode: Annotated[
|
|
398
|
+
str | None,
|
|
399
|
+
typer.Option("--approval-mode", help="Tool-call approval mode for this server."),
|
|
400
|
+
] = None,
|
|
401
|
+
json_output: Annotated[
|
|
402
|
+
bool,
|
|
403
|
+
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
404
|
+
] = False,
|
|
405
|
+
) -> None:
|
|
406
|
+
"""Attach an MCP server to an agent, or update its config (idempotent upsert)."""
|
|
407
|
+
cli: CliContext = ctx.obj
|
|
408
|
+
try:
|
|
409
|
+
response = cli.call_with_refresh(
|
|
410
|
+
lambda client: client.agents.attach_mcp_server(agent_id, server_id, approval_mode=approval_mode)
|
|
411
|
+
)
|
|
412
|
+
except GumloopError as error:
|
|
413
|
+
exit_with_error(error, json_output=json_output)
|
|
414
|
+
|
|
415
|
+
if json_output:
|
|
416
|
+
print_json(response)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
verb = "Attached" if response.created else "Updated"
|
|
420
|
+
console.print(f"[green]{verb}[/green] MCP server {escape_markup(server_id)} on {agent_id}")
|
|
421
|
+
if response.auth_status:
|
|
422
|
+
console.print(f" Auth status: {response.auth_status}", markup=False, highlight=False)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@agents_app.command(
|
|
426
|
+
"detach-mcp-server",
|
|
427
|
+
epilog="Example:\n gumloop agents detach-mcp-server agent_abc gmail",
|
|
428
|
+
)
|
|
429
|
+
def detach_mcp_server(
|
|
430
|
+
ctx: typer.Context,
|
|
431
|
+
agent_id: Annotated[str, typer.Argument(help="ID of the agent to detach the MCP server from.")],
|
|
432
|
+
server_id: Annotated[str, typer.Argument(help="ID of the MCP server to detach.")],
|
|
433
|
+
json_output: Annotated[
|
|
434
|
+
bool,
|
|
435
|
+
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
436
|
+
] = False,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Detach an MCP server from an agent (idempotent)."""
|
|
439
|
+
cli: CliContext = ctx.obj
|
|
440
|
+
try:
|
|
441
|
+
response = cli.call_with_refresh(lambda client: client.agents.detach_mcp_server(agent_id, server_id))
|
|
442
|
+
except GumloopError as error:
|
|
443
|
+
exit_with_error(error, json_output=json_output)
|
|
444
|
+
|
|
445
|
+
if json_output:
|
|
446
|
+
print_json(response)
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
console.print(f"[green]Detached[/green] MCP server {escape_markup(server_id)} from {agent_id}")
|
|
450
|
+
console.print(f" detached: {'true' if response.detached else 'false'}", markup=False, highlight=False)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from gumloop import GumloopError
|
|
8
|
+
from gumloop.cli.console import console
|
|
9
|
+
from gumloop.cli.console import print_json
|
|
10
|
+
from gumloop.cli.context import CliContext
|
|
11
|
+
from gumloop.cli.errors import exit_with_error
|
|
12
|
+
|
|
13
|
+
brain_app = typer.Typer(help="Search your Company Brain.", no_args_is_help=True, rich_markup_mode="rich")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@brain_app.command(
|
|
17
|
+
"search",
|
|
18
|
+
epilog=(
|
|
19
|
+
"Examples:\n"
|
|
20
|
+
' gumloop brain search "onboarding process"\n'
|
|
21
|
+
' gumloop brain search "pricing" --limit 5 --source notion --json'
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
def search_brain(
|
|
25
|
+
ctx: typer.Context,
|
|
26
|
+
query: Annotated[str, typer.Argument(help="Search query.")],
|
|
27
|
+
limit: Annotated[
|
|
28
|
+
int | None,
|
|
29
|
+
typer.Option("--limit", help="Maximum number of results to return (1-50)."),
|
|
30
|
+
] = None,
|
|
31
|
+
source: Annotated[
|
|
32
|
+
list[str] | None,
|
|
33
|
+
typer.Option("--source", help="Filter by source type (repeatable), e.g. notion, google_drive, slack."),
|
|
34
|
+
] = None,
|
|
35
|
+
json_output: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
typer.Option("--json", help="Print the raw SDK response as JSON."),
|
|
38
|
+
] = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Search indexed Company Brain sources."""
|
|
41
|
+
cli: CliContext = ctx.obj
|
|
42
|
+
try:
|
|
43
|
+
response = cli.call_with_refresh(
|
|
44
|
+
lambda client: client.brain.search(query, limit=limit, source_type=source or None)
|
|
45
|
+
)
|
|
46
|
+
except GumloopError as error:
|
|
47
|
+
exit_with_error(error, json_output=json_output)
|
|
48
|
+
|
|
49
|
+
if json_output:
|
|
50
|
+
print_json(response)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if not response.results:
|
|
54
|
+
console.print("No results found.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
console.print("SCORE", "SOURCE", "TITLE", "URL", sep="\t", soft_wrap=True)
|
|
58
|
+
for result in response.results:
|
|
59
|
+
console.print(
|
|
60
|
+
"" if result.score is None else f"{result.score:.3f}",
|
|
61
|
+
result.source or "",
|
|
62
|
+
result.title or "",
|
|
63
|
+
result.url or "",
|
|
64
|
+
sep="\t",
|
|
65
|
+
soft_wrap=True,
|
|
66
|
+
)
|
|
@@ -11,6 +11,7 @@ from gumloop.cli.commands.agents import agents_app
|
|
|
11
11
|
from gumloop.cli.commands.artifacts import artifacts_app
|
|
12
12
|
from gumloop.cli.commands.auth import login as login_command
|
|
13
13
|
from gumloop.cli.commands.auth import logout as logout_command
|
|
14
|
+
from gumloop.cli.commands.brain import brain_app
|
|
14
15
|
from gumloop.cli.commands.chat import chat_app
|
|
15
16
|
from gumloop.cli.commands.mcp import mcp_app
|
|
16
17
|
from gumloop.cli.commands.sessions import sessions_app
|
|
@@ -97,6 +98,7 @@ app.add_typer(sessions_app, name="sessions")
|
|
|
97
98
|
app.add_typer(skills_app, name="skills")
|
|
98
99
|
app.add_typer(artifacts_app, name="artifacts")
|
|
99
100
|
app.add_typer(chat_app, name="chat")
|
|
101
|
+
app.add_typer(brain_app, name="brain")
|
|
100
102
|
|
|
101
103
|
|
|
102
104
|
def _require_supported_platform() -> None:
|
|
@@ -6,6 +6,8 @@ from gumloop.resources.agents import AsyncModels
|
|
|
6
6
|
from gumloop.resources.agents import Models
|
|
7
7
|
from gumloop.resources.artifacts import Artifacts
|
|
8
8
|
from gumloop.resources.artifacts import AsyncArtifacts
|
|
9
|
+
from gumloop.resources.brain import AsyncBrain
|
|
10
|
+
from gumloop.resources.brain import Brain
|
|
9
11
|
from gumloop.resources.chat import AsyncChat
|
|
10
12
|
from gumloop.resources.chat import Chat
|
|
11
13
|
from gumloop.resources.mcp import MCP
|
|
@@ -23,12 +25,14 @@ __all__ = [
|
|
|
23
25
|
"Artifacts",
|
|
24
26
|
"AsyncAgents",
|
|
25
27
|
"AsyncArtifacts",
|
|
28
|
+
"AsyncBrain",
|
|
26
29
|
"AsyncChat",
|
|
27
30
|
"AsyncMCP",
|
|
28
31
|
"AsyncModels",
|
|
29
32
|
"AsyncSessions",
|
|
30
33
|
"AsyncSkills",
|
|
31
34
|
"AsyncTeams",
|
|
35
|
+
"Brain",
|
|
32
36
|
"Chat",
|
|
33
37
|
"Models",
|
|
34
38
|
"Sessions",
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from gumloop._http import AsyncHttpClient
|
|
7
8
|
from gumloop._http import HttpClient
|
|
8
9
|
from gumloop.types import AgentCreateRequest
|
|
9
10
|
from gumloop.types import AgentListResponse
|
|
11
|
+
from gumloop.types import AgentMcpServerDetachResponse
|
|
12
|
+
from gumloop.types import AgentMcpServerResponse
|
|
13
|
+
from gumloop.types import AgentMcpServersResponse
|
|
10
14
|
from gumloop.types import AgentResponse
|
|
15
|
+
from gumloop.types import AgentSkillsResponse
|
|
11
16
|
from gumloop.types import AgentUpdateRequest
|
|
12
17
|
from gumloop.types import EvaluationConfigResponse
|
|
13
18
|
from gumloop.types import EvaluationConfigUpdateRequest
|
|
14
19
|
from gumloop.types import EvaluationResultListResponse
|
|
15
20
|
from gumloop.types import EvaluationResultResponse
|
|
16
21
|
from gumloop.types import ModelListResponse
|
|
22
|
+
from gumloop.types import SkillListResponse
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _skill_id_list(skill_ids: str | Sequence[str]) -> list[str]:
|
|
26
|
+
return [skill_ids] if isinstance(skill_ids, str) else list(skill_ids)
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
class Agents:
|
|
@@ -49,10 +59,48 @@ class Agents:
|
|
|
49
59
|
request: AgentUpdateRequest | Mapping[str, Any] | None = None,
|
|
50
60
|
**kwargs: Any,
|
|
51
61
|
) -> AgentResponse:
|
|
62
|
+
"""``tools`` replaces the entire list when provided (legacy bulk path); prefer
|
|
63
|
+
attach_mcp_server/detach_mcp_server for individual MCP servers and
|
|
64
|
+
attach_skills/detach_skills for skills."""
|
|
52
65
|
return AgentResponse.model_validate(
|
|
53
66
|
self._client.patch(f"agents/{agent_id}", json=AgentUpdateRequest.build(request, **kwargs))
|
|
54
67
|
)
|
|
55
68
|
|
|
69
|
+
def attach_skills(self, agent_id: str, skill_ids: str | Sequence[str]) -> AgentSkillsResponse:
|
|
70
|
+
"""Attach skills to an agent. Idempotent: safe to retry, already-attached ids are reported."""
|
|
71
|
+
return AgentSkillsResponse.model_validate(
|
|
72
|
+
self._client.patch(f"agents/{agent_id}/skills", json={"attach": _skill_id_list(skill_ids)})
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def detach_skills(self, agent_id: str, skill_ids: str | Sequence[str]) -> AgentSkillsResponse:
|
|
76
|
+
"""Detach skills from an agent. Idempotent: safe to retry, already-detached ids are reported."""
|
|
77
|
+
return AgentSkillsResponse.model_validate(
|
|
78
|
+
self._client.patch(f"agents/{agent_id}/skills", json={"detach": _skill_id_list(skill_ids)})
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def list_skills(self, agent_id: str, **kwargs: Any) -> SkillListResponse:
|
|
82
|
+
return SkillListResponse.model_validate(
|
|
83
|
+
self._client.get("skills", params={"agent_id": agent_id, **kwargs})
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def attach_mcp_server(self, agent_id: str, server_id: str, **config: Any) -> AgentMcpServerResponse:
|
|
87
|
+
"""Attach an MCP server, or update its config if already attached (idempotent upsert)."""
|
|
88
|
+
return AgentMcpServerResponse.model_validate(
|
|
89
|
+
self._client.put(
|
|
90
|
+
f"agents/{agent_id}/mcp-servers/{server_id}",
|
|
91
|
+
json={k: v for k, v in config.items() if v is not None},
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def detach_mcp_server(self, agent_id: str, server_id: str) -> AgentMcpServerDetachResponse:
|
|
96
|
+
"""Detach an MCP server from an agent. Idempotent: safe to retry."""
|
|
97
|
+
return AgentMcpServerDetachResponse.model_validate(
|
|
98
|
+
self._client.delete(f"agents/{agent_id}/mcp-servers/{server_id}")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def list_mcp_servers(self, agent_id: str) -> AgentMcpServersResponse:
|
|
102
|
+
return AgentMcpServersResponse.model_validate(self._client.get(f"agents/{agent_id}/mcp-servers"))
|
|
103
|
+
|
|
56
104
|
def get_evaluation_config(self, agent_id: str) -> EvaluationConfigResponse:
|
|
57
105
|
return EvaluationConfigResponse.model_validate(self._client.get(f"agents/{agent_id}/evaluation-config"))
|
|
58
106
|
|
|
@@ -133,9 +181,43 @@ class AsyncAgents:
|
|
|
133
181
|
request: AgentUpdateRequest | Mapping[str, Any] | None = None,
|
|
134
182
|
**kwargs: Any,
|
|
135
183
|
) -> AgentResponse:
|
|
184
|
+
"""``tools`` replaces the entire list when provided (legacy bulk path); prefer
|
|
185
|
+
attach_mcp_server/detach_mcp_server for individual MCP servers and
|
|
186
|
+
attach_skills/detach_skills for skills."""
|
|
136
187
|
data = await self._client.patch(f"agents/{agent_id}", json=AgentUpdateRequest.build(request, **kwargs))
|
|
137
188
|
return AgentResponse.model_validate(data)
|
|
138
189
|
|
|
190
|
+
async def attach_skills(self, agent_id: str, skill_ids: str | Sequence[str]) -> AgentSkillsResponse:
|
|
191
|
+
"""Attach skills to an agent. Idempotent: safe to retry, already-attached ids are reported."""
|
|
192
|
+
data = await self._client.patch(f"agents/{agent_id}/skills", json={"attach": _skill_id_list(skill_ids)})
|
|
193
|
+
return AgentSkillsResponse.model_validate(data)
|
|
194
|
+
|
|
195
|
+
async def detach_skills(self, agent_id: str, skill_ids: str | Sequence[str]) -> AgentSkillsResponse:
|
|
196
|
+
"""Detach skills from an agent. Idempotent: safe to retry, already-detached ids are reported."""
|
|
197
|
+
data = await self._client.patch(f"agents/{agent_id}/skills", json={"detach": _skill_id_list(skill_ids)})
|
|
198
|
+
return AgentSkillsResponse.model_validate(data)
|
|
199
|
+
|
|
200
|
+
async def list_skills(self, agent_id: str, **kwargs: Any) -> SkillListResponse:
|
|
201
|
+
data = await self._client.get("skills", params={"agent_id": agent_id, **kwargs})
|
|
202
|
+
return SkillListResponse.model_validate(data)
|
|
203
|
+
|
|
204
|
+
async def attach_mcp_server(self, agent_id: str, server_id: str, **config: Any) -> AgentMcpServerResponse:
|
|
205
|
+
"""Attach an MCP server, or update its config if already attached (idempotent upsert)."""
|
|
206
|
+
data = await self._client.put(
|
|
207
|
+
f"agents/{agent_id}/mcp-servers/{server_id}",
|
|
208
|
+
json={k: v for k, v in config.items() if v is not None},
|
|
209
|
+
)
|
|
210
|
+
return AgentMcpServerResponse.model_validate(data)
|
|
211
|
+
|
|
212
|
+
async def detach_mcp_server(self, agent_id: str, server_id: str) -> AgentMcpServerDetachResponse:
|
|
213
|
+
"""Detach an MCP server from an agent. Idempotent: safe to retry."""
|
|
214
|
+
data = await self._client.delete(f"agents/{agent_id}/mcp-servers/{server_id}")
|
|
215
|
+
return AgentMcpServerDetachResponse.model_validate(data)
|
|
216
|
+
|
|
217
|
+
async def list_mcp_servers(self, agent_id: str) -> AgentMcpServersResponse:
|
|
218
|
+
data = await self._client.get(f"agents/{agent_id}/mcp-servers")
|
|
219
|
+
return AgentMcpServersResponse.model_validate(data)
|
|
220
|
+
|
|
139
221
|
async def get_evaluation_config(self, agent_id: str) -> EvaluationConfigResponse:
|
|
140
222
|
data = await self._client.get(f"agents/{agent_id}/evaluation-config")
|
|
141
223
|
return EvaluationConfigResponse.model_validate(data)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from gumloop._http import AsyncHttpClient
|
|
6
|
+
from gumloop._http import HttpClient
|
|
7
|
+
from gumloop.types import BrainSearchRequest
|
|
8
|
+
from gumloop.types import BrainSearchResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Brain:
|
|
12
|
+
def __init__(self, client: HttpClient) -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def search(
|
|
16
|
+
self,
|
|
17
|
+
query: str,
|
|
18
|
+
*,
|
|
19
|
+
limit: int | None = None,
|
|
20
|
+
source_type: list[str] | None = None,
|
|
21
|
+
**kwargs: Any,
|
|
22
|
+
) -> BrainSearchResponse:
|
|
23
|
+
return BrainSearchResponse.model_validate(
|
|
24
|
+
self._client.post(
|
|
25
|
+
"brain/search",
|
|
26
|
+
json=BrainSearchRequest.build(query=query, limit=limit, source_type=source_type, **kwargs),
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncBrain:
|
|
32
|
+
def __init__(self, client: AsyncHttpClient) -> None:
|
|
33
|
+
self._client = client
|
|
34
|
+
|
|
35
|
+
async def search(
|
|
36
|
+
self,
|
|
37
|
+
query: str,
|
|
38
|
+
*,
|
|
39
|
+
limit: int | None = None,
|
|
40
|
+
source_type: list[str] | None = None,
|
|
41
|
+
**kwargs: Any,
|
|
42
|
+
) -> BrainSearchResponse:
|
|
43
|
+
data = await self._client.post(
|
|
44
|
+
"brain/search",
|
|
45
|
+
json=BrainSearchRequest.build(query=query, limit=limit, source_type=source_type, **kwargs),
|
|
46
|
+
)
|
|
47
|
+
return BrainSearchResponse.model_validate(data)
|
|
@@ -53,6 +53,7 @@ class AgentCreateRequest(_Model):
|
|
|
53
53
|
system_prompt: str | None = None
|
|
54
54
|
tools: list[dict[str, Any]] = Field(default_factory=list)
|
|
55
55
|
resources: list[dict[str, Any]] = Field(default_factory=list)
|
|
56
|
+
skill_ids: list[str] | None = None
|
|
56
57
|
metadata: dict[str, Any] | None = None
|
|
57
58
|
folder_id: str | None = None
|
|
58
59
|
is_active: bool = True
|
|
@@ -80,6 +81,7 @@ class Agent(_Model):
|
|
|
80
81
|
is_active: bool = False
|
|
81
82
|
tools: list[dict[str, Any]] = Field(default_factory=list)
|
|
82
83
|
resources: list[dict[str, Any]] = Field(default_factory=list)
|
|
84
|
+
skill_ids: list[str] | None = None
|
|
83
85
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
84
86
|
model_name: str | None = None
|
|
85
87
|
system_prompt: str | None = None
|
|
@@ -90,6 +92,33 @@ class Agent(_Model):
|
|
|
90
92
|
creator: CreatorPayload | None = None
|
|
91
93
|
|
|
92
94
|
|
|
95
|
+
class AgentSkillsResponse(_Model):
|
|
96
|
+
agent_id: str
|
|
97
|
+
skill_ids: list[str] = Field(default_factory=list)
|
|
98
|
+
attached: list[str] = Field(default_factory=list)
|
|
99
|
+
detached: list[str] = Field(default_factory=list)
|
|
100
|
+
already_attached: list[str] = Field(default_factory=list)
|
|
101
|
+
already_detached: list[str] = Field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class AgentMcpServerResponse(_Model):
|
|
105
|
+
agent_id: str
|
|
106
|
+
mcp_server: dict[str, Any] = Field(default_factory=dict)
|
|
107
|
+
created: bool = False
|
|
108
|
+
auth_status: str | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AgentMcpServerDetachResponse(_Model):
|
|
112
|
+
agent_id: str
|
|
113
|
+
server_id: str
|
|
114
|
+
detached: bool = False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AgentMcpServersResponse(_Model):
|
|
118
|
+
agent_id: str
|
|
119
|
+
mcp_servers: list[dict[str, Any]] = Field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
|
|
93
122
|
class AgentResponse(_Model):
|
|
94
123
|
agent: Agent
|
|
95
124
|
|
|
@@ -236,6 +265,35 @@ class SkillDownloadResponse(_Model):
|
|
|
236
265
|
major_version: int | None = None
|
|
237
266
|
|
|
238
267
|
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
# Brain types
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class BrainSearchRequest(_Model):
|
|
274
|
+
query: str
|
|
275
|
+
limit: int | None = None
|
|
276
|
+
source_type: list[str] | None = Field(default=None, min_length=1)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class BrainSearchResult(_Model):
|
|
280
|
+
document_id: str | None = None
|
|
281
|
+
source: str | None = None
|
|
282
|
+
title: str | None = None
|
|
283
|
+
content: str | None = None
|
|
284
|
+
url: str | None = None
|
|
285
|
+
score: float | None = None
|
|
286
|
+
updated_at: str | None = None
|
|
287
|
+
owner_name: str | None = None
|
|
288
|
+
owner_email: str | None = None
|
|
289
|
+
parent_title: str | None = None
|
|
290
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class BrainSearchResponse(_Model):
|
|
294
|
+
results: list[BrainSearchResult] = Field(default_factory=list)
|
|
295
|
+
|
|
296
|
+
|
|
239
297
|
# ---------------------------------------------------------------------------
|
|
240
298
|
# Artifact types
|
|
241
299
|
# ---------------------------------------------------------------------------
|