[Bug]:Retrying/Regenerating a turn with attachments permanently hangs UI on "Thinking..." due to Pydantic ValidationError in TurnRequest
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:
- The initial turn completes successfully.
- The user clicks the "Regenerate" (重新生成 / 重试) button on the assistant's message (or issues
/retry). - The frontend interface immediately enters an infinite loading state, continuously showing "推理中..." (Thinking / Reasoning...) with an animated spinner, and never produces any tokens.
- Inspecting the backend service logs (
journalctl -u deeptutor.serviceor 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 TurnRequestBecause 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_corewith strict validation) - OS: Linux / macOS / Windows
- Endpoint: Unified WebSocket (
/ws)
Steps to Reproduce
- Start DeepTutor (
deeptutor startor via systemd). - Open the web interface (
http://localhost:3782). - Create a new chat session and upload any document attachment (e.g. a PDF file).
- Send a prompt, e.g.: "Please analyze this document."
- Wait for the assistant to finish generating the response.
- Click the "Regenerate" icon button below the assistant's message.
- Observe that the UI gets permanently stuck on "推理中..." / "Thinking...", and the backend logs the
ValidationErrorshown 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):
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):
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):
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 = NoneBecause 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:
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 = NoneWhy this fix is correct:
- Zero Breaking Changes: Raw client requests sending only
{type, url, filename}still validate identically. - Seamless Regenerate Support: Database-restored payloads containing previously parsed text (
extracted_text) and IDs pass validation without error. - Performance Benefit: Preserving
extracted_textallowsextract_documents_from_recordsinexecutor.pyto 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:
Source: HKUDS/DeepTutor