linkgravity 1.5.12 → 1.5.13
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.
- package/package.json +1 -1
- package/src/api/ui_routes.py +1 -6
- package/src/approval/tool_formatter.py +0 -3
- package/src/cogs/voice/enrollment.py +0 -8
- package/src/cogs/voice/stt_session.py +0 -2
- package/src/core/platform_health.py +0 -4
- package/src/core/session_manager.py +0 -5
- package/src/main_discord.py +0 -3
- package/src/main_slack.py +0 -3
- package/src/main_telegram.py +0 -3
- package/src/messengers/base.py +2 -8
- package/src/messengers/registry.py +3 -13
- package/src/messengers/slack_adapter.py +2 -6
- package/voice-service/logger.js +7 -0
- package/voice-service/receiver.js +13 -10
- package/voice-service/tts.js +1 -1
package/package.json
CHANGED
package/src/api/ui_routes.py
CHANGED
|
@@ -6,18 +6,13 @@ import uuid
|
|
|
6
6
|
|
|
7
7
|
from aiohttp import web
|
|
8
8
|
|
|
9
|
+
from api.server import is_tool_allowed
|
|
9
10
|
from config import APPROVAL_TIMEOUT_SEC, MAX_EMBED_LEN, logger, session_manager
|
|
10
11
|
from messengers.base import ScopeOption
|
|
11
12
|
from messengers.registry import get_adapter_for_platform, get_adapter_for_thread
|
|
12
13
|
from utils.utils import split_message
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def is_tool_allowed(tool_name, tool_input):
|
|
16
|
-
from api.server import is_tool_allowed as is_tool_allowed_orig
|
|
17
|
-
|
|
18
|
-
return is_tool_allowed_orig(tool_name, tool_input)
|
|
19
|
-
|
|
20
|
-
|
|
21
16
|
async def send_ordered(target_thread_id, send_coro_factory):
|
|
22
17
|
"""Routes through the same per-conversation stream queue as the answer
|
|
23
18
|
text, so tool-call messages can't arrive out of order. Falls back to
|
|
@@ -57,9 +57,6 @@ def format_tool_display(tool_name: str, tool_input: dict) -> tuple[str, str, dic
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def format_bash_display(sub_cmd: str) -> tuple[str, str, dict]:
|
|
60
|
-
"""
|
|
61
|
-
Formats a single bash sub-command for display.
|
|
62
|
-
"""
|
|
63
60
|
is_long = "\n" in sub_cmd or len(sub_cmd) > 50
|
|
64
61
|
display_cmd = sub_cmd.split("\n")[0][:50] + "..." if is_long else sub_cmd
|
|
65
62
|
tool_msg_text = f"● Bash({display_cmd})"
|
|
@@ -91,8 +91,6 @@ class SampleConfirmView(discord.ui.View):
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
class EnrollmentManager:
|
|
94
|
-
"""Owns /sound's recording flow - see _commit_enrollment."""
|
|
95
|
-
|
|
96
94
|
def __init__(self, bot, voice_state: dict, play_audio, bot_settings: dict, save_bot_settings, logger):
|
|
97
95
|
self.bot = bot
|
|
98
96
|
self._voice_state = voice_state # shared with VoiceCog
|
|
@@ -109,11 +107,7 @@ class EnrollmentManager:
|
|
|
109
107
|
def stop(self):
|
|
110
108
|
self.cleanup_stale_enrollments.cancel()
|
|
111
109
|
|
|
112
|
-
def is_enrolling(self, user_id: str) -> bool:
|
|
113
|
-
return user_id in self._enrollment
|
|
114
|
-
|
|
115
110
|
async def handle_voice_service_down(self):
|
|
116
|
-
"""Called when Node dies."""
|
|
117
111
|
stale_user_ids = list(self._enrollment.keys())
|
|
118
112
|
for uid in stale_user_ids:
|
|
119
113
|
session = self._enrollment.pop(uid, None)
|
|
@@ -181,7 +175,6 @@ class EnrollmentManager:
|
|
|
181
175
|
self.logger.warning(f"Failed to send enrollment status message: {e}")
|
|
182
176
|
|
|
183
177
|
async def start_wake_word_recording(self, interaction: discord.Interaction, word: str) -> bool:
|
|
184
|
-
"""Called by /sound's wake_word param."""
|
|
185
178
|
guild_id = interaction.guild_id
|
|
186
179
|
user_id = str(interaction.user.id)
|
|
187
180
|
|
|
@@ -241,7 +234,6 @@ class EnrollmentManager:
|
|
|
241
234
|
return True
|
|
242
235
|
|
|
243
236
|
async def handle_enroll_sample(self, user_id: str, audio_bytes: bytes):
|
|
244
|
-
"""Called via /enroll_sample for each captured sample."""
|
|
245
237
|
session = self._enrollment.get(user_id)
|
|
246
238
|
if not session:
|
|
247
239
|
return # stray sample - recording already finished/cancelled/expired
|
|
@@ -19,13 +19,11 @@ class SttSessionTracker:
|
|
|
19
19
|
self._partial_msg = {}
|
|
20
20
|
|
|
21
21
|
def is_active(self, guild_id: str) -> bool:
|
|
22
|
-
"""Whether the "stay awake" window is still open for this guild."""
|
|
23
22
|
active_duration = self.bot_settings.get("active_timer", 60)
|
|
24
23
|
last_active = self._last_active_time.get(str(guild_id), 0)
|
|
25
24
|
return (time.time() - last_active) < active_duration
|
|
26
25
|
|
|
27
26
|
def mark_tts_finished(self, guild_id: str):
|
|
28
|
-
"""Called via /tts_finished once the spoken reply finishes playing."""
|
|
29
27
|
self.extend_active_window(str(guild_id))
|
|
30
28
|
|
|
31
29
|
def extend_active_window(self, guild_id: str):
|
|
@@ -7,8 +7,6 @@ from core.atomic_io import atomic_write_json, safe_load_json
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class SessionManager:
|
|
10
|
-
"""Manages conversation state, async streaming queues, and user approval states."""
|
|
11
|
-
|
|
12
10
|
def __init__(self, data_dir: Path):
|
|
13
11
|
self.session_file = data_dir / "sessions.json"
|
|
14
12
|
self.persistent_file = data_dir / "persistent_tools.json"
|
|
@@ -120,9 +118,6 @@ class SessionManager:
|
|
|
120
118
|
if conv_id:
|
|
121
119
|
self.active_approval_by_conv[conv_id] = approval_key
|
|
122
120
|
|
|
123
|
-
def get_pending_approval(self, approval_key: str) -> asyncio.Future | None:
|
|
124
|
-
return self.pending_approvals.get(approval_key)
|
|
125
|
-
|
|
126
121
|
def get_pending_approval_by_conv(self, conv_id: str) -> asyncio.Future | None:
|
|
127
122
|
"""Looks up whichever approval is CURRENTLY active for a given
|
|
128
123
|
conversation - for callers that only have the stable
|
package/src/main_discord.py
CHANGED
package/src/main_slack.py
CHANGED
package/src/main_telegram.py
CHANGED
package/src/messengers/base.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""Core messenger interface.
|
|
2
|
-
later) implements MessengerAdapter; business logic never touches
|
|
3
|
-
platform SDK types directly.
|
|
1
|
+
"""Core messenger interface. Business logic never touches platform SDK types directly.
|
|
4
2
|
|
|
5
3
|
Futures for approval/question prompts are owned by business logic, not
|
|
6
4
|
the adapter - the same future can also be resolved by a typed reply,
|
|
@@ -17,7 +15,7 @@ from typing import Any
|
|
|
17
15
|
|
|
18
16
|
@dataclass
|
|
19
17
|
class IncomingAttachment:
|
|
20
|
-
"""
|
|
18
|
+
"""reader defers fetching bytes until the attachment is actually read."""
|
|
21
19
|
|
|
22
20
|
filename: str
|
|
23
21
|
content_type: str | None
|
|
@@ -144,10 +142,6 @@ class MessengerAdapter(ABC):
|
|
|
144
142
|
) -> PromptHandle:
|
|
145
143
|
raise NotImplementedError
|
|
146
144
|
|
|
147
|
-
@property
|
|
148
|
-
def supports_voice(self) -> bool:
|
|
149
|
-
return isinstance(self, VoiceCapable)
|
|
150
|
-
|
|
151
145
|
|
|
152
146
|
class VoiceCapable(ABC):
|
|
153
147
|
@abstractmethod
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
"""Per-platform messenger adapter registry, populated once at startup by
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
looking up which platform a given thread_id/conversation belongs to."""
|
|
1
|
+
"""Per-platform messenger adapter registry, populated once at startup by main.py. All platforms
|
|
2
|
+
share one process, so callers must say which platform they mean - either directly (voice/
|
|
3
|
+
Discord-only code) or by looking up which platform a thread_id belongs to."""
|
|
5
4
|
|
|
6
5
|
from messengers.base import MessengerAdapter
|
|
7
6
|
|
|
@@ -24,12 +23,3 @@ def get_adapter_for_thread(thread_id: str) -> MessengerAdapter:
|
|
|
24
23
|
session = session_manager.get_session(thread_id) or {}
|
|
25
24
|
platform = session.get("platform", "discord") # pre-multi-platform sessions have no tag - assume discord
|
|
26
25
|
return get_adapter_for_platform(platform)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_adapter_for_conv_id(conv_id: str) -> MessengerAdapter | None:
|
|
30
|
-
from config import session_manager
|
|
31
|
-
|
|
32
|
-
for _thread_id, sess in session_manager.get_all_sessions().items():
|
|
33
|
-
if sess.get("conversation_id") == conv_id:
|
|
34
|
-
return get_adapter_for_platform(sess.get("platform", "discord"))
|
|
35
|
-
return None
|
|
@@ -46,8 +46,8 @@ def decode_conversation_id(conversation_id: str) -> tuple[str, str] | None:
|
|
|
46
46
|
|
|
47
47
|
def latest_channel_session(channel: str) -> tuple[str, dict] | None:
|
|
48
48
|
"""Most recently created Slack session in a channel - used as a fallback for un-threaded
|
|
49
|
-
messages (users rarely bother clicking "Reply in thread") and for /model,
|
|
50
|
-
|
|
49
|
+
messages (users rarely bother clicking "Reply in thread") and for /model, which can't target
|
|
50
|
+
a specific thread since Slack slash commands can't be invoked inside one."""
|
|
51
51
|
candidates = [
|
|
52
52
|
(cid, s)
|
|
53
53
|
for cid, s in session_manager.get_all_sessions().items()
|
|
@@ -59,8 +59,6 @@ def latest_channel_session(channel: str) -> tuple[str, dict] | None:
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
class SlackConversationRef:
|
|
62
|
-
"""conversation_ref for Slack - a channel + the thread_ts all replies go under."""
|
|
63
|
-
|
|
64
62
|
__slots__ = ("channel", "thread_ts")
|
|
65
63
|
|
|
66
64
|
def __init__(self, channel: str, thread_ts: str):
|
|
@@ -77,8 +75,6 @@ class SlackConversationRef:
|
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
class SlackMessageRef:
|
|
80
|
-
"""message_ref for edit_message - a specific message within a channel."""
|
|
81
|
-
|
|
82
78
|
__slots__ = ("channel", "ts")
|
|
83
79
|
|
|
84
80
|
def __init__(self, channel: str, ts: str):
|
package/voice-service/logger.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const originalLog = console.log;
|
|
2
2
|
const originalError = console.error;
|
|
3
3
|
|
|
4
|
+
const debugEnabled = (process.env.LOG_LEVEL || 'INFO').toUpperCase() === 'DEBUG';
|
|
5
|
+
|
|
4
6
|
function getTimestamp() {
|
|
5
7
|
const now = new Date();
|
|
6
8
|
const offset = now.getTimezoneOffset() * 60000;
|
|
@@ -14,3 +16,8 @@ console.log = function (...args) {
|
|
|
14
16
|
console.error = function (...args) {
|
|
15
17
|
originalError(`${getTimestamp()} ERROR Voice:`, ...args);
|
|
16
18
|
};
|
|
19
|
+
console.debug = function (...args) {
|
|
20
|
+
if (debugEnabled) {
|
|
21
|
+
originalLog(`${getTimestamp()} DEBUG Voice:`, ...args);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -148,7 +148,7 @@ function setupReceiver(connection, guildId, client) {
|
|
|
148
148
|
const dynamicThreshold = isBotPlaying ? baseThreshold * 3 : baseThreshold;
|
|
149
149
|
if (rms > dynamicThreshold) {
|
|
150
150
|
if (interruptTTS(guildId)) {
|
|
151
|
-
console.
|
|
151
|
+
console.debug(
|
|
152
152
|
`[VAD] Loud voice detected (${Math.round(rms)}), interrupting TTS (Threshold: ${dynamicThreshold})`,
|
|
153
153
|
);
|
|
154
154
|
hasInterrupted = true;
|
|
@@ -236,15 +236,18 @@ function setupReceiver(connection, guildId, client) {
|
|
|
236
236
|
const threshold = wakeThresholdFor(userId);
|
|
237
237
|
wakeConfirmed = bestWakeScoreName !== null;
|
|
238
238
|
matchedWakeWord = wakeConfirmed ? bestWakeScoreName : null;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
239
|
+
if (wakeConfirmed) {
|
|
240
|
+
console.log(
|
|
241
|
+
`[Wake] ${userId}: CONFIRMED (score ${bestWakeScore.toFixed(3)} for ` +
|
|
242
|
+
`"${bestWakeScoreName}", threshold ${threshold})`,
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
console.debug(
|
|
246
|
+
`[Wake] ${userId}: no match (threshold ${threshold}; diagnostic-only ` +
|
|
247
|
+
`closeness ${bestDiagScore.toFixed(3)} for "${bestDiagScoreName ?? 'n/a'}" - ` +
|
|
248
|
+
`different scoring config, not directly comparable to the threshold)`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
248
251
|
}
|
|
249
252
|
|
|
250
253
|
const pcmBuffer = Buffer.concat(chunks);
|
package/voice-service/tts.js
CHANGED
|
@@ -14,7 +14,7 @@ function interruptTTS(guildId) {
|
|
|
14
14
|
|
|
15
15
|
if (player && player.state.status !== AudioPlayerStatus.Idle) {
|
|
16
16
|
player.stop();
|
|
17
|
-
console.
|
|
17
|
+
console.debug(`[VAD] Interrupted TTS in guild ${guildId}`);
|
|
18
18
|
interrupted = true;
|
|
19
19
|
}
|
|
20
20
|
|