#1484·DeepTutor

[Bug]:Retrying/Regenerating a turn with attachments permanently hangs UI on "Thinking..." due to Pydantic ValidationError in TurnRequest

Author: githubhelinCreated Sep 16, 2026Updated Sep 16, 2026
Labelsbug

Do you need to file an issue?

  • I have searched the existing issues and this bug is not already filed.
  • I believe this is a legitimate bug, not just a question or feature request.

Describe the bug

[Bug] Retrying/Regenerating a turn with attachments permanently hangs UI on "Thinking..." due to Pydantic ValidationError in TurnRequest

Issue Title

[Bug] Retrying/Regenerating a turn with attachments permanently hangs UI on "Thinking..." due to Pydantic ValidationError in TurnRequest


Problem Description

When conversing with DeepTutor in any chat session where a document attachment (such as a .pdf, .epub, or .docx) was uploaded:

  1. The initial turn completes successfully.
  2. The user clicks the "Regenerate" (重新生成 / 重试) button on the assistant's message (or issues /retry).
  3. The frontend interface immediately enters an infinite loading state, continuously showing "推理中..." (Thinking / Reasoning...) with an animated spinner, and never produces any tokens.
  4. Inspecting the backend service logs (journalctl -u deeptutor.service or terminal output) reveals an immediate unhandled Pydantic validation crash:
ERROR deeptutor.api.routers.unified_ws - Unified WS error: 3 validation errors for TurnRequest
attachments.0.id
  Extra inputs are not permitted [type=extra_forbidden, input_value='4fc93ff0af5b', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/extra_forbidden
attachments.0.extracted_chars
  Extra inputs are not permitted [type=extra_forbidden, input_value=67619, input_type=int]
    For further information visit https://errors.pydantic.dev/2.13/v/extra_forbidden
attachments.0.extracted_text
  Extra inputs are not permitted [type=extra_forbidden, input_value='--- Page 1 ---\n...', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/extra_forbidden
Traceback (most recent call last):
  File "/path/to/deeptutor/api/routers/unified_ws.py", line 326, in unified_websocket
    _, turn = await turns.regenerate_last_turn(session_id, overrides=overrides)
  File "/path/to/deeptutor/app/service.py", line 68, in regenerate_last_turn
    return await runtime.regenerate_last_turn(session_id, overrides=overrides)
  File "/path/to/deeptutor/services/session/turns/request_preparer.py", line 816, in regenerate_last_turn
    return await self.start_turn(payload)
  File "/path/to/deeptutor/services/session/turns/request_preparer.py", line 98, in start_turn
    payload = TurnRequest.model_validate(
  File "/path/to/.venv/lib/python3.12/site-packages/pydantic/main.py", line 732, in model_validate
    return cls.__pydantic_validator__.validate_python(
pydantic_core._pydantic_core.ValidationError: 3 validation errors for TurnRequest

Because the exception is raised before the turn can be registered or scheduled, the WebSocket connection drops abruptly without emitting a turn_end or error stream event. As a result, the frontend UI client remains permanently stuck waiting for stream events.


Environment & Runtime

  • DeepTutor Version: v1.6.8 (and all versions using TurnRequest.model_validate)
  • Python Version: 3.11 / 3.12
  • Pydantic Version: 2.x (pydantic_core with strict validation)
  • OS: Linux / macOS / Windows
  • Endpoint: Unified WebSocket (/ws)

Steps to Reproduce

  1. Start DeepTutor (deeptutor start or via systemd).
  2. Open the web interface (http://localhost:3782).
  3. Create a new chat session and upload any document attachment (e.g. a PDF file).
  4. Send a prompt, e.g.: "Please analyze this document."
  5. Wait for the assistant to finish generating the response.
  6. Click the "Regenerate" icon button below the assistant's message.
  7. Observe that the UI gets permanently stuck on "推理中..." / "Thinking...", and the backend logs the ValidationError shown above.

Root Cause Analysis

1. Attachment Enrichment vs. Strict Schema Validation Conflict

When a document is first uploaded, deeptutor/services/session/turns/executor.py runs extract_documents_from_records to parse text from the uploaded files. To avoid expensive re-parsing on subsequent turns, the parsed results and metadata are stored alongside the attachment in the SQLite database (messages.attachments_json):

python
attachments = [
    Attachment(
        type=r.get("type", "file"),
        url=r.get("url", ""),
        base64=r.get("base64", ""),
        filename=r.get("filename", ""),
        mime_type=r.get("mime_type", ""),
        id=r.get("id", ""),
        extracted_text=r.get("extracted_text", ""),
    )
    for r in attachment_records
]

These database records contain id, extracted_chars, and extracted_text.

2. regenerate_last_turn Restores DB Records Directly

In deeptutor/services/session/turns/request_preparer.py (line 746):

python
payload: dict[str, Any] = {
    "session_id": session_id,
    "capability": capability,
    ...
    "attachments": list(last_user.get("attachments") or []),
    ...
}
return await self.start_turn(payload)

Here, last_user.get("attachments") contains the enriched dictionaries previously saved to SQLite.

3. OutgoingAttachment Enforces extra="forbid"

In deeptutor/core/turn_request.py (lines 30-37):

python
class OutgoingAttachment(BaseModel):
    model_config = ConfigDict(extra="forbid")

    type: str
    url: str | None = None
    base64: str | None = None
    filename: str | None = None
    mime_type: str | None = None

Because OutgoingAttachment specifies extra="forbid" and does not declare id, extracted_chars, or extracted_text, Pydantic's TurnRequest.model_validate(payload) immediately rejects the restored payload with extra_forbidden.


️ Proposed Fix

In deeptutor/core/turn_request.py, configure OutgoingAttachment to allow extra metadata fields (extra="ignore"), and explicitly declare the extraction metadata fields so that cached text and IDs are cleanly preserved:

python
class OutgoingAttachment(BaseModel):
    model_config = ConfigDict(extra="ignore")

    type: str
    url: str | None = None
    base64: str | None = None
    filename: str | None = None
    mime_type: str | None = None
    id: str | None = None
    extracted_chars: int | None = None
    extracted_text: str | None = None

Why this fix is correct:

  1. Zero Breaking Changes: Raw client requests sending only {type, url, filename} still validate identically.
  2. Seamless Regenerate Support: Database-restored payloads containing previously parsed text (extracted_text) and IDs pass validation without error.
  3. Performance Benefit: Preserving extracted_text allows extract_documents_from_records in executor.py to reuse existing parsed text rather than performing redundant extraction of large PDF/EPUB files on retries.

Steps to reproduce

No response

Expected Behavior

No response

Related Module

Dashboard

Configuration Used

No response

Logs and screenshots

No response

Additional Information

  • DeepTutor Version:
  • Operating System:
  • Python Version:
  • Node.js Version:
  • Browser (if applicable):
  • Related Issues: