Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions line/llm_agent/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
LogMessage,
LogMetric,
OutputEvent,
UserCustomSent,
UserDtmfSent,
UserTextSent,
UserTurnEnded,
Expand Down Expand Up @@ -441,6 +442,7 @@ def _match_matchable(
CallEnded,
AgentHandedOff,
UserTurnStarted,
UserCustomSent,
UserDtmfSent,
UserTextSent,
UserTurnEnded,
Expand Down Expand Up @@ -482,6 +484,7 @@ def _to_history_event(event: object) -> Optional[HistoryEvent]:
CallEnded,
AgentHandedOff,
UserTurnStarted,
UserCustomSent,
UserDtmfSent,
UserTextSent,
UserTurnEnded,
Expand Down
7 changes: 7 additions & 0 deletions line/llm_agent/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
InputEvent,
LogMetric,
OutputEvent,
UserCustomSent,
UserTextSent,
)
from line.llm_agent.background_queue import BackgroundQueue
Expand Down Expand Up @@ -549,6 +550,12 @@ async def _build_messages(
# Handle InputEvent types
if isinstance(event, UserTextSent):
messages.append(Message(role="user", content=event.content or ""))
elif isinstance(event, UserCustomSent):
# Custom events from the client carry metadata that may be useful
# as LLM context (e.g., "user purchased diamonds", "upsell dismissed").
meta = event.metadata or {}
content = json.dumps(meta) if meta else "[custom event]"
messages.append(Message(role="system", content=f"[Client event: {content}]"))
elif isinstance(event, AgentTextSent):
messages.append(Message(role="assistant", content=event.content or ""))
# Handle CustomHistoryEntry (injected history entries)
Expand Down
34 changes: 34 additions & 0 deletions tests/test_llm_agent_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
AgentToolReturned,
CallEnded,
CustomHistoryEntry,
UserCustomSent,
UserTextSent,
)
from line.llm_agent.history import History, _build_full_history
Expand Down Expand Up @@ -62,6 +63,39 @@ async def test_only_input_history_with_user_message(self):
assert isinstance(result[0], UserTextSent)
assert result[0].content == "Hello"

async def test_user_custom_sent_passes_through_in_history(self):
"""UserCustomSent events should pass through history without crashing."""
input_history = [
UserTextSent(content="Hello"),
UserCustomSent(metadata={"kind": "diamonds_purchased"}),
UserTextSent(content="I topped up"),
]
result = _build_full_history(input_history, [], current_event_id="current")

assert len(result) == 3
assert isinstance(result[0], UserTextSent)
assert isinstance(result[1], UserCustomSent)
assert result[1].metadata == {"kind": "diamonds_purchased"}
assert isinstance(result[2], UserTextSent)

async def test_user_custom_sent_with_local_history(self):
"""UserCustomSent mixed with agent responses should not crash."""
input_history = [
UserTextSent(content="Hello", event_id="e1"),
AgentTextSent(content="Hi!", event_id="e2"),
UserCustomSent(metadata={"kind": "diamonds_depleted"}, event_id="e3"),
UserTextSent(content="What happened?", event_id="e4"),
]
local_history = self._annotate(
[AgentSendText(text="Hi!")], "e1"
)
result = _build_full_history(input_history, local_history, current_event_id="current")

# UserCustomSent should be present in the result
custom_events = [e for e in result if isinstance(e, UserCustomSent)]
assert len(custom_events) == 1
assert custom_events[0].metadata == {"kind": "diamonds_depleted"}

async def test_only_input_history_with_observable_event(self):
"""When only input_history exists with observable event, include it (pass through)."""
input_history = [AgentTextSent(content="Hi there")]
Expand Down
64 changes: 64 additions & 0 deletions tests/test_llm_agent_llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CallStarted,
LogMetric,
OutputEvent,
UserCustomSent,
UserTextSent,
)
from line.llm_agent.config import LlmConfig
Expand Down Expand Up @@ -1443,6 +1444,69 @@ async def test_empty_history_no_pending(self):
assert len(messages) == 0


# =============================================================================
# Tests: _build_messages - UserCustomSent serialization
# =============================================================================


class TestBuildMessagesUserCustomSent:
"""Tests for _build_messages handling of UserCustomSent events.

UserCustomSent events should be serialized as system messages containing
the event metadata as JSON, giving the LLM context about client-side events.
"""

async def test_user_custom_sent_with_metadata(self):
"""UserCustomSent with metadata is serialized as a system message with JSON content."""
agent, _ = create_agent_with_mock([])

custom = UserCustomSent(metadata={"kind": "diamonds_purchased", "amount": 100})
user = UserTextSent(content="I topped up")
input_history = [custom, user]

messages = await build_messages_with(agent, input_history, [], "current")

assert len(messages) == 2
assert messages[0].role == "system"
assert '"kind": "diamonds_purchased"' in messages[0].content
assert messages[1].role == "user"
assert messages[1].content == "I topped up"

async def test_user_custom_sent_with_empty_metadata(self):
"""UserCustomSent with empty metadata uses fallback content."""
agent, _ = create_agent_with_mock([])

custom = UserCustomSent(metadata={})
user = UserTextSent(content="Hello")
input_history = [custom, user]

messages = await build_messages_with(agent, input_history, [], "current")

assert len(messages) == 2
assert messages[0].role == "system"
assert "[custom event]" in messages[0].content
assert messages[1].role == "user"

async def test_user_custom_sent_between_conversation_turns(self):
"""UserCustomSent between user and agent turns preserves conversation flow."""
agent, _ = create_agent_with_mock([])

user1 = UserTextSent(content="Tell me more", event_id="e1")
agent1 = AgentTextSent(content="Sure thing!", event_id="e2")
custom = UserCustomSent(metadata={"kind": "diamonds_depleted"}, event_id="e3")
user2 = UserTextSent(content="What happened?", event_id="e4")
input_history = [user1, agent1, custom, user2]

messages = await build_messages_with(agent, input_history, [], "current")

assert len(messages) == 4
assert messages[0].role == "user"
assert messages[1].role == "assistant"
assert messages[2].role == "system"
assert "diamonds_depleted" in messages[2].content
assert messages[3].role == "user"


# =============================================================================
# Tests: add_history_entry
# =============================================================================
Expand Down