Files
ragflow/test/unit_test/rag/test_laws_docx_tables.py
Tim Wang f0f10b6092 Fix: UserFillUp interactive forms not working in agent explore mode (#14589)
## Summary

- **Backend**: `_iter_session_completion_events` in `agent_api.py` was
filtering out `user_inputs` and `workflow_finished` SSE events, causing
agents with UserFillUp components to silently fail in explore mode — the
interactive form never appeared, while the same agent worked correctly
in run (editor) mode.
- **Frontend**: `SessionChat` component in explore mode was missing
`DebugContent` children rendering inside `MessageItem`, so even if the
backend forwarded the events, the form UI would not render. Added
`DebugContent`, `MarkdownContent`, `useAwaitCompentData` hook, and
input-disabling logic to match the run mode's `chat/box.tsx` behavior.

## What was changed

### Backend (`api/apps/restful_apis/agent_api.py`)
- Line 266: Added `"user_inputs"` and `"workflow_finished"` to the
allowed event filter in `_iter_session_completion_events`

### Frontend (`web/src/pages/agent/explore/components/session-chat.tsx`)
- Added imports: `DebugContent`, `MarkdownContent`,
`useAwaitCompentData`, `useParams`
- Added `sendFormMessage` from `useSendSessionMessage()` hook
- Added `useAwaitCompentData` hook for form state management
- Added `DebugContent` as `MessageItem` children for the latest
assistant message (renders UserFillUp form)
- Added `MarkdownContent` + submitted values display for previous
assistant messages
- Updated `NextMessageInput` disabled states to respect `isWaitting`
(form submission in progress)

## Test plan

- [x] Agent with UserFillUp component (e.g., email draft with
send/edit/cancel options) shows interactive form in **explore mode**
- [x] Same agent continues to work correctly in **run (editor) mode**
- [x] Form submission sends data back to the agent and workflow
continues
- [x] Input field is disabled while waiting for form submission
- [ ] Agents without UserFillUp components are unaffected in explore
mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
2026-06-29 09:45:17 +08:00

146 lines
4.7 KiB
Python

#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import types
from importlib import import_module, reload
from io import BytesIO
import pytest
from docx import Document
def _stub(name, **attrs):
mod = types.ModuleType(name)
for key, value in attrs.items():
setattr(mod, key, value)
sys.modules.setdefault(name, mod)
return mod
# Stub laws.py's app-layer siblings that the Docx parser never calls, so the module
# can be imported without pulling in the LLM / vision / storage stacks.
class _DummyBase:
def __init__(self, *a, **k):
pass
@pytest.fixture(scope="module")
def docx_chunker():
original_modules = {
name: sys.modules.get(name)
for name in (
"deepdoc.parser",
"deepdoc.parser.utils",
"rag.app.naive",
"common.parser_config_utils",
)
}
try:
_stub("deepdoc.parser", PdfParser=_DummyBase, DocxParser=_DummyBase, HtmlParser=_DummyBase)
_stub("deepdoc.parser.utils", get_text=lambda *a, **k: "")
_stub("rag.app.naive", by_plaintext=lambda *a, **k: ([], [], None), PARSERS={})
_stub("common.parser_config_utils", normalize_layout_recognizer=lambda x: (x, None))
module = import_module("rag.app.laws")
module = reload(module)
yield module.Docx
finally:
for name, original in original_modules.items():
if original is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = original
def _build_docx(builder):
doc = Document()
builder(doc)
buf = BytesIO()
doc.save(buf)
return buf.getvalue()
@pytest.mark.p2
def test_laws_docx_preserves_table(docx_chunker):
"""Regression for #16008: the laws DOCX parser dropped tables entirely."""
def builder(d):
d.add_heading("Chapter 1 General Provisions", level=1)
d.add_heading("Article 2 Fee Schedule", level=2)
d.add_paragraph("The applicable fees are as follows:")
t = d.add_table(rows=2, cols=2)
t.cell(0, 0).text = "Item"
t.cell(0, 1).text = "Fee"
t.cell(1, 0).text = "Registration"
t.cell(1, 1).text = "100"
chunks = docx_chunker()("law.docx", _build_docx(builder))
assert any("<table>" in c for c in chunks)
table_chunk = next(c for c in chunks if "<table>" in c)
# Table content is present...
assert "Registration" in table_chunk and "100" in table_chunk
# ...and it carries its enclosing section's title path for retrieval context.
assert "Article 2 Fee Schedule" in table_chunk
@pytest.mark.p2
def test_laws_docx_merged_cells_use_colspan(docx_chunker):
def builder(d):
d.add_heading("Heading", level=1)
t = d.add_table(rows=1, cols=3)
# Identical adjacent cell text is collapsed into a single colspan cell.
t.cell(0, 0).text = "Merged"
t.cell(0, 1).text = "Merged"
t.cell(0, 2).text = "Other"
chunks = docx_chunker()("law.docx", _build_docx(builder))
table_chunk = next(c for c in chunks if "<table>" in c)
assert "colspan='2'" in table_chunk
assert "<td>Other</td>" in table_chunk
@pytest.mark.p2
def test_laws_docx_escapes_cell_html(docx_chunker):
def builder(d):
d.add_heading("Heading", level=1)
t = d.add_table(rows=1, cols=1)
t.cell(0, 0).text = "a < b & c > d"
chunks = docx_chunker()("law.docx", _build_docx(builder))
table_chunk = next(c for c in chunks if "<table>" in c)
# Special characters are HTML-escaped so the table markup stays well-formed.
assert "a &lt; b &amp; c &gt; d" in table_chunk
assert "<td>a < b" not in table_chunk
@pytest.mark.p2
def test_laws_docx_tables_only_does_not_crash(docx_chunker):
def builder(d):
t = d.add_table(rows=1, cols=2)
t.cell(0, 0).text = "a"
t.cell(0, 1).text = "b"
chunks = docx_chunker()("law.docx", _build_docx(builder))
assert any("<table>" in c for c in chunks)
@pytest.mark.p2
def test_laws_docx_empty_doc_returns_empty(docx_chunker):
chunks = docx_chunker()("law.docx", _build_docx(lambda d: None))
assert chunks == []