rainbird-ai 0.5.0__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.
- rainbird_ai-0.5.0/.gitignore +84 -0
- rainbird_ai-0.5.0/AGENTS.md +413 -0
- rainbird_ai-0.5.0/CHANGELOG.md +15 -0
- rainbird_ai-0.5.0/LICENSE +21 -0
- rainbird_ai-0.5.0/PKG-INFO +652 -0
- rainbird_ai-0.5.0/README.md +616 -0
- rainbird_ai-0.5.0/examples/README.md +34 -0
- rainbird_ai-0.5.0/pyproject.toml +101 -0
- rainbird_ai-0.5.0/src/rainbird_ai/__init__.py +150 -0
- rainbird_ai-0.5.0/src/rainbird_ai/_answers.py +480 -0
- rainbird_ai-0.5.0/src/rainbird_ai/_http.py +630 -0
- rainbird_ai-0.5.0/src/rainbird_ai/_ops.py +408 -0
- rainbird_ai-0.5.0/src/rainbird_ai/_version.py +3 -0
- rainbird_ai-0.5.0/src/rainbird_ai/aio.py +556 -0
- rainbird_ai-0.5.0/src/rainbird_ai/cli.py +794 -0
- rainbird_ai-0.5.0/src/rainbird_ai/client.py +659 -0
- rainbird_ai-0.5.0/src/rainbird_ai/errors.py +216 -0
- rainbird_ai-0.5.0/src/rainbird_ai/models.py +1335 -0
- rainbird_ai-0.5.0/src/rainbird_ai/py.typed +0 -0
- rainbird_ai-0.5.0/tests/__init__.py +1 -0
- rainbird_ai-0.5.0/tests/conftest.py +627 -0
- rainbird_ai-0.5.0/tests/parity/README.md +635 -0
- rainbird_ai-0.5.0/tests/parity/__init__.py +2 -0
- rainbird_ai-0.5.0/tests/parity/support.py +127 -0
- rainbird_ai-0.5.0/tests/parity/test_go_client.py +582 -0
- rainbird_ai-0.5.0/tests/parity/test_go_evidence.py +551 -0
- rainbird_ai-0.5.0/tests/parity/test_go_inject_query.py +471 -0
- rainbird_ai-0.5.0/tests/parity/test_go_interactions.py +603 -0
- rainbird_ai-0.5.0/tests/parity/test_go_response_undo.py +445 -0
- rainbird_ai-0.5.0/tests/parity/test_go_session_analysis.py +389 -0
- rainbird_ai-0.5.0/tests/parity/test_go_stringers.py +237 -0
- rainbird_ai-0.5.0/tests/parity/test_js_analysis.py +610 -0
- rainbird_ai-0.5.0/tests/parity/test_js_evidence.py +926 -0
- rainbird_ai-0.5.0/tests/parity/test_js_inject.py +428 -0
- rainbird_ai-0.5.0/tests/parity/test_js_query.py +481 -0
- rainbird_ai-0.5.0/tests/parity/test_js_response.py +430 -0
- rainbird_ai-0.5.0/tests/parity/test_js_start.py +377 -0
- rainbird_ai-0.5.0/tests/parity/test_js_undo.py +279 -0
- rainbird_ai-0.5.0/tests/test_aio.py +2553 -0
- rainbird_ai-0.5.0/tests/test_answers.py +2092 -0
- rainbird_ai-0.5.0/tests/test_cli.py +2302 -0
- rainbird_ai-0.5.0/tests/test_client.py +2740 -0
- rainbird_ai-0.5.0/tests/test_edge_cases.py +461 -0
- rainbird_ai-0.5.0/tests/test_http.py +1837 -0
- rainbird_ai-0.5.0/tests/test_integration.py +94 -0
- rainbird_ai-0.5.0/tests/test_models.py +2918 -0
- rainbird_ai-0.5.0/tests/test_public_api.py +1056 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
*.cover
|
|
48
|
+
*.py,cover
|
|
49
|
+
.hypothesis/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
|
|
52
|
+
# Virtual environments
|
|
53
|
+
venv/
|
|
54
|
+
ENV/
|
|
55
|
+
env/
|
|
56
|
+
.venv
|
|
57
|
+
|
|
58
|
+
# IDEs
|
|
59
|
+
.vscode/
|
|
60
|
+
.idea/
|
|
61
|
+
*.swp
|
|
62
|
+
*.swo
|
|
63
|
+
*~
|
|
64
|
+
.DS_Store
|
|
65
|
+
|
|
66
|
+
# mypy
|
|
67
|
+
.mypy_cache/
|
|
68
|
+
.dmypy.json
|
|
69
|
+
dmypy.json
|
|
70
|
+
|
|
71
|
+
# Pyre type checker
|
|
72
|
+
.pyre/
|
|
73
|
+
|
|
74
|
+
# pytype static type analyzer
|
|
75
|
+
.pytype/
|
|
76
|
+
|
|
77
|
+
# Build artifacts
|
|
78
|
+
*.whl
|
|
79
|
+
*.tar.gz
|
|
80
|
+
|
|
81
|
+
# Tooling caches
|
|
82
|
+
.ruff_cache/
|
|
83
|
+
|
|
84
|
+
llms.txt
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# AGENTS.md - using the `rainbird-ai` Python SDK
|
|
2
|
+
|
|
3
|
+
Reference for coding agents that call Rainbird from another project through this SDK.
|
|
4
|
+
Everything below is derived from the SDK source (`src/rainbird_ai/`) and the Rainbird
|
|
5
|
+
OpenAPI 3.1 spec. Do not hand-roll HTTP calls with `requests`; use the SDK.
|
|
6
|
+
|
|
7
|
+
## 1. What Rainbird is
|
|
8
|
+
|
|
9
|
+
Rainbird is a decision-intelligence platform. Domain experts build a **knowledge map**
|
|
10
|
+
(concepts, relationships, rules, written in RBLang XML) in Rainbird Studio and publish it.
|
|
11
|
+
Client code then:
|
|
12
|
+
|
|
13
|
+
1. **starts a session** against a knowledge map (a `kmID` GUID),
|
|
14
|
+
2. optionally **injects facts** it already knows (`subject relationship object` triples with a
|
|
15
|
+
certainty 1-100),
|
|
16
|
+
3. **queries** a relationship (`"speaks"` with `subject="Tom"`),
|
|
17
|
+
4. **answers the questions** the reasoning engine asks until it returns
|
|
18
|
+
5. **results**: triples with a `certainty` (0-100) and a `fact_id`, which unlocks the
|
|
19
|
+
**evidence tree** (the rule and conditions that produced the fact) and natural-language
|
|
20
|
+
explanations.
|
|
21
|
+
|
|
22
|
+
Every `query`, `respond`, `answer`, `skip` and `undo` call returns either a
|
|
23
|
+
`QuestionResponse` (more information needed) or a `ResultResponse` (a decision).
|
|
24
|
+
|
|
25
|
+
## 2. Install and import
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install rainbird-ai # Python >= 3.11, runtime dependencies: httpx, anyio
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from rainbird_ai import Rainbird, SKIP, Answer, Fact, RainbirdError # sync
|
|
33
|
+
from rainbird_ai.aio import AsyncRainbird # async
|
|
34
|
+
from rainbird_ai import engine_version, async_engine_version, UNSET # keyless version check, timeout sentinel
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Console script: `rainbird --help` (`rainbird query -r speaks -s Tom --answers German --json`; `rainbird decide -r speaks -s Tom --json` for a one-shot `Decision`, exit 3 if a question is pending).
|
|
38
|
+
CLI notes for scripting:
|
|
39
|
+
|
|
40
|
+
- Global options `--api-key --base-url --engine --evidence-key --interaction-key --timeout --json`
|
|
41
|
+
are accepted before or after the subcommand (including `map get` / `map create`).
|
|
42
|
+
- `rainbird version` needs no API key: without `--api-key`/`RAINBIRD_API_KEY` it calls
|
|
43
|
+
`engine_version()`.
|
|
44
|
+
- `--answers`: `?` skips, `a,b` answers a plural question, `value@80` sets the certainty (`@` counts
|
|
45
|
+
only when the suffix is numeric, so `tom@example.com` is a plain value; a numeric suffix outside
|
|
46
|
+
0-100, negative included, is rejected like any other bad answer, and so is an empty answer:
|
|
47
|
+
`answer must not be empty`). `?@50` / `skip@50` still skip (the certainty is dropped). Certainty
|
|
48
|
+
applies to the whole answer: `a, b@70` is valid, `a@70, b@80` is rejected with a message saying
|
|
49
|
+
so. Scripted answers are parsed only when the matching question
|
|
50
|
+
arrives, i.e. after the session has started and the query was sent. `--fact S R O [CERTAINTY]`
|
|
51
|
+
accepts a float certainty, validated (0-100) before any request like `--file` JSON, which must be
|
|
52
|
+
a JSON object, a list of fact objects or a list of `[subject, relationship, object, certainty?]`
|
|
53
|
+
lists (a flat `["Tom", "lives in", "Germany"]` is a usage error, not one fact).
|
|
54
|
+
- `--json`: stdout is a single JSON document; interactive questions and prompts go to stderr.
|
|
55
|
+
- Exit codes: `0` success; `1` API/SDK/file error or a rejected `--answers` value
|
|
56
|
+
(`rainbird: <message>` on stderr); `2` usage error, including an invalid `--fact` (non-numeric or
|
|
57
|
+
out-of-range certainty), malformed `--file` JSON or malformed `respond` JSON (all checked before
|
|
58
|
+
any network call); `3` a
|
|
59
|
+
question is still pending and no input is available (`--no-input`, non-tty stdin or Ctrl-D): the
|
|
60
|
+
question goes to stdout (text mode) or `{"session_id": ..., ...questionResponse}` (`--json`), then
|
|
61
|
+
`rainbird: session <SID> is waiting for an answer; resume with --session <SID>` to stderr; `130`
|
|
62
|
+
interrupted. Interactive rejections re-prompt and print ` ! <message>` to stderr, but only for
|
|
63
|
+
answer-level problems (`InvalidAnswerError`, `InvalidFactError`, `BadRequestError`); a 401, a 404
|
|
64
|
+
(session not found), a network error or any other failure exits 1 immediately.
|
|
65
|
+
- `evidence --tree` prints a nested tree: root at column 0, each child evidence node indented two
|
|
66
|
+
more spaces, inline conditions as `- ...` lines beneath their node, facts referenced only from
|
|
67
|
+
expression functions beneath their expression, and any fetched node not reachable from the root
|
|
68
|
+
appended at the end.
|
|
69
|
+
|
|
70
|
+
## 3. Canonical decision loop
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from rainbird_ai import Rainbird
|
|
74
|
+
|
|
75
|
+
rb = Rainbird() # RAINBIRD_API_KEY / RAINBIRD_API_URL from env
|
|
76
|
+
session = rb.start("<knowledge-map-id>") # GET /start/{kmID} -> Session
|
|
77
|
+
session.inject_fact("Tom", "lives in", "Germany") # POST /{sid}/inject (optional, known facts)
|
|
78
|
+
response = session.query("speaks", subject="Tom") # POST /{sid}/query -> Question or Result
|
|
79
|
+
while response.is_question: # Literal-typed: narrows to QuestionResponse under mypy
|
|
80
|
+
q = response.question # q.prompt, q.form, q.options, q.plural ...
|
|
81
|
+
value = True if q.is_first_form else (q.options[0] if q.options else "German")
|
|
82
|
+
response = session.answer(value) # POST /{sid}/response -> Question or Result
|
|
83
|
+
for item in response.results: # ResultResponse (may be empty)
|
|
84
|
+
print(item.subject, item.relationship, item.object, item.certainty, item.fact_id)
|
|
85
|
+
if response.best: # highest-certainty result or None
|
|
86
|
+
evidence = session.evidence(response.best.fact_id) # GET /analysis/evidence/{factID}/{sid}
|
|
87
|
+
rb.close() # or: with Rainbird() as rb: ...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Fully automated variant: `result = session.run("speaks", subject="Tom", answerer=fn)` (section 9).
|
|
91
|
+
|
|
92
|
+
## 4. Configuration
|
|
93
|
+
|
|
94
|
+
`Rainbird(api_key=None, *, base_url=None, evidence_key=None, interaction_key=None, engine=None,
|
|
95
|
+
timeout=UNSET, retry=None, default_headers=None, http_client=None)`. `AsyncRainbird` has the same
|
|
96
|
+
signature with `http_client: httpx.AsyncClient`. Explicit arguments win over env vars.
|
|
97
|
+
|
|
98
|
+
| Argument | Env var | Meaning |
|
|
99
|
+
|-------------------|----------------------------|---------|
|
|
100
|
+
| `api_key` | `RAINBIRD_API_KEY` | Sent as `X-API-Key` (Studio > knowledge map > Publish). Required; missing -> `RainbirdConfigurationError` (only the module-level `engine_version()` works without a key). |
|
|
101
|
+
| `base_url` | `RAINBIRD_API_URL` | API host, default `https://api.rainbird.ai`. A bare host gets `https://`, trailing `/` stripped; `""`/whitespace falls back to the env var, then the default; a non-`http(s)` scheme, a scheme without a host (`"https://"`), a query string or fragment (any `?` or `#`, even an empty trailing one), or an otherwise invalid URL -> `RainbirdConfigurationError`. |
|
|
102
|
+
| `evidence_key` | `RAINBIRD_EVIDENCE_KEY` | Default `x-evidence-key` for evidence calls (evidence is secured by default). |
|
|
103
|
+
| `interaction_key` | `RAINBIRD_INTERACTION_KEY` | Default `x-interaction-key` for interaction logs. |
|
|
104
|
+
| `engine` | `RAINBIRD_ENGINE` | Optional alternate engine, sent as `x-rainbird-engine`. |
|
|
105
|
+
| (start km_id) | `RAINBIRD_KM_ID` | Fallback for `start(km_id=None)`. |
|
|
106
|
+
| `timeout` | - | Seconds, an `httpx.Timeout`, or `None` (no timeout). Default is the `UNSET` sentinel = 30 s on the SDK's own client; with `http_client=` the client's own timeout applies unless you pass `timeout=` explicitly, which is then applied per request. |
|
|
107
|
+
| `retry` | - | `RetryPolicy` (section 11); `NO_RETRY` disables retries. |
|
|
108
|
+
| `default_headers` | - | Extra headers on every request, merged case-insensitively (later wins): `{"x-api-key": ...}` replaces `X-API-Key` instead of duplicating it. |
|
|
109
|
+
| `http_client` | - | Bring your own `httpx.Client`; you close it. The SDK's own clients use `follow_redirects=False` (a 3xx raises `RainbirdAPIError("unexpected redirect to <location>")`); keep that on yours so the API key is never forwarded to another host. |
|
|
110
|
+
|
|
111
|
+
Hosts (`rainbird_ai.Environment`, a `str` enum usable directly as `base_url`):
|
|
112
|
+
|
|
113
|
+
| Account type | `base_url` | Studio (`.studio_url`) |
|
|
114
|
+
|-------------------------|-----------------------------------------|--------------------------------------|
|
|
115
|
+
| `Environment.COMMUNITY` | `https://api.rainbird.ai` | `https://app.rainbird.ai` |
|
|
116
|
+
| `Environment.ENTERPRISE`| `https://enterprise-api.rainbird.ai` | `https://enterprise.rainbird.ai` |
|
|
117
|
+
| `Environment.ENTERPRISE_SOUTH_AMERICA` | `https://sa-enterprise-api.rainbird.ai` | `https://sa-enterprise.rainbird.ai` |
|
|
118
|
+
| Private deployment | `https://<client>-api.rainbird.ai` | `https://<client>.rainbird.ai` |
|
|
119
|
+
|
|
120
|
+
The `Version: v1` header required by `/nl/*` and `/maps` is added by the SDK. Auth headers are
|
|
121
|
+
added by the SDK. `studio_url_for(api_url)` derives the Studio host from any API host.
|
|
122
|
+
|
|
123
|
+
## 5. Public API reference (sync)
|
|
124
|
+
|
|
125
|
+
`AsyncRainbird` / `AsyncSession` (module `rainbird_ai.aio`) expose the same names; every method
|
|
126
|
+
that performs I/O is `async` and must be awaited (`session = await rb.start(...)`,
|
|
127
|
+
`await session.answer(...)`), `close()` becomes `await aclose()`, use
|
|
128
|
+
`async with AsyncRainbird() as rb`. `resume()`, `evidence_url()` and the properties stay
|
|
129
|
+
synchronous; `iter_evidence_tree()` returns an `AsyncIterator` (`async for`).
|
|
130
|
+
`rainbird_ai.aio.async_engine_version()` is the awaitable twin of `engine_version()`.
|
|
131
|
+
|
|
132
|
+
### `Rainbird`
|
|
133
|
+
|
|
134
|
+
- `start(km_id=None, *, use_draft=None, version=None, context_id=None) -> Session` - `GET /start/{kmID}`. Live version by default (draft if nothing is live). `use_draft=True` or `version=N` (positive int), never both (`RainbirdConfigurationError`). `context_id` shares context-scoped facts between sessions.
|
|
135
|
+
- `resume(session_id, *, km_id=None) -> Session` - attach to an existing session id; no request; `pending` is `None` until you query.
|
|
136
|
+
- `decide(relationship, *, km_id=None, subject=None, object=None, facts=None, answerer=None, max_questions=100, evidence="none", evidence_depth=None, evidence_key=None, use_draft=None, version=None, context_id=None) -> Decision` - one-shot: `start()` (+ `inject(facts)` when given) + `query()`; with `answerer` the questions are answered via `run()`. `evidence="root"|"tree"` also fetches the best result's evidence (needs the evidence key; one request per node). Never raises for a question or an empty result: read `Decision.status` (section 9b).
|
|
137
|
+
- `version() -> str` - `GET /version`, engine semver such as `"4.89.0"`. Needs a configured client (so an API key); the endpoint itself is public, use module-level `engine_version()` when you have no key.
|
|
138
|
+
- `evidence(session_id, fact_id, *, evidence_key=None) -> Evidence` - session-independent evidence lookup.
|
|
139
|
+
- `evidence_url(session_id, fact_id, *, studio_url=None) -> str` - `{studio}/evidence?id={factID}&api={base_url}&sid={sid}` link to the Studio evidence UI.
|
|
140
|
+
- `get_map_source(km_id) -> KnowledgeMapSource` - UNDOCUMENTED `GET /analysis/file/{kmID}` (section 12).
|
|
141
|
+
- `create_map(name, rblang, description=None) -> CreateMapResult` - UNDOCUMENTED `POST /maps` (section 12).
|
|
142
|
+
- `close()`; context manager; properties `base_url`, `studio_url`, `http` (the `httpx.Client`), `config`.
|
|
143
|
+
|
|
144
|
+
### `Session` (from `rb.start()` / `rb.resume()`)
|
|
145
|
+
|
|
146
|
+
State: `id` (session id), `km_id`, `km_version_id`, `context_id`, `start_response`,
|
|
147
|
+
`last_response: QueryResponse | None`, `client`. Derived: `pending -> QuestionResponse | None`,
|
|
148
|
+
`pending_question -> Question | None`, `result -> ResultResponse | None`. State is updated only
|
|
149
|
+
after a successful `query/respond/answer/skip/undo`.
|
|
150
|
+
|
|
151
|
+
- `inject(facts, *, batch_size=250) -> InjectResponse` - `POST /{sid}/inject`. Accepts one or many of: `Fact`, `{"subject","relationship","object"[,"certainty"]}`, `("s","r","o"[,certainty])`. Objects in all three forms are coerced like answers (section 8: `date`/`datetime` -> `"YYYY-MM-DD"`, `Decimal` -> number, enum -> `.value`; `None`/dict/list/NaN/infinity -> `InvalidAnswerError`) and certainty is validated (`InvalidFactError`). A 3/4-item tuple or list whose items are none of `Fact`/dict/list/tuple/set is one fact, so `("Tom", "lives in", Country.DE)` works. Lists are chunked into batches of `batch_size` (Rainbird recommends <= 250); returns the last batch's response (`.ok`).
|
|
152
|
+
- `inject_fact(subject, relationship, object, certainty=100) -> InjectResponse`.
|
|
153
|
+
- `query(relationship, *, subject=None, object=None) -> QueryResponse` - `POST /{sid}/query`. Give subject, object or both; `relationship` is required (`RainbirdConfigurationError` if empty).
|
|
154
|
+
- `answer(value, *, certainty=100, question=None, strict=True) -> QueryResponse` - builds the correct payload for `question` (default `pending_question`, else `NoPendingQuestionError`) from a plain Python value (section 8) and sends `POST /{sid}/response`.
|
|
155
|
+
- `skip(*, question=None, strict=True) -> QueryResponse` - sends `unanswered: true` for the pending question. Same as `answer(SKIP)`.
|
|
156
|
+
- `respond(answers) -> QueryResponse` - send prebuilt `Answer` objects/dicts (one or a list). Use for `extra_questions` or `QuestionForm.UNKNOWN`. Dict keys allowed: subject, relationship, object, certainty (or cf), answer, unanswered. `Answer` objects and dicts are both normalised before sending: object coerced (section 8), certainty validated, `answer` normalised to `"yes"`/`"no"` (anything else -> `InvalidAnswerError`), `unanswered` must be a bool or a `"true"/"false"/"yes"/"no"/"1"/"0"`-like string (other values, including numbers, -> `InvalidFactError`).
|
|
157
|
+
- `undo() -> QueryResponse` - `POST /{sid}/undo`: rolls back the last answers array and returns the question that preceded it (or the same question when nothing is left to undo).
|
|
158
|
+
- `run(relationship, *, subject=None, object=None, answerer, max_questions=100) -> ResultResponse` - query, then loop calling `answerer(question)` until a result (section 9). Raises `RainbirdError` after `max_questions` questions.
|
|
159
|
+
- `evidence(fact_id, *, evidence_key=None) -> Evidence` - one node.
|
|
160
|
+
- `evidence_tree(fact_id, *, max_depth=None, evidence_key=None) -> list[Evidence]` - breadth-first walk over `child_fact_ids`, root first, each node fetched once (one GET per node). `iter_evidence_tree(...) -> Iterator[Evidence]` is the lazy form.
|
|
161
|
+
- `evidence_url(fact_id, *, studio_url=None) -> str`.
|
|
162
|
+
- `interactions(*, format="json", interaction_key=None) -> list[InteractionEvent] | str` - `GET /analysis/interactions/{sid}`; overloaded so `format="json"` (default) is typed `list[InteractionEvent]` and `format="csv"` is typed `str` (raw CSV text). Logging must be enabled per map version in Studio before the session starts. A 200 body that is neither a list, an envelope with a list under `values`/`events`/`interactions`, nor a single event object (`event` key) raises `UnexpectedResponseError` carrying any error text found (a secured log may come back as 200 `{"err": [...]}`).
|
|
163
|
+
- `knowledge_map() -> KnowledgeMapVersion` - `GET /analysis/session/{sid}?filter=version`.
|
|
164
|
+
- `facts(*, relationships: str | Iterable[str] | None = None) -> SessionFacts` - `GET /analysis/session/{sid}?filter=facts[&relationships=a,b]`; a single `str` is sent as-is (`relationships="speaks"` -> `relationships=speaks`), an iterable is comma-joined.
|
|
165
|
+
- `interact(prompt) -> InteractResponse` - `POST /nl/interact` (beta). Errors are returned, not raised.
|
|
166
|
+
- `explain(fact_id, *, language="en", instructions=None) -> ExplainResponse` - `POST /nl/explain` (beta). Raises `NaturalLanguageError` when the body has `error` and no `explanation`.
|
|
167
|
+
|
|
168
|
+
### Module-level helpers (`rainbird_ai`)
|
|
169
|
+
|
|
170
|
+
`SKIP` (sentinel, falsy), `build_answers(question, value, *, certainty=100, strict=True) -> list[Answer]`,
|
|
171
|
+
`build_skip(question, *, strict=True) -> list[Answer]`, `parse_query_response(dict) -> QueryResponse`,
|
|
172
|
+
`studio_url_for(api_url) -> str`, `RetryPolicy`, `NO_RETRY`, `DEFAULT_BASE_URL`, `__version__`,
|
|
173
|
+
type aliases `Answerer = Callable[[Question], Any]` and `AsyncAnswerer` (also in `rainbird_ai.aio`).
|
|
174
|
+
|
|
175
|
+
- `engine_version(base_url=None, *, engine=None, timeout=UNSET, http_client=None) -> str` -
|
|
176
|
+
`GET /version` with **no API key** (no `X-API-Key` header is sent). `base_url` falls back to
|
|
177
|
+
`RAINBIRD_API_URL`, then the Community host. Opens and closes its own connection unless you pass
|
|
178
|
+
`http_client` (then you close it). Use it for connectivity/health checks.
|
|
179
|
+
`async_engine_version(...)` (also exported from `rainbird_ai.aio`) is the awaitable twin and takes
|
|
180
|
+
an `httpx.AsyncClient`.
|
|
181
|
+
- `UNSET` - falsy sentinel meaning "argument not given"; the default for `timeout=` in `Rainbird`,
|
|
182
|
+
`AsyncRainbird`, `engine_version` and `async_engine_version`. Distinct from `None`, which disables
|
|
183
|
+
the timeout. Test with `x is UNSET`.
|
|
184
|
+
|
|
185
|
+
## 6. Models (frozen dataclasses; every response model has `.raw` (original JSON) and `.to_dict()`)
|
|
186
|
+
|
|
187
|
+
Inputs (have `.to_payload()`):
|
|
188
|
+
- `Fact(subject: str, relationship: str, object: Any, certainty=100)`.
|
|
189
|
+
- `Answer(subject: str, relationship: str, object: Any, certainty=100, answer: str | None = None, unanswered=False)`. `answer` is `"yes"`/`"no"` for First Form only; `unanswered=True` skips.
|
|
190
|
+
|
|
191
|
+
Decisions:
|
|
192
|
+
- `StartResponse(session_id, km_version_id)`.
|
|
193
|
+
- `InjectResponse(result)`; `.ok` (true for `"OK"`).
|
|
194
|
+
- `Question(subject, relationship, object, type, prompt, data_type, object_type, plural, allow_cf, allow_unknown, can_add, known_answers, concepts: list[Concept], object_metadata)`. Properties: `form -> QuestionForm`, `is_first_form`, `is_second_form`, `asks_for_subject`, `asks_for_object`, `options` (concept values with `invalid_response` false), `is_truth` (`data_type == "truth"`). `str(q)` is the prompt.
|
|
195
|
+
- `Concept(name, value, concept_type, data_type, invalid_response, fsid)`.
|
|
196
|
+
- `QuestionForm`: `FIRST`, `SECOND_OBJECT`, `SECOND_SUBJECT`, `UNKNOWN` (derived from `Question.type`; plain `"Second Form"` is inferred from which side is missing).
|
|
197
|
+
- `QuestionResponse(question, extra_questions, sid)`; `is_question: Literal[True]`, `is_result: Literal[False]` (runtime `True`/`False`), `questions` (main + extra), `results == []`.
|
|
198
|
+
- `ResultItem(subject, relationship, object, certainty, fact_id, relationship_type, subject_metadata, object_metadata)`; also parses the capitalised keys returned by `/nl/interact`.
|
|
199
|
+
- `ResultResponse(results: list[ResultItem], sid)`; `is_question: Literal[False]`, `is_result: Literal[True]`, `question is None`, `best` (max certainty or `None`), `fact_ids`, `len()`, iterating yields `ResultItem`.
|
|
200
|
+
- `Decision(status: DecisionStatus, session_id, relationship, subject, object, decision, certainty, fact_id, question: Question | None, results: list[ResultItem], evidence: list[Evidence], evidence_url, km_id, km_version_id, response: QueryResponse | None, session)`; properties `best`, `is_question`, `is_final`; `to_dict()` is a flat JSON-serialisable summary (no `session`/`response`); `raw` is the underlying response JSON. `DecisionStatus`: `RESULT`, `NO_RESULT`, `QUESTION` (str enum, values `"result"`, `"no_result"`, `"question"`).
|
|
201
|
+
- `QueryResponse = QuestionResponse | ResultResponse` (a type alias, not a class). Because `is_question`/`is_result` are `Literal`-typed, `if response.is_question:` (or `while response.is_question:`) narrows the union to `QuestionResponse` under mypy and the `else` branch to `ResultResponse`, exactly like `isinstance`; `if response.is_result:` narrows the other way.
|
|
202
|
+
|
|
203
|
+
Evidence and analysis:
|
|
204
|
+
- `Evidence(fact_id, source: FactSource | str | None, fact: SessionFact, time (epoch ms), rule: Rule | None)`; `timestamp` (aware datetime), `is_inferred` (`rule is not None`), `child_fact_ids` (fact IDs one level down, deduplicated).
|
|
205
|
+
- `Rule(bindings: dict, conditions: list[Condition | ExpressionCondition], max_certainty)`; `relationship_conditions`, `expression_conditions`.
|
|
206
|
+
- `Condition(subject, relationship, object, certainty, salience, impact, fact_id, fact_key, object_type, alt)`.
|
|
207
|
+
- `ExpressionCondition(text, value, was_met, functions, salience, impact, alt)`; `fact_ids` referenced by evaluated functions.
|
|
208
|
+
- `SessionFact(subject: FactValue, relationship, object: FactValue, certainty, id, source)`; `FactValue(value, concept, data_type)`.
|
|
209
|
+
- `SessionFacts(global_, local, context)`; `all`, `len()`, iterating yields `SessionFact`.
|
|
210
|
+
- `FactSource`: `KNOWLEDGE_MAP="knowledgemap"`, `RULE`, `ANSWER`, `INJECTION`, `DATASOURCE`, `SYNTHESIS` (unknown strings pass through as `str`). `str()` of a `FactSource`, `QuestionForm` or `Environment` member is its `.value` (`str(FactSource.RULE) == "rule"`).
|
|
211
|
+
- `KnowledgeMapVersion(id, name, version_id, version_status, version_created: datetime | None)`; `is_draft`, `version_number` (`3` for `"Version3"`).
|
|
212
|
+
- `InteractionEvent(event, created: datetime | None, values: dict)`.
|
|
213
|
+
|
|
214
|
+
Natural language (beta):
|
|
215
|
+
- `InteractResponse(response_type: "question"|"result"|"error", query, questions: list[Question], results: list[ResultItem], error: NLError | None, metadata: NLMetadata | None, injected_facts, invalid_facts, unmatched: list[str])`; `is_question`, `is_result`, `is_error`, `question` (first or `None`), `prompt` (question prompt or suggested chat response), `raise_for_error()` (raises `NaturalLanguageError`, else returns self).
|
|
216
|
+
- `ExplainResponse(explanation: str | None, error, metadata)`; `str()` is the explanation.
|
|
217
|
+
- `NLError(code, message, suggested_chat_response, status_code)`; `NLMetadata(endpoint, session_id, query_submitted_at, language_engine_response_ms, prompt_tokens, completion_tokens, total_tokens, api_version)`.
|
|
218
|
+
|
|
219
|
+
Knowledge maps (undocumented endpoints):
|
|
220
|
+
- `KnowledgeMapSource(km_id, rblang: str, concepts: list, relationships: list)`.
|
|
221
|
+
- `CreateMapResult(km_id: str (GUID for start()), name, id (Rainbird numeric id))`.
|
|
222
|
+
|
|
223
|
+
## 7. Exceptions (all subclass `RainbirdError`; `except RainbirdError` catches everything the SDK raises on purpose)
|
|
224
|
+
|
|
225
|
+
| Exception | Raised when |
|
|
226
|
+
|-----------|-------------|
|
|
227
|
+
| `RainbirdConfigurationError` (also `ValueError`) | No API key; `base_url` without a host, with a non-`http(s)` scheme, any `?` or `#` (query string or fragment, even empty), or otherwise invalid (also `httpx.InvalidURL`/`UnsupportedProtocol` at request time); empty km_id/session_id/relationship/prompt; `use_draft` with `version`; bad `version`/`batch_size`. |
|
|
228
|
+
| `NoPendingQuestionError` | `answer()`/`skip()` with no pending question and no `question=`. |
|
|
229
|
+
| `InvalidAnswerError` (also `ValueError`) | Value does not fit the question: not yes/no for First Form, list for non-plural, not in `options` when `can_add` is false, skip when `allow_unknown` is false, non-boolean value for a truth object, `QuestionForm.UNKNOWN`; also an unsupported value (`None`, dict, list outside plural, NaN or infinity) wherever a value is coerced, including objects passed to `inject()`/`respond()`. |
|
|
230
|
+
| `InvalidFactError` (also `ValueError`) | Malformed fact/answer payload (missing keys, unknown keys, certainty outside 0-100 or non-numeric, `unanswered` not bool-like, empty list). |
|
|
231
|
+
| `KnowledgeMapError` | `create_map()` got HTTP 200 whose body carries a truthy `err`/`errors`/`error`; `.errors`, `.body`. |
|
|
232
|
+
| `NaturalLanguageError` | `explain()` returned an error object; `interact(...).raise_for_error()`. `.code`, `.message`, `.suggested_chat_response`, `.status_code`, `.body`. |
|
|
233
|
+
| `RainbirdTransportError` > `RainbirdConnectionError` / `RainbirdTimeoutError` | No HTTP response (DNS/TCP/TLS failure, proxy or remote-protocol error, or timeout); `.method`, `.url`. |
|
|
234
|
+
| `UnexpectedResponseError` | 2xx body not in the documented shape, including a non-JSON or empty 2xx body (`expected a JSON body from GET <url> but got: '<snippet>'`), a start response without a session id, an interaction log that is not a list of events (see `interactions()`) and a `GET /version` body that is not a plausible version (JSON must carry a non-empty string or number under `version`/`engine`/`semver`, so `{}`, `[]` and `{"version": null}` raise; plain text must be non-empty, without whitespace or `<`, at most 64 chars); `.body`, `.status_code`. |
|
|
235
|
+
| `RainbirdAPIError` | Any non-2xx, including 3xx (`unexpected redirect to <location>`; redirects are never followed); `.status_code`, `.message`, `.errors` (from `{"err": [...]}`, `{"error": ...}`, `{"message": ...}`), `.body`, `.method`, `.url`, `.retry_after`. Text bodies are whitespace-collapsed and truncated to 300 chars for `.message`/`.errors[0]`; tags are stripped only when the body is HTML (HTML content type or a body starting with `<`: `<html>Bad Gateway</html>` -> `Bad Gateway`), plain text keeps its `<`/`>`; JSON messages are truncated to 300 chars too; `.body` keeps the raw text/JSON. |
|
|
236
|
+
| `BadRequestError` 400 | Missing/invalid parameter, e.g. skipping a question without `allowUnknown` -> `"Please provide an expected boolean value for unanswered."`. |
|
|
237
|
+
| `AuthenticationError` 401 | Invalid/missing API key. |
|
|
238
|
+
| `PermissionDeniedError` 403 | Key not permitted (e.g. secured evidence without `x-evidence-key`). |
|
|
239
|
+
| `NotFoundError` 404 | Unknown session, knowledge map or fact. |
|
|
240
|
+
| `RateLimitError` 429 | Too many requests; `.retry_after`. |
|
|
241
|
+
| `ServerError` 5xx | Platform failure. |
|
|
242
|
+
|
|
243
|
+
`NoActiveSessionError` no longer exists (it was never raised); do not import it. A `Session` from
|
|
244
|
+
`resume()` is always usable; the only "no session state" error is `NoPendingQuestionError`.
|
|
245
|
+
|
|
246
|
+
## 8. Question protocol (what `session.answer(value)` sends)
|
|
247
|
+
|
|
248
|
+
Decide from `q = session.pending_question`:
|
|
249
|
+
|
|
250
|
+
| `q.form` | `q.type` contains | Ask | `answer(value)` builds |
|
|
251
|
+
|----------|-------------------|-----|------------------------|
|
|
252
|
+
| `FIRST` | `"First Form"` | "Does Tom speak French?" | the question's own triple + `answer: "yes"/"no"`. `value`: `True/False`, `"yes"/"no"`, `"y"/"n"`, `1/0`, or an enum member whose `.value` is one of those. |
|
|
253
|
+
| `SECOND_OBJECT` | `"Second Form Object"` | "What does Tom speak?" | `subject=q.subject, object=value`. |
|
|
254
|
+
| `SECOND_SUBJECT` | `"Second Form Subject"` | "Who speaks French?" | `subject=value, object=q.object`. |
|
|
255
|
+
| `UNKNOWN` | anything else | - | `InvalidAnswerError`; build `Answer` objects and call `respond()`. |
|
|
256
|
+
|
|
257
|
+
Rules:
|
|
258
|
+
- **Options**: `q.options` lists the candidate values from `q.concepts`. If `q.can_add` is `False` and options exist, the value must be one of them (strict mode raises `InvalidAnswerError`; the API would reject it anyway). If `can_add` is `True` any value is accepted. Empty options means free text/number/date input. Matching: for a truth-typed Second Form **Object** question the answer and the options are normalised to booleans (`"yes"` matches option `True`). Otherwise an exact string match wins and your value is sent unchanged; a purely numeric match (`5.0` or `"10.0"` against the options `5` / `"10"`) is accepted only when the answer is an `int`/`float`, the Second Form Object question's `data_type` is `number`/`integer`/`float`, or the option itself is a number, and then the **option's own value** is sent (the concept instance the engine knows), so `answer(5.0)` sends `5`. Everything else (Second Form Subject answers included) must equal an option exactly as a string; an `int` too large for a `float` simply fails to match (`InvalidAnswerError`, never `OverflowError`). If every concept is flagged `invalidResponse`, `q.options` is empty and validation is left to the API.
|
|
259
|
+
- **plural**: `q.plural=True` accepts a list/tuple/set of values -> one answer per value in a single request. A list of more than one value on a non-plural question raises `InvalidAnswerError` (strict).
|
|
260
|
+
- **allow_unknown**: `q.allow_unknown=True` allows skipping: `session.skip()` or `answer(SKIP)` sends `{"unanswered": true, certainty: 100}` with `object: ""` (subject questions: `subject: ""`; First Form keeps its object). When `allow_unknown` is `False`, strict mode raises `InvalidAnswerError`; if you bypass it the API answers 400.
|
|
261
|
+
- **allow_cf**: when `True` the engine honours `certainty=` (0-100; API documents 1-100; 100 = certain). The SDK always sends `certainty` (default 100); values outside 0-100 raise `InvalidFactError`.
|
|
262
|
+
- **data_type** (describes the object side): `"string"` -> str; `"number"` -> int/float; `"truth"` -> Python `bool` for Second Form **Object** answers (`True/False`, `1/0`, `"yes"/"y"/"true"/"t"/"1"`, `"no"/"n"/"false"/"f"/"0"`, or an enum member with one of those values, are converted; the payload carries a JSON boolean; anything else -> `InvalidAnswerError`). Second Form **Subject** answers are always sent as plain strings, whatever `data_type` says. `"date"` -> pass `datetime.date`/`datetime.datetime`, sent as `"YYYY-MM-DD"` (the time of day of a `datetime` is dropped; pass an `int` of epoch milliseconds if you need an instant), or an ISO string. `Decimal` becomes `int` when integral, else `float`; enums (including `str`/`int`/`bool`-valued ones) use `.value`, for First Form and truth answers too; `None`, dicts, lists (outside plural) and non-finite numbers (NaN/infinity, as `float` or `Decimal`) raise `InvalidAnswerError`.
|
|
263
|
+
- `certainty` on the answer applies to every generated answer of a plural response.
|
|
264
|
+
- `q.known_answers` lists values already known for this question. `response.extra_questions` are optional additional questions; `answer()` only handles `response.question`, so to answer several at once use `session.respond(build_answers(q1, v1) + build_answers(q2, v2))`.
|
|
265
|
+
- `strict=False` on `answer()`/`skip()`/`build_answers()` skips client-side validation and lets the API judge.
|
|
266
|
+
|
|
267
|
+
## 9. Answerer contract for `session.run(...)`
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from rainbird_ai import SKIP, Question
|
|
271
|
+
|
|
272
|
+
def answerer(q: Question):
|
|
273
|
+
if q.is_first_form: # yes/no
|
|
274
|
+
return False
|
|
275
|
+
if q.is_truth: # truth-typed value question
|
|
276
|
+
return True
|
|
277
|
+
if q.options: # choose among concepts (respect q.can_add / q.plural)
|
|
278
|
+
return q.options[:2] if q.plural else q.options[0]
|
|
279
|
+
if q.allow_unknown: # nothing to say and skipping is allowed
|
|
280
|
+
return SKIP
|
|
281
|
+
return "German" # free value: str / int / float / date
|
|
282
|
+
|
|
283
|
+
result = session.run("speaks", subject="Tom", answerer=answerer, max_questions=50)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Return values accepted: a single value, a list of values (plural), `SKIP`, an `Answer`, or a
|
|
287
|
+
list of `Answer` (sent verbatim via `respond()`). Returning a value for a question the value does
|
|
288
|
+
not fit raises `InvalidAnswerError` from inside `run()`. `AsyncSession.run` accepts a sync or
|
|
289
|
+
`async def` answerer. The loop stops with `RainbirdError` after `max_questions` (default 100).
|
|
290
|
+
`run()` never returns a `QuestionResponse`.
|
|
291
|
+
|
|
292
|
+
## 9b. One-shot decisions with `rb.decide(...)`
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
d = rb.decide("speaks", km_id="<km>", subject="Tom", facts=[("Tom", "lives in", "Germany")])
|
|
296
|
+
if d.status is DecisionStatus.RESULT:
|
|
297
|
+
d.decision, d.certainty, d.fact_id, d.results, d.evidence_url
|
|
298
|
+
elif d.status is DecisionStatus.QUESTION:
|
|
299
|
+
d.question.prompt # engine needs more; d.session has it pending
|
|
300
|
+
response = d.session.answer("German") # or later: rb.resume(d.session_id).answer(v, question=d.question)
|
|
301
|
+
else: # DecisionStatus.NO_RESULT
|
|
302
|
+
... # nothing could be inferred (not an error)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`decision` is the unknown side of the triple: the best result's `object` when no object was
|
|
306
|
+
given (including relationship-only queries), its `subject` when only the object was given, and
|
|
307
|
+
`True`/`False` when both were given (`False` on `NO_RESULT`). `certainty`/`fact_id` come from the
|
|
308
|
+
same best (highest-certainty) result; `results` has them all. Pass `answerer=` (same contract
|
|
309
|
+
as section 9) to make the status always final. `evidence="root"` / `"tree"` fill `d.evidence`
|
|
310
|
+
(`evidence_depth` limits the tree) and need the evidence key; `d.evidence_url` is always set
|
|
311
|
+
when there is a `fact_id`. Errors (401, 404, network, invalid facts, bad `evidence` value ->
|
|
312
|
+
`RainbirdConfigurationError`) raise as usual. CLI: `rainbird decide -r speaks -s Tom [--fact ...]
|
|
313
|
+
[--evidence root|tree] [--json]`, exit 3 when the status is `question`.
|
|
314
|
+
|
|
315
|
+
## 10. Reading results, evidence and explanations
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
result = session.result # ResultResponse after the loop, or response itself
|
|
319
|
+
for item in result: # ResultItem
|
|
320
|
+
item.subject, item.relationship, item.object, item.certainty, item.fact_id
|
|
321
|
+
best = result.best # None when result.results is empty (nothing inferred)
|
|
322
|
+
if best and best.fact_id:
|
|
323
|
+
ev = session.evidence(best.fact_id) # Evidence: ev.source, ev.fact, ev.rule, ev.child_fact_ids
|
|
324
|
+
if ev.rule:
|
|
325
|
+
ev.rule.bindings # {"S": "Tom", "COUNTRY": "Germany", ...}
|
|
326
|
+
for cond in ev.rule.conditions: # Condition (has .fact_id) | ExpressionCondition (.text, .was_met)
|
|
327
|
+
...
|
|
328
|
+
tree = session.evidence_tree(best.fact_id, max_depth=3) # list[Evidence], root first
|
|
329
|
+
url = session.evidence_url(best.fact_id) # Studio evidence-tree link
|
|
330
|
+
text = session.explain(best.fact_id, language="en").explanation # beta, LLM generated
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`fact_id` values are opaque strings (typically `WA:RF:<hash>` for rule-inferred facts, `WA:IF:`
|
|
334
|
+
injected, `WA:AF:` answered); never parse them, pass them to `evidence()`/`explain()`. An empty
|
|
335
|
+
`results` list is a valid decision ("nothing could be inferred"), not an error. Evidence is secured by default: pass `evidence_key=` (or `RAINBIRD_EVIDENCE_KEY`) or a
|
|
336
|
+
403 `PermissionDeniedError` is raised.
|
|
337
|
+
|
|
338
|
+
Natural language (beta, results vary): `r = session.interact("Ben lives in Germany. What does he speak?")`;
|
|
339
|
+
`r.response_type` is `"question"`, `"result"` or `"error"`. Check `r.is_error` / `r.prompt` /
|
|
340
|
+
`r.error.suggested_chat_response`; call `r.raise_for_error()` only if you want an exception.
|
|
341
|
+
`r.injected_facts`, `r.invalid_facts`, `r.unmatched` report what the language layer extracted.
|
|
342
|
+
|
|
343
|
+
## 11. Retries, timeouts and idempotency
|
|
344
|
+
|
|
345
|
+
Default `RetryPolicy(max_retries=2, backoff_factor=0.5, max_backoff=20.0,
|
|
346
|
+
retry_statuses={429,500,502,503,504}, post_retry_statuses={429,503},
|
|
347
|
+
retry_post_on_transport_error=False)`, exponential backoff with jitter, honours `Retry-After`.
|
|
348
|
+
|
|
349
|
+
- `GET` (start, evidence, analysis, version) retries on connection errors, timeouts and 429, 500,
|
|
350
|
+
502, 503 and 504 (`retry_statuses`).
|
|
351
|
+
- `POST` (inject, query, response, undo, nl/*, maps) retries only on 429/503 (the service refused
|
|
352
|
+
the request before processing it) and **never on 502/504, timeouts or connection errors**: a
|
|
353
|
+
gateway can report 502/504 after the engine has already applied the request, and a
|
|
354
|
+
`POST /{sid}/response` that timed out may already have been applied server-side, so retrying
|
|
355
|
+
could answer twice.
|
|
356
|
+
- Only `httpx.TimeoutException`, `NetworkError`, `RemoteProtocolError` and `ProxyError` count as
|
|
357
|
+
retryable transport errors; `httpx.InvalidURL`/`UnsupportedProtocol` raise
|
|
358
|
+
`RainbirdConfigurationError` at once. Redirects are never followed (3xx -> `RainbirdAPIError`).
|
|
359
|
+
Back-off sleeps with `time.sleep` (sync) / `anyio.sleep` (async).
|
|
360
|
+
- After a `RainbirdTimeoutError` from `answer()/respond()/skip()`, `session.pending` still holds
|
|
361
|
+
the question you were answering (state is only updated on success), so you cannot tell whether
|
|
362
|
+
the answer landed. Recover by re-issuing `session.query(...)` with the same arguments: the
|
|
363
|
+
session keeps every fact already accepted and the engine re-asks only what it still needs.
|
|
364
|
+
Alternatively start a fresh session and re-inject known facts. Do not blindly call `undo()`.
|
|
365
|
+
- `retry=NO_RETRY` disables retries; `RetryPolicy(retry_post_on_transport_error=True)` opts into
|
|
366
|
+
POST retries on transport errors and `RetryPolicy(post_retry_statuses={429, 502, 503, 504})`
|
|
367
|
+
restores gateway-status retries for POST; both only if your flow tolerates duplicate answers.
|
|
368
|
+
- Reuse one `Rainbird` client (connection pool) for many sessions; a `Session` object holds
|
|
369
|
+
mutable state (`last_response`) and must not be shared across concurrent tasks. Rainbird
|
|
370
|
+
recommends a new session per query unless several queries must share facts.
|
|
371
|
+
|
|
372
|
+
## 12. Undocumented knowledge-map endpoints (unstable)
|
|
373
|
+
|
|
374
|
+
Not in the OpenAPI spec; supplied by Rainbird maintainers and may change without notice.
|
|
375
|
+
|
|
376
|
+
- `rb.get_map_source(km_id) -> KnowledgeMapSource` - `GET /analysis/file/{kmID}` with `X-API-Key`; returns `{"concepts": [...], "rels": [...], "rblang": "<?xml ..."}` -> `.rblang`, `.concepts`, `.relationships`.
|
|
377
|
+
- `rb.create_map(name, rblang, description=None) -> CreateMapResult` - `POST /maps` with `X-API-Key` and `Version: v1`, body `{"rblang": xml, "name": name}` plus `"description": description` when given -> `{"kmID": "...", "id": 123, ...}` (`.description` echoes the response's value or your argument). On RBLang parse failure Rainbird replies **HTTP 200 with an `error` key**; the SDK raises `KnowledgeMapError` (`.errors`). Only truthy `err`/`errors`/`error` values are treated as failures (`{"error": false}`, `null`, `""`, `0`, `[]` and `{}` are success); an informational `message` on a success body is ignored. Use `result.km_id` with `rb.start()`.
|
|
378
|
+
|
|
379
|
+
Minimal RBLang: `<rbl:kb xmlns:rbl="http://rbl.io/schema/RBLang"><concept name="Person" type="string"/><concept name="Language" type="string"/><rel name="speaks" subject="Person" object="Language"/></rbl:kb>`
|
|
380
|
+
(concept types: `string|number|date|truth`; instances via `<concinst>`/`<relinst>`).
|
|
381
|
+
|
|
382
|
+
## 13. Common mistakes
|
|
383
|
+
|
|
384
|
+
1. Putting a `Second Form Subject` answer in `object` (or vice versa). Use `session.answer(value)`; it reads `q.form`. When building `Answer` by hand, check `q.asks_for_subject`.
|
|
385
|
+
2. Skipping when `q.allow_unknown` is `False`: `InvalidAnswerError` locally, 400 from the API. Provide a value from `q.options` instead.
|
|
386
|
+
3. Answering a First Form question with the object value (`"French"`) instead of yes/no.
|
|
387
|
+
4. Sending several values to a non-plural question, or a value outside `q.options` when `q.can_add` is `False`.
|
|
388
|
+
5. Passing a truth answer as the string `"true"` in a hand-built `Answer` (send a JSON boolean; `answer()` converts for you) or a date as a non-ISO string.
|
|
389
|
+
6. Hardcoding `https://api.rainbird.ai` for Enterprise/private accounts: use `Environment.ENTERPRISE` or `https://<client>-api.rainbird.ai`; a 401 `AuthenticationError` with a valid key usually means the wrong host.
|
|
390
|
+
7. Adding `Version: v1`, `X-API-Key` or `x-rainbird-engine` headers yourself, or using `requests`: the SDK sets all headers.
|
|
391
|
+
8. Treating `interact()` errors as exceptions: they are returned in `InteractResponse` (`is_error`); only `raise_for_error()` raises.
|
|
392
|
+
9. Reading `response.results` on a `QuestionResponse` (always `[]`) or `response.question` on a `ResultResponse` (always `None`) without branching on `is_question`.
|
|
393
|
+
10. Calling `answer()` on a `resume()`d session before a `query()`: no pending question -> `NoPendingQuestionError`; pass `question=` or query first.
|
|
394
|
+
11. Expecting `result.best` to be non-`None`; empty results are legitimate.
|
|
395
|
+
12. Retrying a timed-out `answer()` in your own code (duplicate answers). Re-query instead (section 11).
|
|
396
|
+
13. Using `use_draft=True` together with `version=N`, or forgetting that the default is the live version (draft changes are invisible until published or `use_draft=True`).
|
|
397
|
+
14. Fetching evidence without an evidence key on a secured map (403), or reading `interactions()` on a session started before logging was enabled (empty).
|
|
398
|
+
15. Forgetting `rb.close()` / `await rb.aclose()` (or the context manager) in long-running processes.
|
|
399
|
+
16. Importing `NoActiveSessionError`: it was removed (never raised). Catch `NoPendingQuestionError` for `answer()`/`skip()` without a question and `RainbirdConfigurationError` for bad arguments.
|
|
400
|
+
17. Passing a `datetime` and expecting the time of day to survive: only `YYYY-MM-DD` is sent. Use an `int` of epoch milliseconds for instants.
|
|
401
|
+
18. Constructing `Rainbird()` (which demands a key) just to call `version()` for a health check: `engine_version()` / `async_engine_version()` need no key.
|
|
402
|
+
19. Retrying `POST` on 502/504 yourself, or setting `post_retry_statuses` to include them, without accepting that the answer may be applied twice (section 11).
|
|
403
|
+
|
|
404
|
+
## 14. References
|
|
405
|
+
|
|
406
|
+
- Rainbird docs index for LLMs: https://docs.rainbird.ai/llms.txt
|
|
407
|
+
- Developer docs: https://docs.rainbird.ai/rainbird/developer-docs/overview
|
|
408
|
+
- API request flow: https://docs.rainbird.ai/rainbird/developer-docs/api-guide/api-request-flow
|
|
409
|
+
- Example API requests: https://docs.rainbird.ai/rainbird/developer-docs/api-guide/example-api-requests
|
|
410
|
+
- API keys / publishing: https://docs.rainbird.ai/rainbird/knowledge-modelling/publishing-your-km/api-management
|
|
411
|
+
- Knowledge modelling (RBLang, concepts, rules): https://docs.rainbird.ai/rainbird/knowledge-modelling/overview
|
|
412
|
+
- OpenAPI 3.1 reference (rendered spec, downloadable): https://rainbird.redoc.ly/
|
|
413
|
+
- SDK source of truth: `src/rainbird_ai/client.py` (sync), `aio.py` (async), `models.py`, `_answers.py` (answer rules), `errors.py`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.0] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
First release published on PyPI.
|
|
13
|
+
|
|
14
|
+
[Unreleased]: https://github.com/RainBirdAi/sdk-python/compare/v0.5.0...HEAD
|
|
15
|
+
[0.5.0]: https://github.com/RainBirdAi/sdk-python/releases/tag/v0.5.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rainbird Technologies Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|