Skip to content

Commit 03e57bd

Browse files
authored
Merge pull request #1515 from AnishSarkar22/hotfix/streaming
hotfix(chat): Chat answer streaming and smooth markdown rendering
2 parents b233f15 + bb664a1 commit 03e57bd

4 files changed

Lines changed: 163 additions & 5 deletions

File tree

surfsense_backend/app/tasks/chat/streaming/flows/shared/llm_bundle.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ async def load_llm_bundle(
130130
billing_tier="free",
131131
)
132132
return (
133-
SanitizedChatLiteLLM(model=model_string, **litellm_kwargs),
133+
SanitizedChatLiteLLM(
134+
model=model_string, **{**litellm_kwargs, "streaming": True}
135+
),
134136
agent_config,
135137
None,
136138
)
@@ -174,7 +176,9 @@ async def load_llm_bundle(
174176
billing_tier=str(global_model.get("billing_tier", "free")).lower(),
175177
)
176178
return (
177-
SanitizedChatLiteLLM(model=model_string, **litellm_kwargs),
179+
SanitizedChatLiteLLM(
180+
model=model_string, **{**litellm_kwargs, "streaming": True}
181+
),
178182
agent_config,
179183
None,
180184
)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Contracts for chat LLM construction in streaming flows.
2+
3+
``stream_new_chat`` / ``stream_resume_chat`` depend on LangChain receiving
4+
token chunks from ``ChatLiteLLM``. ``langchain-litellm`` defaults
5+
``streaming`` to ``False``, so the shared bundle loader must opt in
6+
explicitly for both DB-backed and global model paths.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from types import SimpleNamespace
12+
from typing import Any
13+
14+
import pytest
15+
16+
import app.tasks.chat.streaming.flows.shared.llm_bundle as llm_bundle
17+
18+
pytestmark = pytest.mark.unit
19+
20+
21+
class _CapturedChatLiteLLM:
22+
calls: list[dict[str, Any]] = []
23+
24+
def __init__(self, **kwargs: Any) -> None:
25+
self.kwargs = kwargs
26+
self.__class__.calls.append(kwargs)
27+
28+
29+
@pytest.fixture(autouse=True)
30+
def _patch_common_bundle_dependencies(monkeypatch: pytest.MonkeyPatch):
31+
"""Keep these tests focused on the LLM constructor contract."""
32+
33+
_CapturedChatLiteLLM.calls = []
34+
35+
async def _fake_search_space(_session: Any, _search_space_id: int) -> SimpleNamespace:
36+
return SimpleNamespace(id=42, user_id="user-1")
37+
38+
monkeypatch.setattr(llm_bundle, "_load_search_space", _fake_search_space)
39+
monkeypatch.setattr(llm_bundle, "SanitizedChatLiteLLM", _CapturedChatLiteLLM)
40+
monkeypatch.setattr(llm_bundle, "register_model_usage_metadata", lambda **_kw: None)
41+
monkeypatch.setattr(
42+
llm_bundle,
43+
"has_capability",
44+
lambda _model, capability: capability in {"chat", "vision"},
45+
)
46+
47+
return None
48+
49+
50+
async def test_load_llm_bundle_enables_streaming_for_db_models(
51+
monkeypatch: pytest.MonkeyPatch,
52+
) -> None:
53+
connection = SimpleNamespace(
54+
provider="openai",
55+
api_key="sk-test",
56+
base_url=None,
57+
extra={"litellm_params": {"temperature": 0.1}},
58+
)
59+
model = SimpleNamespace(
60+
id=7,
61+
model_id="gpt-4o-mini",
62+
display_name="GPT 4o Mini",
63+
connection=connection,
64+
)
65+
66+
async def _fake_db_model(_session: Any, *, model_id: int, search_space: Any) -> Any:
67+
assert model_id == 7
68+
assert search_space.id == 42
69+
return model
70+
71+
monkeypatch.setattr(llm_bundle, "_load_db_model", _fake_db_model)
72+
monkeypatch.setattr(
73+
llm_bundle,
74+
"to_litellm",
75+
lambda _conn, _model_id: (
76+
"openai/gpt-4o-mini",
77+
{"api_key": "sk-test", "temperature": 0.1},
78+
),
79+
)
80+
81+
llm, agent_config, error = await llm_bundle.load_llm_bundle(
82+
object(),
83+
config_id=7,
84+
search_space_id=42,
85+
)
86+
87+
assert error is None
88+
assert llm is not None
89+
assert agent_config is not None
90+
assert _CapturedChatLiteLLM.calls == [
91+
{
92+
"model": "openai/gpt-4o-mini",
93+
"api_key": "sk-test",
94+
"temperature": 0.1,
95+
"streaming": True,
96+
}
97+
]
98+
99+
100+
async def test_load_llm_bundle_enables_streaming_for_global_models(
101+
monkeypatch: pytest.MonkeyPatch,
102+
) -> None:
103+
global_model = {
104+
"id": -11,
105+
"connection_id": -101,
106+
"model_id": "claude-sonnet-4-5",
107+
"display_name": "Claude Sonnet",
108+
"billing_tier": "premium",
109+
}
110+
global_connection = {
111+
"id": -101,
112+
"provider": "anthropic",
113+
"api_key": "sk-ant-test",
114+
"base_url": None,
115+
"extra": {"litellm_params": {"temperature": 0.2}},
116+
}
117+
monkeypatch.setattr(
118+
llm_bundle.config,
119+
"GLOBAL_MODELS",
120+
[global_model],
121+
raising=False,
122+
)
123+
monkeypatch.setattr(
124+
llm_bundle.config,
125+
"GLOBAL_CONNECTIONS",
126+
[global_connection],
127+
raising=False,
128+
)
129+
monkeypatch.setattr(
130+
llm_bundle,
131+
"to_litellm",
132+
lambda _conn, _model_id: (
133+
"anthropic/claude-sonnet-4-5",
134+
{"api_key": "sk-ant-test", "temperature": 0.2},
135+
),
136+
)
137+
138+
llm, agent_config, error = await llm_bundle.load_llm_bundle(
139+
object(),
140+
config_id=-11,
141+
search_space_id=42,
142+
)
143+
144+
assert error is None
145+
assert llm is not None
146+
assert agent_config is not None
147+
assert _CapturedChatLiteLLM.calls == [
148+
{
149+
"model": "anthropic/claude-sonnet-4-5",
150+
"api_key": "sk-ant-test",
151+
"temperature": 0.2,
152+
"streaming": True,
153+
}
154+
]

surfsense_web/components/assistant-ui/chat-viewport.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export interface ChatViewportProps {
2727
export const ChatViewport: FC<ChatViewportProps> = ({ children, footer }) => (
2828
<ThreadPrimitive.Viewport
2929
turnAnchor="top"
30-
autoScroll={false}
31-
scrollToBottomOnRunStart={false}
30+
autoScroll
31+
scrollToBottomOnRunStart
3232
scrollToBottomOnInitialize
3333
scrollToBottomOnThreadSwitch
3434
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 scroll-smooth"

surfsense_web/components/assistant-ui/markdown-text.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const MarkdownTextImpl = () => {
110110
return (
111111
<CitationUrlMapContext.Provider value={urlMapRef}>
112112
<MarkdownTextPrimitive
113-
smooth={false}
113+
smooth
114114
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
115115
rehypePlugins={[rehypeKatex]}
116116
className="aui-md"

0 commit comments

Comments
 (0)