Create Relay Bot MVP

This commit is contained in:
Emil
2026-07-24 22:36:04 +03:00
commit d6dc624301
51 changed files with 2226 additions and 0 deletions
View File
+30
View File
@@ -0,0 +1,30 @@
import tiktoken
from app.database.models import Message, Summary
class ContextBuilder:
def __init__(self, token_limit: int):
self.token_limit = token_limit
try:
self.encoder = tiktoken.get_encoding("cl100k_base")
except Exception:
self.encoder = None
def count_tokens(self, text: str) -> int:
return len(self.encoder.encode(text)) if self.encoder else max(1, len(text) // 4)
def build(self, summary: Summary | None, messages: list[Message]) -> str:
sections = [f"Current memory summary:\n{summary.text}" if summary else ""]
recent: list[str] = []
budget = self.token_limit - self.count_tokens(sections[0])
for item in reversed(messages):
line = f"[{item.sent_at.isoformat()}] {item.user_name}: {item.text}"
cost = self.count_tokens(line)
if cost > budget:
break
recent.append(line)
budget -= cost
if recent:
sections.append("Recent messages:\n" + "\n".join(reversed(recent)))
return "\n\n".join(part for part in sections if part)
+49
View File
@@ -0,0 +1,49 @@
from datetime import UTC, datetime
from app.database.models import Message, TelegramUser
from app.database.repositories import MemoryRepository
class MemoryService:
def __init__(self, repository: MemoryRepository):
self.repository = repository
async def record(
self,
*,
chat_id: int,
message_id: int,
thread_id: int | None,
user_id: int | None,
user_name: str,
text: str,
sent_at: datetime,
reply_to: int | None,
message_type: str,
attachment_data: dict | None = None,
edited: bool = False,
) -> Message:
if user_id is not None:
user = await self.repository.session.get(TelegramUser, user_id)
if user is None:
self.repository.session.add(
TelegramUser(telegram_id=user_id, display_name=user_name)
)
else:
user.display_name = user_name
return await self.repository.save_message(
Message(
chat_id=chat_id,
telegram_message_id=message_id,
thread_id=thread_id,
user_id=user_id,
user_name=user_name,
text=text,
sent_at=sent_at.astimezone(UTC),
reply_to_message_id=reply_to,
message_type=message_type,
attachment_data=attachment_data,
edited=edited,
edited_at=datetime.now(UTC) if edited else None,
)
)
+38
View File
@@ -0,0 +1,38 @@
from app.database.models import Summary
from app.database.repositories import MemoryRepository
from app.llm.prompts import SUMMARY_PROMPT
from app.memory.context_builder import ContextBuilder
class Summarizer:
def __init__(self, memory: MemoryRepository, builder: ContextBuilder, llm, trigger_tokens: int):
self.memory, self.builder, self.llm, self.trigger_tokens = (
memory,
builder,
llm,
trigger_tokens,
)
async def needs_summary(self, chat_id: int, thread_id: int | None) -> bool:
messages = await self.memory.context_messages(chat_id, thread_id, limit=300)
return self.builder.count_tokens("\n".join(m.text for m in messages)) >= self.trigger_tokens
async def summarize(self, chat_id: int, thread_id: int | None) -> Summary | None:
previous = await self.memory.latest_summary(chat_id, thread_id)
messages = await self.memory.context_messages(chat_id, thread_id, limit=300)
if not messages:
return None
context = self.builder.build(previous, messages)
text = await self.llm.complete(
[{"role": "user", "content": f"{SUMMARY_PROMPT}\n\n{context}"}]
)
summary = Summary(
chat_id=chat_id,
thread_id=thread_id,
version=(previous.version if previous else 0) + 1,
text=text,
from_message_id=messages[0].telegram_message_id,
to_message_id=messages[-1].telegram_message_id,
)
await self.memory.save_summary(summary)
return summary