fix: update Interrupt object attribute access for LangGraph 1.0+ (#730) (#731)

* Update uv.lock to sync with pyproject.toml

* fix: update Interrupt object attribute access for LangGraph 1.0+ (#730)

The Interrupt class in LangGraph 1.0 no longer has the 'ns' attribute.
This change updates _create_interrupt_event() to use the new 'id'
attribute instead, with a fallback to thread_id for compatibility.

Changes:
- Replace event_data["__interrupt__"][0].ns[0] with interrupt.id
- Use getattr() with fallback for backward compatibility
- Update debug log message from 'ns=' to 'id='
- Add unit tests for _create_interrupt_event function

* fix the unit test error and address review comment

---------

Co-authored-by: Willem Jiang <143703838+willem-bd@users.noreply.github.com>
This commit is contained in:
Willem Jiang
2025-12-02 11:16:00 +08:00
committed by GitHub
parent e1772d52a9
commit c36ab393f1
2 changed files with 105 additions and 6 deletions

View File

@@ -12,7 +12,12 @@ from langchain_core.messages import AIMessageChunk, ToolMessage
from langgraph.types import Command
from src.config.report_style import ReportStyle
from src.server.app import _astream_workflow_generator, _make_event, app
from src.server.app import (
_astream_workflow_generator,
_create_interrupt_event,
_make_event,
app,
)
@pytest.fixture
@@ -657,9 +662,9 @@ class TestAstreamWorkflowGenerator:
@pytest.mark.asyncio
@patch("src.server.app.graph")
async def test_astream_workflow_generator_interrupt_event(self, mock_graph):
# Mock interrupt data
# Mock interrupt data with the new 'id' attribute (LangGraph 1.0+)
mock_interrupt = MagicMock()
mock_interrupt.ns = ["interrupt_id"]
mock_interrupt.id = "interrupt_id"
mock_interrupt.value = "Plan requires approval"
interrupt_data = {"__interrupt__": [mock_interrupt]}
@@ -920,3 +925,94 @@ class TestGenerateProseEndpoint:
response = client.post("/api/prose/generate", json=request_data)
assert response.status_code == 500
assert response.json()["detail"] == "Internal Server Error"
class TestCreateInterruptEvent:
"""Tests for _create_interrupt_event function (Issue #730 fix)."""
def test_create_interrupt_event_with_id_attribute(self):
"""Test that _create_interrupt_event works with LangGraph 1.0+ Interrupt objects that have 'id' attribute."""
# Create a mock Interrupt object with the new 'id' attribute (LangGraph 1.0+)
mock_interrupt = MagicMock()
mock_interrupt.id = "interrupt-123"
mock_interrupt.value = "Please review the research plan"
event_data = {"__interrupt__": [mock_interrupt]}
thread_id = "thread-456"
result = _create_interrupt_event(thread_id, event_data)
# Verify the result is a properly formatted SSE event
assert "event: interrupt\n" in result
assert '"thread_id": "thread-456"' in result
assert '"id": "interrupt-123"' in result
assert '"content": "Please review the research plan"' in result
assert '"finish_reason": "interrupt"' in result
assert '"role": "assistant"' in result
def test_create_interrupt_event_fallback_to_thread_id(self):
"""Test that _create_interrupt_event falls back to thread_id when 'id' attribute is None."""
# Create a mock Interrupt object where id is None
mock_interrupt = MagicMock()
mock_interrupt.id = None
mock_interrupt.value = "Plan review needed"
event_data = {"__interrupt__": [mock_interrupt]}
thread_id = "thread-789"
result = _create_interrupt_event(thread_id, event_data)
# Verify it falls back to thread_id
assert '"id": "thread-789"' in result
assert '"thread_id": "thread-789"' in result
assert '"content": "Plan review needed"' in result
def test_create_interrupt_event_without_id_attribute(self):
"""Test that _create_interrupt_event handles objects without 'id' attribute (backward compatibility)."""
# Create a mock object that doesn't have 'id' attribute at all
class MockInterrupt:
pass
mock_interrupt = MockInterrupt()
mock_interrupt.value = "Waiting for approval"
event_data = {"__interrupt__": [mock_interrupt]}
thread_id = "thread-abc"
result = _create_interrupt_event(thread_id, event_data)
# Verify it falls back to thread_id when id attribute doesn't exist
assert '"id": "thread-abc"' in result
assert '"content": "Waiting for approval"' in result
def test_create_interrupt_event_options(self):
"""Test that _create_interrupt_event includes correct options."""
mock_interrupt = MagicMock()
mock_interrupt.id = "int-001"
mock_interrupt.value = "Review plan"
event_data = {"__interrupt__": [mock_interrupt]}
thread_id = "thread-xyz"
result = _create_interrupt_event(thread_id, event_data)
# Verify options are included
assert '"options":' in result
assert '"text": "Edit plan"' in result
assert '"value": "edit_plan"' in result
assert '"text": "Start research"' in result
assert '"value": "accepted"' in result
def test_create_interrupt_event_with_complex_value(self):
"""Test that _create_interrupt_event handles complex content values."""
mock_interrupt = MagicMock()
mock_interrupt.id = "int-complex"
mock_interrupt.value = {"plan": "Research AI", "steps": ["step1", "step2"]}
event_data = {"__interrupt__": [mock_interrupt]}
thread_id = "thread-complex"
result = _create_interrupt_event(thread_id, event_data)
# Verify complex value is included (will be serialized as JSON)
assert '"id": "int-complex"' in result
assert "Research AI" in result or "plan" in result