Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7e51e4230 |
+26
-17
@@ -401,35 +401,44 @@ class FjerkroaBot(commands.Bot):
|
|||||||
def get_ai_responder(self, channel_name):
|
def get_ai_responder(self, channel_name):
|
||||||
return self.aichannels[channel_name] if channel_name in self.aichannels else self.airesponder
|
return self.aichannels[channel_name] if channel_name in self.aichannels else self.airesponder
|
||||||
|
|
||||||
|
async def _ingest_attachments(self, message, channel_name: str, airesponder) -> list:
|
||||||
|
"""Cache-first attachment handling; CDN URLs never travel further (IMG-10/11)."""
|
||||||
|
urls = []
|
||||||
|
for attachment in message.attachments:
|
||||||
|
if airesponder.image_cache is None:
|
||||||
|
urls.append(attachment.url)
|
||||||
|
continue
|
||||||
|
sha = await airesponder.image_cache.ingest_url(attachment.url, channel_name, message.author.name, str(message.id))
|
||||||
|
if sha is not None:
|
||||||
|
recent = airesponder.image_cache.recent(channel_name, 8)
|
||||||
|
ext = next((row["ext"] for row in recent if row["sha256"] == sha), "png")
|
||||||
|
data_url = airesponder.image_cache.data_url(sha, ext)
|
||||||
|
if data_url:
|
||||||
|
urls.append(data_url)
|
||||||
|
return urls
|
||||||
|
|
||||||
async def handle_message_through_responder(self, message):
|
async def handle_message_through_responder(self, message):
|
||||||
"""Handle a message through the AI responder"""
|
"""Handle a message through the AI responder"""
|
||||||
message_content = str(message.content).strip()
|
message_content = str(message.content).strip()
|
||||||
if message.reference and message.reference.resolved and isinstance(message.reference.resolved.content, str):
|
if message.reference and message.reference.resolved and isinstance(message.reference.resolved.content, str):
|
||||||
reference_content = str(message.reference.resolved.content).replace("\n", "> \n")
|
reference_content = str(message.reference.resolved.content).replace("\n", "> \n")
|
||||||
message_content = f"> {reference_content}\n\n{message_content}"
|
message_content = f"> {reference_content}\n\n{message_content}"
|
||||||
|
channel_name = self.get_channel_name(message.channel)
|
||||||
|
airesponder = self.get_ai_responder(channel_name)
|
||||||
|
attachment_urls = []
|
||||||
|
if message.attachments:
|
||||||
|
attachment_urls = await self._ingest_attachments(message, channel_name, airesponder)
|
||||||
if len(message_content) < 1:
|
if len(message_content) < 1:
|
||||||
|
# image-only posts: cached + observed, no reply (IMG-17)
|
||||||
|
if attachment_urls:
|
||||||
|
await airesponder.observe_event(message.author.name, "image", f"posted {len(attachment_urls)} image(s)")
|
||||||
return
|
return
|
||||||
message_content = self._resolve_mentions(message_content)
|
message_content = self._resolve_mentions(message_content)
|
||||||
channel_name = self.get_channel_name(message.channel)
|
|
||||||
msg = AIMessage(
|
msg = AIMessage(
|
||||||
message.author.name, message_content, channel_name, self.user in message.mentions or isinstance(message.channel, DMChannel)
|
message.author.name, message_content, channel_name, self.user in message.mentions or isinstance(message.channel, DMChannel)
|
||||||
)
|
)
|
||||||
airesponder = self.get_ai_responder(channel_name)
|
if attachment_urls:
|
||||||
if message.attachments:
|
msg.urls = attachment_urls
|
||||||
for attachment in message.attachments:
|
|
||||||
if not msg.urls:
|
|
||||||
msg.urls = []
|
|
||||||
if airesponder.image_cache is not None:
|
|
||||||
# cache-first: CDN URLs never travel further (IMG-10/11)
|
|
||||||
sha = await airesponder.image_cache.ingest_url(attachment.url, channel_name, message.author.name, str(message.id))
|
|
||||||
if sha is not None:
|
|
||||||
recent = airesponder.image_cache.recent(channel_name, 8)
|
|
||||||
ext = next((row["ext"] for row in recent if row["sha256"] == sha), "png")
|
|
||||||
data_url = airesponder.image_cache.data_url(sha, ext)
|
|
||||||
if data_url:
|
|
||||||
msg.urls.append(data_url)
|
|
||||||
else:
|
|
||||||
msg.urls.append(attachment.url)
|
|
||||||
|
|
||||||
# Reply/ignore classifier gate — direct messages bypass (BEH-01/02/03/07)
|
# Reply/ignore classifier gate — direct messages bypass (BEH-01/02/03/07)
|
||||||
handled, factual = await self._classifier_gate(message, msg, airesponder, channel_name)
|
handled, factual = await self._classifier_gate(message, msg, airesponder, channel_name)
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ SAF-08/MEM-09).
|
|||||||
Bot-generated images are ingested like uploads (user `assistant`), so
|
Bot-generated images are ingested like uploads (user `assistant`), so
|
||||||
"make a variant of that" remix chains work on the bot's own output.
|
"make a variant of that" remix chains work on the bot's own output.
|
||||||
|
|
||||||
|
### IMG-17 — Image-only messages are cached (coverage: test)
|
||||||
|
|
||||||
|
A message consisting only of attachments (no text) is ingested into
|
||||||
|
the cache and recorded as an observation, even though no reply is
|
||||||
|
produced — the image must be available for later `picture_edit` and
|
||||||
|
vision follow-ups. (Previously the empty-text early-return dropped
|
||||||
|
such posts entirely.)
|
||||||
|
|
||||||
### IMG-16 — The prompt announces editable images (coverage: test)
|
### IMG-16 — The prompt announces editable images (coverage: test)
|
||||||
|
|
||||||
When the answer channel has cached images, the context suffix states
|
When the answer channel has cached images, the context suffix states
|
||||||
|
|||||||
@@ -158,6 +158,28 @@ class TestGeneratedImagesCached(OpsBase):
|
|||||||
self.assertEqual(rows[0]["user"], "assistant")
|
self.assertEqual(rows[0]["user"], "assistant")
|
||||||
|
|
||||||
|
|
||||||
|
class TestImageOnlyMessages(OpsBase):
|
||||||
|
async def test_image_only_post_cached_no_reply(self):
|
||||||
|
"""IMG-17: attachment without text -> cached + observed, no reply."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
store, cache = make_cache(tmp)
|
||||||
|
self.bot.airesponder.image_cache = cache
|
||||||
|
self.bot.airesponder.observe_event = AsyncMock()
|
||||||
|
self.bot.respond = AsyncMock()
|
||||||
|
message = self.public_msg("")
|
||||||
|
message.content = ""
|
||||||
|
message.channel.name = "chat"
|
||||||
|
attachment = Mock()
|
||||||
|
attachment.url = "https://cdn.discordapp.com/attachments/1/2/silent.png"
|
||||||
|
message.attachments = [attachment]
|
||||||
|
message.id = 77
|
||||||
|
with patch.object(ImageCache, "_download", new_callable=AsyncMock, return_value=PNG):
|
||||||
|
await self.bot.on_message(message)
|
||||||
|
self.assertEqual(len(store.images_recent("chat", 5)), 1)
|
||||||
|
self.bot.airesponder.observe_event.assert_awaited_once()
|
||||||
|
self.bot.respond.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
class TestContextAnnouncesImages(unittest.IsolatedAsyncioTestCase):
|
class TestContextAnnouncesImages(unittest.IsolatedAsyncioTestCase):
|
||||||
def test_suffix_mentions_picture_edit(self):
|
def test_suffix_mentions_picture_edit(self):
|
||||||
"""IMG-16: cached channel images are announced in the context suffix."""
|
"""IMG-16: cached channel images are announced in the context suffix."""
|
||||||
|
|||||||
Reference in New Issue
Block a user