Current section
Files
Jump to
Current section
Files
README.md
# Gleanex
Elixir client for [Glean](https://www.glean.com), covering all four of Glean's
public APIs. The typed layer is generated from the OpenAPI descriptions Glean
publishes at [gleanwork/open-api](https://github.com/gleanwork/open-api), so it
tracks the real API rather than a hand-picked subset.
## Install
```elixir
def deps do
[
{:gleanex, "~> 0.1.0"}
]
end
```
## Use it
```elixir
config = Gleanex.new(domain: "mycompany", token: System.fetch_env!("GLEAN_API_TOKEN"))
{:ok, response} = Gleanex.search(config, "company holidays")
{:ok, answer} = Gleanex.chat(config, "What are the company holidays this year?")
```
`domain` is the backend subdomain Glean gave you, usually your email domain
without the TLD. Requests go to `https://{domain}-be.glean.com`.
With `GLEAN_INSTANCE` and `GLEAN_API_TOKEN` exported you can leave the config
out and each call falls back to the environment.
Those two shortcuts cover the common case. Everything else is a generated
operation:
```elixir
{:ok, response} =
Gleanex.Client.Search.search(
%{query: "holidays", pageSize: 50},
config: config
)
{:ok, agents} = Gleanex.Client.Agents.search_agents(%{}, config: config)
{:ok, _} = Gleanex.Admin.Governance.listpolicies(config: config)
```
The config travels in the trailing options, alongside per-call overrides like
`:receive_timeout`, `:retry` and `:req_options`.
## The four APIs
| Namespace | What it covers | Token |
| --- | --- | --- |
| `Gleanex.Client` | search, chat, agents, documents, collections, pins, insights | client |
| `Gleanex.Indexing` | pushing documents, people, groups and permissions into the index | indexing |
| `Gleanex.Platform` | agents, skills and the newer search endpoints | client |
| `Gleanex.Admin` | governance policies, reports, findings, datasource administration | client |
Client and Indexing tokens are not interchangeable. Build one config per scope,
and a mismatched call fails before it leaves your machine:
```elixir
indexing = Gleanex.new(domain: "mycompany", token: indexing_token, scope: :indexing)
{:ok, _} = Gleanex.Indexing.Documents.indexdocument(%{document: document}, config: indexing)
```
## Results
Every operation returns `{:ok, result}` or `{:error, %Gleanex.Error{}}`, never
both. Match on `reason` to tell failures apart:
```elixir
case Gleanex.search(config, "holidays") do
{:ok, response} ->
response.results
{:error, %Gleanex.Error{reason: :rate_limited, retry_after: seconds}} ->
back_off(seconds)
{:error, %Gleanex.Error{reason: :problem_detail, problem: problem}} ->
Logger.error(problem.detail)
{:error, error} ->
raise error
end
```
Successful responses are decoded into structs. Field names are Glean's own
camelCase, matching their documentation, so request maps and response structs
agree with each other:
```elixir
response.trackingToken
response.hasMoreResults
```
## Retries and timeouts
Transient failures are retried by default, honouring `Retry-After` on rate
limits. Change the policy globally or for one call:
```elixir
config = Gleanex.new(domain: "mycompany", token: token, retry: %Gleanex.Retry{max_retries: 5})
Gleanex.search(config, "holidays", retry: Gleanex.Retry.disabled())
Gleanex.search(config, "holidays", receive_timeout: 60_000)
```
## Paging
Cursor-paginated endpoints become a `Stream`:
```elixir
config
|> Gleanex.Pagination.stream(&Gleanex.Client.Search.search/2, %{query: "holidays"})
|> Stream.flat_map(& &1.results)
|> Enum.take(100)
```
## Streaming
Chat and agent runs can be consumed as they arrive:
```elixir
{:ok, chunks} = Gleanex.Streaming.chat(config, %{messages: messages})
{:ok, events} = Gleanex.Streaming.agent_run(config, %{agentId: "abc", input: %{}})
for event <- events do
case Gleanex.SSE.json_data(event) do
{:ok, payload} -> handle(payload)
{:error, _} -> :ok
end
end
```
## Bulk indexing
Bulk uploads are paged, and Glean only swaps in the new batch once it has seen
the last page. `Gleanex.Bulk` drives that protocol:
```elixir
Gleanex.Bulk.upload(
indexing_config,
&Gleanex.Indexing.Documents.bulkindexdocuments/2,
%{datasource: "mydatasource"},
:documents,
documents,
page_size: 500
)
```
## Telemetry
Every request emits a `[:gleanex, :request]` span with `:api`, `:operation`,
`:method`, `:url` and, on stop, `:status`.
## Working on Gleanex
The typed layer is generated and committed, so users need no Java, no Docker and
no generator dependency.
```sh
mix glean.specs # download Glean's descriptions into priv/openapi/
mix glean.gen # regenerate lib/gleanex/{client,indexing,platform,admin}/
mix test --cover # the suite, at an enforced 100% threshold
```
`priv/openapi/.api-version` records the exact upstream commit the committed code
came from. Regeneration is deterministic: with unchanged descriptions it should
leave the working tree clean.
Descriptions are taken from `source_specs/` upstream, not `final_specs/`. The
latter has code samples merged in, which inflates the Client API description
from under 400 KB to about 19 MB without adding anything a generator can use.
Do not hand-edit anything under `lib/gleanex/client`, `lib/gleanex/indexing`,
`lib/gleanex/platform` or `lib/gleanex/admin`. Naming and rendering are steered
from `config/config.exs` and the plugin in `dev/gleanex/generator/processor.ex`.
### Integration tests
The suite runs against stubs, which prove the library does what Gleanex expects
of it, not that this is what Glean expects. A wrong path prefix or a field name
that no longer matches the description would pass every stubbed test.
A separate read-only smoke test covers that, against a real deployment. It is
excluded unless asked for:
```sh
GLEAN_INSTANCE=mycompany GLEAN_API_TOKEN=... mix test --include integration
```
It only reads, and only through the Client API. The Indexing API writes to a
real search index, and a bulk upload replaces the previous batch, so it is left
to the stubbed tests rather than pointed at a live deployment.
### Releasing
Releases are driven by [release-please](https://github.com/googleapis/release-please),
run through [release-mate](https://github.com/release-mate/action) with a
short-lived GitHub App token.
Every Conventional Commit landed on `main` is collected into a release pull
request that stays open and updates itself. Merging it does four things: bumps
`@version` in `mix.exs`, rewrites `CHANGELOG.md`, tags the commit `vX.Y.Z` and
cuts the GitHub release. Nothing to run by hand, and no version to remember to
bump.
Which commits appear in the changelog follows `release-please-config.json`:
`feat`, `fix`, `perf` and `revert` are listed, everything else is recorded but
hidden. `bump-minor-pre-major` keeps breaking changes inside `0.x` rather than
jumping to `1.0.0`, and `initial-version` makes the very first release `0.1.0`
rather than release-please's default of `1.0.0`.
Cutting the GitHub release triggers `.github/workflows/publish.yml`, which runs
`mix hex.publish --yes`. It needs a `HEX_API_KEY` secret.
That makes merging the release pull request the point of no return: a Hex
version can never be reused or withdrawn, only deprecated.
## Licence
BSD 2-Clause. Gleanex is not affiliated with or endorsed by Glean.