linkgravity 1.2.2 → 1.4.0
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/bin/cli.js +160 -80
- package/bin/platforms.js +63 -0
- package/bin/setup.js +172 -76
- package/hooks/hook.py +3 -4
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/src/api/server.py +2 -4
- package/src/api/ui_routes.py +59 -18
- package/src/api/voice_routes.py +5 -8
- package/src/cogs/general_cog.py +4 -1
- package/src/cogs/voice/enrollment.py +1 -1
- package/src/cogs/voice_cog.py +28 -23
- package/src/config.py +9 -4
- package/src/core/agy_runner.py +36 -3
- package/src/core/session_manager.py +25 -0
- package/src/handlers/message_router.py +6 -9
- package/src/handlers/thread_reply.py +63 -33
- package/src/main.py +40 -278
- package/src/main_discord.py +311 -0
- package/src/main_telegram.py +202 -0
- package/src/messengers/base.py +46 -7
- package/src/messengers/discord_adapter.py +72 -5
- package/src/messengers/registry.py +28 -12
- package/src/messengers/telegram_adapter.py +372 -0
- package/src/services/audio_service.py +2 -2
- package/src/services/discord_helpers.py +3 -4
- package/src/services/response.py +2 -2
- package/src/services/streaming.py +24 -16
- package/src/utils/utils.py +2 -0
- package/voice-service/index.js +25 -22
- package/src/services/discord_mcp.py +0 -50
package/src/cogs/voice_cog.py
CHANGED
|
@@ -7,6 +7,7 @@ from discord import app_commands
|
|
|
7
7
|
from discord.ext import commands, tasks
|
|
8
8
|
|
|
9
9
|
from config import allowed, logger
|
|
10
|
+
from messengers.registry import get_adapter_for_platform
|
|
10
11
|
|
|
11
12
|
from .voice.enrollment import EnrollmentManager
|
|
12
13
|
from .voice.stt_session import SttSessionTracker
|
|
@@ -175,22 +176,19 @@ class VoiceCog(commands.Cog):
|
|
|
175
176
|
guild_id = interaction.guild_id
|
|
176
177
|
|
|
177
178
|
wake_word_map = self.bot_settings.get("wake_words") or {}
|
|
178
|
-
|
|
179
|
+
own_word = wake_word_map.get(str(interaction.user.id))
|
|
179
180
|
active_timer = self.bot_settings.get("active_timer", 60)
|
|
180
181
|
|
|
181
|
-
if
|
|
182
|
-
# dict.fromkeys dedupes while keeping first-registered order (each user has their own word).
|
|
183
|
-
ww_list = [f"`{w.strip()}`" for w in dict.fromkeys(wake_word_map.values()) if w.strip()]
|
|
184
|
-
ww_str = ", ".join(ww_list[:-1]) + f", or {ww_list[-1]}" if len(ww_list) > 1 else ww_list[0]
|
|
182
|
+
if own_word:
|
|
185
183
|
msg = (
|
|
186
184
|
f"🎤 Connected to `{vc_chan.name}`.\n"
|
|
187
|
-
f"💡 Say {
|
|
185
|
+
f"💡 Say `{own_word}` to activate me. Once awake, I'll keep listening for {active_timer} seconds after each interaction.\n"
|
|
188
186
|
f"⚙️ You can customize settings using `/sound`."
|
|
189
187
|
)
|
|
190
188
|
else:
|
|
191
189
|
msg = (
|
|
192
190
|
f"🎤 Connected to `{vc_chan.name}`.\n"
|
|
193
|
-
f"🎙️
|
|
191
|
+
f"🎙️ You haven't set up a wake word yet, so I can't hear you - run `/sound wake_word:<word>` "
|
|
194
192
|
f"and say your chosen word a few times to register it in your voice."
|
|
195
193
|
)
|
|
196
194
|
await interaction.response.send_message(msg)
|
|
@@ -211,17 +209,17 @@ class VoiceCog(commands.Cog):
|
|
|
211
209
|
if data.get("success"):
|
|
212
210
|
self._voice_state[str(guild_id)] = interaction.channel_id
|
|
213
211
|
|
|
214
|
-
if
|
|
212
|
+
if own_word:
|
|
215
213
|
if self.bot_settings.get("tts_enabled", True):
|
|
216
|
-
welcome_audio = await self.tts("
|
|
214
|
+
welcome_audio = await self.tts("Voice connected.")
|
|
217
215
|
if welcome_audio:
|
|
218
|
-
await self._play_audio(str(guild_id), welcome_audio)
|
|
216
|
+
await self._play_audio(str(guild_id), welcome_audio, suppress_active_window=True)
|
|
219
217
|
elif self.bot_settings.get("tts_enabled", True):
|
|
220
218
|
prompt_audio = await self.tts(
|
|
221
219
|
"No wake word is set up yet. Please use the sound command to set one."
|
|
222
220
|
)
|
|
223
221
|
if prompt_audio:
|
|
224
|
-
await self._play_audio(str(guild_id), prompt_audio)
|
|
222
|
+
await self._play_audio(str(guild_id), prompt_audio, suppress_active_window=True)
|
|
225
223
|
else:
|
|
226
224
|
await interaction.channel.send(f"⚠️ Node.js integration failed: {data.get('error')}")
|
|
227
225
|
except aiohttp.ClientConnectorError:
|
|
@@ -452,9 +450,18 @@ class VoiceCog(commands.Cog):
|
|
|
452
450
|
|
|
453
451
|
self.logger.debug(f"STT recognized: {text} -> AI: {text_to_ai}")
|
|
454
452
|
|
|
455
|
-
|
|
453
|
+
sess = self.session_manager.get_session(str(thread_id))
|
|
454
|
+
if not sess:
|
|
455
|
+
sess = {"status": "pending", "user_id": str(user_id)}
|
|
456
|
+
self.session_manager.set_session(str(thread_id), sess)
|
|
457
|
+
|
|
458
|
+
conv_id = sess.get("conversation_id")
|
|
459
|
+
pa = self.session_manager.get_pending_approval_by_conv(conv_id) if conv_id else None
|
|
460
|
+
has_pending_approval = bool(conv_id and pa and not pa.done())
|
|
461
|
+
|
|
462
|
+
# Cancel a stale in-flight turn, unless a tool/question approval is pending (this utterance may be its answer).
|
|
456
463
|
prev_task = self._active_turns.get(str(guild_id))
|
|
457
|
-
if prev_task and not prev_task.done():
|
|
464
|
+
if prev_task and not prev_task.done() and not has_pending_approval:
|
|
458
465
|
prev_task.cancel()
|
|
459
466
|
|
|
460
467
|
from core.agy_runner import stop_active_process
|
|
@@ -493,15 +500,7 @@ class VoiceCog(commands.Cog):
|
|
|
493
500
|
await self._play_audio(str(guild_id), audio_reply)
|
|
494
501
|
return
|
|
495
502
|
|
|
496
|
-
|
|
497
|
-
if not sess:
|
|
498
|
-
sess = {"status": "pending", "user_id": str(user_id)}
|
|
499
|
-
self.session_manager.set_session(str(thread_id), sess)
|
|
500
|
-
|
|
501
|
-
conv_id = sess.get("conversation_id")
|
|
502
|
-
pa = self.session_manager.get_pending_approval_by_conv(conv_id) if conv_id else None
|
|
503
|
-
|
|
504
|
-
if conv_id and pa and not pa.done():
|
|
503
|
+
if has_pending_approval:
|
|
505
504
|
app_type = self.session_manager.get_pending_approval_type_by_conv(conv_id)
|
|
506
505
|
if app_type == "ask_question":
|
|
507
506
|
pa.set_result(text)
|
|
@@ -530,7 +529,7 @@ class VoiceCog(commands.Cog):
|
|
|
530
529
|
self.session_manager.register_queue(str(thread.id), queue)
|
|
531
530
|
|
|
532
531
|
ctx = {"status_msg": None}
|
|
533
|
-
consume_task = asyncio.create_task(self.stream_thinking_latest(thread, ctx, queue))
|
|
532
|
+
consume_task = asyncio.create_task(self.stream_thinking_latest(thread, str(thread.id), ctx, queue))
|
|
534
533
|
|
|
535
534
|
try:
|
|
536
535
|
if is_new_session:
|
|
@@ -545,6 +544,12 @@ class VoiceCog(commands.Cog):
|
|
|
545
544
|
sess["status"] = "active"
|
|
546
545
|
self.session_manager.set_session(str(thread_id), sess)
|
|
547
546
|
conv_id = new_conv_id
|
|
547
|
+
|
|
548
|
+
from utils.utils import generate_thread_title, update_agy_conversation_title
|
|
549
|
+
|
|
550
|
+
new_title = await generate_thread_title(text_to_ai, raw_ans)
|
|
551
|
+
await get_adapter_for_platform("discord").rename_conversation(thread, new_title)
|
|
552
|
+
await update_agy_conversation_title(new_conv_id, new_title)
|
|
548
553
|
else:
|
|
549
554
|
logger.debug("Voice: calling agy_send...")
|
|
550
555
|
raw_ans = await self.agy_send(
|
package/src/config.py
CHANGED
|
@@ -7,8 +7,7 @@ WORKSPACE_DIR = Path.home() / ".gemini" / "linkgravity"
|
|
|
7
7
|
WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
|
|
8
8
|
DATA_DIR = WORKSPACE_DIR / "data"
|
|
9
9
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
10
|
-
# Per-user wake-word recordings
|
|
11
|
-
# Defined early so load_bot_settings' migration below can read it.
|
|
10
|
+
# Per-user wake-word recordings (see EnrollmentManager); defined early so load_bot_settings' migration below can read it.
|
|
12
11
|
WAKE_REF_DIR = WORKSPACE_DIR / "wake_refs"
|
|
13
12
|
WAKE_REF_DIR.mkdir(parents=True, exist_ok=True)
|
|
14
13
|
|
|
@@ -16,6 +15,8 @@ LGY_CONFIG_FILE = WORKSPACE_DIR / "lgy.json"
|
|
|
16
15
|
|
|
17
16
|
DEFAULT_LGY_CONFIG = {
|
|
18
17
|
"discord_token": "",
|
|
18
|
+
"telegram_token": "",
|
|
19
|
+
"telegram_allowed_user_ids": "",
|
|
19
20
|
"session_scopes": [],
|
|
20
21
|
"allowed_user_ids": "",
|
|
21
22
|
# user_id (str) -> registered word, one per person (see EnrollmentManager._commit_enrollment).
|
|
@@ -74,6 +75,7 @@ logger = init_logger(WORKSPACE_DIR)
|
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
DISCORD_TOKEN = bot_settings.get("discord_token", "")
|
|
78
|
+
TELEGRAM_TOKEN = bot_settings.get("telegram_token", "")
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
def _parse_session_scopes(raw_scopes) -> dict:
|
|
@@ -97,6 +99,7 @@ def _parse_session_scopes(raw_scopes) -> dict:
|
|
|
97
99
|
|
|
98
100
|
SESSION_SCOPES = _parse_session_scopes(bot_settings.get("session_scopes"))
|
|
99
101
|
ALLOWED_IDS = set(int(x) for x in bot_settings.get("allowed_user_ids", "").split(",") if x.strip())
|
|
102
|
+
TELEGRAM_ALLOWED_IDS = set(int(x) for x in bot_settings.get("telegram_allowed_user_ids", "").split(",") if x.strip())
|
|
100
103
|
TTS_VOICE = bot_settings.get("tts_voice", "ko-KR-SunHiNeural")
|
|
101
104
|
|
|
102
105
|
|
|
@@ -120,6 +123,7 @@ TMP_VOICE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
120
123
|
|
|
121
124
|
MAX_EMBED_LEN = 1900
|
|
122
125
|
STREAM_RATE_LIMIT_SEC = 0.5
|
|
126
|
+
APPROVAL_TIMEOUT_SEC = 1800
|
|
123
127
|
PERSISTENT_FILE = DATA_DIR / "persistent_tools.json"
|
|
124
128
|
SESSION_FILE = DATA_DIR / "sessions.json"
|
|
125
129
|
|
|
@@ -136,5 +140,6 @@ AGY_BIN = os.getenv("AGY_BIN_PATH", str(Path.home() / ".local/bin/agy"))
|
|
|
136
140
|
session_manager = SessionManager(DATA_DIR)
|
|
137
141
|
|
|
138
142
|
|
|
139
|
-
def allowed(user_id: int) -> bool:
|
|
140
|
-
|
|
143
|
+
def allowed(user_id: int, platform: str = "discord") -> bool:
|
|
144
|
+
ids = TELEGRAM_ALLOWED_IDS if platform == "telegram" else ALLOWED_IDS
|
|
145
|
+
return not ids or user_id in ids
|
package/src/core/agy_runner.py
CHANGED
|
@@ -118,10 +118,10 @@ async def run_agy(
|
|
|
118
118
|
for attempt in range(max_retries):
|
|
119
119
|
try:
|
|
120
120
|
env = os.environ.copy()
|
|
121
|
-
env["
|
|
121
|
+
env["LGY_APPROVAL_HOOK"] = "1"
|
|
122
122
|
env["PYTHONUNBUFFERED"] = "1"
|
|
123
123
|
if thread_id:
|
|
124
|
-
env["
|
|
124
|
+
env["LGY_THREAD_ID"] = thread_id
|
|
125
125
|
|
|
126
126
|
libstdbuf_path = _find_libstdbuf() # works around agy's output-truncation bug
|
|
127
127
|
if libstdbuf_path:
|
|
@@ -206,7 +206,7 @@ async def run_agy(
|
|
|
206
206
|
gather_task = asyncio.create_task(_gather_pipes())
|
|
207
207
|
wait_task = asyncio.create_task(proc.wait())
|
|
208
208
|
|
|
209
|
-
# Slices let the timeout pause during a pending
|
|
209
|
+
# Slices let the timeout pause during a pending tool approval (up to 3600s).
|
|
210
210
|
from config import session_manager as _sm
|
|
211
211
|
|
|
212
212
|
poll_slice = 5.0
|
|
@@ -376,3 +376,36 @@ async def generate_thread_title(user_input: str, response: str) -> str:
|
|
|
376
376
|
except Exception as e:
|
|
377
377
|
logger.warning(f"AI thread-title generation failed, falling back to raw input: {e}")
|
|
378
378
|
return fallback
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def update_agy_conversation_title(conv_id: str, title: str) -> None:
|
|
382
|
+
"""Antigravity CLI's own conversation list reads its display name from
|
|
383
|
+
conversation_summaries.db's `preview` column (the `title` column exists
|
|
384
|
+
but is unused/always empty - confirmed by inspecting the db directly).
|
|
385
|
+
Without this, agy shows its own auto-generated name while the Discord
|
|
386
|
+
thread shows ours, and the two drift apart for the same conversation."""
|
|
387
|
+
if not conv_id:
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
import sqlite3
|
|
391
|
+
from pathlib import Path
|
|
392
|
+
|
|
393
|
+
db_path = Path.home() / ".gemini/antigravity-cli/conversation_summaries.db"
|
|
394
|
+
if not db_path.exists():
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
def _update():
|
|
398
|
+
conn = sqlite3.connect(str(db_path), timeout=5)
|
|
399
|
+
try:
|
|
400
|
+
conn.execute(
|
|
401
|
+
"UPDATE conversation_summaries SET preview = ? WHERE conversation_id = ?",
|
|
402
|
+
(title, conv_id),
|
|
403
|
+
)
|
|
404
|
+
conn.commit()
|
|
405
|
+
finally:
|
|
406
|
+
conn.close()
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
await asyncio.get_running_loop().run_in_executor(None, _update)
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.warning(f"Failed to sync title into agy's conversation_summaries.db for {conv_id}: {e}")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -79,6 +80,30 @@ class SessionManager:
|
|
|
79
80
|
def get_active_queue_keys(self) -> list:
|
|
80
81
|
return list(self.active_queues.keys())
|
|
81
82
|
|
|
83
|
+
def cleanup_stale_sessions(self, pending_max_age_days: int = 7) -> int:
|
|
84
|
+
"""Only ever removes 'pending' sessions (started with /new but never actually used - no real conversation attached) older than pending_max_age_days, or with no created_at at all (pre-dates that field, safe to treat as stale). 'active' sessions are NEVER removed by age: Discord threads must stay resumable indefinitely, and Telegram's 1-chat-1-session model already overwrites its one entry on each /new, so there's nothing to accumulate there either. Returns how many were removed."""
|
|
85
|
+
now = datetime.now()
|
|
86
|
+
to_remove = []
|
|
87
|
+
for thread_id, session in self.sessions.items():
|
|
88
|
+
if session.get("status") != "pending":
|
|
89
|
+
continue
|
|
90
|
+
created_at_str = session.get("created_at")
|
|
91
|
+
if not created_at_str:
|
|
92
|
+
to_remove.append(thread_id)
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
age_days = (now - datetime.fromisoformat(created_at_str)).days
|
|
96
|
+
except ValueError:
|
|
97
|
+
continue
|
|
98
|
+
if age_days > pending_max_age_days:
|
|
99
|
+
to_remove.append(thread_id)
|
|
100
|
+
|
|
101
|
+
for thread_id in to_remove:
|
|
102
|
+
self.sessions.pop(thread_id, None)
|
|
103
|
+
if to_remove:
|
|
104
|
+
self.save_sessions()
|
|
105
|
+
return len(to_remove)
|
|
106
|
+
|
|
82
107
|
def get_tts_task(self, thread_id: str) -> asyncio.Task | None:
|
|
83
108
|
return self.active_tts_tasks.get(str(thread_id))
|
|
84
109
|
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import discord
|
|
2
|
-
|
|
3
1
|
from config import allowed
|
|
4
2
|
from handlers.thread_reply import handle_thread_reply
|
|
3
|
+
from messengers.base import MessengerAdapter
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
async def handle_message(bot,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if message.author.bot:
|
|
6
|
+
async def handle_message(bot, raw_event, adapter: MessengerAdapter):
|
|
7
|
+
incoming = adapter.to_incoming_message(raw_event)
|
|
8
|
+
if incoming is None:
|
|
11
9
|
return
|
|
12
|
-
if not allowed(
|
|
10
|
+
if not allowed(incoming.author_id, incoming.platform):
|
|
13
11
|
return
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
await handle_thread_reply(bot, message)
|
|
13
|
+
await handle_thread_reply(bot, incoming)
|
|
@@ -2,10 +2,9 @@ import asyncio
|
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
|
-
import discord
|
|
6
|
-
|
|
7
5
|
from config import session_manager
|
|
8
|
-
from messengers.
|
|
6
|
+
from messengers.base import IncomingMessage
|
|
7
|
+
from messengers.registry import get_adapter_for_platform
|
|
9
8
|
from services.response import render_thought_process, send_agy_response
|
|
10
9
|
from services.streaming import stream_thinking_latest
|
|
11
10
|
from utils.utils import (
|
|
@@ -16,13 +15,19 @@ from utils.utils import (
|
|
|
16
15
|
generate_thread_title,
|
|
17
16
|
handle_image_attachments,
|
|
18
17
|
stt,
|
|
18
|
+
update_agy_conversation_title,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
async def handle_approval_reply(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
async def handle_approval_reply(incoming: IncomingMessage, session: dict, content: str, pa) -> bool:
|
|
23
|
+
adapter = get_adapter_for_platform(incoming.platform)
|
|
24
|
+
thread = incoming.conversation_ref
|
|
25
|
+
|
|
26
|
+
if session_manager.get_pending_approval_type_by_conv(incoming.conversation_id) == "ask_question":
|
|
27
|
+
pa.set_result(content)
|
|
28
|
+
await adapter.send_message(thread, f'✅ *Answer Received (Write in): "{content}"*')
|
|
29
|
+
return True
|
|
30
|
+
|
|
26
31
|
if content.lower() in ("yes", "y", "allow", "승인"):
|
|
27
32
|
pa.set_result("allow")
|
|
28
33
|
await adapter.send_message(thread, f'✅ *Answer Received (Write in): "{content}"*')
|
|
@@ -37,37 +42,42 @@ async def handle_approval_reply(
|
|
|
37
42
|
return True
|
|
38
43
|
elif content.lower() in ("clear", "reset"):
|
|
39
44
|
await adapter.send_message(thread, "🧹 Conversation context cleared.")
|
|
40
|
-
old_sess = session_manager.remove_session(
|
|
45
|
+
old_sess = session_manager.remove_session(incoming.conversation_id)
|
|
41
46
|
if old_sess:
|
|
42
|
-
session_manager.set_session(
|
|
47
|
+
session_manager.set_session(incoming.conversation_id, old_sess)
|
|
43
48
|
return True
|
|
44
49
|
return False
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
async def handle_pending_session(
|
|
48
|
-
bot,
|
|
53
|
+
bot, incoming: IncomingMessage, session: dict, agy_content: str, content: str, image_paths: list
|
|
49
54
|
):
|
|
50
|
-
adapter =
|
|
55
|
+
adapter = get_adapter_for_platform(incoming.platform)
|
|
56
|
+
thread = incoming.conversation_ref
|
|
51
57
|
try:
|
|
52
58
|
async with adapter.typing(thread):
|
|
53
59
|
ctx = {"status_msg": None}
|
|
54
60
|
start_time = time.time()
|
|
55
61
|
queue = asyncio.Queue()
|
|
56
|
-
session_manager.register_queue(
|
|
57
|
-
stream_task = asyncio.create_task(
|
|
62
|
+
session_manager.register_queue(incoming.conversation_id, queue)
|
|
63
|
+
stream_task = asyncio.create_task(
|
|
64
|
+
stream_thinking_latest(bot, thread, incoming.conversation_id, context_dict=ctx, queue=queue)
|
|
65
|
+
)
|
|
58
66
|
|
|
59
67
|
cwd = session.get("cwd")
|
|
60
68
|
model = session.get("model")
|
|
61
69
|
result_text, new_conv_id = await agy_new_conversation(
|
|
62
|
-
agy_content, model=model, stream_queue=queue, thread_id=
|
|
70
|
+
agy_content, model=model, stream_queue=queue, thread_id=incoming.conversation_id, cwd=cwd
|
|
63
71
|
)
|
|
64
72
|
|
|
65
73
|
await queue.put(("__END__", True))
|
|
66
74
|
await stream_task
|
|
67
75
|
|
|
68
76
|
response_text = result_text
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
if adapter.supports_renaming:
|
|
78
|
+
new_title = await generate_thread_title(content, response_text)
|
|
79
|
+
await adapter.rename_conversation(thread, new_title)
|
|
80
|
+
await update_agy_conversation_title(new_conv_id, new_title)
|
|
71
81
|
|
|
72
82
|
response_text = await render_thought_process(new_conv_id, ctx, response_text, thread)
|
|
73
83
|
|
|
@@ -82,22 +92,25 @@ async def handle_pending_session(
|
|
|
82
92
|
|
|
83
93
|
|
|
84
94
|
async def handle_existing_session(
|
|
85
|
-
bot,
|
|
95
|
+
bot, incoming: IncomingMessage, session: dict, conv_id: str, agy_content: str, image_paths: list
|
|
86
96
|
):
|
|
87
|
-
adapter =
|
|
97
|
+
adapter = get_adapter_for_platform(incoming.platform)
|
|
98
|
+
thread = incoming.conversation_ref
|
|
88
99
|
try:
|
|
89
100
|
async with adapter.typing(thread):
|
|
90
101
|
ctx = {"status_msg": None}
|
|
91
102
|
start_time = time.time()
|
|
92
103
|
queue = asyncio.Queue()
|
|
93
|
-
session_manager.register_queue(
|
|
94
|
-
stream_task = asyncio.create_task(
|
|
104
|
+
session_manager.register_queue(incoming.conversation_id, queue)
|
|
105
|
+
stream_task = asyncio.create_task(
|
|
106
|
+
stream_thinking_latest(bot, thread, incoming.conversation_id, context_dict=ctx, queue=queue)
|
|
107
|
+
)
|
|
95
108
|
result_text = await agy_send_message(
|
|
96
109
|
conv_id,
|
|
97
110
|
agy_content,
|
|
98
111
|
model=session.get("model"),
|
|
99
112
|
stream_queue=queue,
|
|
100
|
-
thread_id=
|
|
113
|
+
thread_id=incoming.conversation_id,
|
|
101
114
|
cwd=session.get("cwd"),
|
|
102
115
|
)
|
|
103
116
|
await queue.put(("__END__", True))
|
|
@@ -110,14 +123,14 @@ async def handle_existing_session(
|
|
|
110
123
|
cleanup_images(image_paths)
|
|
111
124
|
|
|
112
125
|
|
|
113
|
-
async def handle_thread_reply(bot,
|
|
114
|
-
|
|
115
|
-
session = session_manager.get_session(str(thread.id))
|
|
126
|
+
async def handle_thread_reply(bot, incoming: IncomingMessage):
|
|
127
|
+
session = session_manager.get_session(incoming.conversation_id)
|
|
116
128
|
if not session:
|
|
117
129
|
return
|
|
118
130
|
|
|
119
|
-
adapter =
|
|
120
|
-
|
|
131
|
+
adapter = get_adapter_for_platform(incoming.platform)
|
|
132
|
+
thread = incoming.conversation_ref
|
|
133
|
+
content = incoming.content.strip()
|
|
121
134
|
if content.startswith("/new"):
|
|
122
135
|
await adapter.send_message(
|
|
123
136
|
thread,
|
|
@@ -125,10 +138,11 @@ async def handle_thread_reply(bot, message: discord.Message):
|
|
|
125
138
|
)
|
|
126
139
|
return
|
|
127
140
|
|
|
128
|
-
for att in
|
|
141
|
+
for att in incoming.attachments:
|
|
129
142
|
ct = att.content_type or ""
|
|
130
143
|
if "audio" in ct or att.filename.endswith((".ogg", ".mp3", ".m4a", ".wav")):
|
|
131
|
-
|
|
144
|
+
if incoming.add_reaction:
|
|
145
|
+
await incoming.add_reaction("🎤")
|
|
132
146
|
audio_bytes = await att.read()
|
|
133
147
|
text = await stt(audio_bytes)
|
|
134
148
|
if text:
|
|
@@ -136,9 +150,10 @@ async def handle_thread_reply(bot, message: discord.Message):
|
|
|
136
150
|
await adapter.send_message(thread, f'🎤 *Speech Recognized: "{text}"*')
|
|
137
151
|
break
|
|
138
152
|
|
|
139
|
-
image_paths = await handle_image_attachments(
|
|
153
|
+
image_paths = await handle_image_attachments(incoming.attachments)
|
|
140
154
|
if image_paths:
|
|
141
|
-
|
|
155
|
+
if incoming.add_reaction:
|
|
156
|
+
await incoming.add_reaction("📎")
|
|
142
157
|
await adapter.send_message(thread, f"📎 *{len(image_paths)} file(s) attached*")
|
|
143
158
|
|
|
144
159
|
if not content and not image_paths:
|
|
@@ -151,16 +166,31 @@ async def handle_thread_reply(bot, message: discord.Message):
|
|
|
151
166
|
pa = session_manager.get_pending_approval_by_conv(conv_id) if conv_id else None
|
|
152
167
|
|
|
153
168
|
if conv_id and pa and not pa.done():
|
|
154
|
-
handled = await handle_approval_reply(
|
|
169
|
+
handled = await handle_approval_reply(incoming, session, content, pa)
|
|
155
170
|
if handled:
|
|
156
171
|
return
|
|
157
172
|
|
|
173
|
+
has_pending_approval = bool(conv_id and pa and not pa.done())
|
|
174
|
+
if session_manager.get_queue(incoming.conversation_id) is not None and not has_pending_approval:
|
|
175
|
+
from core.agy_runner import stop_active_process
|
|
176
|
+
|
|
177
|
+
stop_active_process(incoming.conversation_id)
|
|
178
|
+
session_manager.remove_queue(incoming.conversation_id)
|
|
179
|
+
|
|
180
|
+
prev_session = session_manager.get_session(incoming.conversation_id)
|
|
181
|
+
if prev_session:
|
|
182
|
+
session_manager.set_session(
|
|
183
|
+
incoming.conversation_id, {**prev_session, "status": "pending", "conversation_id": None}
|
|
184
|
+
)
|
|
185
|
+
session = session_manager.get_session(incoming.conversation_id)
|
|
186
|
+
conv_id = None
|
|
187
|
+
|
|
158
188
|
if not conv_id:
|
|
159
189
|
if session.get("status") == "pending":
|
|
160
|
-
await handle_pending_session(bot,
|
|
190
|
+
await handle_pending_session(bot, incoming, session, agy_content, content, image_paths)
|
|
161
191
|
return
|
|
162
192
|
else:
|
|
163
193
|
await adapter.send_message(thread, "⚠️ Session ID not found. Start a new session with `/new`.")
|
|
164
194
|
return
|
|
165
195
|
|
|
166
|
-
await handle_existing_session(bot,
|
|
196
|
+
await handle_existing_session(bot, incoming, session, conv_id, agy_content, image_paths)
|