gumloop 0.3.2__tar.gz → 0.3.3__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.3}/PKG-INFO +1 -1
- {gumloop-0.3.2 → gumloop-0.3.3}/pyproject.toml +1 -1
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/__init__.py +1 -1
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/_client.py +7 -3
- gumloop-0.3.3/src/gumloop/cli/commands/brain.py +66 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/main.py +2 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/__init__.py +4 -0
- gumloop-0.3.3/src/gumloop/resources/brain.py +47 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/types.py +29 -0
- gumloop-0.3.3/tests/cli/test_brain.py +81 -0
- gumloop-0.3.3/tests/sdk/test_brain.py +75 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/uv.lock +1 -1
- {gumloop-0.3.2 → gumloop-0.3.3}/.env.example +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/.gitignore +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/.pre-commit-config.yaml +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/.python-version +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/LICENSE +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/README.md +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/scripts/check +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/scripts/fmt +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/scripts/sdk_examples.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/_http.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/agents.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/errors.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/oauth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/py.typed +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/agents.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_agents.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_context.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_login.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_main.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/cli/test_skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/integration/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/integration/test_live.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/__init__.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/conftest.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/helpers.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_agents.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_client.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_mcp.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_sessions.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.3.2 → gumloop-0.3.3}/tests/test_client.py +0 -0
|
@@ -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:
|
|
@@ -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",
|
|
@@ -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)
|
|
@@ -236,6 +236,35 @@ class SkillDownloadResponse(_Model):
|
|
|
236
236
|
major_version: int | None = None
|
|
237
237
|
|
|
238
238
|
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Brain types
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class BrainSearchRequest(_Model):
|
|
245
|
+
query: str
|
|
246
|
+
limit: int | None = None
|
|
247
|
+
source_type: list[str] | None = Field(default=None, min_length=1)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class BrainSearchResult(_Model):
|
|
251
|
+
document_id: str | None = None
|
|
252
|
+
source: str | None = None
|
|
253
|
+
title: str | None = None
|
|
254
|
+
content: str | None = None
|
|
255
|
+
url: str | None = None
|
|
256
|
+
score: float | None = None
|
|
257
|
+
updated_at: str | None = None
|
|
258
|
+
owner_name: str | None = None
|
|
259
|
+
owner_email: str | None = None
|
|
260
|
+
parent_title: str | None = None
|
|
261
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class BrainSearchResponse(_Model):
|
|
265
|
+
results: list[BrainSearchResult] = Field(default_factory=list)
|
|
266
|
+
|
|
267
|
+
|
|
239
268
|
# ---------------------------------------------------------------------------
|
|
240
269
|
# Artifact types
|
|
241
270
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import respx
|
|
7
|
+
from typer.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from gumloop.cli.credentials import Credentials
|
|
10
|
+
from gumloop.cli.credentials import save_credentials
|
|
11
|
+
from gumloop.cli.main import app
|
|
12
|
+
from tests.sdk.helpers import API_BASE
|
|
13
|
+
|
|
14
|
+
_RESULT = {
|
|
15
|
+
"document_id": "notion:doc_1",
|
|
16
|
+
"source": "notion",
|
|
17
|
+
"title": "Onboarding",
|
|
18
|
+
"content": "How we onboard new teammates.",
|
|
19
|
+
"url": "https://notion.so/doc_1",
|
|
20
|
+
"score": 0.87,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@respx.mock
|
|
25
|
+
def test_brain_search_forwards_query_limit_and_sources(cli_runner: CliRunner) -> None:
|
|
26
|
+
route = respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": []}))
|
|
27
|
+
save_credentials(Credentials(api_key="key"))
|
|
28
|
+
|
|
29
|
+
result = cli_runner.invoke(
|
|
30
|
+
app,
|
|
31
|
+
["brain", "search", "onboarding", "--limit", "5", "--source", "notion", "--source", "slack", "--json"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert result.exit_code == 0, result.output
|
|
35
|
+
body = json.loads(route.calls[0].request.content)
|
|
36
|
+
assert body == {"query": "onboarding", "limit": 5, "source_type": ["notion", "slack"]}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@respx.mock
|
|
40
|
+
def test_brain_search_renders_result_table(cli_runner: CliRunner) -> None:
|
|
41
|
+
respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": [_RESULT]}))
|
|
42
|
+
save_credentials(Credentials(api_key="key"))
|
|
43
|
+
|
|
44
|
+
result = cli_runner.invoke(app, ["brain", "search", "onboarding"])
|
|
45
|
+
|
|
46
|
+
assert result.exit_code == 0, result.output
|
|
47
|
+
assert "Onboarding" in result.output
|
|
48
|
+
assert "notion" in result.output
|
|
49
|
+
assert "0.870" in result.output
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@respx.mock
|
|
53
|
+
def test_brain_search_prints_empty_state(cli_runner: CliRunner) -> None:
|
|
54
|
+
respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": []}))
|
|
55
|
+
save_credentials(Credentials(api_key="key"))
|
|
56
|
+
|
|
57
|
+
result = cli_runner.invoke(app, ["brain", "search", "nothing"])
|
|
58
|
+
|
|
59
|
+
assert result.exit_code == 0, result.output
|
|
60
|
+
assert "No results found." in result.output
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@respx.mock
|
|
64
|
+
def test_brain_search_surfaces_api_errors(cli_runner: CliRunner) -> None:
|
|
65
|
+
respx.post(f"{API_BASE}/brain/search").mock(
|
|
66
|
+
return_value=httpx.Response(
|
|
67
|
+
402,
|
|
68
|
+
json={
|
|
69
|
+
"error": {
|
|
70
|
+
"code": "credit_limit_exceeded",
|
|
71
|
+
"message": "Credit limit exceeded.",
|
|
72
|
+
"type": "payment_required_error",
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
save_credentials(Credentials(api_key="key"))
|
|
78
|
+
|
|
79
|
+
result = cli_runner.invoke(app, ["brain", "search", "q", "--json"])
|
|
80
|
+
|
|
81
|
+
assert result.exit_code != 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
import respx
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from gumloop import AsyncGumloop
|
|
11
|
+
from gumloop import Gumloop
|
|
12
|
+
from tests.sdk.helpers import API_BASE
|
|
13
|
+
from tests.sdk.helpers import request_json
|
|
14
|
+
|
|
15
|
+
_RESULT = {
|
|
16
|
+
"document_id": "notion:doc_1",
|
|
17
|
+
"source": "notion",
|
|
18
|
+
"title": "Onboarding",
|
|
19
|
+
"content": "How we onboard new teammates.",
|
|
20
|
+
"url": "https://notion.so/doc_1",
|
|
21
|
+
"score": 0.87,
|
|
22
|
+
"updated_at": "2026-01-02T03:04:05+00:00",
|
|
23
|
+
"owner_name": "Ada",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@respx.mock
|
|
28
|
+
def test_brain_search_posts_query_and_drops_unset_fields(client: Gumloop) -> None:
|
|
29
|
+
route = respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": [_RESULT]}))
|
|
30
|
+
|
|
31
|
+
result = client.brain.search("onboarding")
|
|
32
|
+
|
|
33
|
+
assert request_json(route.calls[0].request) == {"query": "onboarding"}
|
|
34
|
+
assert len(result.results) == 1
|
|
35
|
+
assert result.results[0].title == "Onboarding"
|
|
36
|
+
assert result.results[0].source == "notion"
|
|
37
|
+
assert result.results[0].score == 0.87
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@respx.mock
|
|
41
|
+
def test_brain_search_forwards_limit_and_source_type(client: Gumloop) -> None:
|
|
42
|
+
route = respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": []}))
|
|
43
|
+
|
|
44
|
+
result = client.brain.search("pricing", limit=5, source_type=["notion", "slack"])
|
|
45
|
+
|
|
46
|
+
assert request_json(route.calls[0].request) == {
|
|
47
|
+
"query": "pricing",
|
|
48
|
+
"limit": 5,
|
|
49
|
+
"source_type": ["notion", "slack"],
|
|
50
|
+
}
|
|
51
|
+
assert result.results == []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_brain_search_rejects_empty_source_type(client: Gumloop) -> None:
|
|
55
|
+
with pytest.raises(ValidationError):
|
|
56
|
+
client.brain.search("pricing", source_type=[])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@respx.mock
|
|
60
|
+
def test_brain_search_passes_through_unknown_kwargs(client: Gumloop) -> None:
|
|
61
|
+
route = respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": []}))
|
|
62
|
+
|
|
63
|
+
client.brain.search("q", future_param="x")
|
|
64
|
+
|
|
65
|
+
assert request_json(route.calls[0].request) == {"query": "q", "future_param": "x"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@respx.mock
|
|
69
|
+
def test_async_brain_search_mirrors_sync(async_client: AsyncGumloop) -> None:
|
|
70
|
+
route = respx.post(f"{API_BASE}/brain/search").mock(return_value=httpx.Response(200, json={"results": [_RESULT]}))
|
|
71
|
+
|
|
72
|
+
result = asyncio.run(async_client.brain.search("onboarding", limit=3))
|
|
73
|
+
|
|
74
|
+
assert request_json(route.calls[0].request) == {"query": "onboarding", "limit": 3}
|
|
75
|
+
assert result.results[0].document_id == "notion:doc_1"
|
|
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
|
|
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
|
|
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
|