Skip to content

Commit 4694a09

Browse files
lucaseduoliautofix-ci[bot]coderabbitai[bot]CodeRabbit
authored
fix: improve nudges messages by getting more context from messages (#1617)
* improve nudges fetching * style: ruff format (auto) * style: ruff autofix (auto) * fixed coderabbit suggestions * style: ruff autofix (auto) * fix ruff * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
1 parent efd5f67 commit 4694a09

4 files changed

Lines changed: 280 additions & 17 deletions

File tree

flows/openrag_nudges.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,18 @@
283283
"display_name": "Prompt Template",
284284
"documentation": "https://docs.langflow.org/components-prompts",
285285
"edited": false,
286+
"error": null,
286287
"field_order": [
287288
"template",
288289
"use_double_brackets",
289290
"tool_placeholder"
290291
],
291292
"frozen": false,
293+
"full_path": null,
292294
"icon": "prompts",
295+
"is_composition": null,
296+
"is_input": null,
297+
"is_output": null,
293298
"legacy": false,
294299
"metadata": {
295300
"code_hash": "3d3fd7b8a36f",
@@ -305,6 +310,7 @@
305310
"module": "custom_components.prompt_template"
306311
},
307312
"minimized": false,
313+
"name": "",
308314
"output_types": [],
309315
"outputs": [
310316
{
@@ -327,6 +333,8 @@
327333
}
328334
],
329335
"pinned": false,
336+
"priority": null,
337+
"replacement": null,
330338
"template": {
331339
"_type": "Component",
332340
"code": {
@@ -410,7 +418,7 @@
410418
"trace_as_input": true,
411419
"track_in_telemetry": false,
412420
"type": "prompt",
413-
"value": "You are generating prompt nudges to help a user explore a corpus.\n\nTask:\n1) Skim the documents to infer common themes, entities, or tasks.\n2) Propose exactly three concise, distinct prompt nudges that encourage useful next queries.\n3) If the chat history is provided, use it to generate new questions that the user might have, based on the llm's response to his previous query. DO NOT repeat user questions.\n4) Make the nudges concise, close to 40 characters.\n5) The nudges are questions or commands that the user can make to the chatbot, which will respond looking at the corpus.\n4) Return strings only, separating the nudges by a newline. Don't include quotation marks.\n5) If any error occured, return blank. This will be used in production, so don't ask for more info or confirm a info like you're talking to me still. If, for some reason, you can't provide the nudges, your job failed and you just return blank.\nRules: Be brief. No duplicates. No explanations outside the strings of the nudges. English only.\n\nExamples:\n Show me this quarter's top 10 deals\n Summarize recent client interactions\n Search OpenSearch for mentions of our competitors\n\nChat history:\n{prompt}\n\nDocuments:\n{docs}\n\n"
421+
"value": "You are generating prompt nudges to help a user explore a corpus.\n\nTask:\n1) Skim the documents to infer common themes, entities, or tasks.\n2) Propose exactly three concise, distinct prompt nudges (close to 40 characters each) that encourage useful next queries.\n3) Ensure the nudges are questions or commands that the user can send to the chatbot.\n4) Return strings only, separated by a newline. Do not include quotation marks.\n5) If an error occurs, return a blank string.\n\nRules: Be brief. No duplicates. No explanations outside the strings of the nudges. English only.\n\nIf chat history is provided, follow these rules:\n1) Generate recommendations based solely on the chat history and its chunks, not on the documents.\n2) Generate new questions that the user might have based on the LLM's response to their previous query. DO NOT repeat user questions.\n3) Match the style of the previous questions (e.g., if the user asked a question, generate nudges as questions).\n\nExamples:\n What are this quarter's top 10 deals?\n Summarize recent client interactions\n Search OpenSearch for mentions of our competitors\n\n--------------------------------------------------------\n\nChat history:\n{prompt}\n\n\n--------------------------------------------------------\n\nDocuments (ignore if chat history is not empty):\n{docs}"
414422
},
415423
"tool_placeholder": {
416424
"_input_type": "MessageTextInput",
@@ -759,14 +767,14 @@
759767
},
760768
"tool_mode": false
761769
},
762-
"showNode": false,
770+
"showNode": true,
763771
"type": "ChatInput"
764772
},
765773
"dragging": false,
766774
"id": "ChatInput-7W1BE",
767775
"measured": {
768-
"height": 52,
769-
"width": 192
776+
"height": 207,
777+
"width": 320
770778
},
771779
"position": {
772780
"x": 908.3744348370491,

src/services/chat_service.py

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -320,20 +320,140 @@ async def langflow_nudges_chat(
320320
)
321321
prompt = ""
322322
if previous_response_id:
323-
from agent import get_conversation_thread
324-
325-
conversation_history = get_conversation_thread(
326-
conversation_user_id, previous_response_id
327-
)
328-
if conversation_history:
329-
conversation_history = "\n".join(
330-
[
331-
f"{msg['role']}: {msg['content']}"
332-
for msg in conversation_history["messages"]
333-
if msg["role"] in ["user", "assistant"]
334-
]
323+
messages = []
324+
# Try in-memory active conversation first
325+
from agent import active_conversations
326+
327+
if (
328+
conversation_user_id in active_conversations
329+
and previous_response_id in active_conversations[conversation_user_id]
330+
):
331+
messages = active_conversations[conversation_user_id][previous_response_id].get(
332+
"messages", []
335333
)
336-
prompt = f"{conversation_history}"
334+
335+
# Filter out system messages
336+
user_ast_messages = [m for m in messages if m.get("role") in ["user", "assistant"]]
337+
338+
# If no history in memory, try fetching from Langflow persistent history
339+
if not user_ast_messages:
340+
from services.langflow_history_service import langflow_history_service
341+
342+
try:
343+
lf_messages = await langflow_history_service.get_session_messages(
344+
conversation_user_id, previous_response_id
345+
)
346+
if lf_messages:
347+
messages = lf_messages
348+
user_ast_messages = [
349+
m for m in messages if m.get("role") in ["user", "assistant"]
350+
]
351+
except Exception as e:
352+
logger.warning(f"Failed to fetch session messages for nudges: {e}")
353+
354+
if user_ast_messages:
355+
# Return at max 8 messages (the last ones)
356+
user_ast_messages = user_ast_messages[-8:]
357+
358+
formatted_messages = []
359+
360+
def trim_results(r, depth=0):
361+
MAX_DEPTH = 3
362+
MAX_LIST_LEN = 3
363+
MAX_DICT_KEYS = 5
364+
MAX_STR_LEN = 1000
365+
366+
if isinstance(r, str):
367+
return r[:MAX_STR_LEN] + ("..." if len(r) > MAX_STR_LEN else "")
368+
elif isinstance(r, list):
369+
if depth >= MAX_DEPTH:
370+
return "[Max depth reached]"
371+
return [trim_results(x, depth + 1) for x in r[:MAX_LIST_LEN]]
372+
elif isinstance(r, dict):
373+
if depth >= MAX_DEPTH:
374+
return "[Max depth reached]"
375+
trimmed = {}
376+
for i, (k, v) in enumerate(r.items()):
377+
if i >= MAX_DICT_KEYS:
378+
trimmed["_more_keys"] = f"... ({len(r) - MAX_DICT_KEYS} omitted)"
379+
break
380+
trimmed[k] = trim_results(v, depth + 1)
381+
return trimmed
382+
return r
383+
384+
for msg in user_ast_messages:
385+
role = msg.get("role")
386+
content = msg.get("content", "")
387+
msg_str = f"{role}: {content}"
388+
389+
# Extract tool calls and chunks if this is an assistant message
390+
if role == "assistant":
391+
extracted_chunks = []
392+
393+
# 1. From chunks list
394+
chunks = msg.get("chunks") or []
395+
if isinstance(chunks, list):
396+
last_tc = None
397+
for chunk in chunks:
398+
if isinstance(chunk, dict):
399+
item = chunk.get("item", {})
400+
if isinstance(item, dict) and item.get("type") in [
401+
"tool_call",
402+
"retrieval_call",
403+
]:
404+
t_name = item.get("tool_name") or item.get("name") or "tool"
405+
res = trim_results(item.get("results"))
406+
tc = {"tool_name": t_name, "results": res}
407+
extracted_chunks.append(tc)
408+
last_tc = tc
409+
elif chunk.get("type") in [
410+
"response.tool_call.result",
411+
"tool_call_result",
412+
]:
413+
res = trim_results(chunk.get("result") or chunk)
414+
if last_tc:
415+
last_tc["results"] = res
416+
else:
417+
extracted_chunks.append(
418+
{"tool_name": "tool", "results": res}
419+
)
420+
421+
# 2. From response_data dict/string
422+
resp_data = msg.get("response_data")
423+
if resp_data:
424+
if isinstance(resp_data, str):
425+
try:
426+
resp_data = json.loads(resp_data)
427+
except Exception:
428+
resp_data = {}
429+
if isinstance(resp_data, dict):
430+
t_calls = resp_data.get("tool_calls") or []
431+
if isinstance(t_calls, list):
432+
for tc in t_calls:
433+
if isinstance(tc, dict):
434+
t_name = tc.get("name")
435+
func = tc.get("function")
436+
if isinstance(func, dict) and not t_name:
437+
t_name = func.get("name")
438+
res = trim_results(tc.get("result"))
439+
extracted_chunks.append(
440+
{"tool_name": t_name or "tool", "results": res}
441+
)
442+
443+
if extracted_chunks:
444+
chunks_strs = []
445+
for tc in extracted_chunks:
446+
t_name = tc.get("tool_name", "tool")
447+
res = tc.get("results")
448+
if res is not None:
449+
res_str = json.dumps(res, ensure_ascii=False, default=str)
450+
chunks_strs.append(f"[Tool: {t_name}, Results: {res_str}]")
451+
if chunks_strs:
452+
msg_str += "\nContext Chunks:\n" + "\n".join(chunks_strs)
453+
454+
formatted_messages.append(msg_str)
455+
456+
prompt = "\n\n".join(formatted_messages)
337457

338458
from agent import async_langflow_chat
339459

tests/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import asyncio
22
import os
3+
import sys
34
import tempfile
45
from pathlib import Path
56

67
import pytest
78
import pytest_asyncio
89
from dotenv import load_dotenv
910

11+
# Ensure src is in sys.path
12+
ROOT = Path(__file__).resolve().parents[1]
13+
SRC = ROOT / "src"
14+
if str(SRC) not in sys.path:
15+
sys.path.insert(0, str(SRC))
16+
1017
# Load environment variables
1118
load_dotenv()
1219

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Tests for ChatService langflow_nudges_chat extraction logic."""
2+
3+
from unittest.mock import AsyncMock, MagicMock
4+
5+
import pytest
6+
7+
import agent
8+
from services.chat_service import ChatService
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_langflow_nudges_chat_with_chunks_in_memory(monkeypatch):
13+
"""Ensure in-memory conversation chunks are formatted into the nudges prompt."""
14+
monkeypatch.setattr("services.chat_service.LANGFLOW_URL", "http://localhost:7860")
15+
monkeypatch.setattr("services.chat_service.NUDGES_FLOW_ID", "nudges-flow-id")
16+
17+
# Mock clients
18+
mock_lf_client = MagicMock()
19+
monkeypatch.setattr(
20+
"services.chat_service.clients.ensure_langflow_client",
21+
AsyncMock(return_value=mock_lf_client),
22+
)
23+
24+
# Prepopulate active_conversations with a message containing tool call chunks
25+
test_user_id = "test-user"
26+
test_resp_id = "test-resp"
27+
28+
chunks_data = [
29+
{
30+
"item": {
31+
"type": "tool_call",
32+
"tool_name": "Retrieval",
33+
"results": [{"filename": "doc.md", "text": "Important retrieved content."}],
34+
}
35+
}
36+
]
37+
38+
agent.active_conversations[test_user_id] = {
39+
test_resp_id: {
40+
"messages": [
41+
{"role": "user", "content": "What is OpenRAG?"},
42+
{"role": "assistant", "content": "It is an AI platform.", "chunks": chunks_data},
43+
]
44+
}
45+
}
46+
47+
# Mock async_langflow_chat to capture prompt
48+
captured_prompt = None
49+
50+
async def mock_async_lf_chat(client, flow_id, prompt, user_id, **kwargs):
51+
nonlocal captured_prompt
52+
captured_prompt = prompt
53+
return "Nudge 1\nNudge 2", "nudge-resp", []
54+
55+
monkeypatch.setattr(agent, "async_langflow_chat", mock_async_lf_chat)
56+
57+
svc = ChatService()
58+
res = await svc.langflow_nudges_chat(user_id=test_user_id, previous_response_id=test_resp_id)
59+
60+
assert res["response"] == "Nudge 1\nNudge 2"
61+
assert captured_prompt is not None
62+
assert "user: What is OpenRAG?" in captured_prompt
63+
assert "assistant: It is an AI platform." in captured_prompt
64+
assert "Context Chunks:" in captured_prompt
65+
assert "Important retrieved content." in captured_prompt
66+
67+
# Clean up
68+
agent.active_conversations.pop(test_user_id, None)
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_langflow_nudges_chat_with_chunks_langflow_history(monkeypatch):
73+
"""Ensure persistent Langflow history chunks are formatted into the nudges prompt."""
74+
monkeypatch.setattr("services.chat_service.LANGFLOW_URL", "http://localhost:7860")
75+
monkeypatch.setattr("services.chat_service.NUDGES_FLOW_ID", "nudges-flow-id")
76+
77+
# Mock clients
78+
mock_lf_client = MagicMock()
79+
monkeypatch.setattr(
80+
"services.chat_service.clients.ensure_langflow_client",
81+
AsyncMock(return_value=mock_lf_client),
82+
)
83+
84+
# Ensure active_conversations is empty for this test
85+
test_user_id = "lf-user"
86+
test_resp_id = "lf-resp"
87+
agent.active_conversations.pop(test_user_id, None)
88+
89+
# Mock langflow_history_service.get_session_messages
90+
lf_messages = [
91+
{"role": "user", "content": "Tell me about security."},
92+
{
93+
"role": "assistant",
94+
"content": "Security is robust.",
95+
"chunks": [
96+
{
97+
"item": {
98+
"type": "tool_call",
99+
"tool_name": "OpenSearch",
100+
"results": {"hits": ["Security info chunk."]},
101+
}
102+
}
103+
],
104+
},
105+
]
106+
mock_get_messages = AsyncMock(return_value=lf_messages)
107+
from services.langflow_history_service import langflow_history_service
108+
109+
monkeypatch.setattr(langflow_history_service, "get_session_messages", mock_get_messages)
110+
111+
# Mock async_langflow_chat to capture prompt
112+
captured_prompt = None
113+
114+
async def mock_async_lf_chat(client, flow_id, prompt, user_id, **kwargs):
115+
nonlocal captured_prompt
116+
captured_prompt = prompt
117+
return "Nudge A\nNudge B", "nudge-resp", []
118+
119+
monkeypatch.setattr(agent, "async_langflow_chat", mock_async_lf_chat)
120+
121+
svc = ChatService()
122+
res = await svc.langflow_nudges_chat(user_id=test_user_id, previous_response_id=test_resp_id)
123+
124+
assert res["response"] == "Nudge A\nNudge B"
125+
assert captured_prompt is not None
126+
assert "user: Tell me about security." in captured_prompt
127+
assert "Context Chunks:" in captured_prompt
128+
assert "Security info chunk." in captured_prompt

0 commit comments

Comments
 (0)