ligaxai 0.1.0.dev13__tar.gz → 0.1.0.dev15__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.
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/CHANGELOG.md +14 -0
- ligaxai-0.1.0.dev15/PKG-INFO +605 -0
- ligaxai-0.1.0.dev15/README.md +571 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/examples/fastapi-app/README.md +1 -1
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/pyproject.toml +4 -2
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/__init__.py +2 -2
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_client.py +29 -8
- ligaxai-0.1.0.dev15/src/ligaxai/_version.py +1 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_client.py +39 -7
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_client_tokens.py +2 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_end_user_connections.py +1 -1
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_fastapi.py +2 -1
- ligaxai-0.1.0.dev15/tests/unit/test_no_deployment_hosts.py +71 -0
- ligaxai-0.1.0.dev15/tests/unit/test_readme.py +56 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_resources.py +3 -2
- ligaxai-0.1.0.dev13/PKG-INFO +0 -675
- ligaxai-0.1.0.dev13/README.md +0 -643
- ligaxai-0.1.0.dev13/src/ligaxai/_version.py +0 -1
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/.gitignore +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/LICENSE +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/conformance/README.md +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/conformance/cases/runs-create-payment-required.json +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/conformance/cases/runs-create.json +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/conformance/cases/runs-get.json +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_base_client.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_exceptions.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_generated/__init__.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_generated/models.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_logging.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_models.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_pagination.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_run_stream.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_streaming.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_types.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/_webhooks.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/fastapi.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/py.typed +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/__init__.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/_base.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/client_tokens.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/connect_sessions.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/conversations.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/end_users.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/files.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/keys.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/profiles.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/runs.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/usage.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/resources/webhooks.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/types.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/src/ligaxai/webhooks.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/__init__.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/conformance/__init__.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/conformance/test_cases.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/conftest.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/fixtures/chat-turn-long.json +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/__init__.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_errors.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_files.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_logging.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_pagination.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_retries.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_run_stream.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_streaming.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_text_assembler.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_webhook_endpoints.py +0 -0
- {ligaxai-0.1.0.dev13 → ligaxai-0.1.0.dev15}/tests/unit/test_webhooks.py +0 -0
|
@@ -8,6 +8,20 @@ All notable changes to this project are documented here. The format follows
|
|
|
8
8
|
|
|
9
9
|
### Changed
|
|
10
10
|
|
|
11
|
+
- **Docs: the README is rewritten as the user guide and PyPI page.** What you need and the
|
|
12
|
+
words it uses come first, then install, configure (both required variables), a complete
|
|
13
|
+
quickstart, common tasks, web apps, webhooks, reference tables and a section for AI coding
|
|
14
|
+
assistants. Every link is absolute (relative links break on PyPI), the CI badge points at
|
|
15
|
+
this repository, and contributor material moved to `CONTRIBUTING.md`.
|
|
16
|
+
`tests/unit/test_readme.py` checks that the examples parse and import only real names.
|
|
17
|
+
PyPI now lists Documentation and Issues links.
|
|
18
|
+
- **Breaking: no built-in base URL.** `DEFAULT_BASE_URL` is removed. The API root comes from
|
|
19
|
+
`base_url=` or `LIGAX_BASE_URL` for every environment, production included; without one the
|
|
20
|
+
constructor raises the new `MissingBaseURLError` (`APIError` + `ValueError`) before any
|
|
21
|
+
request. The vendored spec's `servers` entry is the relative `/api/public/v1`, and
|
|
22
|
+
`scripts/sync_spec.py` rewrites it that way on each sync, so no deployment host ships in
|
|
23
|
+
the package or its docs.
|
|
24
|
+
|
|
11
25
|
- CI runs on pushes to `main` and `dev` and ends in one `ci-gate` job for branch protection.
|
|
12
26
|
Pull requests from logins outside `.github/CONTRIBUTORS` are closed; issues stay open.
|
|
13
27
|
- Spec pinned to `public-api-spec-v0.7.0`: a profile reports `tool_access` (`read_only`,
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ligaxai
|
|
3
|
+
Version: 0.1.0.dev15
|
|
4
|
+
Summary: Official Python client for the LigaX Agent API: run your LigaX AI agents from Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ligaxai/ligaxai-python
|
|
6
|
+
Project-URL: Documentation, https://github.com/ligaxai/ligaxai-python#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/ligaxai/ligaxai-python
|
|
8
|
+
Project-URL: Issues, https://github.com/ligaxai/ligaxai-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/ligaxai/ligaxai-python/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Security, https://github.com/ligaxai/ligaxai-python/blob/main/SECURITY.md
|
|
11
|
+
Author: LigaX AI authors
|
|
12
|
+
License-Expression: Apache-2.0
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: LigaX AI,agent api,ai agents,client,ligax,sdk
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: anyio<5,>=4
|
|
27
|
+
Requires-Dist: httpx2<3,>=2.16
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
29
|
+
Requires-Dist: standardwebhooks<2,>=1.0
|
|
30
|
+
Requires-Dist: typing-extensions>=4.10
|
|
31
|
+
Provides-Extra: fastapi
|
|
32
|
+
Requires-Dist: fastapi<1,>=0.115; extra == 'fastapi'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# LigaX Python SDK
|
|
36
|
+
|
|
37
|
+
[](https://github.com/ligaxai/ligaxai-python/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/ligaxai/)
|
|
39
|
+
[](https://pypi.org/project/ligaxai/)
|
|
40
|
+
|
|
41
|
+
Run the AI agents you build in LigaX from your own Python code.
|
|
42
|
+
|
|
43
|
+
`ligaxai` is the official Python client for the LigaX Agent API. It has a sync client
|
|
44
|
+
(`LigaX`) and an async client (`AsyncLigaX`) with the same methods, typed results, safe
|
|
45
|
+
automatic retries, and streaming that can resume after a dropped connection.
|
|
46
|
+
|
|
47
|
+
**Contents:**
|
|
48
|
+
[What you need](https://github.com/ligaxai/ligaxai-python#what-you-need) ·
|
|
49
|
+
[Install](https://github.com/ligaxai/ligaxai-python#install) ·
|
|
50
|
+
[Configure](https://github.com/ligaxai/ligaxai-python#configure) ·
|
|
51
|
+
[Quickstart](https://github.com/ligaxai/ligaxai-python#quickstart) ·
|
|
52
|
+
[Common tasks](https://github.com/ligaxai/ligaxai-python#common-tasks) ·
|
|
53
|
+
[Web apps](https://github.com/ligaxai/ligaxai-python#web-apps) ·
|
|
54
|
+
[Webhooks](https://github.com/ligaxai/ligaxai-python#webhooks) ·
|
|
55
|
+
[Configuration reference](https://github.com/ligaxai/ligaxai-python#configuration-reference) ·
|
|
56
|
+
[Errors](https://github.com/ligaxai/ligaxai-python#errors) ·
|
|
57
|
+
[Retries and timeouts](https://github.com/ligaxai/ligaxai-python#retries-and-timeouts) ·
|
|
58
|
+
[Security](https://github.com/ligaxai/ligaxai-python#security) ·
|
|
59
|
+
[API reference](https://github.com/ligaxai/ligaxai-python#api-reference) ·
|
|
60
|
+
[For AI coding assistants](https://github.com/ligaxai/ligaxai-python#for-ai-coding-assistants)
|
|
61
|
+
|
|
62
|
+
## What you need
|
|
63
|
+
|
|
64
|
+
Get these four things from your LigaX workspace before you write any code:
|
|
65
|
+
|
|
66
|
+
| You need | Looks like | Where to get it |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| **API base URL** | `https://<your-ligax-host>/api/public/v1` | Your LigaX administrator. It is the address of *your* LigaX platform. |
|
|
69
|
+
| **Secret API key** | `lgx_sk_…` | LigaX → **Settings → Developers → API keys**. Keep it on your server. |
|
|
70
|
+
| **Agent profile ID** | `prf_…` | LigaX → **Settings → Developers**. A profile is a published agent your key may run. |
|
|
71
|
+
| **Python** | 3.11 or newer | |
|
|
72
|
+
|
|
73
|
+
Words used on this page:
|
|
74
|
+
|
|
75
|
+
- **Agent profile** (`prf_…`): an agent you built in LigaX and published for API use.
|
|
76
|
+
- **Run**: one request to an agent. You send `input` text and get back `output.text`.
|
|
77
|
+
- **Conversation** (`conv_…`): a thread of runs. Pass its id to continue where you left off.
|
|
78
|
+
- **End user**: a person in *your* app that a run acts for, named by your own user id.
|
|
79
|
+
- **Approval**: a run can stop with status `requires_action` to ask before it acts. You
|
|
80
|
+
approve or reject it.
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install ligaxai
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Add the FastAPI helpers with `pip install "ligaxai[fastapi]"`. With uv: `uv add ligaxai`.
|
|
89
|
+
|
|
90
|
+
## Configure
|
|
91
|
+
|
|
92
|
+
Set two environment variables. Both are required.
|
|
93
|
+
|
|
94
|
+
| Variable | Required | Value |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `LIGAX_BASE_URL` | yes | Your platform's API root, for example `https://<your-ligax-host>/api/public/v1` |
|
|
97
|
+
| `LIGAX_API_KEY` | yes | Your secret API key, `lgx_sk_…` |
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export LIGAX_BASE_URL="https://<your-ligax-host>/api/public/v1"
|
|
101
|
+
export LIGAX_API_KEY="lgx_sk_..."
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`<your-ligax-host>` is a placeholder: replace it with your platform's host. The SDK has **no
|
|
105
|
+
built-in address**, for any environment. If the base URL is missing, creating the client
|
|
106
|
+
raises `MissingBaseURLError`. If the key is missing, it raises `MissingAPIKeyError`. Both
|
|
107
|
+
happen before any network call.
|
|
108
|
+
|
|
109
|
+
You can also pass both values in code: `LigaX(base_url="...", api_key="...")`. An argument
|
|
110
|
+
wins over the environment variable.
|
|
111
|
+
|
|
112
|
+
## Quickstart
|
|
113
|
+
|
|
114
|
+
Save this as `hello.py` and run `python hello.py`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from ligaxai import LigaX
|
|
118
|
+
|
|
119
|
+
client = LigaX() # reads LIGAX_BASE_URL and LIGAX_API_KEY
|
|
120
|
+
|
|
121
|
+
run = client.runs.wait(
|
|
122
|
+
profile_id="prf_...", # your agent profile ID
|
|
123
|
+
input="Summarise yesterday's P1 incidents.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if run.status == "completed":
|
|
127
|
+
print(run.output.text)
|
|
128
|
+
else:
|
|
129
|
+
print("Run ended with status:", run.status, run.error)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
It prints the agent's answer. `runs.wait` sends the request and waits until the run
|
|
133
|
+
finishes or needs your approval.
|
|
134
|
+
|
|
135
|
+
To check your setup first, call `client.keys.self()`. It returns your key's name, scopes
|
|
136
|
+
and spend limit. `client.profiles.list()` returns the profiles your key may run.
|
|
137
|
+
|
|
138
|
+
## Common tasks
|
|
139
|
+
|
|
140
|
+
### Stream the answer as it is written
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from ligaxai import LigaX
|
|
144
|
+
|
|
145
|
+
client = LigaX()
|
|
146
|
+
|
|
147
|
+
with client.runs.stream(profile_id="prf_...", input="Draft a reply to ticket 4411.") as stream:
|
|
148
|
+
for text in stream.text_stream():
|
|
149
|
+
print(text, end="", flush=True)
|
|
150
|
+
|
|
151
|
+
print()
|
|
152
|
+
print(stream.status, stream.run_id, stream.conversation_id)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`text_stream()` yields only the new text each time. To see every event (tool calls,
|
|
156
|
+
sources, artifacts), iterate `stream` itself: each item is a `RunEvent` with `type`, `seq`,
|
|
157
|
+
`payload` and `run_id`. `stream.final_text()` returns the whole answer.
|
|
158
|
+
|
|
159
|
+
If the connection drops, resume from the last event you received. Nothing is repeated:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
resumed = client.runs.follow_events(stream.run_id, after=stream.last_seq)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Use async
|
|
166
|
+
|
|
167
|
+
`AsyncLigaX` has the same methods. Await them, and use `async with` for streams:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import asyncio
|
|
171
|
+
|
|
172
|
+
from ligaxai import AsyncLigaX
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def main() -> None:
|
|
176
|
+
async with AsyncLigaX() as client:
|
|
177
|
+
run = await client.runs.wait(profile_id="prf_...", input="Hello")
|
|
178
|
+
print(run.output.text)
|
|
179
|
+
|
|
180
|
+
async with client.runs.stream(profile_id="prf_...", input="And again") as stream:
|
|
181
|
+
async for text in stream.text_stream():
|
|
182
|
+
print(text, end="", flush=True)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
asyncio.run(main())
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Continue a conversation
|
|
189
|
+
|
|
190
|
+
Every run belongs to a conversation. Pass `conversation_id` to continue it:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
first = client.runs.wait(profile_id="prf_...", input="Open a ticket for the VPN outage")
|
|
194
|
+
follow_up = client.runs.wait(
|
|
195
|
+
profile_id="prf_...",
|
|
196
|
+
input="Make it priority 2",
|
|
197
|
+
conversation_id=first.conversation_id,
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Act for one of your users
|
|
202
|
+
|
|
203
|
+
Pass `end_user` with your own user id. The run then uses that user's connected accounts
|
|
204
|
+
and conversations, and only that user can continue them:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
run = client.runs.wait(profile_id="prf_...", input="What is on my calendar?", end_user="user-42")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Take the user id from your own login session, never from the browser's request.
|
|
211
|
+
|
|
212
|
+
### Approve or reject an action
|
|
213
|
+
|
|
214
|
+
An agent can stop and ask before it acts, for example before it sends an email. The run
|
|
215
|
+
then has status `requires_action`:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
run = client.runs.wait(profile_id="prf_...", input="Email the team about the outage")
|
|
219
|
+
|
|
220
|
+
if run.status == "requires_action" and run.required_action:
|
|
221
|
+
print("The agent asks:", run.required_action.prompt)
|
|
222
|
+
run = client.runs.resolve_interrupt(
|
|
223
|
+
run.id, run.required_action.interrupt_id, decision="approve" # or "reject"
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Send a file
|
|
228
|
+
|
|
229
|
+
Upload a file, then attach its id to a run. The key needs the `files:write` scope:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
f = client.files.create(file="contract.pdf") # a path, bytes, or a file opened in "rb" mode
|
|
233
|
+
run = client.runs.wait(
|
|
234
|
+
profile_id="prf_...",
|
|
235
|
+
input="Summarise the termination clause",
|
|
236
|
+
attachments=[f.id],
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- Up to 25 MB per file and 10 files per run. A larger file raises `ValueError` before upload.
|
|
241
|
+
- Files are kept for 30 days. `client.files.delete(f.id)` removes one early.
|
|
242
|
+
- `files.get(id)` returns metadata only. There is no download.
|
|
243
|
+
- Whether a file type can be read depends on the agent's model.
|
|
244
|
+
|
|
245
|
+
### Check a run later
|
|
246
|
+
|
|
247
|
+
Start a run without waiting, then read it:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
run = client.runs.create(profile_id="prf_...", input="Prepare the weekly report", mode="async")
|
|
251
|
+
run = client.runs.get(run.id) # status: queued, running, completed, ...
|
|
252
|
+
client.runs.cancel(run.id) # stop it if you no longer need it
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A run's `status` is one of `queued`, `running`, `requires_action`, `completed`, `failed`,
|
|
256
|
+
`canceled` or `expired`.
|
|
257
|
+
|
|
258
|
+
## Web apps
|
|
259
|
+
|
|
260
|
+
### Stream to a browser with FastAPI
|
|
261
|
+
|
|
262
|
+
Keep the secret key on your server. The browser talks to your route, and your route talks
|
|
263
|
+
to LigaX:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from fastapi import FastAPI, Request
|
|
267
|
+
from ligaxai.fastapi import client_dependency, run_stream_route
|
|
268
|
+
|
|
269
|
+
app = FastAPI()
|
|
270
|
+
get_client = client_dependency() # one AsyncLigaX per process, from the environment
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def current_user(request: Request) -> str:
|
|
274
|
+
return request.state.user_id # from YOUR login session
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
app.post("/chat")(run_stream_route(get_client, "prf_...", current_user))
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The browser sends `POST /chat` with `{"input": "…", "conversation_id": "conv_…"}` and reads
|
|
281
|
+
a `text/event-stream`. The profile is fixed on the server and the user comes from your
|
|
282
|
+
session, so one person cannot read or continue another's conversation. Without a signed-in
|
|
283
|
+
user the route answers `401`. A complete app is in
|
|
284
|
+
[examples/fastapi-app](https://github.com/ligaxai/ligaxai-python/tree/main/examples/fastapi-app).
|
|
285
|
+
|
|
286
|
+
### Let a browser call LigaX directly (client tokens)
|
|
287
|
+
|
|
288
|
+
Your server can mint a short-lived **client token** (`lgx_ct_…`) for the signed-in user.
|
|
289
|
+
The browser uses it instead of your secret key:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
token = client.client_tokens.create(
|
|
293
|
+
profile_id="prf_...",
|
|
294
|
+
end_user={"id": "user-42", "name": "Ada"}, # or just "user-42"
|
|
295
|
+
expires_in=900, # seconds, 60 to 3600; default 600
|
|
296
|
+
)
|
|
297
|
+
# Send token.token and token.expires_at to that user's browser. Never log the token.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
A client token runs one profile, as one end user, for a few minutes. It cannot manage
|
|
301
|
+
keys, webhooks or usage. Two settings must be in place:
|
|
302
|
+
|
|
303
|
+
- the secret key that mints it needs the `client_tokens:write` scope;
|
|
304
|
+
- the profile must list your website's origin (for example `https://app.example.com`)
|
|
305
|
+
under **allowed origins** in Settings → Developers.
|
|
306
|
+
|
|
307
|
+
With FastAPI, `client_token_route` is that endpoint:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from fastapi import FastAPI, Request
|
|
311
|
+
from ligaxai.fastapi import client_dependency, client_token_route
|
|
312
|
+
|
|
313
|
+
app = FastAPI()
|
|
314
|
+
get_client = client_dependency()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def current_user(request: Request) -> tuple[str, str] | None:
|
|
318
|
+
user = request.state.user # from YOUR login session
|
|
319
|
+
return (user.id, user.name) if user else None # None answers 401
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
app.post("/ligax/token")(client_token_route(get_client, "prf_...", current_user))
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
It answers `{"token": "lgx_ct_…", "expires_at": "…"}` with `Cache-Control: no-store`. In
|
|
326
|
+
Django or Flask, write the same three steps in your own view: require a signed-in user,
|
|
327
|
+
call `client.client_tokens.create(...)`, return `token` and `expires_at`.
|
|
328
|
+
|
|
329
|
+
### Let your users connect their own accounts
|
|
330
|
+
|
|
331
|
+
By default an agent uses the workspace's shared connections. If the profile allows it
|
|
332
|
+
(*End users can connect their own accounts*, on the agent's **API & Integrations** tab),
|
|
333
|
+
each of your users can connect their own Gmail, Outlook and so on. Runs with that
|
|
334
|
+
`end_user` then use the user's own account.
|
|
335
|
+
|
|
336
|
+
Your server starts a **connect session** (scope `connections:write`) and gives the
|
|
337
|
+
browser its `connect_url`:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
session = client.connect_sessions.create(
|
|
341
|
+
connector_key="gmail",
|
|
342
|
+
profile_id="prf_...",
|
|
343
|
+
end_user="user-42",
|
|
344
|
+
)
|
|
345
|
+
# In the browser, open session.connect_url in a popup from a click handler.
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Then wait until the user finishes. `denied`, `failed` and `expired` are results, not
|
|
349
|
+
errors. `wait` raises `TimeoutError` if the session is still pending after `timeout`
|
|
350
|
+
seconds:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
session = client.connect_sessions.wait(session.id, end_user_id="user-42", timeout=300)
|
|
354
|
+
if session.status == "succeeded":
|
|
355
|
+
print("Connected:", session.connection_id)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
List or remove a user's own accounts (never the shared ones):
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
for conn in client.end_users.connections.list("user-42").data:
|
|
362
|
+
print(conn.id, conn.connector_key, conn.status)
|
|
363
|
+
client.end_users.connections.delete("user-42", conn.id)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Webhooks
|
|
367
|
+
|
|
368
|
+
LigaX can call your server when something happens, so you don't have to poll. Managing
|
|
369
|
+
endpoints needs the `webhooks:manage` scope.
|
|
370
|
+
|
|
371
|
+
| Event | Sent when |
|
|
372
|
+
| --- | --- |
|
|
373
|
+
| `run.completed`, `run.failed`, `run.canceled` | a run ends |
|
|
374
|
+
| `run.requires_action` | a run waits for your approval |
|
|
375
|
+
| `api_key.limit_reached` | a key reaches its spend limit |
|
|
376
|
+
| `connection.created`, `connection.deleted` | an end user connects or removes an account |
|
|
377
|
+
| `webhook.test` | you call `webhooks.test` |
|
|
378
|
+
|
|
379
|
+
**1. Create an endpoint and store its secret.** The secret (`whsec_…`) is returned only
|
|
380
|
+
once:
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
endpoint = client.webhooks.create(
|
|
384
|
+
url="https://example.com/hooks/ligax",
|
|
385
|
+
event_types=["run.completed", "run.failed", "run.requires_action"],
|
|
386
|
+
profile_ids=["prf_..."], # optional: only these profiles' runs
|
|
387
|
+
)
|
|
388
|
+
print(endpoint.id, endpoint.secret) # store the secret in your secret manager
|
|
389
|
+
client.webhooks.test(endpoint.id) # sends a webhook.test event now
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**2. Verify every delivery** against the raw request body. `webhooks.verify` checks the
|
|
393
|
+
signature and the timestamp, then returns a typed event:
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from ligaxai import WebhookVerificationError, webhooks
|
|
397
|
+
from ligaxai.types import RunWebhookEvent
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def handle(raw_body: bytes, headers: dict[str, str], secret: str) -> int:
|
|
401
|
+
try:
|
|
402
|
+
event = webhooks.verify(raw_body, headers, secret=secret)
|
|
403
|
+
except WebhookVerificationError:
|
|
404
|
+
return 400
|
|
405
|
+
if isinstance(event, RunWebhookEvent) and event.type == "run.completed":
|
|
406
|
+
run = client.runs.get(event.data.id) # the event holds ids, not the answer
|
|
407
|
+
print(run.output.text)
|
|
408
|
+
return 204
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
In FastAPI, `ligaxai.fastapi.webhook_dependency(get_secret)` does the reading and
|
|
412
|
+
verifying for you and answers `400` on a bad signature.
|
|
413
|
+
|
|
414
|
+
**3. Follow these rules:**
|
|
415
|
+
|
|
416
|
+
- **Use the raw bytes.** Re-encoding parsed JSON changes what was signed.
|
|
417
|
+
- **Answer 2xx within 15 seconds.** Do slow work in the background. Failed deliveries are
|
|
418
|
+
retried for about 27 hours; an endpoint that keeps failing is disabled.
|
|
419
|
+
- **Skip duplicates.** A retry has the same `webhook-id` header. Remember the ids you handled.
|
|
420
|
+
- **Fetch the content.** Events carry ids and a status only. Read the run with `runs.get`.
|
|
421
|
+
|
|
422
|
+
Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com). For Zapier, Make
|
|
423
|
+
or n8n, map *subscribe* to `webhooks.create` and *unsubscribe* to `webhooks.delete`.
|
|
424
|
+
|
|
425
|
+
## Use an agent from MCP or A2A clients
|
|
426
|
+
|
|
427
|
+
Every published agent profile is also an MCP server and an A2A agent, so Claude Code,
|
|
428
|
+
Cursor or another agent can call it without this SDK. Use a secret key with the **Start
|
|
429
|
+
runs** and **Read runs** scopes:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
claude mcp add --transport http ligax-support \
|
|
433
|
+
https://<your-ligax-host>/api/public/v1/mcp/prf_... \
|
|
434
|
+
--header "Authorization: Bearer $LIGAX_API_KEY"
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- MCP endpoint: `https://<your-ligax-host>/api/public/v1/mcp/prf_...` (tools `ask`,
|
|
438
|
+
`get_run`, `respond`).
|
|
439
|
+
- A2A agent card: `https://<your-ligax-host>/api/public/v1/a2a/prf_.../.well-known/agent-card.json`
|
|
440
|
+
(A2A 1.0 and 0.3; send the same `Authorization` header).
|
|
441
|
+
- Add the header `X-LigaX-End-User: <your user id>` to act for one of your users.
|
|
442
|
+
|
|
443
|
+
## Configuration reference
|
|
444
|
+
|
|
445
|
+
| Setting | Argument | Environment variable | Default |
|
|
446
|
+
| --- | --- | --- | --- |
|
|
447
|
+
| API base URL | `base_url=` | `LIGAX_BASE_URL` | none, **required** |
|
|
448
|
+
| API key | `api_key=` | `LIGAX_API_KEY` | none, **required** |
|
|
449
|
+
| Timeout | `timeout=` | | 600 s per request, 5 s to connect |
|
|
450
|
+
| Retries | `max_retries=` | | `2` |
|
|
451
|
+
| Extra headers | `default_headers=` | | none |
|
|
452
|
+
| HTTP client | `http_client=` | | the SDK's own `httpx2.Client` |
|
|
453
|
+
| Logging | | `LIGAX_LOG` | `off` |
|
|
454
|
+
|
|
455
|
+
Change options for a few calls with `client.with_options(max_retries=0, timeout=10)`. It
|
|
456
|
+
returns a copy that shares the connection pool. Any single call also accepts
|
|
457
|
+
`options=RequestOptions(timeout=..., max_retries=..., headers=..., idempotency_key=...)`.
|
|
458
|
+
|
|
459
|
+
`LIGAX_LOG` takes `debug`, `info`, `warn`, `error` or `off`. Logs never show your key.
|
|
460
|
+
|
|
461
|
+
## Errors
|
|
462
|
+
|
|
463
|
+
Every error the SDK raises is a subclass of `ligaxai.APIError`:
|
|
464
|
+
|
|
465
|
+
```python
|
|
466
|
+
from ligaxai import APIConnectionError, APIStatusError, LigaX, PaymentRequiredError, RateLimitError
|
|
467
|
+
|
|
468
|
+
client = LigaX()
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
client.runs.create(profile_id="prf_...", input="Hi")
|
|
472
|
+
except PaymentRequiredError as e: # the key or workspace reached its spend limit
|
|
473
|
+
print(e.code, e.response.headers.get("x-limit-resets-at"))
|
|
474
|
+
except RateLimitError as e: # still limited after the SDK's retries
|
|
475
|
+
print(e.status_code, e.request_id)
|
|
476
|
+
except APIStatusError as e: # any other 4xx or 5xx answer
|
|
477
|
+
print(e.status_code, e.code, e.message, e.request_id)
|
|
478
|
+
except APIConnectionError: # network problem or timeout (APITimeoutError)
|
|
479
|
+
raise
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
| HTTP status | Exception |
|
|
483
|
+
| --- | --- |
|
|
484
|
+
| 400 | `BadRequestError` |
|
|
485
|
+
| 401 | `AuthenticationError` |
|
|
486
|
+
| 402 | `PaymentRequiredError` |
|
|
487
|
+
| 403 | `PermissionDeniedError` |
|
|
488
|
+
| 404 | `NotFoundError` |
|
|
489
|
+
| 409 | `ConflictError` |
|
|
490
|
+
| 422 | `UnprocessableEntityError` |
|
|
491
|
+
| 429 | `RateLimitError` |
|
|
492
|
+
| 500 and above | `InternalServerError` |
|
|
493
|
+
| no response | `APIConnectionError`, `APITimeoutError` |
|
|
494
|
+
| missing setup | `MissingBaseURLError`, `MissingAPIKeyError` (also `ValueError`) |
|
|
495
|
+
|
|
496
|
+
Each status error has `status_code`, `code`, `message`, `param` and `request_id`. Every
|
|
497
|
+
successful result has `request_id` too. Quote it when you contact support.
|
|
498
|
+
|
|
499
|
+
## Retries and timeouts
|
|
500
|
+
|
|
501
|
+
- **Retried, 2 times by default:** connection errors, timeouts, and answers 408, 409, 429
|
|
502
|
+
and 5xx. The wait grows from 0.5 s to 8 s, and the SDK honours the server's `Retry-After`.
|
|
503
|
+
- **Never retried:** 402, 403 and 422, and a stream that has already started.
|
|
504
|
+
- **No duplicate runs:** every `POST` carries an `Idempotency-Key` that stays the same
|
|
505
|
+
across retries. Pass your own `idempotency_key=` to make a retry safe across processes.
|
|
506
|
+
- **Timeout:** 600 seconds per request, so `runs.wait` has room. The server answers a long
|
|
507
|
+
run after up to 240 seconds, and `runs.wait` then keeps polling for you.
|
|
508
|
+
|
|
509
|
+
## Security
|
|
510
|
+
|
|
511
|
+
An agent run with your key acts with your workspace's connected accounts (mail, files,
|
|
512
|
+
tickets) and spends your credit.
|
|
513
|
+
|
|
514
|
+
- **Keep secret keys (`lgx_sk_…`) on your server.** Never in a browser, a mobile app, a
|
|
515
|
+
repository or a log. Give each key only the profiles and scopes it needs, and a spend limit.
|
|
516
|
+
- **Treat website keys (`lgx_pk_…`) as public.** Anyone can copy one from your page.
|
|
517
|
+
Visitors on a website key can only chat. Give it a small spend limit.
|
|
518
|
+
- **Take the user's identity from your own session**, never from the request body or a
|
|
519
|
+
header the browser sets.
|
|
520
|
+
- **Keep approvals on** for sending, writing and deleting. Anything the agent reads (an
|
|
521
|
+
email, a web page) can try to give it instructions.
|
|
522
|
+
- **Treat the agent's answer as untrusted input.** Never `eval` it or insert it into HTML
|
|
523
|
+
unescaped.
|
|
524
|
+
- **Verify every webhook** and skip duplicates.
|
|
525
|
+
- **Pin the version** in your lockfile. Releases carry PyPI provenance attestations.
|
|
526
|
+
|
|
527
|
+
Report a vulnerability to security@ligaments.ai (see
|
|
528
|
+
[SECURITY.md](https://github.com/ligaxai/ligaxai-python/blob/main/SECURITY.md)).
|
|
529
|
+
|
|
530
|
+
## API reference
|
|
531
|
+
|
|
532
|
+
Every method exists on `LigaX` and `AsyncLigaX` with the same name and parameters.
|
|
533
|
+
Results are typed models from `ligaxai.types`, with field names exactly as in the API
|
|
534
|
+
(snake_case). Fields the server adds later are kept in `model.model_extra`.
|
|
535
|
+
|
|
536
|
+
| Method | API call | Returns |
|
|
537
|
+
| --- | --- | --- |
|
|
538
|
+
| `runs.create(profile_id=, input=, conversation_id=, end_user=, model=, mode="wait"\|"async", metadata=, attachments=, idempotency_key=)` | `POST /runs` | `Run` |
|
|
539
|
+
| `runs.wait(...)`: create, then poll until the run ends or needs approval | `POST /runs`, `GET /runs/{id}` | `Run` |
|
|
540
|
+
| `runs.stream(...)` | `POST /runs` (streamed) | `RunStream` |
|
|
541
|
+
| `runs.get(run_id)` | `GET /runs/{id}` | `Run` |
|
|
542
|
+
| `runs.events(run_id, after=0)` | `GET /runs/{id}/events` | `RunEventList` |
|
|
543
|
+
| `runs.follow_events(run_id, after=0)` | `GET /runs/{id}/events?stream=true` | `RunStream` |
|
|
544
|
+
| `runs.cancel(run_id)` | `POST /runs/{id}/cancel` | `Run` |
|
|
545
|
+
| `runs.resolve_interrupt(run_id, interrupt_id, decision="approve"\|"reject"\|"answer", payload=)` | `POST /runs/{id}/interrupts/{iid}` | `Run` |
|
|
546
|
+
| `conversations.get(conversation_id, end_user_id=None)` | `GET /conversations/{id}` | `Conversation` |
|
|
547
|
+
| `profiles.list()`, `profiles.get(profile_id)` | `GET /profiles[/{id}]` | `ProfileList`, `Profile` |
|
|
548
|
+
| `keys.self()` | `GET /keys/self` | `ApiKey` |
|
|
549
|
+
| `usage.get(start=, end=, group_by=)` | `GET /usage` | `UsageReport` |
|
|
550
|
+
| `files.create(file=, end_user_id=None)` | `POST /files` | `FileObject` |
|
|
551
|
+
| `files.get(file_id)`, `files.delete(file_id)` | `GET`, `DELETE /files/{id}` | `FileObject`, `FileDeleted` |
|
|
552
|
+
| `webhooks.create(url=, event_types=, description=, profile_ids=, idempotency_key=)` | `POST /webhooks` | `WebhookEndpointWithSecret` |
|
|
553
|
+
| `webhooks.list()`, `webhooks.get(webhook_id)` | `GET /webhooks[/{id}]` | `WebhookEndpointList`, `WebhookEndpoint` |
|
|
554
|
+
| `webhooks.update(webhook_id, url=, description=, event_types=, profile_ids=, status=)` | `PATCH /webhooks/{id}` | `WebhookEndpoint` |
|
|
555
|
+
| `webhooks.delete(webhook_id)` | `DELETE /webhooks/{id}` | `WebhookEndpointDeleted` |
|
|
556
|
+
| `webhooks.test(webhook_id)` | `POST /webhooks/{id}/test` | `WebhookDelivery` |
|
|
557
|
+
| `client_tokens.create(profile_id=, end_user=, conversation_id=, expires_in=)` | `POST /client_tokens` | `ClientToken` |
|
|
558
|
+
| `connect_sessions.create(connector_key=, profile_id=, end_user=)` | `POST /connect_sessions` | `ConnectSession` |
|
|
559
|
+
| `connect_sessions.get(session_id, end_user_id=None)` | `GET /connect_sessions/{id}` | `ConnectSession` |
|
|
560
|
+
| `connect_sessions.wait(session_id, end_user_id=None, poll_interval=2, timeout=600)` | `GET /connect_sessions/{id}` | `ConnectSession` |
|
|
561
|
+
| `end_users.connections.list(end_user_id)` | `GET /end_users/{id}/connections` | `EndUserConnectionList` |
|
|
562
|
+
| `end_users.connections.delete(end_user_id, connection_id)` | `DELETE /end_users/{id}/connections/{cid}` | `EndUserConnectionDeleted` |
|
|
563
|
+
|
|
564
|
+
Module-level helpers: `ligaxai.webhooks.verify` (with `verify_signature` and `parse_event`),
|
|
565
|
+
`ligaxai.TextAssembler` (rebuilds answer text from recorded events), and
|
|
566
|
+
`ligaxai.fastapi` (`client_dependency`, `run_stream_route`, `client_token_route`,
|
|
567
|
+
`webhook_dependency`).
|
|
568
|
+
|
|
569
|
+
## For AI coding assistants
|
|
570
|
+
|
|
571
|
+
If you are an AI assistant writing code with this package, follow these facts exactly:
|
|
572
|
+
|
|
573
|
+
1. Install `ligaxai`. Import from the top-level package: `from ligaxai import LigaX, AsyncLigaX`.
|
|
574
|
+
2. There is **no default base URL**. The code must read `LIGAX_BASE_URL` from the
|
|
575
|
+
environment or pass `base_url=`. Never invent or hard-code a host. Without it the
|
|
576
|
+
client raises `MissingBaseURLError`.
|
|
577
|
+
3. The key comes from `LIGAX_API_KEY` or `api_key=`. A secret key (`lgx_sk_…`) belongs in
|
|
578
|
+
server code only. For a browser, mint a client token with `client.client_tokens.create`.
|
|
579
|
+
4. Create the client once and reuse it: `client = LigaX()`.
|
|
580
|
+
5. The main call is `client.runs.wait(profile_id="prf_...", input="...")`. The answer is
|
|
581
|
+
`run.output.text` when `run.status == "completed"`. Handle `"requires_action"` with
|
|
582
|
+
`runs.resolve_interrupt`.
|
|
583
|
+
6. Parameter and field names are snake_case, exactly as in the API (`profile_id`,
|
|
584
|
+
`conversation_id`, `end_user`). The async client has the same names; `await` each call.
|
|
585
|
+
7. Catch `ligaxai.APIStatusError` (or a subclass from the table in
|
|
586
|
+
[Errors](https://github.com/ligaxai/ligaxai-python#errors)). Do not add your own retry
|
|
587
|
+
loop: the client already retries safely.
|
|
588
|
+
8. Take `end_user` from the app's login session, never from user input.
|
|
589
|
+
|
|
590
|
+
## Versions and support
|
|
591
|
+
|
|
592
|
+
- **Python:** 3.11, 3.12, 3.13 and 3.14, on Linux, macOS and Windows.
|
|
593
|
+
- **Release channels:** `pip install ligaxai` installs the latest stable release. Preview
|
|
594
|
+
builds from the `dev` branch are versioned like `0.1.0.dev14`; install one with
|
|
595
|
+
`pip install --pre ligaxai` or an exact pin. Every channel needs `LIGAX_BASE_URL`.
|
|
596
|
+
- **Transport:** [`httpx2`](https://pypi.org/project/httpx2/). If your app imports plain
|
|
597
|
+
`httpx` and you want one shared stack, call `httpx2.alias_httpx()` before any `httpx`
|
|
598
|
+
import.
|
|
599
|
+
- **Changes:** [CHANGELOG.md](https://github.com/ligaxai/ligaxai-python/blob/main/CHANGELOG.md).
|
|
600
|
+
- **Bugs and questions:** [GitHub issues](https://github.com/ligaxai/ligaxai-python/issues).
|
|
601
|
+
- **Contributing:** [CONTRIBUTING.md](https://github.com/ligaxai/ligaxai-python/blob/main/CONTRIBUTING.md).
|
|
602
|
+
|
|
603
|
+
## License
|
|
604
|
+
|
|
605
|
+
Apache-2.0. See [LICENSE](https://github.com/ligaxai/ligaxai-python/blob/main/LICENSE).
|