Integrations
LlamaIndex
llama-index-wity lets you use Wity inside LlamaIndex. Ask typed questions in a workflow, let Wity pick the query engine for each query, and stop risky tool calls before they run.
It has three parts:
Install#
pip install llama-index-wity
- Needs Python 3.10 or newer.
- It's built on the Python SDK, so answers have the same fields.
- The package is called
llama-index-wity, but in code you import it asllama_index.wity.
API key#
Get an API key from the keys page. Save it in your environment variables as WITY_API_KEY, and the classifier reads it by itself:
export WITY_API_KEY="wity_..."
To pass a key from somewhere else, like a secret manager, use WityClassifier(api_key=...).
Your first request#
This example reads a customer's message and asks Wity three questions about it: which team should handle it, whether it's urgent, and how much it hurts the customer. All three are answered in one request.
from llama_index.wity import WityClassifier, choice, noul, scoreclassifier = WityClassifier() # reads your key from WITY_API_KEYres = classifier.classify(# The text you want Wity to judge."I was charged twice for my subscription this month. Please refund one of them today.",{# Pick one of these teams."team": choice("Which team should handle this ticket?", {"billing": "Payments, charges and refunds","technical": "Bugs, errors and outages","other": "Anything else",}),# Yes or no."urgent": noul("Does the customer ask for something to happen soon?",yes="They ask for fast action",no="No time pressure",),# A level on a scale, from lowest to highest."severity": score("How much is this hurting the customer?", ["Not at all", "A little", "A lot"]),},)res.choices["team"].choice # "billing"res.choices["team"].probabilities # {"billing": 0.99993..., "technical": 0.00004..., "other": 0.00001...}res.nouls["urgent"].noul # 0.99986...: the probability of yesres.scores["severity"].score # 1.97852...: the expected level, from 0 (Not at all) to 2 (A lot)
- The first argument is the state: the text Wity judges. The second names each question, and you use that name to find its answer.
- Answers are grouped by type in
res.choices,res.noulsandres.scores. They're the Python SDK's answer objects, unchanged. - Use
await classifier.aclassify(...)in async code. - Keep one classifier and reuse it, so later calls reuse its connection. Call
close()when you're done.
Probabilities
In a workflow#
Give the classifier its questions when you create it. Then each call takes only the state. State can be text, a dict, a list, or LlamaIndex ChatMessages, so Wity can judge a whole chat:
from llama_index.core.workflow import StartEvent, StopEvent, Workflow, stepfrom llama_index.wity import WityClassifier, choice# Questions set once, so each call takes only the chat.team = WityClassifier(questions={"team": choice("Which team should handle this chat?", {"billing": "Payments, charges and refunds","technical": "Bugs, errors and outages",})},reasoning="off",)class Triage(Workflow):@stepasync def route(self, ev: StartEvent) -> StopEvent:res = await team.aclassify(ev.chat)answer = res.choices["team"]if answer.confidence < 0.7:return StopEvent(result="Sent to a person: Wity isn't sure.")return StopEvent(result=f"Sent to {answer.choice}.")await Triage().run(chat=chat) # "Sent to technical."
- Give questions to the constructor or to each call, not both.
- Images, audio and files in messages are sent by type only, like
{"type": "image"}.
Router#
WitySelector plugs into RouterQueryEngine and RouterRetriever. Each tool's description is one option, and Wity picks one for each query.
from llama_index.core.query_engine import RouterQueryEnginefrom llama_index.core.tools import QueryEngineToolfrom llama_index.wity import WitySelector# billing_engine and api_engine are your own query engines.router = RouterQueryEngine(selector=WitySelector(),query_engine_tools=[QueryEngineTool.from_defaults(billing_engine, description="Plans, invoices, charges and refunds"),QueryEngineTool.from_defaults(api_engine, description="API endpoints, parameters and errors"),],llm=llm,)response = router.query("What does a 429 from the API mean?") # answered by api_engineresponse.metadata["selector_result"].reason# "Wity picked this choice with probability 0.999333297708751."
- To change how Wity picks, pass
WitySelector(instructions="Which docs can answer this question?"). - It picks exactly one. Merging answers from several engines isn't supported.
- With only one tool, it's picked without a call to Wity.
- If Wity fails, the error is raised. The selector never guesses.
- Pass
RouterQueryEngineanllmanyway, even though Wity does the picking. Without one, LlamaIndex falls back to its default, OpenAI.
Tool-risk gate#
WityToolGate checks a tool before it runs. Wrap each risky tool with gate.guard(), and give the wrapped tools to your agent. Wity sees the proposed call and the conversation, and answers one yes-or-no question: should this call be stopped?
from llama_index.core.agent.workflow import FunctionAgentfrom llama_index.wity import WityToolGate# read_page, send_email and delete_file are your own tools.# Only the wrapped tools are checked.gate = WityToolGate()agent = FunctionAgent(tools=[read_page, gate.guard(send_email), gate.guard(delete_file)], llm=llm)
- It stops calls the user didn't ask for, calls that do more than the user asked, and calls that expose private data.
- A call that does exactly what the user asked runs, even one like deleting the file they named.
- Only the user's own messages can approve a call. Text in a web page or file can't, even if it pretends to give orders.
- A stopped tool doesn't run. The agent gets an error result instead, so it can tell the user. The same happens if Wity fails.
- The wrapped tool keeps its name, description and arguments, so the LLM sees no difference.
- Call wrapped tools through a LlamaIndex agent, like
FunctionAgentorReActAgent. The gate reads the conversation from the agent.
Tuning the gate
A call is stopped when the probability of risky is at or above threshold, 0.5 by default. Try the gate on some of your own calls, safe and risky. Then set threshold between where they land:
from llama_index.wity import NoulCriteria# Block only when Wity is more sure the call is risky.gate = WityToolGate(threshold=0.8)# Or say what risky and safe mean for your tools.gate = WityToolGate(criteria=NoulCriteria(true="The call writes, deletes, publishes or changes access.",false="The call only reads public or user-provided data.",),)
Reasoning#
Wity answers most questions in one quick pass. On close calls it can think first, which usually takes 1 to 4 seconds. You choose when with reasoning:
classifier = WityClassifier(reasoning="off", max_latency_ms=2000)# The selector and the gate take a classifier of their own, without questions.selector = WitySelector(classifier=WityClassifier(reasoning="off"))gate = WityToolGate(classifier=WityClassifier(reasoning="always"))
max_latency_ms caps how long one request may take, from 200 to 120000 ms. More in Reasoning & auto mode.
Options#
Everything WityClassifier takes:
auto.WITY_API_KEY.WITY_BASE_URL, then Wity's address.Errors#
- Errors are the Python SDK errors, like
RateLimitError. You can import them fromllama_index.wity, and all of them are aWityError. - The classifier already retries. Set
max_retries=0before you add retries of your own. - Each call is a span in LlamaIndex's tracing, so it shows up in tools like Arize Phoenix and Langfuse.
- Your key never appears in errors, logs or traces.
The package is llama-index-wity on PyPI. Questions and bugs: info@alphanimble.com. It's licensed Apache-2.0.
