diff --git a/api/providers/vdb/vdb-tencent/src/dify_vdb_tencent/tencent_vector.py b/api/providers/vdb/vdb-tencent/src/dify_vdb_tencent/tencent_vector.py index dc7e7edd4a8..9d1cc8ff132 100644 --- a/api/providers/vdb/vdb-tencent/src/dify_vdb_tencent/tencent_vector.py +++ b/api/providers/vdb/vdb-tencent/src/dify_vdb_tencent/tencent_vector.py @@ -202,6 +202,11 @@ class TencentVector(BaseVector): if metadatas is None: continue metadata = metadatas[i] or {} + if metadata.get("is_summary") is True: + # Tencent VectorDB JSON fields do not support boolean values. + # Use an integer only for the summary marker so other metadata + # and other vector backends keep their existing types. + metadata = {**metadata, "is_summary": 1} doc = document.Document( id=metadata.get("doc_id"), vector=embeddings[i], diff --git a/api/providers/vdb/vdb-tencent/tests/unit_tests/test_tencent_vector.py b/api/providers/vdb/vdb-tencent/tests/unit_tests/test_tencent_vector.py index 7ad76e1960f..41db20db61a 100644 --- a/api/providers/vdb/vdb-tencent/tests/unit_tests/test_tencent_vector.py +++ b/api/providers/vdb/vdb-tencent/tests/unit_tests/test_tencent_vector.py @@ -281,6 +281,23 @@ def test_create_add_delete_and_search_behaviour(tencent_module): vector._client.drop_collection.assert_called_once() +def test_add_texts_converts_only_true_summary_marker(tencent_module): + vector = tencent_module.TencentVector("collection_1", _config(tencent_module)) + summary_metadata = {"doc_id": "summary", "is_summary": True, "published": False} + regular_metadata = {"doc_id": "regular", "published": False} + docs = [ + Document(page_content="summary", metadata=summary_metadata), + Document(page_content="regular", metadata=regular_metadata), + ] + + vector.add_texts(docs, [[0.1], [0.2]]) + + upserted = vector._client.upsert.call_args.kwargs["documents"] + assert upserted[0].metadata == {"doc_id": "summary", "is_summary": 1, "published": False} + assert upserted[1].metadata == regular_metadata + assert summary_metadata["is_summary"] is True + + def test_tencent_factory_existing_and_generated_collection(tencent_module, monkeypatch: pytest.MonkeyPatch): factory = tencent_module.TencentVectorFactory() dataset_with_index = Dataset( diff --git a/api/services/summary_index_service.py b/api/services/summary_index_service.py index 9d820d14659..2f5d832c0e1 100644 --- a/api/services/summary_index_service.py +++ b/api/services/summary_index_service.py @@ -517,8 +517,16 @@ class SummaryIndexService: summary_record_id, original_session is not None, ) - # Always create a new session for error handling to avoid issues with closed sessions - # Even if original_session was provided, we create a new one for safety + if original_session is not None: + # Keep the error update in the caller-owned transaction. Opening another + # session here can deadlock when the caller has already flushed this row. + summary_record.status = SummaryStatus.ERROR + summary_record.error = f"Vectorization failed: {str(e)}" + summary_record.updated_at = datetime.now(UTC).replace(tzinfo=None) + original_session.add(summary_record) + raise + + # Standalone callers still need this method to persist the error itself. with session_factory.create_session() as error_session: # Try to find the record by id first # Note: Using assignment only (no type annotation) to avoid redeclaration error diff --git a/api/tests/unit_tests/services/test_summary_index_service.py b/api/tests/unit_tests/services/test_summary_index_service.py index 1eb1baa40a3..c35318749e7 100644 --- a/api/tests/unit_tests/services/test_summary_index_service.py +++ b/api/tests/unit_tests/services/test_summary_index_service.py @@ -281,6 +281,45 @@ def test_vectorize_summary_final_failure_updates_error_status(monkeypatch: pytes error_session.commit.assert_called_once() +def test_vectorize_summary_failure_with_provided_session_does_not_open_error_session( + monkeypatch: pytest.MonkeyPatch, +) -> None: + dataset = _dataset() + segment = _segment() + summary = _summary_record(summary_content="sum", node_id=None) + + monkeypatch.setattr(summary_module.uuid, "uuid4", MagicMock(return_value="uuid-1")) + monkeypatch.setattr(summary_module.helper, "generate_text_hash", MagicMock(return_value="hash-1")) + monkeypatch.setattr( + summary_module, + "Vector", + MagicMock(return_value=MagicMock(add_texts=MagicMock(side_effect=RuntimeError("boom")))), + ) + monkeypatch.setattr( + summary_module.ModelManager, + "for_tenant", + MagicMock(return_value=MagicMock(get_model_instance=MagicMock(return_value=None))), + ) + + session = MagicMock(name="provided_session") + create_session_mock = MagicMock() + monkeypatch.setattr( + summary_module, + "session_factory", + SimpleNamespace(create_session=create_session_mock), + ) + + with pytest.raises(RuntimeError, match="boom"): + SummaryIndexService.vectorize_summary(summary, segment, dataset, session=session) + + create_session_mock.assert_not_called() + session.add.assert_called_once_with(summary) + session.commit.assert_not_called() + session.flush.assert_not_called() + assert summary.status == SummaryStatus.ERROR + assert summary.error == "Vectorization failed: boom" + + def test_batch_create_summary_records_no_segments_noop(monkeypatch: pytest.MonkeyPatch) -> None: create_session_mock = MagicMock() monkeypatch.setattr(summary_module, "session_factory", SimpleNamespace(create_session=create_session_mock))