Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f91e5e5b |
3 changed files with 60 additions and 15 deletions
|
|
@ -37,7 +37,7 @@ services:
|
|||
- chroma_data:/app/chroma_data
|
||||
networks:
|
||||
- default
|
||||
- speech_net
|
||||
- danvics_speech
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
@ -74,7 +74,7 @@ services:
|
|||
env_file:
|
||||
- ./telegram-bot/.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://pedquiz:<password>@postgres:5432/pedquiz
|
||||
- DATABASE_URL=postgresql://pedquiz:${POSTGRES_PASSWORD}@postgres:5432/pedquiz
|
||||
- PUBLIC_APP_URL=${APP_URL:-https://pedshub.com}
|
||||
depends_on:
|
||||
postgres:
|
||||
|
|
@ -147,5 +147,5 @@ volumes:
|
|||
promtail_positions:
|
||||
|
||||
networks:
|
||||
speech_net:
|
||||
danvics_speech:
|
||||
external: true
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ 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 quiz-* containers
|
||||
- source_labels: ['__meta_docker_container_name']
|
||||
regex: '.*quiz.*'
|
||||
# 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'
|
||||
action: keep
|
||||
# Extract container name as label
|
||||
- source_labels: ['__meta_docker_container_name']
|
||||
|
|
|
|||
|
|
@ -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("/")
|
||||
MAX_EXPLANATION_CHARS = int(os.getenv("MAX_EXPLANATION_CHARS", "1800"))
|
||||
TELEGRAM_MESSAGE_LIMIT = 4096
|
||||
|
||||
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
NUMBER_RE = re.compile(r"^\s*(\d{1,3})\s*$")
|
||||
|
|
@ -269,15 +269,56 @@ def truncate_text(value: str, limit: int) -> str:
|
|||
return value[:limit].rsplit(" ", 1)[0].rstrip() + "..."
|
||||
|
||||
|
||||
def answer_feedback(question: dict[str, Any], chosen_idx: int, correct_idx: int, ok: bool) -> str:
|
||||
def answer_feedback_header(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 "?"
|
||||
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)}",
|
||||
])
|
||||
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)
|
||||
|
||||
|
||||
def answered_question_text(state: QuizState, question: dict[str, Any], chosen_idx: int, correct_idx: int) -> str:
|
||||
|
|
@ -527,7 +568,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 context.bot.send_message(chat_id, answer_feedback(question, chosen_idx, correct_idx, ok), parse_mode=ParseMode.HTML)
|
||||
await send_answer_feedback(context, chat_id, question, chosen_idx, correct_idx, ok)
|
||||
else:
|
||||
await query.edit_message_reply_markup(reply_markup=None)
|
||||
state.index += 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue