Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .qwen/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"permissions": {
"allow": [
"Agent(Explore)",
"Skill(qc-helper)",
"Bash(python *)",
"Bash(pytest *)",
"Bash(uv *)",
"Bash(find *)",
"Agent(general-purpose)",
"Bash(sleep *)",
"Bash(do *)",
"Bash(done)",
"Bash(python3 *)",
"Bash(ruff *)",
"Bash(\"\"\"debug submitted_code persistence.\"\"\")",
"Bash(from *)",
"Bash(engine *)",
"Bash(\"sqlite:///:memory:\",)",
"Bash(false},)",
"Bash(base.metadata.create_all)",
"Bash(session_factory *)",
"Bash(db_module.sessionlocal *)",
"Bash(session *)",
"Bash(interview *)",
"Bash(trackselection)",
"Bash(session.add)",
"Bash(session.flush)",
"Bash(section *)",
"Bash(session, *)",
"Bash(coding_sel *)",
"Bash(.coding_creation)",
"Bash(section.selection_spec *)",
"Bash(coding_sel)",
"Bash(tasks *)",
"Bash(session.commit)",
"Bash(task_id *)",
"Bash(print)",
"Bash(uow *)",
"Bash(section_agg *)",
"Bash(\"debug-1\")",
"Bash(updated *)",
"Bash(task_id,)",
"Bash(uow.coding_sections.save_aggregate)",
"Bash(uow.flush)",
"Bash(uow.commit)",
"Bash(uow.close)",
"Bash(uow2 *)",
"Bash(section2 *)",
"Bash(uow2.close)",
"Bash(pyeof)"
]
},
"$version": 4
}
54 changes: 54 additions & 0 deletions .qwen/settings.json.orig
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"permissions": {
"allow": [
"Agent(Explore)",
"Skill(qc-helper)",
"Bash(python *)",
"Bash(pytest *)",
"Bash(uv *)",
"Bash(find *)",
"Agent(general-purpose)",
"Bash(sleep *)",
"Bash(do *)",
"Bash(done)",
"Bash(python3 *)",
"Bash(ruff *)",
"Bash(\"\"\"debug submitted_code persistence.\"\"\")",
"Bash(from *)",
"Bash(engine *)",
"Bash(\"sqlite:///:memory:\",)",
"Bash(false},)",
"Bash(base.metadata.create_all)",
"Bash(session_factory *)",
"Bash(db_module.sessionlocal *)",
"Bash(session *)",
"Bash(interview *)",
"Bash(trackselection)",
"Bash(session.add)",
"Bash(session.flush)",
"Bash(section *)",
"Bash(session, *)",
"Bash(coding_sel *)",
"Bash(.coding_creation)",
"Bash(section.selection_spec *)",
"Bash(coding_sel)",
"Bash(tasks *)",
"Bash(session.commit)",
"Bash(task_id *)",
"Bash(print)",
"Bash(uow *)",
"Bash(section_agg *)",
"Bash(\"debug-1\")",
"Bash(updated *)",
"Bash(task_id,)",
"Bash(uow.coding_sections.save_aggregate)",
"Bash(uow.flush)",
"Bash(uow.commit)",
"Bash(uow.close)",
"Bash(uow2 *)",
"Bash(section2 *)",
"Bash(uow2.close)"
]
},
"$version": 4
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ Work in progress is accumulated under `[Unreleased]`; on release, that section b

### Removed

## 2026.7.14

### Added

### Changed

- **Question bank overhaul** — restructured and expanded question sets across all tracks (Python, Database, Airflow, Docker, Kubernetes, Observability, Kafka, RabbitMQ); updated questions map

### Fixed

- **Session timeout** — fixed timer handling that caused premature session expiration or stuck rounds
## 2026.6.16

### Added
Expand Down
17 changes: 14 additions & 3 deletions app/ai/faster_whisper_transcriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
"""faster-whisper implementation of :class:`~app.ai.speech_transcriber.SpeechTranscriber`."""

import asyncio
import logging

from faster_whisper import WhisperModel
import numpy as np
import numpy.typing as npt

from app.shared.locales import normalize_locale

logger = logging.getLogger(__name__)


class FasterWhisperTranscriber:
"""Transcribe audio using an in-memory ``WhisperModel``."""
Expand Down Expand Up @@ -39,12 +42,20 @@ async def transcribe(
language = normalize_locale(locale)

def _transcribe() -> str:
segments, _info = self._model.transcribe(
segments, info = self._model.transcribe(
audio,
language=language,
task="transcribe",
vad_filter=True,
vad_filter=False,
)
segment_list = list(segments)
result = "".join((segment.text or "") for segment in segment_list).strip()
logger.info(
"Whisper transcript: language=%s segments=%d result=%r",
info.language,
len(segment_list),
result,
)
return "".join(segment.text for segment in segments).strip()
return result

return await asyncio.to_thread(_transcribe)
65 changes: 64 additions & 1 deletion app/coding/api/ws_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
"""WebSocket message handling for coding sessions."""

from collections.abc import AsyncIterator
from dataclasses import replace
import logging
from typing import Any

from app.ai.base import AIProvider
from app.coding.api.errors import coding_ws_error_payload
from app.coding.api.ws_protocol import coding_event_to_message
from app.coding.domain.exceptions import CodingDomainError
from app.coding.domain.entities import CodingTask
from app.coding.domain.exceptions import CodingDomainError, CodingSectionNotFoundError
from app.coding.repositories.uow import CodingUnitOfWork
from app.coding.services.events import CodingFeedbackEvent
from app.coding.services.navigation import CodingNavigationService
from app.coding.services.submission import CodingSubmissionService
from app.interview.domain.exceptions import InterviewDomainError
from app.interview.services.ai_errors import ai_error_message_for_client
Expand Down Expand Up @@ -103,6 +108,64 @@ async def _handle_timeout(
raw: dict[str, Any],
*,
interview_id: str,
) -> AsyncIterator[dict[str, Any]]:
task_id = str(raw.get("task_id", "")).strip()
if not task_id:
yield {
"type": "error",
"message": "task_id is required",
}
return

try:
with CodingUnitOfWork(auto_commit=True) as uow:
section = uow.coding_sections.get_aggregate(interview_id)
if section is None:
raise CodingSectionNotFoundError(interview_id)
section.ensure_active()
current = section.require_current_task(task_id)
round_num = current.round
order = current.order

# Mark the current task as timed out: score 0, no feedback
tasks = tuple(
replace(
task,
score=0,
feedback="Time expired",
submitted_code=CodingTask.TIME_EXPIRED_CODE,
submit_test_summary=None,
)
if task.id == current.id
else task
for task in section.tasks
)
updated = replace(section, tasks=tasks)
uow.coding_sections.save_aggregate(updated)

# Advance to the next unsubmitted task
next_task_data, timer_remaining = (
CodingNavigationService.advance_to_next_unsubmitted(
uow,
interview_id,
task_id=task_id,
round_num=round_num,
)
)

yield coding_event_to_message(
CodingFeedbackEvent(
task_id=task_id,
order=order,
round=round_num,
follow_up_needed=False,
follow_up_text=None,
follow_up_mode=None,
next_task=next_task_data,
feedback="Time expired",
timer_remaining_seconds=timer_remaining,
)
)
submission_service: CodingSubmissionService,
) -> AsyncIterator[dict[str, Any]]:
task_id = str(raw.get("task_id") or raw.get("question_id") or "").strip()
Expand Down
25 changes: 25 additions & 0 deletions app/theory/services/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,23 @@ async def _transcribe_and_persist(
Returns:
Final transcript text (may be empty).
"""
logger.info(
"[AudioAnswer] Starting transcription for interview=%s question=%s round=%d",
interview_id,
question_id,
round_num,
)
samples = wav_bytes_to_float32(wav_bytes)
transcript = await transcriber.transcribe(samples, locale)
logger.info("[AudioAnswer] Transcription result: %r", transcript)
with TheoryUnitOfWork(auto_commit=True) as uow:
section = uow.theory_sections.get_aggregate(interview_id)
if section is None:
raise TheorySectionNotFoundError(interview_id)
current = section.find_task(question_id, round_num)
updated = section.with_task_text(current.id, transcript)
uow.theory_sections.save_aggregate(updated)
logger.info("[AudioAnswer] Persisted transcript to DB")
section = self._uow.theory_sections.get_aggregate(interview_id)
if section is None:
raise TheorySectionNotFoundError(interview_id)
Expand Down Expand Up @@ -579,16 +594,26 @@ async def _iter_audio_answer_submission(
) -> AsyncIterator[InterviewEvent]:
self.require_audio_answer_enabled()
validate_wav_bytes(wav_bytes)
logger.info(
"[AudioAnswer] Starting audio submission for interview=%s question=%s",
interview_id,
question_id,
)

ctx: TheorySubmissionContext | None = None
async for item in self._open_submission(interview_id, question_id, ""):
if isinstance(item, TheorySubmissionContext):
ctx = item
break
logger.info(
"[AudioAnswer] Yielding pre-context event: %s", type(item).__name__
)
yield item
if ctx is None:
logger.warning("[AudioAnswer] No submission context returned")
return

logger.info("[AudioAnswer] Context opened, round=%d", ctx.round_num)
self._release_submission_write_lock()

yield AnswerSavedEvent()
Expand Down
78 changes: 78 additions & 0 deletions data/coding/python/junior/kafka.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
category: "Kafka Client"
track: "python"
level: "junior"

description: "Implement a Kafka producer in Python with JSON serialization, delivery callbacks, and async sending"

tasks:
- id: "kafka-j-001"
difficulty: 2
tags: ["kafka", "producer", "json", "serialization", "confluent-kafka", "callbacks"]
coding:
language: python
evaluation_mode: ai
assignment:
en: |
Context:
You need to implement a Kafka producer that sends JSON messages to a topic
and handles delivery reports. The producer should use confluent-kafka-python.

Your task:
Complete the `produce` method so that it:
- Serializes the `data` dict to JSON (bytes)
- Sends the message with the given `key`
- Registers the provided `delivery_callback` for delivery confirmation
- Calls `poll(0)` after `produce()` to trigger callbacks

The `flush` method is already implemented.

Keep the existing producer config as-is.
ru: |
Контекст:
Вам нужно реализовать Kafka продюсера, который отправляет JSON-сообщения
и обрабатывает отчёты о доставке. Используется confluent-kafka-python.

Задача:
Реализуйте метод `produce` так, чтобы он:
- Сериализовал `data` (dict) в JSON (bytes)
- Отправлял сообщение с указанным `key`
- Регистрировал `delivery_callback` для подтверждения доставки
- Вызывал `poll(0)` после `produce()`

Метод `flush` уже реализован. Конфигурацию продюсера не меняйте.
starter_code: |
import json
from confluent_kafka import Producer


class JsonProducer:
def __init__(self, bootstrap_servers="localhost:9092"):
self.conf = {
"bootstrap.servers": bootstrap_servers,
"client.id": "py-producer",
}
self.producer = Producer(self.conf)

def produce(self, topic, key, data, delivery_callback):
# TODO: serialize data to JSON bytes, produce with callback, poll(0)
pass

def flush(self, timeout=None):
self.producer.flush(timeout)


def delivery_report(err, msg):
if err:
print(f"Failed: {err}")
else:
print(f"Delivered to {msg.topic()}[{msg.partition()}] @ {msg.offset()}")


producer = JsonProducer()
producer.produce("events", key="alice", data={"user": "alice", "action": "login"}, delivery_callback=delivery_report)
producer.flush()
expected_points:
- "Serializes data dict to JSON bytes with json.dumps().encode()"
- "Calls producer.produce() with topic, key, value, and callback"
- "Calls producer.poll(0) after produce() to trigger callbacks"
- "flush() is separate and handles final delivery wait"
Loading
Loading