Metadata-Version: 2.4
Name: woise
Version: 1.0.1
Summary: Woise: voice and chat agents for your systems. Calls, campaigns, chat, live audio and webhooks.
Author: Stele Labs Private Limited
License-Expression: MIT
Project-URL: Homepage, https://woise.ai
Project-URL: Documentation, https://woise.ai/docs/account/sdks
Project-URL: Repository, https://github.com/woise-ai/woise-sdk
Project-URL: Issues, https://github.com/woise-ai/woise-sdk/issues
Keywords: voice,ai,agents,telephony,calls,chat,webhooks
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24
Provides-Extra: live
Requires-Dist: websockets>=12; extra == "live"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: websockets>=12; extra == "dev"
Dynamic: license-file

# woise (Python)

Voice and chat agents for your systems: your CRM, app or phone system places
calls, runs campaigns, chats with agents, talks to them live, and gets the
results back as webhooks.

```bash
pip install woise            # live audio: pip install "woise[live]"
```

Python 3.9+. One dependency (`httpx`). `Woise` is sync, `AsyncWoise` is the same
API with `await`. The API key picks your region: `sk-woise-us-...` talks to
`https://us.woise.ai`, `sk-woise-in-...` to `https://in.woise.ai`. Older keys
without a region need `Woise(key, region="us")`. Local dev or a partner domain:
`Woise(key, base_url="http://localhost:8000")`.

## Build an agent and place a call

```python
import os
from woise import Woise, Agent

woise = Woise(os.environ["WOISE_API_KEY"])

agent = Agent("Smile Dental", prompt="You are Priya, the receptionist at Smile Dental.")
agent.start("greet", say="Hi, this is Priya from Smile Dental.", prompt="Confirm tomorrow's visit.",
            next={"done": "they confirmed or cancelled"})
agent.end("done", say="Thanks, bye.")
agent.after_call(collect={"outcome": "confirmed, cancelled or rescheduled"})
saved = woise.agents.save(agent, publish=True)          # save again later = new draft

call = woise.calls.create(agent=saved, to="+14155550123", variables={"first_name": "Ravi"})
print(call.status, call.id)                              # queued, enr_...
```

`next` maps the step to go to, to the condition in plain words.
`agent.anytime("human", when="asks for a person", transfer="+14155550100")` adds a
step that can fire from anywhere. `agent.node(id, type, data)` adds any other node.

## Run a campaign from a CSV

```python
# leads.csv: phone_number,first_name,plan   (every other column becomes a {{variable}})
campaign = woise.campaigns.create(agent=saved, contacts="leads.csv", number="+14155550100")
campaign.launch()
print(campaign.progress())
campaign.pause(); campaign.resume()
campaign.add_contacts([{"phone_number": "+14155550124", "first_name": "Sita"}])
```

## Get results back: `call.ended` in Flask, with the signature check

```python
hook = woise.webhooks.create(url="https://crm.example.com/woise", events=["call.ended"])
print(hook.secret)                                       # shown ONCE: store it as WOISE_WEBHOOK_SECRET
```

```python
from flask import Flask, abort, request
from woise import verify_signature

app = Flask(__name__)

@app.post("/woise")
def woise_event():
    if not verify_signature(request.get_data(), request.headers.get("X-Woise-Signature"),
                            os.environ["WOISE_WEBHOOK_SECRET"]):
        abort(401)
    event = request.get_json()
    print(event["event"], event["call_id"], event["call_disposition"], event["collected"])
    return "", 204
```

Events: `call.started`, `call.ended`, `chat.ended`, `campaign.finished`. Failed
deliveries are retried with backoff; `woise.webhooks.deliveries(hook)` shows the log.

## Chat

```python
chat = woise.chats.start(saved, variables={"first_name": "Ravi"})
print(chat.greeting)
print(chat.send("Are you open on Sunday?").text)
chat.end()
```

## Runs: transcripts, outcomes, recordings

```python
for run in woise.runs.list(agent=saved, limit=20):
    print(run.id, run.outcome, run.collected)
    print(run.transcript)                                # "agent: ...\ncaller: ..."
print(woise.runs.recording_url(run))                     # short-lived link
```

## Live voice: Twilio Media Streams -> Woise in ~20 lines

Point a Twilio `<Connect><Stream url="wss://you.example.com/twilio"/></Connect>`
at this relay. It forwards Twilio's messages to Woise verbatim and Woise answers
in the same format (media, mark, clear), so barge-in works unchanged.

```python
import asyncio, json, os
import websockets
from woise import AsyncWoise

woise = AsyncWoise(os.environ["WOISE_API_KEY"])
AGENT = "wf_..."                                          # your agent id

async def relay(twilio):
    async with woise.live.voice(AGENT, format="twilio") as call:
        async def up():                                   # Twilio -> Woise
            async for message in twilio:
                await call.send_raw(message)
        async def down():                                 # Woise -> Twilio
            async for event in call:
                await twilio.send(json.dumps(event.data))
        await asyncio.gather(up(), down())

async def main():
    async with websockets.serve(relay, "0.0.0.0", 8080):
        await asyncio.Future()

asyncio.run(main())
```

Raw PCM16 instead (your own media server, 8/16/24 kHz mono):

```python
with woise.live.voice(AGENT, sample_rate=16000) as call:
    call.send_audio(pcm16_bytes)
    for event in call:
        if event.type == "audio": speaker.write(event.audio)
        elif event.type == "interrupt": speaker.flush()     # the caller barged in
        elif event.type == "transcript": print(event.data["role"], event.text)
```

`woise.live.chat(agent)` is the same socket for text: `send_text`, then stream
`agent_text` events.

## More

- Numbers: `woise.numbers.connections()`, `.carrier_numbers(conn)`, `.import_byo(conn, ["+1..."])`,
  `.list(connection_id=conn)`, `.attach(number, agent, connection_id=conn)`, `.detach(number, connection_id=conn)`
  (managed numbers: `.list()`, `.attach(number, agent)`)
- `woise.tools.create_http("lookup_patient", url="https://crm.example.com/patients", parameters={"phone": "the caller's number"})`
- `woise.knowledge.add("faq.pdf")`
- Errors are `WoiseError` with `.status`, `.code`, `.message`, `.request_id`.
  GET/PUT/DELETE are retried on 429/5xx; `calls.create` never is (pass
  `idempotency_key=` to retry safely).

MIT. Copyright (c) 2026 Stele Labs Private Limited.
