woise 1.0.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.
woise-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: woise
3
+ Version: 1.0.0
4
+ Summary: Woise: voice and chat agents for your systems. Calls, campaigns, chat, live audio and webhooks.
5
+ Author: Woise
6
+ License-Expression: BSD-2-Clause
7
+ Project-URL: Homepage, https://woise.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.24
13
+ Provides-Extra: live
14
+ Requires-Dist: websockets>=12; extra == "live"
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=7; extra == "dev"
17
+ Requires-Dist: websockets>=12; extra == "dev"
18
+
19
+ # woise (Python)
20
+
21
+ Voice and chat agents for your systems: your CRM, app or phone system places
22
+ calls, runs campaigns, chats with agents, talks to them live, and gets the
23
+ results back as webhooks.
24
+
25
+ ```bash
26
+ pip install woise # live audio: pip install "woise[live]"
27
+ ```
28
+
29
+ Python 3.9+. One dependency (`httpx`). `Woise` is sync, `AsyncWoise` is the same
30
+ API with `await`. The API key picks your region: `sk-woise-us-...` talks to
31
+ `https://us.woise.ai`, `sk-woise-in-...` to `https://in.woise.ai`. Older keys
32
+ without a region need `Woise(key, region="us")`. Local dev or a partner domain:
33
+ `Woise(key, base_url="http://localhost:8000")`.
34
+
35
+ ## Build an agent and place a call
36
+
37
+ ```python
38
+ import os
39
+ from woise import Woise, Agent
40
+
41
+ woise = Woise(os.environ["WOISE_API_KEY"])
42
+
43
+ agent = Agent("Smile Dental", prompt="You are Priya, the receptionist at Smile Dental.")
44
+ agent.start("greet", say="Hi, this is Priya from Smile Dental.", prompt="Confirm tomorrow's visit.",
45
+ next={"done": "they confirmed or cancelled"})
46
+ agent.end("done", say="Thanks, bye.")
47
+ agent.after_call(collect={"outcome": "confirmed, cancelled or rescheduled"})
48
+ saved = woise.agents.save(agent, publish=True) # save again later = new draft
49
+
50
+ call = woise.calls.create(agent=saved, to="+14155550123", variables={"first_name": "Ravi"})
51
+ print(call.status, call.id) # queued, enr_...
52
+ ```
53
+
54
+ `next` maps the step to go to, to the condition in plain words.
55
+ `agent.anytime("human", when="asks for a person", transfer="+14155550100")` adds a
56
+ step that can fire from anywhere. `agent.node(id, type, data)` adds any other node.
57
+
58
+ ## Run a campaign from a CSV
59
+
60
+ ```python
61
+ # leads.csv: phone_number,first_name,plan (every other column becomes a {{variable}})
62
+ campaign = woise.campaigns.create(agent=saved, contacts="leads.csv", number="+14155550100")
63
+ campaign.launch()
64
+ print(campaign.progress())
65
+ campaign.pause(); campaign.resume()
66
+ campaign.add_contacts([{"phone_number": "+14155550124", "first_name": "Sita"}])
67
+ ```
68
+
69
+ ## Get results back: `call.ended` in Flask, with the signature check
70
+
71
+ ```python
72
+ hook = woise.webhooks.create(url="https://crm.example.com/woise", events=["call.ended"])
73
+ print(hook.secret) # shown ONCE: store it as WOISE_WEBHOOK_SECRET
74
+ ```
75
+
76
+ ```python
77
+ from flask import Flask, abort, request
78
+ from woise import verify_signature
79
+
80
+ app = Flask(__name__)
81
+
82
+ @app.post("/woise")
83
+ def woise_event():
84
+ if not verify_signature(request.get_data(), request.headers.get("X-Woise-Signature"),
85
+ os.environ["WOISE_WEBHOOK_SECRET"]):
86
+ abort(401)
87
+ event = request.get_json()
88
+ print(event["event"], event["call_id"], event["call_disposition"], event["collected"])
89
+ return "", 204
90
+ ```
91
+
92
+ Events: `call.started`, `call.ended`, `chat.ended`, `campaign.finished`. Failed
93
+ deliveries are retried with backoff; `woise.webhooks.deliveries(hook)` shows the log.
94
+
95
+ ## Chat
96
+
97
+ ```python
98
+ chat = woise.chats.start(saved, variables={"first_name": "Ravi"})
99
+ print(chat.greeting)
100
+ print(chat.send("Are you open on Sunday?").text)
101
+ chat.end()
102
+ ```
103
+
104
+ ## Runs: transcripts, outcomes, recordings
105
+
106
+ ```python
107
+ for run in woise.runs.list(agent=saved, limit=20):
108
+ print(run.id, run.outcome, run.collected)
109
+ print(run.transcript) # "agent: ...\ncaller: ..."
110
+ print(woise.runs.recording_url(run)) # short-lived link
111
+ ```
112
+
113
+ ## Live voice: Twilio Media Streams -> Woise in ~20 lines
114
+
115
+ Point a Twilio `<Connect><Stream url="wss://you.example.com/twilio"/></Connect>`
116
+ at this relay. It forwards Twilio's messages to Woise verbatim and Woise answers
117
+ in the same format (media, mark, clear), so barge-in works unchanged.
118
+
119
+ ```python
120
+ import asyncio, json, os
121
+ import websockets
122
+ from woise import AsyncWoise
123
+
124
+ woise = AsyncWoise(os.environ["WOISE_API_KEY"])
125
+ AGENT = "wf_..." # your agent id
126
+
127
+ async def relay(twilio):
128
+ async with woise.live.voice(AGENT, format="twilio") as call:
129
+ async def up(): # Twilio -> Woise
130
+ async for message in twilio:
131
+ await call.send_raw(message)
132
+ async def down(): # Woise -> Twilio
133
+ async for event in call:
134
+ await twilio.send(json.dumps(event.data))
135
+ await asyncio.gather(up(), down())
136
+
137
+ async def main():
138
+ async with websockets.serve(relay, "0.0.0.0", 8080):
139
+ await asyncio.Future()
140
+
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ Raw PCM16 instead (your own media server, 8/16/24 kHz mono):
145
+
146
+ ```python
147
+ with woise.live.voice(AGENT, sample_rate=16000) as call:
148
+ call.send_audio(pcm16_bytes)
149
+ for event in call:
150
+ if event.type == "audio": speaker.write(event.audio)
151
+ elif event.type == "interrupt": speaker.flush() # the caller barged in
152
+ elif event.type == "transcript": print(event.data["role"], event.text)
153
+ ```
154
+
155
+ `woise.live.chat(agent)` is the same socket for text: `send_text`, then stream
156
+ `agent_text` events.
157
+
158
+ ## More
159
+
160
+ - Numbers: `woise.numbers.connections()`, `.carrier_numbers(conn)`, `.import_byo(conn, ["+1..."])`,
161
+ `.list(connection_id=conn)`, `.attach(number, agent, connection_id=conn)`, `.detach(number, connection_id=conn)`
162
+ (managed numbers: `.list()`, `.attach(number, agent)`)
163
+ - `woise.tools.create_http("lookup_patient", url="https://crm.example.com/patients", parameters={"phone": "the caller's number"})`
164
+ - `woise.knowledge.add("faq.pdf")`
165
+ - Errors are `WoiseError` with `.status`, `.code`, `.message`, `.request_id`.
166
+ GET/PUT/DELETE are retried on 429/5xx; `calls.create` never is (pass
167
+ `idempotency_key=` to retry safely).
168
+
169
+ BSD-2-Clause.
woise-1.0.0/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # woise (Python)
2
+
3
+ Voice and chat agents for your systems: your CRM, app or phone system places
4
+ calls, runs campaigns, chats with agents, talks to them live, and gets the
5
+ results back as webhooks.
6
+
7
+ ```bash
8
+ pip install woise # live audio: pip install "woise[live]"
9
+ ```
10
+
11
+ Python 3.9+. One dependency (`httpx`). `Woise` is sync, `AsyncWoise` is the same
12
+ API with `await`. The API key picks your region: `sk-woise-us-...` talks to
13
+ `https://us.woise.ai`, `sk-woise-in-...` to `https://in.woise.ai`. Older keys
14
+ without a region need `Woise(key, region="us")`. Local dev or a partner domain:
15
+ `Woise(key, base_url="http://localhost:8000")`.
16
+
17
+ ## Build an agent and place a call
18
+
19
+ ```python
20
+ import os
21
+ from woise import Woise, Agent
22
+
23
+ woise = Woise(os.environ["WOISE_API_KEY"])
24
+
25
+ agent = Agent("Smile Dental", prompt="You are Priya, the receptionist at Smile Dental.")
26
+ agent.start("greet", say="Hi, this is Priya from Smile Dental.", prompt="Confirm tomorrow's visit.",
27
+ next={"done": "they confirmed or cancelled"})
28
+ agent.end("done", say="Thanks, bye.")
29
+ agent.after_call(collect={"outcome": "confirmed, cancelled or rescheduled"})
30
+ saved = woise.agents.save(agent, publish=True) # save again later = new draft
31
+
32
+ call = woise.calls.create(agent=saved, to="+14155550123", variables={"first_name": "Ravi"})
33
+ print(call.status, call.id) # queued, enr_...
34
+ ```
35
+
36
+ `next` maps the step to go to, to the condition in plain words.
37
+ `agent.anytime("human", when="asks for a person", transfer="+14155550100")` adds a
38
+ step that can fire from anywhere. `agent.node(id, type, data)` adds any other node.
39
+
40
+ ## Run a campaign from a CSV
41
+
42
+ ```python
43
+ # leads.csv: phone_number,first_name,plan (every other column becomes a {{variable}})
44
+ campaign = woise.campaigns.create(agent=saved, contacts="leads.csv", number="+14155550100")
45
+ campaign.launch()
46
+ print(campaign.progress())
47
+ campaign.pause(); campaign.resume()
48
+ campaign.add_contacts([{"phone_number": "+14155550124", "first_name": "Sita"}])
49
+ ```
50
+
51
+ ## Get results back: `call.ended` in Flask, with the signature check
52
+
53
+ ```python
54
+ hook = woise.webhooks.create(url="https://crm.example.com/woise", events=["call.ended"])
55
+ print(hook.secret) # shown ONCE: store it as WOISE_WEBHOOK_SECRET
56
+ ```
57
+
58
+ ```python
59
+ from flask import Flask, abort, request
60
+ from woise import verify_signature
61
+
62
+ app = Flask(__name__)
63
+
64
+ @app.post("/woise")
65
+ def woise_event():
66
+ if not verify_signature(request.get_data(), request.headers.get("X-Woise-Signature"),
67
+ os.environ["WOISE_WEBHOOK_SECRET"]):
68
+ abort(401)
69
+ event = request.get_json()
70
+ print(event["event"], event["call_id"], event["call_disposition"], event["collected"])
71
+ return "", 204
72
+ ```
73
+
74
+ Events: `call.started`, `call.ended`, `chat.ended`, `campaign.finished`. Failed
75
+ deliveries are retried with backoff; `woise.webhooks.deliveries(hook)` shows the log.
76
+
77
+ ## Chat
78
+
79
+ ```python
80
+ chat = woise.chats.start(saved, variables={"first_name": "Ravi"})
81
+ print(chat.greeting)
82
+ print(chat.send("Are you open on Sunday?").text)
83
+ chat.end()
84
+ ```
85
+
86
+ ## Runs: transcripts, outcomes, recordings
87
+
88
+ ```python
89
+ for run in woise.runs.list(agent=saved, limit=20):
90
+ print(run.id, run.outcome, run.collected)
91
+ print(run.transcript) # "agent: ...\ncaller: ..."
92
+ print(woise.runs.recording_url(run)) # short-lived link
93
+ ```
94
+
95
+ ## Live voice: Twilio Media Streams -> Woise in ~20 lines
96
+
97
+ Point a Twilio `<Connect><Stream url="wss://you.example.com/twilio"/></Connect>`
98
+ at this relay. It forwards Twilio's messages to Woise verbatim and Woise answers
99
+ in the same format (media, mark, clear), so barge-in works unchanged.
100
+
101
+ ```python
102
+ import asyncio, json, os
103
+ import websockets
104
+ from woise import AsyncWoise
105
+
106
+ woise = AsyncWoise(os.environ["WOISE_API_KEY"])
107
+ AGENT = "wf_..." # your agent id
108
+
109
+ async def relay(twilio):
110
+ async with woise.live.voice(AGENT, format="twilio") as call:
111
+ async def up(): # Twilio -> Woise
112
+ async for message in twilio:
113
+ await call.send_raw(message)
114
+ async def down(): # Woise -> Twilio
115
+ async for event in call:
116
+ await twilio.send(json.dumps(event.data))
117
+ await asyncio.gather(up(), down())
118
+
119
+ async def main():
120
+ async with websockets.serve(relay, "0.0.0.0", 8080):
121
+ await asyncio.Future()
122
+
123
+ asyncio.run(main())
124
+ ```
125
+
126
+ Raw PCM16 instead (your own media server, 8/16/24 kHz mono):
127
+
128
+ ```python
129
+ with woise.live.voice(AGENT, sample_rate=16000) as call:
130
+ call.send_audio(pcm16_bytes)
131
+ for event in call:
132
+ if event.type == "audio": speaker.write(event.audio)
133
+ elif event.type == "interrupt": speaker.flush() # the caller barged in
134
+ elif event.type == "transcript": print(event.data["role"], event.text)
135
+ ```
136
+
137
+ `woise.live.chat(agent)` is the same socket for text: `send_text`, then stream
138
+ `agent_text` events.
139
+
140
+ ## More
141
+
142
+ - Numbers: `woise.numbers.connections()`, `.carrier_numbers(conn)`, `.import_byo(conn, ["+1..."])`,
143
+ `.list(connection_id=conn)`, `.attach(number, agent, connection_id=conn)`, `.detach(number, connection_id=conn)`
144
+ (managed numbers: `.list()`, `.attach(number, agent)`)
145
+ - `woise.tools.create_http("lookup_patient", url="https://crm.example.com/patients", parameters={"phone": "the caller's number"})`
146
+ - `woise.knowledge.add("faq.pdf")`
147
+ - Errors are `WoiseError` with `.status`, `.code`, `.message`, `.request_id`.
148
+ GET/PUT/DELETE are retried on 429/5xx; `calls.create` never is (pass
149
+ `idempotency_key=` to retry safely).
150
+
151
+ BSD-2-Clause.
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "woise"
7
+ version = "1.0.0"
8
+ description = "Woise: voice and chat agents for your systems. Calls, campaigns, chat, live audio and webhooks."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "BSD-2-Clause"
12
+ authors = [{ name = "Woise" }]
13
+ dependencies = ["httpx>=0.24"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Typing :: Typed",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ live = ["websockets>=12"]
21
+ dev = ["pytest>=7", "websockets>=12"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://woise.ai"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+
29
+ [tool.setuptools.package-data]
30
+ woise = ["py.typed"]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+
35
+ [tool.ruff]
36
+ target-version = "py39"
37
+ line-length = 100
woise-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,61 @@
1
+ """Woise: voice and chat agents for your systems.
2
+
3
+ from woise import Woise, Agent
4
+ woise = Woise("sk-woise-us-...")
5
+ woise.calls.create(agent="wf_...", to="+14155550123")
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._core import AsyncTransport, SyncTransport, WoiseError, cell_from_key
11
+ from .agent import Agent
12
+ from .live import LiveEvent
13
+ from .signature import SIGNATURE_HEADER, sign, verify_signature
14
+ from .types import (
15
+ Call,
16
+ Campaign,
17
+ ChatReply,
18
+ Document,
19
+ Number,
20
+ Run,
21
+ SavedAgent,
22
+ Tool,
23
+ Webhook,
24
+ )
25
+
26
+ __version__ = "1.0.0"
27
+
28
+
29
+ class Woise(SyncTransport):
30
+ """The Woise client. ``Woise(api_key)``; the key picks the region.
31
+
32
+ Options: ``base_url`` (local dev, partner domains), ``region`` ("us"/"in")
33
+ for old keys without a region, ``timeout`` (seconds), ``max_retries``.
34
+ """
35
+
36
+
37
+ class AsyncWoise(AsyncTransport):
38
+ """Same as ``Woise``, every method is ``await``-able."""
39
+
40
+
41
+ __all__ = [
42
+ "SIGNATURE_HEADER",
43
+ "Agent",
44
+ "AsyncWoise",
45
+ "Call",
46
+ "Campaign",
47
+ "ChatReply",
48
+ "Document",
49
+ "LiveEvent",
50
+ "Number",
51
+ "Run",
52
+ "SavedAgent",
53
+ "Tool",
54
+ "Webhook",
55
+ "Woise",
56
+ "WoiseError",
57
+ "__version__",
58
+ "cell_from_key",
59
+ "sign",
60
+ "verify_signature",
61
+ ]