Skip to content

Commit 109acd2

Browse files
authored
fix (AI Assistant): improve docs search accuracy (baserow#4538)
1 parent c6a93c3 commit 109acd2

File tree

7 files changed

+209
-344
lines changed

7 files changed

+209
-344
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Improve docs search accuracy for the AI Assistant",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "core",
7+
"bullet_points": [],
8+
"created_at": "2026-01-14"
9+
}

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

Lines changed: 26 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from django.conf import settings
66
from django.core.cache import cache
77
from django.utils import translation
8-
from django.utils.translation import gettext as _
98

109
import udspy
1110
from udspy.callback import BaseCallback
@@ -21,7 +20,7 @@
2120
from baserow_enterprise.assistant.tools.registries import assistant_tool_registry
2221

2322
from .models import AssistantChat, AssistantChatMessage, AssistantChatPrediction
24-
from .signatures import ChatSignature, RequestRouter
23+
from .signatures import ChatSignature
2524
from .types import (
2625
AiMessage,
2726
AiMessageChunk,
@@ -200,33 +199,10 @@ def _init_assistant(self):
200199
"temperature": settings.BASEROW_ENTERPRISE_ASSISTANT_LLM_TEMPERATURE,
201200
"response_format": {"type": "json_object"},
202201
}
203-
self.search_user_docs_tool = self._get_search_user_docs_tool(tools)
204-
self.agent_tools = tools
205-
self._request_router = udspy.ChainOfThought(RequestRouter, **module_kwargs)
206202
self._assistant = udspy.ReAct(
207-
ChatSignature, tools=self.agent_tools, max_iters=20, **module_kwargs
203+
ChatSignature, tools=tools, max_iters=20, **module_kwargs
208204
)
209205

210-
def _get_search_user_docs_tool(
211-
self, tools: list[udspy.Tool | Callable]
212-
) -> udspy.Tool | None:
213-
"""
214-
Retrieves the search_user_docs tool from the list of tools if available.
215-
216-
:param tools: The list of tools to search through.
217-
:return: The search_user_docs as udspy.Tool or None if not found.
218-
"""
219-
220-
search_user_docs_tool = next(
221-
(tool for tool in tools if tool.name == "search_user_docs"), None
222-
)
223-
if search_user_docs_tool is None or isinstance(
224-
search_user_docs_tool, udspy.Tool
225-
):
226-
return search_user_docs_tool
227-
228-
return udspy.Tool(search_user_docs_tool)
229-
230206
async def acreate_chat_message(
231207
self,
232208
role: AssistantChatMessage.Role,
@@ -300,14 +276,14 @@ def list_chat_messages(
300276
)
301277
return list(reversed(messages))
302278

303-
async def afetch_chat_history(self, limit=30):
279+
async def afetch_chat_history(self, limit: int = 50) -> udspy.History:
304280
"""
305281
Loads the chat history into a udspy.History object. It only loads complete
306282
message pairs (human + AI). The history will be in chronological order and must
307283
respect the module signature (question, answer).
308284
309285
:param limit: The maximum number of message pairs to load.
310-
:return: None
286+
:return: A udspy.History instance containing the chat history.
311287
"""
312288

313289
history = udspy.History()
@@ -425,82 +401,6 @@ def _check_cancellation(self, cache_key: str, message_id: str) -> None:
425401
cache.delete(cache_key)
426402
raise AssistantMessageCancelled(message_id=message_id)
427403

428-
async def get_router_stream(
429-
self, message: HumanMessage
430-
) -> AsyncGenerator[Any, None]:
431-
"""
432-
Returns an async generator that streams the router's response to a user
433-
434-
:param message: The current user message that needs context from history.
435-
:return: An async generator that yields stream events.
436-
"""
437-
438-
self.history = await self.afetch_chat_history()
439-
440-
return self._request_router.astream(
441-
question=message.content,
442-
conversation_history=RequestRouter.format_conversation_history(
443-
self.history
444-
),
445-
)
446-
447-
async def _process_router_stream(
448-
self,
449-
event: Any,
450-
human_msg: AssistantChatMessage,
451-
) -> Tuple[list[AssistantMessageUnion], bool, udspy.Prediction | None]:
452-
"""
453-
Process a single event from the smart router output stream.
454-
455-
:param event: The event to process.
456-
:param human_msg: The human message instance.
457-
:return: a tuple of (messages_to_yield, prediction).
458-
"""
459-
460-
messages = []
461-
prediction = None
462-
463-
if isinstance(event, (AiThinkingMessage, AiNavigationMessage)):
464-
messages.append(event)
465-
return messages, prediction
466-
467-
# Stream the final answer
468-
if isinstance(event, udspy.OutputStreamChunk):
469-
if event.field_name == "answer" and event.content.strip():
470-
messages.append(
471-
AiMessageChunk(
472-
content=event.content,
473-
sources=self._assistant_callbacks.sources,
474-
)
475-
)
476-
477-
elif isinstance(event, udspy.Prediction):
478-
if hasattr(event, "routing_decision"):
479-
prediction = event
480-
481-
if getattr(event, "routing_decision", None) == "delegate_to_agent":
482-
messages.append(AiThinkingMessage(content=_("Thinking...")))
483-
elif getattr(event, "routing_decision", None) == "search_user_docs":
484-
if self.search_user_docs_tool is not None:
485-
await self.search_user_docs_tool(question=event.search_query)
486-
else:
487-
messages.append(
488-
AiMessage(
489-
content=_(
490-
"I wanted to search the documentation for you, "
491-
"but the search tool isn't currently available.\n\n"
492-
"To enable documentation search, you'll need to set up "
493-
"the local knowledge base. \n\n"
494-
"You can find setup instructions at: https://baserow.io/user-docs"
495-
),
496-
)
497-
)
498-
elif getattr(event, "answer", None):
499-
ai_msg = await self._acreate_ai_message_response(human_msg, event)
500-
messages.append(ai_msg)
501-
502-
return messages, prediction
503-
504404
async def _process_agent_stream(
505405
self,
506406
event: Any,
@@ -547,7 +447,7 @@ async def _process_agent_stream(
547447
return messages, prediction
548448

549449
def get_agent_stream(
550-
self, message: HumanMessage, extracted_context: str
450+
self, message: HumanMessage, conversation_history: udspy.History | None = None
551451
) -> AsyncGenerator[Any, None]:
552452
"""
553453
Returns an async generator that streams the ReAct agent's response to a user
@@ -557,12 +457,19 @@ def get_agent_stream(
557457
:return: An async generator that yields stream events.
558458
"""
559459

560-
ui_context = message.ui_context.format() if message.ui_context else None
460+
formatted_history = (
461+
ChatSignature.format_conversation_history(conversation_history)
462+
if conversation_history
463+
else []
464+
)
465+
formatted_ui_context = (
466+
message.ui_context.format() if message.ui_context else None
467+
)
561468

562469
return self._assistant.astream(
563470
question=message.content,
564-
context=extracted_context,
565-
ui_context=ui_context,
471+
conversation_history=formatted_history,
472+
ui_context=formatted_ui_context,
566473
)
567474

568475
async def _process_stream(
@@ -618,31 +525,18 @@ async def astream_messages(
618525
message_id = str(human_msg.id)
619526
yield AiStartedMessage(message_id=message_id)
620527

621-
router_stream = await self.get_router_stream(message)
622-
routing_decision, extracted_context = None, ""
528+
history = await self.afetch_chat_history(limit=30)
623529

624-
async for msg, prediction in self._process_stream(
625-
human_msg, router_stream, self._process_router_stream
530+
agent_stream = self.get_agent_stream(message, history)
531+
532+
async for msg, __ in self._process_stream(
533+
human_msg, agent_stream, self._process_agent_stream
626534
):
627-
if prediction is not None:
628-
routing_decision = prediction.routing_decision
629-
extracted_context = prediction.extracted_context
630535
yield msg
631536

632-
if routing_decision == "delegate_to_agent":
633-
agent_stream = self.get_agent_stream(
634-
message,
635-
extracted_context=extracted_context,
636-
)
637-
638-
async for msg, __ in self._process_stream(
639-
human_msg, agent_stream, self._process_agent_stream
640-
):
641-
yield msg
642-
643-
# Generate chat title if needed
644-
if not self._chat.title:
645-
chat_title = await self._generate_chat_title(human_msg.content)
646-
self._chat.title = chat_title
647-
await self._chat.asave(update_fields=["title", "updated_on"])
648-
yield ChatTitleMessage(content=chat_title)
537+
# Generate chat title if needed
538+
if not self._chat.title:
539+
chat_title = await self._generate_chat_title(human_msg.content)
540+
self._chat.title = chat_title
541+
await self._chat.asave(update_fields=["title", "updated_on"])
542+
yield ChatTitleMessage(content=chat_title)

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

Lines changed: 49 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -110,103 +110,58 @@
110110
AGENT_SYSTEM_PROMPT = (
111111
ASSISTANT_SYSTEM_PROMPT_BASE
112112
+ """
113-
**CRITICAL:** You MUST use your action tools to fulfill the request, loading additional tools if needed.
114-
115-
### YOUR TOOLS:
116-
- **Action tools**: Navigate, list databases, tables, fields, views, filters, workflows, rows, etc.
117-
- **Tool loaders**: Load additional specialized tools (e.g., load_rows_tools, load_views_tools). Use them to access capabilities not currently available.
118-
119-
**IMPORTANT - HOW TO UNDERSTAND YOUR TOOLS:**
120-
- Read each tool's NAME, DESCRIPTION, and ARGUMENTS carefully
121-
- Tool names and descriptions tell you what they do (e.g., "list_tables", "create_rows_in_table_X")
122-
- Arguments show what inputs they need
123-
- **NEVER use search_user_docs to learn about tools** - it contains end-user documentation, NOT information about which tools to use or how to call them
124-
- Inspect available tools directly to decide what to use
125-
126-
### HOW TO WORK:
127-
1. **Use action tools** to accomplish the user's goal
128-
2. **If a needed tool isn't available**, call a tool loader to load it (e.g., if you need to create a field but don't have the tool, load field creation tools)
129-
3. **Keep using tools** until the goal is reached or you confirm NO tool can help and NO tool loader can provide the needed tool
130-
131-
### EXAMPLE - CORRECT USE OF TOOL LOADERS:
132-
**User request:** "Change all 'Done' tasks to 'Todo'"
133-
134-
**CORRECT approach:**
135-
✓ Step 1: Identify that Tasks is a table in the open database, and status is the field to update
136-
✓ Step 2: Notice you need to update rows but don't have the tool
137-
✓ Step 3: Call the row tool loader (e.g., `load_rows_tools` for table X, requesting update capabilities)
138-
✓ Step 4: Use the newly loaded `update_rows` tool to update the rows
139-
✓ Step 5: Complete the task
140-
141-
**CRITICAL:** Before giving up, ALWAYS check if a tool loader can provide the necessary tools to complete the task.
142-
143-
### IF YOU CANNOT COMPLETE THE REQUEST:
144-
If you've exhausted all available tools and loaders and cannot complete the task, offer: "I wasn't able to complete this using my available tools. Would you like me to search the documentation for instructions on how to do this manually?"
145-
146-
### YOUR PRIORITY:
147-
1. **First**: Use action tools to complete the request
148-
2. **If tool missing**: Try loading it with a tool loader (scan all available loaders)
149-
3. **If truly unable**: Explain the issue and offer to search documentation (never provide instructions from memory)
150-
151-
The router determined this requires action. You were chosen because the user wants you to DO something, not provide information.
152-
153-
Be aware of your limitations. If users ask for something outside your capabilities, finish immediately, explain what you can and cannot do based on the limitations below, and offer to search the documentation for further help.
113+
## YOUR TOOLS
114+
115+
**CRITICAL - Understanding your tools:**
116+
- Learn what each tool does ONLY from its **name** and **description**
117+
- **NEVER use `search_user_docs` to learn about your tools** - it contains end-user documentation, NOT information about your available tools or how to call them
118+
- `search_user_docs` is ONLY for answering user questions about Baserow features and providing manual instructions
119+
120+
## REQUEST HANDLING
121+
122+
### ACTION REQUESTS - CHECK FIRST
123+
124+
**CRITICAL: Before treating a request as a question, determine if it's an action you can perform.**
125+
126+
Recognize action requests by:
127+
- Imperative verbs: "Show...", "Filter...", "Create...", "Add...", "Delete...", "Update...", "Sort...", "Hide..."
128+
- Desired states: "I want only...", "I need a field that...", "Make it show..."
129+
- Example: "Show only rows where the primary field is empty" → This is an ACTION (create a filter), not a question about filtering
130+
131+
**DO vs EXPLAIN:**
132+
- If you have tools to do it → **DO IT**
133+
- If you lack tools → **THEN explain** how to do it manually
134+
- **NEVER explain how to do something you can do yourself**
135+
136+
**Workflow:**
137+
1. Check your tools - can you fulfill this?
138+
2. **YES**: Execute (ask for clarification only if request is ambiguous)
139+
3. **NO** (see LIMITATIONS): Explain you can't, then provide manual instructions from docs
140+
141+
### QUESTIONS (only after ruling out action requests)
142+
143+
**FACTUAL QUESTIONS** - asking what Baserow IS or HAS:
144+
- Examples: "Does Baserow have X feature?", "How does Y work?", "What options exist for Z?"
145+
- These have objectively correct/incorrect answers that must come from documentation
146+
- **ALWAYS search documentation first** using `search_user_docs`
147+
- Check the `reliability_note` in the response:
148+
- **HIGH CONFIDENCE**: Present the answer confidently with sources
149+
- **PARTIAL MATCH**: Provide the answer but note some details may be incomplete
150+
- **LOW CONFIDENCE / NOTHING FOUND**: Tell the user you couldn't find this in the documentation. **DO NOT guess or assume features exist** - if docs don't mention it (e.g., a "barcode field"), it likely doesn't exist. Suggest checking the community forum or contacting support.
151+
- **NEVER fabricate Baserow features or capabilities**
152+
153+
**ADVISORY QUESTIONS** - asking how to USE or APPLY Baserow:
154+
- Examples: "How should I structure X?", "What's a good approach for Y?", "Help me build Z", "Which field type works best for W?"
155+
- These ask for your expertise in applying Baserow to solve problems - there's no single correct answer
156+
- **Use your knowledge** of Baserow's real capabilities (field types, views, formulas, automations, linking, etc.) to provide helpful recommendations
157+
- You may search docs for reference, but can also directly advise based on your understanding of Baserow
158+
- Focus on practical solutions using actual Baserow functionality
159+
160+
**Key principle**: Never fabricate what Baserow CAN do. Freely advise on HOW to use what Baserow actually offers.
154161
"""
155162
+ AGENT_LIMITATIONS
156163
+ """
157-
### TASK INSTRUCTIONS:
158-
"""
159-
)
160164
161-
162-
REQUEST_ROUTER_PROMPT = (
163-
ASSISTANT_SYSTEM_PROMPT_BASE
164-
+ """
165-
Route based on what the user wants YOU to do:
166-
167-
**delegate_to_agent** (DEFAULT) - User wants YOU to perform an action
168-
- Commands/requests for YOU: "Create...", "Delete...", "Update...", "Add...", "Show me...", "List...", "Find..."
169-
- Vague/unclear requests
170-
- Anything not explicitly asking for instructions
171-
172-
**search_user_docs** - User wants to learn HOW TO do something themselves
173-
- ONLY when explicitly asking for instructions: "How do I...", "How can I...", "What are the steps to..."
174-
- ONLY when asking for explanations: "What is...", "What does... mean", "Explain..."
175-
- NOT for action requests even if phrased as questions
176-
177-
## Critical Rules
178-
- "Create X" → delegate_to_agent (action request for YOU)
179-
- "How do I create X?" → search_user_docs (asking for instructions)
180-
- When uncertain → delegate_to_agent
181-
182-
## Output Requirements
183-
**delegate_to_agent:**
184-
- extracted_context: Comprehensive details from conversation history (IDs, names, actions, specs)
185-
- search_query: empty
186-
187-
**search_user_docs:**
188-
- search_query: Clear question using Baserow terminology and the answer language if not English
189-
- extracted_context: empty
190-
191-
## Examples
192-
193-
**Example 1 - delegate_to_agent (action):**
194-
question: "Create a calendar view"
195-
→ routing_decision: "delegate_to_agent"
196-
→ search_query: ""
197-
→ extracted_context: "User wants to create a calendar view."
198-
199-
**Example 2 - search_user_docs (instructions):**
200-
question: "How do I create a calendar view?"
201-
→ routing_decision: "search_user_docs"
202-
→ search_query: "How to create a calendar view in Baserow"
203-
→ extracted_context: ""
204-
205-
**Example 3 - delegate_to_agent (with history):**
206-
question: "Assign them to Bob"
207-
conversation_history: ["[0] (user): Show urgent tasks", "[1] (assistant): Found 5 tasks in table 'Tasks' (ID: 123)"]
208-
→ routing_decision: "delegate_to_agent"
209-
→ search_query: ""
210-
→ extracted_context: "User wants to assign urgent tasks to Bob. Tasks in table 'Tasks' (ID: 123). Found 5 urgent tasks."
165+
## TASK INSTRUCTIONS:
211166
"""
212167
)

0 commit comments

Comments
 (0)