Skip to content

Commit f97c21f

Browse files
authored
chore(AI Assistant): improve context awereness (baserow#4261)
1 parent 0a5a356 commit f97c21f

File tree

6 files changed

+74
-43
lines changed

6 files changed

+74
-43
lines changed

enterprise/backend/src/baserow_enterprise/assistant/assistant.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,12 @@ class ChatSignature(udspy.Signature):
122122
__doc__ = f"{ASSISTANT_SYSTEM_PROMPT}\n TASK INSTRUCTIONS: \n"
123123

124124
question: str = udspy.InputField()
125+
context: str = udspy.InputField(
126+
description="Context and facts extracted from the history to help answer the question."
127+
)
125128
ui_context: dict[str, Any] | None = udspy.InputField(
126129
default=None,
127-
desc=(
130+
description=(
128131
"The context the user is currently in. "
129132
"It contains information about the user, the workspace, open table, view, etc."
130133
"Whenever make sense, use it to ground your answer."
@@ -133,6 +136,29 @@ class ChatSignature(udspy.Signature):
133136
answer: str = udspy.OutputField()
134137

135138

139+
class QuestionContextSummarizationSignature(udspy.Signature):
140+
"""
141+
Extract relevant facts from conversation history that provide context for answering
142+
the current question. Do not answer the question or modify it - only extract and
143+
summarize the relevant historical facts that will help in decision-making.
144+
"""
145+
146+
question: str = udspy.InputField(
147+
description="The current user question that needs context from history."
148+
)
149+
previous_messages: list[str] = udspy.InputField(
150+
description="Conversation history as alternating user/assistant messages."
151+
)
152+
facts: str = udspy.OutputField(
153+
description=(
154+
"Relevant facts extracted from the conversation history as a concise "
155+
"paragraph. Include only information that provides necessary context for "
156+
"answering the question. Do not answer the question itself, do not modify "
157+
"the question, and do not include irrelevant details."
158+
)
159+
)
160+
161+
136162
def get_assistant_cancellation_key(chat_uuid: str) -> str:
137163
"""
138164
Get the Redis cache key for cancellation tracking.
@@ -176,7 +202,7 @@ def _init_assistant(self):
176202
)
177203
self.callbacks = AssistantCallbacks(self.tool_helpers)
178204
self._assistant = udspy.ReAct(ChatSignature, tools=tools, max_iters=20)
179-
self.history = None
205+
self.history: list[str] = []
180206

181207
async def acreate_chat_message(
182208
self,
@@ -265,7 +291,6 @@ async def aload_chat_history(self, limit=30):
265291
msg async for msg in self._chat.messages.order_by("-created_on")[:limit]
266292
]
267293

268-
self.history = udspy.History()
269294
while len(last_saved_messages) >= 2:
270295
# Pop the oldest message pair to respect chronological order.
271296
first_message = last_saved_messages.pop()
@@ -276,9 +301,9 @@ async def aload_chat_history(self, limit=30):
276301
):
277302
continue
278303

279-
self.history.add_user_message(first_message.content)
304+
self.history.append(f"Human: {first_message.content}")
280305
ai_answer = last_saved_messages.pop()
281-
self.history.add_assistant_message(ai_answer.content)
306+
self.history.append(f"AI: {ai_answer.content}")
282307

283308
@lru_cache(maxsize=1)
284309
def check_llm_ready_or_raise(self):
@@ -376,17 +401,24 @@ def _check_cancellation(self, cache_key: str, message_id: str) -> None:
376401
cache.delete(cache_key)
377402
raise AssistantMessageCancelled(message_id=message_id)
378403

379-
async def _enhance_question_with_history(self, question: str) -> str:
380-
"""Enhance the user question with chat history context if available."""
404+
async def _summarize_context_from_history(self, question: str) -> str:
405+
"""
406+
Extract relevant facts from chat history to provide context for the question or
407+
return an empty string if there is no history.
408+
409+
:param question: The current user question that needs context from history.
410+
:return: A string containing relevant facts from the conversation history.
411+
"""
381412

382-
if not self.history.messages:
383-
return question
413+
if not self.history:
414+
return ""
384415

385-
predictor = udspy.Predict("question, context -> enhanced_question")
416+
predictor = udspy.Predict(QuestionContextSummarizationSignature)
386417
result = await predictor.aforward(
387-
question=question, context=self.history.messages
418+
question=question,
419+
previous_messages=self.history,
388420
)
389-
return result.enhanced_question
421+
return result.facts
390422

391423
async def _process_stream_event(
392424
self,
@@ -458,12 +490,13 @@ async def astream_messages(
458490
if self.history is None:
459491
await self.aload_chat_history()
460492

461-
user_question = await self._enhance_question_with_history(
493+
context_from_history = await self._summarize_context_from_history(
462494
human_message.content
463495
)
464496

465497
output_stream = self._assistant.astream(
466-
question=user_question,
498+
question=human_message.content,
499+
context=context_from_history,
467500
ui_context=human_message.ui_context.model_dump_json(exclude_none=True),
468501
)
469502

enterprise/backend/src/baserow_enterprise/assistant/tools/search_docs/tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(self):
4343
self.rag = udspy.ChainOfThought(SearchDocsSignature)
4444

4545
def forward(self, question: str, *args, **kwargs):
46-
context = KnowledgeBaseHandler().search(question, num_results=10)
46+
context = KnowledgeBaseHandler().search(question, num_results=7)
4747
return self.rag(context=context, question=question)
4848

4949

enterprise/backend/tests/baserow_enterprise_tests/assistant/test_assistant.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -207,25 +207,18 @@ def test_aload_chat_history_formats_as_question_answer_pairs(
207207

208208
# History should contain user/assistant message pairs
209209
assert assistant.history is not None
210-
assert len(assistant.history.messages) == 4
210+
assert len(assistant.history) == 4
211211

212212
# First pair
213-
assert assistant.history.messages[0]["content"] == "What is Baserow?"
214-
assert assistant.history.messages[0]["role"] == "user"
215-
assert (
216-
assistant.history.messages[1]["content"]
217-
== "Baserow is a no-code database platform."
218-
)
219-
assert assistant.history.messages[1]["role"] == "assistant"
213+
assert assistant.history[0] == "Human: What is Baserow?"
214+
assert assistant.history[1] == "AI: Baserow is a no-code database platform."
220215

221216
# Second pair
222-
assert assistant.history.messages[2]["content"] == "How do I create a table?"
223-
assert assistant.history.messages[2]["role"] == "user"
217+
assert assistant.history[2] == "Human: How do I create a table?"
224218
assert (
225-
assistant.history.messages[3]["content"]
226-
== "You can create a table by clicking the + button."
219+
assistant.history[3]
220+
== "AI: You can create a table by clicking the + button."
227221
)
228-
assert assistant.history.messages[3]["role"] == "assistant"
229222

230223
def test_aload_chat_history_respects_limit(self, enterprise_data_fixture):
231224
"""Test that history loading respects the limit parameter"""
@@ -251,7 +244,7 @@ def test_aload_chat_history_respects_limit(self, enterprise_data_fixture):
251244
async_to_sync(assistant.aload_chat_history)(limit=6) # Last 6 messages
252245

253246
# Should only load the most recent 6 messages (3 pairs)
254-
assert len(assistant.history.messages) == 6
247+
assert len(assistant.history) == 6
255248

256249
def test_aload_chat_history_handles_incomplete_pairs(self, enterprise_data_fixture):
257250
"""
@@ -281,11 +274,9 @@ def test_aload_chat_history_handles_incomplete_pairs(self, enterprise_data_fixtu
281274
async_to_sync(assistant.aload_chat_history)()
282275

283276
# Should only include the complete pair (2 messages: user + assistant)
284-
assert len(assistant.history.messages) == 2
285-
assert assistant.history.messages[0]["content"] == "Question 1"
286-
assert assistant.history.messages[0]["role"] == "user"
287-
assert assistant.history.messages[1]["content"] == "Answer 1"
288-
assert assistant.history.messages[1]["role"] == "assistant"
277+
assert len(assistant.history) == 2
278+
assert assistant.history[0] == "Human: Question 1"
279+
assert assistant.history[1] == "AI: Answer 1"
289280

290281

291282
@pytest.mark.django_db

enterprise/web-frontend/modules/baserow_enterprise/components/assistant/AssistantMessageActions.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@
3939
</template>
4040
</div>
4141

42-
<div v-if="message.can_submit_feedback" class="assistant__disclaimer">
43-
{{ $t('assistantMessageActions.disclaimer') }}
44-
</div>
45-
4642
<!-- Additional user feedback context for the thumb down button -->
4743
<Context
4844
ref="feedbackContext"

enterprise/web-frontend/modules/baserow_enterprise/components/assistant/AssistantMessageList.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="assistant__messages-list">
33
<div
4-
v-for="message in messages"
4+
v-for="(message, index) in messages"
55
:key="message.id"
66
class="assistant__message"
77
:class="{
@@ -33,7 +33,6 @@
3333
></div>
3434
</div>
3535

36-
<!-- Sources section - only show for AI messages with sources -->
3736
<AssistantMessageSources
3837
v-if="message.role === 'ai'"
3938
:sources="message.sources"
@@ -44,6 +43,12 @@
4443
</div>
4544

4645
<AssistantMessageActions :message="message" />
46+
<div
47+
v-if="message.can_submit_feedback && isLastMessage(index)"
48+
class="assistant__disclaimer"
49+
>
50+
{{ $t('assistantMessageList.disclaimer') }}
51+
</div>
4752
</div>
4853
</div>
4954
</div>
@@ -129,6 +134,10 @@ export default {
129134
!this.expandedSources[messageId]
130135
)
131136
},
137+
138+
isLastMessage(index) {
139+
return index === this.messages.length - 1
140+
},
132141
},
133142
}
134143
</script>

enterprise/web-frontend/modules/baserow_enterprise/locales/en.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,13 +368,15 @@
368368
"assistantMessageSources": {
369369
"sources": "{count} source | {count} sources"
370370
},
371+
"assistantMessageList": {
372+
"disclaimer": "Kuma can make mistakes, please double-check responses"
373+
},
371374
"assistantMessageActions": {
372375
"feedbackContextTitle": "Help us improve",
373376
"feedbackContextPlaceholder": "What could we improve? (optional)",
374377
"copiedToClipboard": "Copied to clipboard",
375378
"copiedContentToast": "The Assistant's response content has been copied to your clipboard",
376-
"copyFailed": "Failed to copy to clipboard",
377-
"disclaimer": "Kuma can make mistakes, please double-check responses"
379+
"copyFailed": "Failed to copy to clipboard"
378380
},
379381
"chatwootSupportSidebarWorkspace": {
380382
"directSupport": "Direct support"
@@ -683,7 +685,7 @@
683685
"fieldInvalidTitle": "Date dependency field error"
684686
},
685687
"dateDependency": {
686-
"invalidChildRow": "Successor row is invalid",
688+
"invalidChildRow": "Successor row is invalid",
687689
"invalidParentRow": "Predecessor row is invalid",
688690
"invalidParentEndDateAfterChildStartDate": "Predecessor row end date is after successor start date",
689691
"invalidStartDateEmpty": "Start date is empty",

0 commit comments

Comments
 (0)