feat(telegram): stop generation from the client, questions survive a restart, images reach the model inline, albums become one turn
This commit is contained in:
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlmodel import col, select
|
||||
@@ -70,13 +70,23 @@ class InjectQueue:
|
||||
self._db = db
|
||||
|
||||
async def push(
|
||||
self, *, conversation_id: int, priority: Priority, origin: str, text: str
|
||||
self,
|
||||
*,
|
||||
conversation_id: int,
|
||||
priority: Priority,
|
||||
origin: str,
|
||||
text: str,
|
||||
attachments: list[dict[str, Any]] | None = None,
|
||||
) -> InjectQueueItem:
|
||||
if priority not in PRIORITY_RANK:
|
||||
msg = f"unknown priority {priority!r}"
|
||||
raise ValueError(msg)
|
||||
row = InjectQueueItem(
|
||||
conversation_id=conversation_id, priority=priority, origin=origin, text=text
|
||||
conversation_id=conversation_id,
|
||||
priority=priority,
|
||||
origin=origin,
|
||||
text=text,
|
||||
attachments=attachments or None,
|
||||
)
|
||||
async with self._db.session() as session:
|
||||
session.add(row)
|
||||
|
||||
@@ -20,13 +20,19 @@ _log = logging.getLogger(__name__)
|
||||
|
||||
class Messaging(Turns):
|
||||
async def post(
|
||||
self, conv: Conversation, text: str, *, origin: str = "user"
|
||||
self,
|
||||
conv: Conversation,
|
||||
text: str,
|
||||
*,
|
||||
origin: str = "user",
|
||||
attachments: list[dict[str, Any]] | None = None,
|
||||
) -> InjectQueueItem:
|
||||
item = await self._queue.push(
|
||||
conversation_id=cast("int", conv.id),
|
||||
priority="user",
|
||||
origin=origin,
|
||||
text=text,
|
||||
attachments=attachments,
|
||||
)
|
||||
await self.touch_user(conv)
|
||||
self._bus.publish(
|
||||
@@ -86,6 +92,14 @@ class Messaging(Turns):
|
||||
self._ensure_worker(cast("int", conv.id))
|
||||
return item
|
||||
|
||||
async def interrupt(self, conv: Conversation) -> bool:
|
||||
"""Cut the running turn; the reply so far still lands."""
|
||||
try:
|
||||
backend = self._backend(conv.agent_name)
|
||||
except LookupError:
|
||||
return False
|
||||
return await backend.interrupt(conv.external_id)
|
||||
|
||||
async def say(self, conv: Conversation, text: str) -> dict[str, Any]:
|
||||
runner = self._runners.get(cast("int", conv.id))
|
||||
_log.info("say[%s]: %s", conv.external_id, text[:200])
|
||||
|
||||
@@ -44,6 +44,7 @@ class Spawning(Messaging):
|
||||
origin: str = "api",
|
||||
binding: tuple[str, str] | None = None,
|
||||
flags: dict[str, Any] | None = None,
|
||||
attachments: list[dict[str, Any]] | None = None,
|
||||
) -> Conversation:
|
||||
"""Create a conversation in a window and queue its seed.
|
||||
|
||||
@@ -95,6 +96,7 @@ class Spawning(Messaging):
|
||||
priority="user",
|
||||
origin=f"seed:{seed}" if seed == "brief" else origin,
|
||||
text=await self.seed_text(ctx, window=window),
|
||||
attachments=attachments,
|
||||
)
|
||||
self._ensure_worker(cast("int", conv.id))
|
||||
return conv
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import contextlib
|
||||
import inspect
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -34,7 +36,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from beaver_gateway.events.stream import MessageStreamEvent
|
||||
|
||||
__all__ = ["Turns"]
|
||||
__all__ = ["Turns", "content_of", "image_block"]
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
@@ -125,13 +127,14 @@ class Turns(Seeds):
|
||||
tools: bool = True,
|
||||
turn_id: str | None = None,
|
||||
item_origin: str | None = None,
|
||||
attachments: Sequence[dict[str, Any]] | None = None,
|
||||
) -> tuple[str, TurnCapture]:
|
||||
capture = TurnCapture()
|
||||
acc = StreamAccumulator()
|
||||
agent = self._claude_agent(conv.agent_name)
|
||||
async for event in self.turn(
|
||||
conv,
|
||||
messages=[{"role": "user", "content": text}],
|
||||
messages=[{"role": "user", "content": content_of(text, attachments)}],
|
||||
origin=origin,
|
||||
capture=capture,
|
||||
tools=tools,
|
||||
@@ -224,7 +227,12 @@ class Turns(Seeds):
|
||||
prompt = f"{seed}\n\n{prompt}"
|
||||
try:
|
||||
text, capture = await self.run_text_turn(
|
||||
conv, prompt, origin=origin, turn_id=turn_id, item_origin=head.origin
|
||||
conv,
|
||||
prompt,
|
||||
origin=origin,
|
||||
turn_id=turn_id,
|
||||
item_origin=head.origin,
|
||||
attachments=head.attachments if origin == "user" else None,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
_log.exception("turn %s on %s failed", turn_id, conv.external_id)
|
||||
@@ -402,6 +410,33 @@ class Turns(Seeds):
|
||||
await self._update(conv, apply)
|
||||
|
||||
|
||||
IMAGE_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
||||
|
||||
def content_of(
|
||||
text: str, attachments: Sequence[dict[str, Any]] | None
|
||||
) -> str | list[dict[str, Any]]:
|
||||
"""Plain text, or text plus the images that ride with it."""
|
||||
blocks = [b for a in attachments or () if (b := image_block(a)) is not None]
|
||||
if not blocks:
|
||||
return text
|
||||
return [{"type": "text", "text": text}, *blocks]
|
||||
|
||||
|
||||
def image_block(attachment: dict[str, Any]) -> dict[str, Any] | None:
|
||||
media_type = str(attachment.get("media_type") or "")
|
||||
path = Path(str(attachment.get("path") or ""))
|
||||
if not media_type.startswith("image/") or not path.is_file():
|
||||
return None
|
||||
if path.stat().st_size > IMAGE_MAX_BYTES:
|
||||
return None
|
||||
data = base64.b64encode(path.read_bytes()).decode("ascii")
|
||||
return {
|
||||
"type": "image",
|
||||
"source": {"type": "base64", "media_type": media_type, "data": data},
|
||||
}
|
||||
|
||||
|
||||
def _prompt_preview(messages: Sequence[Any], limit: int = 400) -> str | None:
|
||||
if not messages:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user