Skip to main content

aivana

Python client for the Aivana Intelligence API.

Requires Python 3.10 or newer. Built on httpx, with sync, async, and streaming interfaces.

Install

pip install aivana

Quick start

Configuration is module-level, Stripe-style:

import aivana

aivana.api_key = "ai_live_xxx"

res = aivana.generate("Should we enter the EU market in 2027?")
print(res.answer)

Your API key is the only required setting. aivana.api_key = ... and aivana.set_api_key(...) are equivalent.

api_base defaults to the hosted API at https://developers.aivana.ai, the same default as @aivana/sdk. Point it elsewhere only to run against your own deployment:

aivana.set_api_base("http://localhost:8088")   # local engine

Command line

The package also installs an aivana command. To use it without adding the SDK to a project, install it as a standalone tool:

pipx install aivana        # or run it without installing: uvx aivana "..."
export AIVANA_API_KEY=ai_live_xxx
aivana "Should we enter the EU market in 2027?"

The answer streams to stdout as it is written; progress, the trace and errors go to stderr. So aivana "..." > answer.md saves just the answer, and the command works in pipes. Text piped in is sent ahead of the question:

git diff | aivana "Review this change" --effort high
aivana "Summarise the three biggest risks" < contract.txt
aivana "What's driving the dip in this chart?" --image chart.png
aivana "Postgres or DynamoDB for a write-heavy API?" --shape tradeoffs --trace
aivana "Extract the invoice number and total" --shape extract --json < invoice.txt

Every option maps to one the SDK already has:

option SDK equivalent
--effort auto|low|medium|high effort
--shape SHAPE output_shape
--web / --no-web web_search=True / False; neither means no search, the default for API keys
--system TEXT system
--assistant-name NAME assistant_name
--max-tokens N max_tokens
--temperature T temperature
--image PATH (repeatable) attachments
--trace intelligence_trace=True, printed to stderr
--json the whole GenerateResponse as JSON, without streaming

As in the SDK, there is deliberately no option to choose a model or provider. AIVANA_API_BASE points the command at another deployment, like set_api_base(); python -m aivana runs the same program.

The Node CLI (npm install -g @aivana/cli) installs a command with the same name, and both must behave identically. That is enforced by a shared conformance suite, conformance/cli.json, which each implementation runs in its own tests.

Scripts can branch on the exit code:

code meaning
0 success
1 the request failed
2 bad usage
3 authentication problem: no key, or an invalid or expired one
4 rate limited: wait, then retry
5 out of credits: top up in AI Studio
6 temporary failure (network, timeout, upstream): safe to retry
130 interrupted

Two things worth knowing:

  • The key comes from AIVANA_API_KEY only. There is no --api-key flag, on purpose: a key typed as an argument is saved in your shell history and is visible to other users in the process list.
  • Quote the question. Unquoted words work, but the shell acts on characters such as ?, * and ' before aivana sees them. aivana ask "..." is the same as aivana "...", for a question that starts with a word the CLI keeps for future commands (chat, usage, ...).

Streaming

async for chunk in aivana.generate_stream("Explain the CAP theorem"):
    print(chunk.delta, end="")

Async

res = await aivana.generate_async("Summarise our Q3 churn drivers.")

Conversation history

Aivana is stateless — it never stores your conversations. To ask a follow-up you send the prior turns back with the next question. Two ways to do that.

Let the SDK track it. Chat keeps the history in memory and appends each turn:

chat = aivana.Chat()
chat.send("We're choosing between Postgres and DynamoDB.")
chat.send("What changes if write volume triples?")   # remembers the above
chat.reset()                                         # start fresh

Or manage it yourself — which is what you want when the history lives in your own database and spans processes, requests or machines:

messages = [
    {"role": "user",      "content": "We're choosing between Postgres and DynamoDB."},
    {"role": "assistant", "content": previous_answer},
    {"role": "user",      "content": "What changes if write volume triples?"},
]

res = aivana.generate(messages=messages)

Pass messages instead of a prompt — the last user message is the current question. Each item is {"role": ..., "content": ...} in chronological order, with role "user", "assistant" or "system".

A "system" message is lifted into the system field for you, so these are equivalent:

aivana.generate(messages=[{"role": "system", "content": "Answer in bullets."},
                          {"role": "user", "content": "Why did latency spike?"}])

aivana.generate("Why did latency spike?", system="Answer in bullets.")

If you pass both, the explicit system argument wins and the system message is ignored — the two are never concatenated, because a merge order you cannot see is worse than a rule you can.

Follow-ups that refer back

For replies like "yes, do that" or "the second one", also pass previous_intent and pending_action from the previous response. They are what let Aivana resolve a bare "yes" against what was actually offered:

first = aivana.generate("Should we migrate to DynamoDB?")

second = aivana.generate(
    messages=messages,
    previous_intent=first.intent.name,
    pending_action=first.pending_action,
)

Chat does this for you automatically — it is only manual history that needs it.

Limits

current default
Messages per request 40
Characters per message 200,000
Characters per prompt 200,000

History is re-sent on every turn and billed every turn, because nothing is stored server-side. On a long conversation, trim or summarise old turns rather than replaying all 40 — that is the single biggest lever on what a chat costs.

Files and documents

Only images can be attached. image/png, image/jpeg, image/webp, image/gif — up to 6 per request, 8 MiB each decoded.

PDF, Word and Excel files are not supported. Sending one raises InvalidRequestError — the file is rejected, never silently ignored. The message is deliberately terse ("request input was rejected"): it confirms the request was the problem, but it does not name the offending type, so check the list above rather than the error text.

To ask about a document, extract its text yourself and pass that as the prompt. The extraction libraries below are not dependencies of this SDK — install whichever you need:

PDF — pip install pypdf

from pypdf import PdfReader
text = "\n".join(page.extract_text() or "" for page in PdfReader("report.pdf").pages)

Word — pip install python-docx

from docx import Document
text = "\n".join(p.text for p in Document("contract.docx").paragraphs)

Excel / CSV — pip install pandas openpyxl

import pandas as pd
text = pd.read_excel("q3.xlsx").to_markdown(index=False)

Then send the text as the prompt:

res = aivana.generate(
    f"Summarise the three biggest risks in this document.\n\n{text}",
    system="Quote the passage each risk comes from.",
)

Two things worth doing:

  • Send text, not markup. Stripped text costs far fewer tokens than raw HTML or XML, and answers are usually better for it.
  • Watch the size. A long document can approach the 200,000-character prompt limit. For a large file, extract the relevant sections rather than the whole thing — and remember every character is billed.

A scanned PDF with no text layer extracts to nothing. Render those pages to PNG and send them as image attachments instead.

Options

Every option is optional, and every entrypoint — generate, generate_async, generate_stream, Chat — accepts all of them as keyword arguments.

Omitting one is not a gap to fill in: it hands the decision to Aivana, which tunes temperature, answer length and web search per question. Pass a value to override that.

option type default when omitted
system str no persona — Aivana's own voice
assistant_name str the assistant does not name itself
web_search bool no search: the default for API keys
effort "auto"/"low"/"medium"/"high" "auto" — Aivana decides
temperature 0.0–2.0 chosen per request
top_p 0.0–1.0 each model's own default
stop_sequences list[str] none — the answer ends naturally
intelligence_trace bool off — no trace is returned
max_tokens int sized to the question
output_shape str "auto"
attachments list none
metadata dict none

Choose how much intelligence to spend

effort is the one option that is about Aivana rather than about a model. temperature, top_p and max_tokens shape how a model writes; effort decides how much work goes into the answer in the first place.

# A lookup you want back fast and cheap.
aivana.generate("What's the default port for Postgres?", effort="low")

# A decision you are going to act on.
aivana.generate(
    "Should we move billing off Stripe before the Series A?",
    effort="high",
)
band what happens when to reach for it
"auto" Aivana judges from the question the default — leave it alone unless you know better than the question does
"low" the fastest, cheapest path: one independent perspective lookups, classification, formatting, anything with one right answer
"medium" two independent perspectives, compared and reconciled contested or subjective questions with a bounded blast radius
"high" three or more perspectives hard, high-stakes, open-ended questions

Two things worth knowing:

  • It is a bound, not an instruction. Aivana still reads the question and still decides how to answer it — effort only constrains how far it may go.
  • It is not a length control. "low" does not mean "short"; use max_tokens for that. Cost scales roughly with the band, so "high" on a trivial question spends more for no gain — which is exactly the judgement "auto" exists to make.

Give the assistant a persona

system is your own instructions — tone, format, domain, things to refuse:

res = aivana.generate(
    "A customer wants a refund after 40 days.",
    system=("You are a support lead for a UK retailer. Answer in three short "
            "bullets. Cite the policy rule you applied. Never promise a refund "
            "outside policy."),
)

assistant_name sets the name it presents as:

res = aivana.generate("Who are you?", assistant_name="Acme Copilot")
# → "I'm Acme Copilot..."

Use assistant_name rather than writing "your name is Acme" into system. A name asked for inside a system prompt is only a request the model may or may not honour — measured at roughly one time in three. assistant_name is substituted before the model sees anything, so it always holds.

Two things to know:

system is additive, not a replacement. Aivana keeps its own instructions and they win on conflict. You can shape persona, tone, format and domain focus — you cannot use it to make Aivana reveal which underlying models produced an answer.

Keep it short. Aivana may re-send your system prompt internally more than once while answering, so a long persona costs more tokens than its length suggests. The 8000-character limit is checked client-side, so an oversized prompt raises InvalidRequestError before the request is sent.

Randomness: temperature or top_p, not both

Both control how varied an answer is; they just do it differently. temperature reshapes the whole probability distribution, top_p narrows the pool to the most likely tokens whose probabilities add up to your value.

aivana.generate("Extract the invoice number", top_p=0.1)    # tight, predictable
aivana.generate("Ten campaign taglines", temperature=1.2)    # varied

Set one of the two. Sending both is accepted, but you are then steering the same thing with two dials and the result is harder to reason about — and for some questions Aivana cannot pass both through, in which case top_p wins and the temperature is dropped for that call.

top_p=0.0 is legal and is the most deterministic setting there is. Omitting the option is not the same as 1.0: omitted leaves the choice to Aivana.

Stop sequences

Up to four strings. The answer ends where the first one appears, and the string itself is not returned.

aivana.generate("Answer, then stop.", stop_sequences=["###", "\n\nUser:"])

Useful when you are parsing the answer and know its end marker. Two things to know: it shapes the output you receive, so it behaves the same on every question — and the text past the marker is still generated and still billed, so this is not a way to spend less.

Intelligence Trace

Aivana decides how much intelligence each request needs. intelligence_trace asks it to show its working.

resp = aivana.generate(
    "Compare Postgres and DynamoDB for a write-heavy API.",
    intelligence_trace=True,
)

print(resp.trace["summary"]["route"])       # "2-model verification"
for step in resp.trace["steps"]:
    print(step["at_ms"], step["title"], "—", step["detail"])
1204 Understanding Request — Multi-part request with several considerations
1240 Fresh Data Check — Existing knowledge is sufficient; no live lookup needed
1255 Selecting Intelligence Path — A 2-model verification path would provide the most reliable answer
3980 Establishing Lead Perspective — Primary analysis generated
6310 Independent Perspectives — A second perspective independently analyzed the request
6402 Evaluating Perspectives — Perspectives agreed; the answer was confirmed rather than changed
8120 Final Synthesis — Findings combined into a single coherent answer
8155 Completed — Response delivered

The trace changes with the route. A simple question shows four steps and says additional perspectives were unlikely to improve the answer; a question needing current information shows the live-source lookup and the validation against what came back. trace["summary"] carries the route, how many perspectives were engaged, and whether fresh data was used; trace["why_this_route"] explains the choice in plain sentences.

With stream=True the same information arrives as trace chunks while the answer is being produced, so you can render the run as it happens:

for chunk in aivana.generate("...", stream=True, intelligence_trace=True):
    if chunk.event == "delta":
        print(chunk.delta, end="")
    elif chunk.event == "trace" and "step" in chunk.data:
        step = chunk.data["step"]
        print(f"\n[{step['status']}] {step['title']}")

Each step arrives twice — once as it starts, once as it finishes — followed by a single consolidated trace at the end.

The trace describes decisions and outcomes. It does not name the models that answered, and it never exposes scoring, thresholds or prompts.

web_search has three states:

aivana.generate("What did the EU AI Act change in August?", web_search=True)
aivana.generate("Explain how quicksort works", web_search=False)
aivana.generate("Is our pricing still competitive?")          # the default: no search
value behaviour
True always search the web before answering
False never search
omitted the API's default applies; for an API key, that is no search

On an API key the default is off — a search never happens unless you ask for one, so it cannot turn up unannounced on your bill. Omitting the option and passing False are still different requests: False stays "never search" even if the API's default changes, while an omitted option follows it. Grounding costs extra tokens and latency, so reach for True on current events, prices, releases, competitors and anything else that dates; leave it off for reasoning, code, writing and explanation, which do not improve with a web lookup.

Temperature and length

res = aivana.generate(
    "Draft a launch plan.",
    temperature=0.2,   # omit → chosen per request
    max_tokens=500,    # omit → sized to the question
)

temperature accepts 0.0–2.0. Lower is more deterministic, higher more varied.

max_tokens is a ceiling, not a target. It can only shorten an answer — it never raises the model-aware limit, so a very large value has no effect. When you lower it, Aivana shortens what it aims to write rather than letting a full-length answer get cut off mid-sentence.

Recipes

A support assistant that never searches the web. Options passed to Chat apply to every turn, so set the persona and the search policy once:

support = aivana.Chat(
    assistant_name="Acme Support",
    system=("You are a support lead for Acme. Three bullets maximum. "
            "If the answer isn't in policy, say so and offer to escalate."),
    web_search=False,   # answers come from policy, not the open web
    temperature=0.2,    # consistent phrasing across tickets
)

support.send("Customer wants a refund after 40 days.")
support.send("They're now threatening a chargeback.")   # remembers the above

A research assistant that always searches, overridden per turn where it does not need to:

research = aivana.Chat(web_search=True)

research.send("What are the current enterprise pricing tiers for our competitors?")
research.send("Summarise that as a table", web_search=False)   # no lookup needed

Keyword arguments passed to send() win over the ones set on Chat for that turn.

Structured output you can parse. Combine a low temperature with output_shape:

res = aivana.generate(
    transcript,
    output_shape="extract",
    temperature=0,
    system="Return only the fields asked for. Use null when a field is absent.",
)

print(res.structured)         # parsed dict, or None
print(res.structured_error)   # why parsing failed, if it did

Streaming with a persona. Options behave identically on the streaming calls:

for chunk in aivana.generate(
    "Explain our outage to a customer.",
    stream=True,
    assistant_name="Acme Copilot",
    system="Plain language. No blame. Lead with impact, then the fix.",
    max_tokens=300,
):
    print(chunk.delta, end="", flush=True)

Images. Send a screenshot or diagram with the question:

import base64

res = aivana.generate(
    "What's driving the dip in this chart?",
    attachments=[{
        "mime_type": "image/png",
        "data": base64.b64encode(open("chart.png", "rb").read()).decode(),
    }],
)

data takes raw base64 or a full data URL. Up to 6 images per request, 8 MiB each decoded, in image/png, image/jpeg, image/webp or image/gif. Attachments apply to the current turn only and are never replayed, so resend the image with a follow-up about it.

Errors

Every failure raises a subclass of AivanaError:

AuthError · RateLimitError · InvalidRequestError · UpstreamError

from aivana import RateLimitError

try:
    aivana.generate("...")
except RateLimitError as err:
    backoff(err)

License

Apache-2.0

Release files for aivana 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aivana 0.2.0
File Size Uploaded
aivana-0.2.0.tar.gz 43.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aivana 0.2.0
File Interpreter ABI Platform
aivana-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 73.2 kB

Release files / aivana-0.2.0.tar.gz

Download URL aivana-0.2.0.tar.gz
Size 43.4 kB
Tags Source
SHA-256 checksum
How to use checksums
f142fd4a8aab5f26c0872f175cdfdebf6e20edb3486f6d6d78476cef4ddf45d8
BLAKE2b-256 checksum
How to use checksums
07a3e90603a10602922e9631ab4b62dcb2990e6c8e76db408fa77c3c4a50d9af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / aivana-0.2.0-py3-none-any.whl

Download URL aivana-0.2.0-py3-none-any.whl
Size 29.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
90e594ade6bf3394ce4f108b9ef3e23c35ecd58584d5f184a1562c272f94b66c
BLAKE2b-256 checksum
How to use checksums
4620ca985959108ba0a257157a3507501173012690d3c3aebbdff24a72de0bce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page