Skip to content

Commit d8760cc

Browse files
committed
Merge branch 'ai-assistant-ui-context' into 'develop'
AI Assistant: update UI context See merge request baserow/baserow!3739
2 parents 19fa3ef + c1f33c4 commit d8760cc

File tree

13 files changed

+900
-63
lines changed

13 files changed

+900
-63
lines changed

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,76 @@ class AssistantChatsRequestSerializer(serializers.Serializer):
1717
limit = serializers.IntegerField(default=100, max_value=100, min_value=1)
1818

1919

20+
class UIContextApplicationSerializer(serializers.Serializer):
21+
id = serializers.CharField(help_text="The unique ID of the application.")
22+
name = serializers.CharField(help_text="The name of the application.")
23+
24+
25+
class UIContextTableSerializer(serializers.Serializer):
26+
id = serializers.IntegerField(help_text="The ID of the table.")
27+
name = serializers.CharField(help_text="The name of the table.")
28+
29+
30+
class UIContextViewSerializer(serializers.Serializer):
31+
id = serializers.IntegerField(help_text="The ID of the view.")
32+
name = serializers.CharField(help_text="The name of the view.")
33+
type = serializers.CharField(help_text="The type of the view.")
34+
35+
2036
class UIContextWorkspaceSerializer(serializers.Serializer):
2137
id = serializers.IntegerField(help_text="The ID of the workspace.")
2238
name = serializers.CharField(help_text="The name of the workspace.")
2339

2440

41+
class UIContextPageSerializer(serializers.Serializer):
42+
id = serializers.CharField(help_text="The unique ID of the page.")
43+
name = serializers.CharField(help_text="The name of the page.")
44+
45+
46+
class UIContextWorkflowSerializer(serializers.Serializer):
47+
id = serializers.CharField(help_text="The unique ID of the workflow.")
48+
name = serializers.CharField(help_text="The name of the workflow.")
49+
50+
2551
class UIContextSerializer(serializers.Serializer):
2652
workspace = UIContextWorkspaceSerializer()
53+
# database builder context
54+
database = UIContextApplicationSerializer(
55+
required=False,
56+
help_text="The application the user is currently in, e.g. 'database'.",
57+
)
58+
table = UIContextTableSerializer(
59+
required=False,
60+
help_text="The table the user is currently viewing, if any.",
61+
)
62+
view = UIContextViewSerializer(
63+
required=False,
64+
help_text="The view the user is currently viewing, if any.",
65+
)
66+
# application builder context
67+
application = UIContextApplicationSerializer(
68+
required=False,
69+
help_text="The application the user is currently in, e.g. 'application'.",
70+
)
71+
page = UIContextPageSerializer(
72+
required=False,
73+
help_text="The page the user is currently viewing, if any.",
74+
)
75+
# automation builder context
76+
automation = UIContextApplicationSerializer(
77+
required=False,
78+
help_text="The application the user is currently in, e.g. 'automation'.",
79+
)
80+
workflow = UIContextWorkflowSerializer(
81+
required=False,
82+
help_text="The workflow the user is currently viewing, if any.",
83+
)
84+
# dashboard builder context
85+
dashboard = UIContextApplicationSerializer(
86+
required=False,
87+
help_text="The application the user is currently in, e.g. 'dashboard'.",
88+
)
89+
# user context
2790
timezone = serializers.CharField(
2891
required=False,
2992
help_text="The timezone of the user, e.g. 'Europe/Amsterdam'.",

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
)
3232
from baserow_enterprise.assistant.handler import AssistantHandler
3333
from baserow_enterprise.assistant.operations import ChatAssistantChatOperationType
34-
from baserow_enterprise.assistant.types import AssistantMessageUnion, UIContext
34+
from baserow_enterprise.assistant.types import (
35+
AssistantMessageUnion,
36+
HumanMessage,
37+
UIContext,
38+
)
3539
from baserow_enterprise.features import ASSISTANT
3640

3741
from .serializers import (
@@ -142,7 +146,7 @@ class AssistantChatView(APIView):
142146
def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse:
143147
feature_flag_is_enabled(FF_ASSISTANT, raise_if_disabled=True)
144148

145-
ui_context = UIContext(**data["ui_context"])
149+
ui_context = UIContext.from_validate_request(request, data["ui_context"])
146150
workspace_id = ui_context.workspace.id
147151
workspace = CoreHandler().get_workspace(workspace_id)
148152
LicenseHandler.raise_if_user_doesnt_have_feature(
@@ -158,12 +162,10 @@ def post(self, request: Request, chat_uuid: str, data) -> StreamingHttpResponse:
158162
handler = AssistantHandler()
159163
chat, _ = handler.get_or_create_chat(request.user, workspace, chat_uuid)
160164
assistant = handler.get_assistant(chat)
161-
human_message = data["content"]
165+
human_message = HumanMessage(content=data["content"], ui_context=ui_context)
162166

163167
async def stream_assistant_messages():
164-
async for msg in assistant.astream_messages(
165-
human_message, ui_context=ui_context
166-
):
168+
async for msg in assistant.astream_messages(human_message):
167169
yield self._stream_assistant_message(msg)
168170

169171
response = StreamingHttpResponse(

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
class ChatSignature(dspy.Signature):
2727
question: str = dspy.InputField()
2828
history: dspy.History = dspy.InputField()
29+
ui_context: UIContext = dspy.InputField(
30+
default=None,
31+
description=(
32+
"The frontend UI content the user is currently in. "
33+
"Whenever make sense, use it to ground your answer."
34+
),
35+
)
2936
answer: str = dspy.OutputField()
3037

3138

@@ -131,12 +138,13 @@ def _get_chat_signature(self) -> dspy.Signature:
131138
history, and answer fields.
132139
"""
133140

141+
chat_signature_instructions = "## INSTRUCTIONS\n\nGiven the fields `question`, `history`, and `ui_context`, produce the fields `answer`"
134142
if self._chat.title: # only inject our base system prompt
135143
return ChatSignature.with_instructions(
136144
"\n".join(
137145
[
138146
ASSISTANT_SYSTEM_PROMPT,
139-
"Given the fields `question`, `history`, produce the fields `answer`.",
147+
f"{chat_signature_instructions}.",
140148
]
141149
)
142150
)
@@ -155,7 +163,7 @@ def _get_chat_signature(self) -> dspy.Signature:
155163
instructions="\n".join(
156164
[
157165
ASSISTANT_SYSTEM_PROMPT,
158-
"Given the fields `question`, `history`, produce the fields `answer`, `chat_title`.",
166+
f"{chat_signature_instructions}, `chat_title`.",
159167
]
160168
),
161169
)
@@ -250,13 +258,12 @@ async def aload_chat_history(self, limit=20):
250258
self.history = dspy.History(messages=messages)
251259

252260
async def astream_messages(
253-
self, user_message: str, ui_context: UIContext
261+
self, human_message: HumanMessage
254262
) -> AsyncGenerator[AssistantMessageUnion, None]:
255263
"""
256264
Streams the response to a user message.
257265
258-
:param user_message: The message from the user.
259-
:param ui_context: The UI context where the message was sent.
266+
:param human_message: The message from the user.
260267
:return: An async generator that yields the response messages.
261268
"""
262269

@@ -281,10 +288,16 @@ async def astream_messages(
281288
self._assistant,
282289
stream_listeners=stream_listeners,
283290
)
284-
output_stream = stream_predict(question=user_message, history=self.history)
291+
output_stream = stream_predict(
292+
history=self.history,
293+
question=human_message.content,
294+
ui_context=human_message.ui_context.model_dump_json(
295+
exclude_none=True, indent=2
296+
),
297+
)
285298

286299
await self.acreate_chat_message(
287-
AssistantChatMessage.Role.HUMAN, user_message
300+
AssistantChatMessage.Role.HUMAN, human_message.content
288301
)
289302

290303
chat_title, answer = "", ""

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
**Key concepts**:
1717
• **Fields**: Define schema (30+ types including link_row for relationships); one primary field per table
1818
• **Views**: Present data with filters/sorts/grouping/colors; can be shared, personal, or public
19-
• **Snapshots**: Database backups; **Data sync**: Table replication; **Webhooks**: Row/field event triggers
19+
• **Snapshots**: Database backups; **Data sync**: Table replication; **Webhooks**: Row/field/view event triggers
2020
• **Permissions**: RBAC at workspace/database/table/field levels; database tokens for API
2121
"""
2222

@@ -40,11 +40,12 @@
4040
4141
You know:
4242
1. **Core concepts** (below) - answer directly
43-
2. **Detailed docs** - use knowledge retrieval tool to search when needed
43+
2. **Detailed docs** - use search_docs tool to search when needed
4444
3. **API specs** - guide users to https://api.baserow.io/api/schema.json
4545
4646
## HOW TO HELP
4747
48+
• Use American English spelling and grammar
4849
• Be clear, concise, and actionable
4950
• For troubleshooting: ask for error messages or describe expected vs actual results
5051
• If uncertain: acknowledge it, then suggest how to find the answer (search docs, check API, etc.)
@@ -63,7 +64,5 @@
6364
+ APPLICATION_BUILDER_CONCEPTS
6465
+ "\n"
6566
"""
66-
67-
## INSTRUCTIONS
6867
"""
6968
)

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

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import datetime, timezone
22
from enum import StrEnum
33
from typing import Literal, Optional
44

@@ -10,12 +10,76 @@ class WorkspaceUIContext(BaseModel):
1010
name: str
1111

1212

13+
class ApplicationUIContext(BaseModel):
14+
id: str
15+
name: str
16+
17+
18+
class TableUIContext(BaseModel):
19+
id: int
20+
name: str
21+
22+
23+
class ViewUIContext(BaseModel):
24+
id: int
25+
name: str
26+
type: str
27+
28+
29+
class UserUIContext(BaseModel):
30+
id: int
31+
name: str
32+
email: str
33+
34+
@classmethod
35+
def from_user(cls, user) -> "UserUIContext":
36+
return cls(id=user.id, name=user.first_name, email=user.email)
37+
38+
39+
class PageUIContext(BaseModel):
40+
id: str
41+
name: str
42+
43+
44+
class WorkflowUIContext(BaseModel):
45+
id: str
46+
name: str
47+
48+
49+
class DashboardUIContext(BaseModel):
50+
id: str
51+
name: str
52+
53+
1354
class UIContext(BaseModel):
1455
workspace: WorkspaceUIContext
15-
timezone: Optional[str] = Field(
56+
# database builder context
57+
database: Optional[ApplicationUIContext] = None
58+
table: Optional[TableUIContext] = None
59+
view: Optional[ViewUIContext] = None
60+
# application builder context
61+
application: Optional[ApplicationUIContext] = None
62+
page: Optional[PageUIContext] = None
63+
# automation context
64+
automation: Optional[ApplicationUIContext] = None
65+
workflow: Optional[WorkflowUIContext] = None
66+
# dashboard context
67+
dashboard: Optional[DashboardUIContext] = None
68+
# user and time context
69+
user: UserUIContext
70+
timestamp: datetime = Field(
71+
default_factory=lambda: datetime.now(tz=timezone.utc),
72+
description="The UTC timestamp when the message was sent",
73+
)
74+
timezone: str = Field(
1675
default="UTC", description="The timezone of the user, e.g. 'Europe/Amsterdam'"
1776
)
1877

78+
@classmethod
79+
def from_validate_request(cls, request, ui_context_data) -> "UIContext":
80+
user_context = UserUIContext.from_user(request.user)
81+
return cls(user=user_context, **ui_context_data)
82+
1983

2084
class AssistantMessageType(StrEnum):
2185
HUMAN = "human"

0 commit comments

Comments
 (0)