pipecat-piopiy 0.1.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.
Potentially problematic release.
This version of pipecat-piopiy might be problematic. Click here for more details.
- pipecat_piopiy-0.1.0/LICENSE +21 -0
- pipecat_piopiy-0.1.0/PKG-INFO +180 -0
- pipecat_piopiy-0.1.0/README.md +147 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/__init__.py +44 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/call.py +92 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/control.py +321 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/frames.py +40 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/processors.py +165 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/runner.py +163 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy/tools.py +207 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy.egg-info/PKG-INFO +180 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy.egg-info/SOURCES.txt +19 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy.egg-info/dependency_links.txt +1 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy.egg-info/requires.txt +12 -0
- pipecat_piopiy-0.1.0/pipecat_piopiy.egg-info/top_level.txt +1 -0
- pipecat_piopiy-0.1.0/pyproject.toml +48 -0
- pipecat_piopiy-0.1.0/setup.cfg +4 -0
- pipecat_piopiy-0.1.0/tests/test_call.py +53 -0
- pipecat_piopiy-0.1.0/tests/test_control.py +105 -0
- pipecat_piopiy-0.1.0/tests/test_processor.py +69 -0
- pipecat_piopiy-0.1.0/tests/test_tools.py +92 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TeleCMI
|
|
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.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipecat-piopiy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Piopiy (TeleCMI) telephony for Pipecat voice agents: take calls, transfer them, hang up
|
|
5
|
+
Author-email: TeleCMI <support@telecmi.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://piopiy.com
|
|
8
|
+
Project-URL: Repository, https://github.com/telecmi/pipecat-piopiy
|
|
9
|
+
Project-URL: Documentation, https://github.com/telecmi/pipecat-piopiy#readme
|
|
10
|
+
Keywords: pipecat,voice,telephony,sip,piopiy,telecmi
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Communications :: Telephony
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pipecat-ai[livekit]<2,>=1.8.0
|
|
23
|
+
Requires-Dist: piopiy-agent>=1.0.0
|
|
24
|
+
Requires-Dist: aiohttp>=3.9
|
|
25
|
+
Provides-Extra: example
|
|
26
|
+
Requires-Dist: pipecat-ai[deepgram,livekit,openai,silero]<2,>=1.8.0; extra == "example"
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0; extra == "example"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# pipecat-piopiy
|
|
35
|
+
|
|
36
|
+
Piopiy telephony for [Pipecat](https://github.com/pipecat-ai/pipecat) voice
|
|
37
|
+
agents. Your Pipecat bot takes real phone calls on the Piopiy platform by
|
|
38
|
+
TeleCMI: calls the Piopiy API places, calls arriving on your numbers, and
|
|
39
|
+
calls arriving from your PBX over SIP Connect. Mid-call it can transfer the
|
|
40
|
+
caller to a human, warm or blind, and hang up.
|
|
41
|
+
|
|
42
|
+
Piopiy bridges each call into a LiveKit room and hands your worker the room.
|
|
43
|
+
Media runs through Pipecat's own `LiveKitTransport`; this package supplies the
|
|
44
|
+
rest - taking the call, knowing who is calling, acting on the call, and hearing
|
|
45
|
+
about transfers.
|
|
46
|
+
|
|
47
|
+
**Tested with Pipecat v1.8.1.** Community-maintained by
|
|
48
|
+
[TeleCMI](https://telecmi.com); not part of the Pipecat core.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pipecat-piopiy
|
|
54
|
+
# for the example, with Deepgram + OpenAI + Silero VAD:
|
|
55
|
+
pip install "pipecat-piopiy[example]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Python 3.10+. Depends on `pipecat-ai[livekit]` and `piopiy-agent`, the
|
|
59
|
+
framework-agnostic Piopiy worker SDK.
|
|
60
|
+
|
|
61
|
+
## Use with a pipeline
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from pipecat_piopiy import PiopiyRunner, PiopiyCallControl, piopiy_tools
|
|
65
|
+
from pipecat_piopiy.processors import PiopiyEventsProcessor
|
|
66
|
+
|
|
67
|
+
async def bot(transport, call):
|
|
68
|
+
control = PiopiyCallControl(call)
|
|
69
|
+
tools = piopiy_tools(control, transfer_number="919876543210",
|
|
70
|
+
transfer_caller_id="911203134087")
|
|
71
|
+
|
|
72
|
+
context = LLMContext(messages=[{"role": "system", "content": PROMPT}],
|
|
73
|
+
tools=tools.schemas)
|
|
74
|
+
aggregators = LLMContextAggregatorPair(
|
|
75
|
+
context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()))
|
|
76
|
+
|
|
77
|
+
pipeline = Pipeline([
|
|
78
|
+
transport.input(),
|
|
79
|
+
PiopiyEventsProcessor(call), # transfer progress -> frames + narration
|
|
80
|
+
stt, aggregators.user(), llm, tts,
|
|
81
|
+
transport.output(), aggregators.assistant(),
|
|
82
|
+
])
|
|
83
|
+
task = PipelineTask(pipeline)
|
|
84
|
+
|
|
85
|
+
@transport.event_handler("on_first_participant_joined")
|
|
86
|
+
async def on_caller_joined(_t, participant_id):
|
|
87
|
+
await task.queue_frames([LLMRunFrame()])
|
|
88
|
+
|
|
89
|
+
@transport.event_handler("on_participant_left")
|
|
90
|
+
async def on_caller_left(_t, participant_id, reason):
|
|
91
|
+
await task.cancel()
|
|
92
|
+
|
|
93
|
+
await PipelineRunner(handle_sigint=False).run(task)
|
|
94
|
+
|
|
95
|
+
PiopiyRunner().run(bot)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`PiopiyRunner` connects to Piopiy as a worker for your agent, and for every
|
|
99
|
+
call builds a `LiveKitTransport` pointed at the call's room and calls `bot`.
|
|
100
|
+
It accepts the call when the transport connects, which is when the caller is
|
|
101
|
+
bridged in. Use `PipelineRunner(handle_sigint=False)`: the worker owns the
|
|
102
|
+
process signals.
|
|
103
|
+
|
|
104
|
+
## Run the example
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd examples/foundational
|
|
108
|
+
cp .env.example .env # agent id, token, API base, transfer number, keys
|
|
109
|
+
python 01_piopiy_agent.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then call your agent: place a call with `POST /v3/voice/ai/call`, ring one of
|
|
113
|
+
your numbers mapped to the agent, or dial the agent id from a PBX registered
|
|
114
|
+
over SIP Connect. Ask for a person to see a warm transfer, ask for "the billing
|
|
115
|
+
line" to see a blind transfer, and say goodbye to see it hang up.
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
| variable | what |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `PIOPIY_AGENT_ID` | the agent this worker serves, from the dashboard |
|
|
122
|
+
| `PIOPIY_TOKEN` | the Bearer token, the same one that creates calls |
|
|
123
|
+
| `PIOPIY_API_URL` | the platform's `/v3` base URL, as given in your account |
|
|
124
|
+
| `PIOPIY_REGISTER` | optional; `host:port` of the worker register |
|
|
125
|
+
| `PIOPIY_TLS` | optional; `false` to talk to the register without TLS (development) |
|
|
126
|
+
| `PIOPIY_MAX_SESSIONS` | calls one process handles at once |
|
|
127
|
+
|
|
128
|
+
## What the package gives you
|
|
129
|
+
|
|
130
|
+
**`PiopiyCall`** - who is calling whom: `call_id`, `direction`, `from_number`,
|
|
131
|
+
`to_number`, `agent_id`, `variables` from the create request, and
|
|
132
|
+
`sip_account_id` on SIP Connect calls so one agent can tell your PBXs apart.
|
|
133
|
+
|
|
134
|
+
**`PiopiyCallControl`** - actions on the live call over the Piopiy API:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
result = await control.warm_transfer(to_number="9198...", transfer_summary="Refund on order A-1042")
|
|
138
|
+
result = await control.warm_transfer(sip_uri="sip:desk@pbx.example.com", sip_headers={"X-Ticket": "A-1042"})
|
|
139
|
+
result = await control.blind_transfer(to_number="9198...", caller_id="9112...")
|
|
140
|
+
await control.hangup(reason="resolved")
|
|
141
|
+
verdict = await control.wait_for_transfer(result.request_id) # queued -> completed | failed
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A warm transfer rings the human while the caller stays in conversation with
|
|
145
|
+
the agent; on answer the caller is handed over and the agent leaves; if nobody
|
|
146
|
+
answers the conversation simply continues. A blind transfer hands the caller
|
|
147
|
+
over at once. One transfer at a time per call: a second one is refused with
|
|
148
|
+
`PiopiyAPIError(409, "transfer_in_progress")` carrying the running transfer's
|
|
149
|
+
`request_id`.
|
|
150
|
+
|
|
151
|
+
**`piopiy_tools()`** - `transfer_call` and `end_call` as LLM function calls.
|
|
152
|
+
Each `FunctionSchema` carries its handler, so advertising `tools.schemas` on
|
|
153
|
+
the `LLMContext` is all the wiring. The destination is fixed in code by
|
|
154
|
+
default; pass `allow_model_destination=True` to let the model choose a
|
|
155
|
+
number, and `transfer_caller_id` for the DID to present, which SIP Connect
|
|
156
|
+
calls require.
|
|
157
|
+
|
|
158
|
+
**`PiopiyEventsProcessor`** - the platform pushes every transfer's progress
|
|
159
|
+
into the call's room. The processor turns each message into a
|
|
160
|
+
`PiopiyTransferStatusFrame` (`started`, `failed` with a reason, `completed`)
|
|
161
|
+
and, by default, speaks it: "I'm connecting you now" as the target rings, an
|
|
162
|
+
apology when it fails, plus a note into the LLM context so the model carries
|
|
163
|
+
on sensibly. Pass `narrate=False` to handle the frames yourself. `completed`
|
|
164
|
+
is best-effort: at that moment the agent is being removed from the call, and
|
|
165
|
+
`on_participant_left` fires.
|
|
166
|
+
|
|
167
|
+
## Notes
|
|
168
|
+
|
|
169
|
+
- Every action uses the call's customer leg, which `PiopiyCall.call_id` is.
|
|
170
|
+
- SIP Connect calls consume no phone number, so a transfer to a phone from one
|
|
171
|
+
needs `transfer_caller_id` (a DID you own).
|
|
172
|
+
- Accept timing is handled for you: the runner accepts on `on_connected`, and
|
|
173
|
+
if the join missed the platform's deadline it cancels the bot so two agents
|
|
174
|
+
never share a call.
|
|
175
|
+
- Pipecat changes quickly. This release is tested against v1.8.1; the pinned
|
|
176
|
+
range in `pyproject.toml` is `>=1.8,<2`.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT. Copyright TeleCMI.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# pipecat-piopiy
|
|
2
|
+
|
|
3
|
+
Piopiy telephony for [Pipecat](https://github.com/pipecat-ai/pipecat) voice
|
|
4
|
+
agents. Your Pipecat bot takes real phone calls on the Piopiy platform by
|
|
5
|
+
TeleCMI: calls the Piopiy API places, calls arriving on your numbers, and
|
|
6
|
+
calls arriving from your PBX over SIP Connect. Mid-call it can transfer the
|
|
7
|
+
caller to a human, warm or blind, and hang up.
|
|
8
|
+
|
|
9
|
+
Piopiy bridges each call into a LiveKit room and hands your worker the room.
|
|
10
|
+
Media runs through Pipecat's own `LiveKitTransport`; this package supplies the
|
|
11
|
+
rest - taking the call, knowing who is calling, acting on the call, and hearing
|
|
12
|
+
about transfers.
|
|
13
|
+
|
|
14
|
+
**Tested with Pipecat v1.8.1.** Community-maintained by
|
|
15
|
+
[TeleCMI](https://telecmi.com); not part of the Pipecat core.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install pipecat-piopiy
|
|
21
|
+
# for the example, with Deepgram + OpenAI + Silero VAD:
|
|
22
|
+
pip install "pipecat-piopiy[example]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Python 3.10+. Depends on `pipecat-ai[livekit]` and `piopiy-agent`, the
|
|
26
|
+
framework-agnostic Piopiy worker SDK.
|
|
27
|
+
|
|
28
|
+
## Use with a pipeline
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from pipecat_piopiy import PiopiyRunner, PiopiyCallControl, piopiy_tools
|
|
32
|
+
from pipecat_piopiy.processors import PiopiyEventsProcessor
|
|
33
|
+
|
|
34
|
+
async def bot(transport, call):
|
|
35
|
+
control = PiopiyCallControl(call)
|
|
36
|
+
tools = piopiy_tools(control, transfer_number="919876543210",
|
|
37
|
+
transfer_caller_id="911203134087")
|
|
38
|
+
|
|
39
|
+
context = LLMContext(messages=[{"role": "system", "content": PROMPT}],
|
|
40
|
+
tools=tools.schemas)
|
|
41
|
+
aggregators = LLMContextAggregatorPair(
|
|
42
|
+
context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()))
|
|
43
|
+
|
|
44
|
+
pipeline = Pipeline([
|
|
45
|
+
transport.input(),
|
|
46
|
+
PiopiyEventsProcessor(call), # transfer progress -> frames + narration
|
|
47
|
+
stt, aggregators.user(), llm, tts,
|
|
48
|
+
transport.output(), aggregators.assistant(),
|
|
49
|
+
])
|
|
50
|
+
task = PipelineTask(pipeline)
|
|
51
|
+
|
|
52
|
+
@transport.event_handler("on_first_participant_joined")
|
|
53
|
+
async def on_caller_joined(_t, participant_id):
|
|
54
|
+
await task.queue_frames([LLMRunFrame()])
|
|
55
|
+
|
|
56
|
+
@transport.event_handler("on_participant_left")
|
|
57
|
+
async def on_caller_left(_t, participant_id, reason):
|
|
58
|
+
await task.cancel()
|
|
59
|
+
|
|
60
|
+
await PipelineRunner(handle_sigint=False).run(task)
|
|
61
|
+
|
|
62
|
+
PiopiyRunner().run(bot)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`PiopiyRunner` connects to Piopiy as a worker for your agent, and for every
|
|
66
|
+
call builds a `LiveKitTransport` pointed at the call's room and calls `bot`.
|
|
67
|
+
It accepts the call when the transport connects, which is when the caller is
|
|
68
|
+
bridged in. Use `PipelineRunner(handle_sigint=False)`: the worker owns the
|
|
69
|
+
process signals.
|
|
70
|
+
|
|
71
|
+
## Run the example
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cd examples/foundational
|
|
75
|
+
cp .env.example .env # agent id, token, API base, transfer number, keys
|
|
76
|
+
python 01_piopiy_agent.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then call your agent: place a call with `POST /v3/voice/ai/call`, ring one of
|
|
80
|
+
your numbers mapped to the agent, or dial the agent id from a PBX registered
|
|
81
|
+
over SIP Connect. Ask for a person to see a warm transfer, ask for "the billing
|
|
82
|
+
line" to see a blind transfer, and say goodbye to see it hang up.
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
| variable | what |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `PIOPIY_AGENT_ID` | the agent this worker serves, from the dashboard |
|
|
89
|
+
| `PIOPIY_TOKEN` | the Bearer token, the same one that creates calls |
|
|
90
|
+
| `PIOPIY_API_URL` | the platform's `/v3` base URL, as given in your account |
|
|
91
|
+
| `PIOPIY_REGISTER` | optional; `host:port` of the worker register |
|
|
92
|
+
| `PIOPIY_TLS` | optional; `false` to talk to the register without TLS (development) |
|
|
93
|
+
| `PIOPIY_MAX_SESSIONS` | calls one process handles at once |
|
|
94
|
+
|
|
95
|
+
## What the package gives you
|
|
96
|
+
|
|
97
|
+
**`PiopiyCall`** - who is calling whom: `call_id`, `direction`, `from_number`,
|
|
98
|
+
`to_number`, `agent_id`, `variables` from the create request, and
|
|
99
|
+
`sip_account_id` on SIP Connect calls so one agent can tell your PBXs apart.
|
|
100
|
+
|
|
101
|
+
**`PiopiyCallControl`** - actions on the live call over the Piopiy API:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
result = await control.warm_transfer(to_number="9198...", transfer_summary="Refund on order A-1042")
|
|
105
|
+
result = await control.warm_transfer(sip_uri="sip:desk@pbx.example.com", sip_headers={"X-Ticket": "A-1042"})
|
|
106
|
+
result = await control.blind_transfer(to_number="9198...", caller_id="9112...")
|
|
107
|
+
await control.hangup(reason="resolved")
|
|
108
|
+
verdict = await control.wait_for_transfer(result.request_id) # queued -> completed | failed
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A warm transfer rings the human while the caller stays in conversation with
|
|
112
|
+
the agent; on answer the caller is handed over and the agent leaves; if nobody
|
|
113
|
+
answers the conversation simply continues. A blind transfer hands the caller
|
|
114
|
+
over at once. One transfer at a time per call: a second one is refused with
|
|
115
|
+
`PiopiyAPIError(409, "transfer_in_progress")` carrying the running transfer's
|
|
116
|
+
`request_id`.
|
|
117
|
+
|
|
118
|
+
**`piopiy_tools()`** - `transfer_call` and `end_call` as LLM function calls.
|
|
119
|
+
Each `FunctionSchema` carries its handler, so advertising `tools.schemas` on
|
|
120
|
+
the `LLMContext` is all the wiring. The destination is fixed in code by
|
|
121
|
+
default; pass `allow_model_destination=True` to let the model choose a
|
|
122
|
+
number, and `transfer_caller_id` for the DID to present, which SIP Connect
|
|
123
|
+
calls require.
|
|
124
|
+
|
|
125
|
+
**`PiopiyEventsProcessor`** - the platform pushes every transfer's progress
|
|
126
|
+
into the call's room. The processor turns each message into a
|
|
127
|
+
`PiopiyTransferStatusFrame` (`started`, `failed` with a reason, `completed`)
|
|
128
|
+
and, by default, speaks it: "I'm connecting you now" as the target rings, an
|
|
129
|
+
apology when it fails, plus a note into the LLM context so the model carries
|
|
130
|
+
on sensibly. Pass `narrate=False` to handle the frames yourself. `completed`
|
|
131
|
+
is best-effort: at that moment the agent is being removed from the call, and
|
|
132
|
+
`on_participant_left` fires.
|
|
133
|
+
|
|
134
|
+
## Notes
|
|
135
|
+
|
|
136
|
+
- Every action uses the call's customer leg, which `PiopiyCall.call_id` is.
|
|
137
|
+
- SIP Connect calls consume no phone number, so a transfer to a phone from one
|
|
138
|
+
needs `transfer_caller_id` (a DID you own).
|
|
139
|
+
- Accept timing is handled for you: the runner accepts on `on_connected`, and
|
|
140
|
+
if the join missed the platform's deadline it cancels the bot so two agents
|
|
141
|
+
never share a call.
|
|
142
|
+
- Pipecat changes quickly. This release is tested against v1.8.1; the pinned
|
|
143
|
+
range in `pyproject.toml` is `>=1.8,<2`.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT. Copyright TeleCMI.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Piopiy (TeleCMI) telephony for Pipecat voice agents.
|
|
2
|
+
|
|
3
|
+
from pipecat_piopiy import PiopiyRunner, PiopiyCallControl, piopiy_tools
|
|
4
|
+
from pipecat_piopiy.processors import PiopiyEventsProcessor
|
|
5
|
+
|
|
6
|
+
async def bot(transport, call):
|
|
7
|
+
control = PiopiyCallControl(call)
|
|
8
|
+
tools = piopiy_tools(control, transfer_number="919876543210")
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
PiopiyRunner().run(bot)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .call import PiopiyCall
|
|
15
|
+
from .control import (
|
|
16
|
+
TRANSFER_FAILURE_REASONS,
|
|
17
|
+
PiopiyAPIError,
|
|
18
|
+
PiopiyCallControl,
|
|
19
|
+
TransferResult,
|
|
20
|
+
TransferStatus,
|
|
21
|
+
build_connect_pipeline,
|
|
22
|
+
)
|
|
23
|
+
from .frames import PiopiyTransferStatusFrame
|
|
24
|
+
from .processors import PiopiyEventsProcessor
|
|
25
|
+
from .runner import PiopiyRunner
|
|
26
|
+
from .tools import PiopiyTools, piopiy_tools
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"TRANSFER_FAILURE_REASONS",
|
|
32
|
+
"PiopiyAPIError",
|
|
33
|
+
"PiopiyCall",
|
|
34
|
+
"PiopiyCallControl",
|
|
35
|
+
"PiopiyEventsProcessor",
|
|
36
|
+
"PiopiyRunner",
|
|
37
|
+
"PiopiyTools",
|
|
38
|
+
"PiopiyTransferStatusFrame",
|
|
39
|
+
"TransferResult",
|
|
40
|
+
"TransferStatus",
|
|
41
|
+
"__version__",
|
|
42
|
+
"build_connect_pipeline",
|
|
43
|
+
"piopiy_tools",
|
|
44
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""The call a Piopiy worker was handed: who is calling whom, and how to act on it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class PiopiyCall:
|
|
11
|
+
"""One phone call, as offered to your agent.
|
|
12
|
+
|
|
13
|
+
Built by :class:`~pipecat_piopiy.runner.PiopiyRunner` from the job the
|
|
14
|
+
Piopiy platform dispatched. Every live-call action (transfer, hangup,
|
|
15
|
+
status) keys on ``call_id``, which is the customer leg of the call - the
|
|
16
|
+
id the platform expects on ``/v3/calls/{call_id}/actions/*``.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
call_id: The customer leg's call id. Use it for every action.
|
|
20
|
+
room_name: The LiveKit room the call's media is bridged into.
|
|
21
|
+
direction: ``"inbound"`` when someone called the agent, ``"outbound"``
|
|
22
|
+
when the agent called them.
|
|
23
|
+
from_number: The caller's number. On SIP Connect calls this is the
|
|
24
|
+
PBX's SIP user, not a phone number.
|
|
25
|
+
to_number: The number dialled. On SIP Connect calls this is the agent
|
|
26
|
+
id or extension the PBX dialled.
|
|
27
|
+
agent_id: The Piopiy agent handling this call.
|
|
28
|
+
variables: Key/values passed on call creation as ``variables`` - a
|
|
29
|
+
campaign's context for the conversation. Empty on inbound calls
|
|
30
|
+
unless the platform attached any.
|
|
31
|
+
sip_headers: SIP headers the platform forwarded, when any.
|
|
32
|
+
sip_account_id: On SIP Connect calls, the SIP account the call came in
|
|
33
|
+
on, so one agent can tell PBXs or sites apart. ``None`` on calls
|
|
34
|
+
that arrived over a phone number.
|
|
35
|
+
trace_id: The platform's trace id for this call. Quote it to support.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
call_id: str
|
|
39
|
+
room_name: str
|
|
40
|
+
direction: str
|
|
41
|
+
from_number: str
|
|
42
|
+
to_number: str
|
|
43
|
+
agent_id: str
|
|
44
|
+
variables: dict[str, str] = field(default_factory=dict)
|
|
45
|
+
sip_headers: dict[str, str] = field(default_factory=dict)
|
|
46
|
+
sip_account_id: str | None = None
|
|
47
|
+
trace_id: str = ""
|
|
48
|
+
|
|
49
|
+
# Processors that want the platform's room messages register here; the
|
|
50
|
+
# runner fans each message out to them. Not part of the public surface.
|
|
51
|
+
_listeners: list[Any] = field(default_factory=list, repr=False, compare=False)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_inbound(self) -> bool:
|
|
55
|
+
"""True when the person on the line called us."""
|
|
56
|
+
return self.direction == "inbound"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_sip_connect(self) -> bool:
|
|
60
|
+
"""True when the call arrived over SIP Connect rather than a phone number."""
|
|
61
|
+
return self.sip_account_id is not None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_job(cls, job: Any, *, agent_id: str) -> PiopiyCall:
|
|
65
|
+
"""Build a call from a ``piopiy_agent.Job``.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
job: The job the worker received.
|
|
69
|
+
agent_id: The agent id this worker serves.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The call, ready to hand to your bot.
|
|
73
|
+
"""
|
|
74
|
+
headers = dict(getattr(job.call, "sip_headers", {}) or {})
|
|
75
|
+
variables = dict(getattr(job.call, "variables", {}) or {})
|
|
76
|
+
sip_account_id = (
|
|
77
|
+
headers.get("X-SIP-Account-ID")
|
|
78
|
+
or headers.get("x-sip-account-id")
|
|
79
|
+
or variables.get("sip_account_id")
|
|
80
|
+
)
|
|
81
|
+
return cls(
|
|
82
|
+
call_id=job.call.call_uuid,
|
|
83
|
+
room_name=job.room_name,
|
|
84
|
+
direction=job.call.direction or "inbound",
|
|
85
|
+
from_number=job.call.from_number,
|
|
86
|
+
to_number=job.call.to_number,
|
|
87
|
+
agent_id=agent_id,
|
|
88
|
+
variables=variables,
|
|
89
|
+
sip_headers=headers,
|
|
90
|
+
sip_account_id=sip_account_id,
|
|
91
|
+
trace_id=getattr(job, "trace_id", "") or "",
|
|
92
|
+
)
|