Compare commits

..

No commits in common. "master" and "v5" have entirely different histories.
master ... v5

3 changed files with 15 additions and 60 deletions

View file

@ -37,7 +37,7 @@ services:
- chroma_data:/app/chroma_data
networks:
- default
- danvics_speech
- speech_net
depends_on:
postgres:
condition: service_healthy
@ -74,7 +74,7 @@ services:
env_file:
- ./telegram-bot/.env
environment:
- DATABASE_URL=postgresql://pedquiz:${POSTGRES_PASSWORD}@postgres:5432/pedquiz
- DATABASE_URL=postgresql://pedquiz:<password>@postgres:5432/pedquiz
- PUBLIC_APP_URL=${APP_URL:-https://pedshub.com}
depends_on:
postgres:
@ -147,5 +147,5 @@ volumes:
promtail_positions:
networks:
danvics_speech:
speech_net:
external: true

View file

@ -13,14 +13,10 @@ scrape_configs:
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
filters:
- name: label
values: ["com.docker.compose.project=quiz"]
relabel_configs:
# Keep only this compose project; the Docker API filter above prevents
# Promtail from opening non-readable log streams from other stacks.
- source_labels: ['__meta_docker_container_label_com_docker_compose_project']
regex: 'quiz'
# Keep only quiz-* containers
- source_labels: ['__meta_docker_container_name']
regex: '.*quiz.*'
action: keep
# Extract container name as label
- source_labels: ['__meta_docker_container_name']

View file

@ -24,7 +24,7 @@ DATABASE_URL = os.environ["DATABASE_URL"]
DEFAULT_QUIZ_SIZE = int(os.getenv("DEFAULT_QUIZ_SIZE", "20"))
MAX_QUIZ_SIZE = int(os.getenv("MAX_QUIZ_SIZE", "50"))
PUBLIC_APP_URL = os.getenv("PUBLIC_APP_URL", "https://pedshub.com").rstrip("/")
TELEGRAM_MESSAGE_LIMIT = 4096
MAX_EXPLANATION_CHARS = int(os.getenv("MAX_EXPLANATION_CHARS", "1800"))
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NUMBER_RE = re.compile(r"^\s*(\d{1,3})\s*$")
@ -269,56 +269,15 @@ def truncate_text(value: str, limit: int) -> str:
return value[:limit].rsplit(" ", 1)[0].rstrip() + "..."
def answer_feedback_header(chosen_idx: int, correct_idx: int, ok: bool) -> str:
def answer_feedback(question: dict[str, Any], chosen_idx: int, correct_idx: int, ok: bool) -> str:
chosen = LETTERS[chosen_idx] if chosen_idx < len(LETTERS) else "?"
correct = LETTERS[correct_idx] if correct_idx < len(LETTERS) else "?"
return "Correct." if ok else f"Incorrect. You chose {chosen}; correct answer: {correct}."
def split_plain_text(text: str, limit: int) -> list[str]:
if len(text) <= limit:
return [text]
chunks = []
remaining = text
while remaining:
if len(remaining) <= limit:
chunks.append(remaining)
break
split_at = remaining.rfind("\n\n", 0, limit)
if split_at < limit // 2:
split_at = remaining.rfind("\n", 0, limit)
if split_at < limit // 2:
split_at = remaining.rfind(" ", 0, limit)
if split_at < limit // 2:
split_at = limit
chunks.append(remaining[:split_at].rstrip())
remaining = remaining[split_at:].lstrip()
return chunks
async def send_answer_feedback(
context: ContextTypes.DEFAULT_TYPE,
chat_id: int,
question: dict[str, Any],
chosen_idx: int,
correct_idx: int,
ok: bool,
) -> None:
header = answer_feedback_header(chosen_idx, correct_idx, ok)
explanation = (question.get("explanation") or "No explanation available.").strip()
first_prefix = f"{header}\n\n<b>Explanation:</b> "
next_prefix = "<b>Explanation continued:</b> "
first_limit = TELEGRAM_MESSAGE_LIMIT - len(first_prefix) - 200
next_limit = TELEGRAM_MESSAGE_LIMIT - len(next_prefix) - 200
chunks = split_plain_text(explanation, max(1000, first_limit))
for idx, chunk in enumerate(chunks):
prefix = first_prefix if idx == 0 else next_prefix
if idx > 0 and len(chunk) > next_limit:
# First chunk has a smaller budget because it includes answer feedback.
for subchunk in split_plain_text(chunk, next_limit):
await context.bot.send_message(chat_id, f"{next_prefix}{html.escape(subchunk)}", parse_mode=ParseMode.HTML)
continue
await context.bot.send_message(chat_id, f"{prefix}{html.escape(chunk)}", parse_mode=ParseMode.HTML)
explanation = truncate_text(question.get("explanation") or "No explanation available.", MAX_EXPLANATION_CHARS)
return "\n".join([
"Correct." if ok else f"Incorrect. You chose {chosen}; correct answer: {correct}.",
"",
f"<b>Explanation:</b> {html.escape(explanation)}",
])
def answered_question_text(state: QuizState, question: dict[str, Any], chosen_idx: int, correct_idx: int) -> str:
@ -568,7 +527,7 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -
answered_question_text(state, question, chosen_idx, correct_idx),
parse_mode=ParseMode.HTML,
)
await send_answer_feedback(context, chat_id, question, chosen_idx, correct_idx, ok)
await context.bot.send_message(chat_id, answer_feedback(question, chosen_idx, correct_idx, ok), parse_mode=ParseMode.HTML)
else:
await query.edit_message_reply_markup(reply_markup=None)
state.index += 1