Repository navigation
feat(twilio): per-agent relay settings, the cut line said again, a si… - #1489
Conversation
…lence chain that asks the question again, tails dropped
- ConversationRelaySetting.Agents: an agent's own transcription provider, speech model, interrupt sensitivity and the
settings below, over the shared ones (For(agentId)); the TwiML and the session use them, so one agent can move to
Deepgram Flux and a low interrupt sensitivity without touching the others
- ResumeAfterCutInSeconds: a cut-in nothing follows (a noise, the line's own voice) gets the line a tool gave said again,
once; cut into again it is not said a third time
- IdleTimeouts and IdleAsksAgain: the first idle prompt asks the last line's question again ("Sorry, I didn't catch
that. ..."), the next asks if the caller is still there, and the hang-up says goodbye (PromptTexts
say-it-again-question, still-there, idle-goodbye)
- DropBackchannelTails: an acknowledgement that comes in before the caller could hear the question just asked is the
tail of an utterance the transcription split, and is dropped, unless they heard a few words of the line first
- A transcript with no letters (".") never starts a turn
- A line queued behind something the caller cut into (a filler) is told to the next turn as not heard at all
Off by default; each comes from a finding on real calls in the RSP call lab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
✅ AegisCode Code Review Report: PassCounted: 1 Medium, 17 Low. None of them blocks this change. Medium · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:415 · logic: The resume marker is set only after SpeakAsync completes, separately from the state updates in its…The resume marker is set only after SpeakAsync completes, separately from the state updates in its canSpeak callback. An interrupt can arrive after the resumed line is sent but before this assignment. NoteLine has already reset _lastWasResume to false, so NoteCutIn can schedule that same line again. Setting the marker afterward does not clear the scheduled _resumeLine, and ResumeIfSilentAsync does not recheck the marker, allowing a third playback. Suggested fix: [Not confirmed by a second review; reviewer rated High, needs a human decision] [Needs Review - Inconclusive] Set _lastWasResume = true immediately after NoteLine(line) inside the canSpeak callback, under the same synchronization used for the other resume-state updates, and remove the later assignment. Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelayLines.cs:11 · Constant naming: Constant 'ShortQuestionWords' (line 11) is not UPPER_CASEConstant 'ShortQuestionWords' (line 11) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'ShortQuestionWords' to 'SHORT_QUESTION_WORDS' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelayLines.cs:14 · logic: Every period is treated as a sentence boundary, including periods inside decimal values.Every period is treated as a sentence boundary, including periods inside decimal values. For example, LastQuestion("The total is $12.50. Is that correct?") returns "50. Is that correct?" because the short-question branch retains only the fragment immediately preceding the question. The helper works for simple sentences, but this case loses the information the caller is being asked to confirm. Suggested fix: [Needs Review - Inconclusive] Use sentence-boundary detection that preserves internal punctuation, at least periods between digits, before selecting the preceding context sentence. Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelayLines.cs:16 · Line length: This line is 139 characters long; the maximum is 125This line is 139 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelayLines.cs:17 · style: The private static readonly fields Sentence and Acknowledgement use PascalCase instead of the…The private static readonly fields Sentence and Acknowledgement use PascalCase instead of the required _camelCase private-field naming. The current names are readable and functional, but aligning them with the C# standard makes fields easier to distinguish from types and properties. Suggested fix: [Needs Review - Inconclusive] Rename Sentence to _sentence and Acknowledgement to _acknowledgement, updating their references. Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelayLines.cs:32 · business-logic: A question ending in mixed punctuation is treated as containing no question.A question ending in mixed punctuation is treated as containing no question. For example, LastQuestion("Are you still there?!") returns null: the sentence regex consumes both punctuation characters, but EndsWith('?') rejects the result. This contradicts the intended behavior that "the first idle prompt asks the last line's question again" for this input. Suggested fix: [Validated by static analysis] Recognize a question mark anywhere in the terminal punctuation sequence, rather than requiring it to be the final character. Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:20 · Constant naming: Constant 'DefaultAskAgainText' (line 20) is not UPPER_CASEConstant 'DefaultAskAgainText' (line 20) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'DefaultAskAgainText' to 'DEFAULT_ASK_AGAIN_TEXT' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:21 · Constant naming: Constant 'DefaultStillThereText' (line 21) is not UPPER_CASEConstant 'DefaultStillThereText' (line 21) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'DefaultStillThereText' to 'DEFAULT_STILL_THERE_TEXT' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:22 · Constant naming: Constant 'DefaultIdleGoodbyeText' (line 22) is not UPPER_CASEConstant 'DefaultIdleGoodbyeText' (line 22) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'DefaultIdleGoodbyeText' to 'DEFAULT_IDLE_GOODBYE_TEXT' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:78 · business-logic: A numeric-only transcript such as "123" passes IsLetterOrDigit and is enqueued.A numeric-only transcript such as "123" passes IsLetterOrDigit and is enqueued. This contradicts the stated requirement: "A transcript with no letters (\".\") never starts a turn." Punctuation-only transcripts are filtered correctly, but the implementation does not enforce the broader no-letters condition. Suggested fix: [Needs Review - Inconclusive] Use prompt.Any(char.IsLetter) to implement the stated transcript rule. Keep keypad handling separate. Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:88 · business-logic: The interrupt's heard text is counted without checking which utterance it belongs to.The interrupt's heard text is counted without checking which utterance it belongs to. If Twilio reports three or more words of a filler while the question is still queued behind it, this sets _lastLineHeardSome to true. IsBackchannelTail then unconditionally accepts the next acknowledgement even though none of the question was heard. This contradicts the intent: acknowledgements are dropped "unless they heard a few words of the line first." The new PlayingBeforeAfterTurnLine logic explicitly recognizes this queued-line scenario, but this assignment ignores it. Suggested fix: [Needs Review - Inconclusive] Only mark the last line as partly heard after establishing that the interrupted utterance belongs to that line. Keep the flag false when the interrupt identifies an earlier filler or other queued predecessor. Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:123 · Constant naming: Constant 'UtterancesKept' (line 123) is not UPPER_CASEConstant 'UtterancesKept' (line 123) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'UtterancesKept' to 'UTTERANCES_KEPT' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:358 · Line length: This line is 133 characters long; the maximum is 125This line is 133 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:404 · Constant naming: Constant 'HeardSomeWords' (line 404) is not UPPER_CASEConstant 'HeardSomeWords' (line 404) is not UPPER_CASE Suggested fix: [Validated by static analysis] Rename 'HeardSomeWords' to 'HEARD_SOME_WORDS' Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:811 · Line length: This line is 129 characters long; the maximum is 125This line is 129 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:827 · Line length: This line is 137 characters long; the maximum is 125This line is 137 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:838 · Line length: This line is 134 characters long; the maximum is 125This line is 134 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters Low · .../BotSharp.Plugin.Twilio/Services/ConversationRelaySession.cs:916 · Line length: This line is 132 characters long; the maximum is 125This line is 132 characters long; the maximum is 125 Suggested fix: [Validated by static analysis] Wrap the line so that no line exceeds 125 characters |
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
Code Review:BotSharp PR #1489总体评价:一般,合并前需要修。 五个新开关默认都关,不打开就不影响现有通话。但打开 🔴 严重问题1.
2.
3. 没开 partialPrompts 时,
🟡 警告4.
5.
6.
7.
🟢 建议
✅ 做得好的
|
… again, nothing said over a caller still talking
- A filler said again on a later turn ("One moment, please.") is the one playing at a cut-in, not the same words said
before the line: only the latest utterance with the words played counts, so a line heard in full is not told to the
next turn as cut off (this check runs for every relay agent)
- A line cut into is not said again once a newer line went out (NoteLine clears it)
- With ResumeAfterCutInSeconds on, the TwiML asks for partialPrompts (Deepgram Flux sends them): an unfinalized
transcript is the caller still talking, so the cut line isn't said over them; its words are not kept, as the final
prompt carries the whole turn
- A line said again is marked so as it goes out, under the lock: a cut-in into it, however soon, isn't followed by a
third time
- The last question of a line is found without ending a sentence at a decimal ("$12.50") or an abbreviation ("St.")
- A tail is only an acknowledgement with a word that acknowledges and no question in it: "Is that it?" is kept
- Only words of the last line heard before a cut-in count as having heard some of it, not a filler's
Co-Authored-By: Claude Opus 5.5 <[email protected]>
第二轮 Review:PR #1489(新增提交
|
| # | 问题 | 结果 |
|---|---|---|
| 1 | 填充语匹配到历史同文本 | ✅ 改成只看最新一条匹配 playing 的 utterance(LastOrDefault),只有它在 after-turn line 之前才算 |
| 2 | 旧句子过期后还会被重说 | ✅ NoteLine 里清掉 _resumeLine;流式回复和 after-turn line 都会走 NoteLine |
| 3 | 来电者还没说完就重说,盖过对方 | ✅ 开了 ResumeAfterCutInSeconds 就在 TwiML 里加 partialPrompts,并在注释写明非 Flux 时 N 要大于一句话的时长 |
| 4 | _lastWasResume 设置太晚,有竞态 |
✅ 移进 canSpeak,放在 NoteLine 之后,先后顺序正确 |
| 5 | 按 . 切句会切坏地址和金额 |
✅ 改成"标点后跟空白才切,排除常见缩写"。$12.50 和 p.m. 中间都没有空白,不会被切开 |
| 6 | 确认词表过宽 | ✅ 拆成确认词和陪衬词,要求至少有一个确认词、且不含 ?,"Is that it?" 不会再被丢 |
| 7 | _lastLineHeardSome 统计到填充语 |
✅ 现在要求 heard 是 _lastLine 的开头才算 |
被删掉的 _partialPrompt 拼接逻辑:已按 Twilio 文档确认,可以删。
- 关掉 partialPrompts(默认)时,只会收到
last: true的 prompt,所以原来的拼接代码一直没生效过。 - 打开后,
last: false的消息是未定稿的转写和 eager end-of-turn。文档要求应用在最终转写与 eager 猜测一致时重发回复,所以最终那条 prompt 就是完整一轮的话,不该和前面的片段拼起来。 - 文档也写明 partialPrompts 只对 Deepgram Flux 生效,和 PR 注释一致。
- 有一点没找到文档说明:每条
voicePrompt是累计的全文,还是只有最新一段。如果是累计的,原来的拼接会把同一句话重复好几遍,所以删掉更对。这一点最好在真实通话上看一眼确认。
🟡 剩下的问题
1. 被打断的流式回复:NoteLine 在 turn 结束时才调用,可能丢掉来电者的 "Yes"(CloseUtteranceAsync → NoteLine(_lastStreamSegment))。可信度:有可能发生,还没复现。
- 场景:流式问题 "Is the address 100 Main Street?" 播到一半,来电者说 "Yes" 打断。
- interrupt 到达时
_lastLine还是上一句,所以_lastLineHeardSome算成 false。 - 之后 turn 结束,
CloseUtteranceAsync→NoteLine把_lastLineSentUtc设为当前时间,并把_lastLineHeardSome重置为 false。 - 如果 "Yes" 的 prompt 在这之后 1 秒内到达,
IsBackchannelTail会认为它"在问题发出前就说了",于是丢掉。
- interrupt 到达时
- 建议:
_dropStream为 true(流已因打断被丢弃)时,CloseUtteranceAsync不要用当前时间刷新_lastLineSentUtc、也不要重置_lastLineHeardSome。或者改用第一段发出的时间(类似已有的_utteranceStartMark)。 - 这是第一版就有的代码,我上轮漏了。只在同时开了
DropBackchannelTails和StreamReplies时才会触发。
2. 陪衬词组合在没有问号时仍会被丢(低)
- "Is that right":
is、that是陪衬词,right是确认词,满足条件。如果 STT 没打出问号,又在问题发出后 1 秒内到达,就会被当成尾巴丢掉。 - Deepgram 一般会加标点,而且窗口只有 1 秒,影响很小,可以接受。
🟢 建议
ConversationRelayLines.LastQuestion还是没有单元测试。这次的切句正则改动较大,建议补几条用例:St.、$12.50、p.m.?、"And …" 开头、句前是确认语。
Sources:
… and a question without its mark is no tail - The caller said "Yes" over a streamed question that had all gone out; the turn closed it after that, and NoteLine dated it from the close and forgot what was heard, so the "Yes." was dropped as a tail of something said before it. What the caller heard is now checked against the line as far as it was streamed, and a line cut into while it streamed keeps that and the time its first piece went out (only with StreamReplies and DropBackchannelTails on) - "Is that right" without its question mark is a question, not an acknowledgement Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
reviewed |
…lence chain that asks the question again, tails dropped
Off by default; each comes from a finding on real calls in the RSP call lab.