Skip to content

Commit a832c8d

Browse files
RockChinQclaudehappy-otter
committed
feat: add session message monitoring tab to bot detail dialog
Add a new "Sessions" tab in the bot detail dialog that displays sent & received messages grouped by sessions. Users can select any session to view its messages in a chat-bubble style layout. Backend changes: - Add sessionId filter to monitoring messages endpoint - Add role column to MonitoringMessage (user/assistant) - Record bot responses in monitoring via record_query_response() - Add DB migration (dbm019) for the new role column Frontend changes: - New BotSessionMonitor component with session list + message viewer - Add Sessions sidebar tab to BotDetailDialog - Add getBotSessions/getSessionMessages API methods to BackendClient - Add i18n translations (en-US, zh-Hans, zh-Hant, ja-JP) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent b8df0db commit a832c8d

File tree

13 files changed

+744
-3
lines changed

13 files changed

+744
-3
lines changed

src/langbot/pkg/api/http/controller/groups/monitoring.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async def get_messages() -> str:
5252
# Parse query parameters
5353
bot_ids = quart.request.args.getlist('botId')
5454
pipeline_ids = quart.request.args.getlist('pipelineId')
55+
session_ids = quart.request.args.getlist('sessionId')
5556
start_time_str = quart.request.args.get('startTime')
5657
end_time_str = quart.request.args.get('endTime')
5758
limit = int(quart.request.args.get('limit', 100))
@@ -64,6 +65,7 @@ async def get_messages() -> str:
6465
messages, total = await self.ap.monitoring_service.get_messages(
6566
bot_ids=bot_ids if bot_ids else None,
6667
pipeline_ids=pipeline_ids if pipeline_ids else None,
68+
session_ids=session_ids if session_ids else None,
6769
start_time=start_time,
6870
end_time=end_time,
6971
limit=limit,

src/langbot/pkg/api/http/service/monitoring.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ async def record_message(
3232
user_id: str | None = None,
3333
runner_name: str | None = None,
3434
variables: str | None = None,
35+
role: str = 'user',
3536
) -> str:
3637
"""Record a message"""
3738
message_id = str(uuid.uuid4())
@@ -50,6 +51,7 @@ async def record_message(
5051
'user_id': user_id,
5152
'runner_name': runner_name,
5253
'variables': variables,
54+
'role': role,
5355
}
5456

5557
await self.ap.persistence_mgr.execute_async(
@@ -355,6 +357,7 @@ async def get_messages(
355357
self,
356358
bot_ids: list[str] | None = None,
357359
pipeline_ids: list[str] | None = None,
360+
session_ids: list[str] | None = None,
358361
start_time: datetime.datetime | None = None,
359362
end_time: datetime.datetime | None = None,
360363
limit: int = 100,
@@ -367,6 +370,8 @@ async def get_messages(
367370
conditions.append(persistence_monitoring.MonitoringMessage.bot_id.in_(bot_ids))
368371
if pipeline_ids:
369372
conditions.append(persistence_monitoring.MonitoringMessage.pipeline_id.in_(pipeline_ids))
373+
if session_ids:
374+
conditions.append(persistence_monitoring.MonitoringMessage.session_id.in_(session_ids))
370375
if start_time:
371376
conditions.append(persistence_monitoring.MonitoringMessage.timestamp >= start_time)
372377
if end_time:

src/langbot/pkg/entity/persistence/monitoring.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class MonitoringMessage(Base):
2222
user_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
2323
runner_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) # Runner name for this query
2424
variables = sqlalchemy.Column(sqlalchemy.Text, nullable=True) # Query variables as JSON string
25+
role = sqlalchemy.Column(sqlalchemy.String(50), nullable=True, default='user') # user, assistant
2526

2627

2728
class MonitoringLLMCall(Base):
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import sqlalchemy
2+
from .. import migration
3+
4+
5+
@migration.migration_class(19)
6+
class DBMigrateMonitoringMessageRole(migration.DBMigration):
7+
"""Add role column to monitoring_messages table"""
8+
9+
async def upgrade(self):
10+
"""Upgrade"""
11+
try:
12+
sql_text = sqlalchemy.text(
13+
"ALTER TABLE monitoring_messages ADD COLUMN role VARCHAR(50) DEFAULT 'user'"
14+
)
15+
await self.ap.persistence_mgr.execute_async(sql_text)
16+
except Exception:
17+
# Column may already exist
18+
pass
19+
20+
async def downgrade(self):
21+
"""Downgrade"""
22+
try:
23+
sql_text = sqlalchemy.text('ALTER TABLE monitoring_messages DROP COLUMN role')
24+
await self.ap.persistence_mgr.execute_async(sql_text)
25+
except Exception:
26+
pass

src/langbot/pkg/pipeline/monitoring_helper.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,60 @@ async def record_query_success(
114114
except Exception as e:
115115
ap.logger.error(f'Failed to record query success: {e}')
116116

117+
@staticmethod
118+
async def record_query_response(
119+
ap: app.Application,
120+
query: pipeline_query.Query,
121+
bot_id: str,
122+
bot_name: str,
123+
pipeline_id: str,
124+
pipeline_name: str,
125+
runner_name: str | None = None,
126+
):
127+
"""Record bot response message to monitoring"""
128+
try:
129+
session_id = f'{query.launcher_type}_{query.launcher_id}'
130+
131+
# Extract response content from resp_message_chain
132+
if hasattr(query, 'resp_message_chain') and query.resp_message_chain:
133+
# Serialize the last response message chain
134+
last_resp = query.resp_message_chain[-1]
135+
if hasattr(last_resp, 'model_dump'):
136+
message_content = json.dumps(last_resp.model_dump(), ensure_ascii=False)
137+
else:
138+
message_content = str(last_resp)
139+
elif hasattr(query, 'resp_messages') and query.resp_messages:
140+
last_resp = query.resp_messages[-1]
141+
if hasattr(last_resp, 'get_content_platform_message_chain'):
142+
chain = last_resp.get_content_platform_message_chain()
143+
if hasattr(chain, 'model_dump'):
144+
message_content = json.dumps(chain.model_dump(), ensure_ascii=False)
145+
else:
146+
message_content = str(chain)
147+
else:
148+
message_content = str(last_resp)
149+
else:
150+
return # No response to record
151+
152+
await ap.monitoring_service.record_message(
153+
bot_id=bot_id,
154+
bot_name=bot_name,
155+
pipeline_id=pipeline_id,
156+
pipeline_name=pipeline_name,
157+
message_content=message_content,
158+
session_id=session_id,
159+
status='success',
160+
level='info',
161+
platform=query.launcher_type.value
162+
if hasattr(query.launcher_type, 'value')
163+
else str(query.launcher_type),
164+
user_id=query.sender_id,
165+
runner_name=runner_name,
166+
role='assistant',
167+
)
168+
except Exception as e:
169+
ap.logger.error(f'Failed to record query response: {e}')
170+
117171
@staticmethod
118172
async def record_query_error(
119173
ap: app.Application,

src/langbot/pkg/pipeline/pipelinemgr.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,20 @@ async def process_query(self, query: pipeline_query.Query):
339339
except Exception as e:
340340
self.ap.logger.error(f'Failed to record query success: {e}')
341341

342+
# Record bot response message
343+
try:
344+
await monitoring_helper.MonitoringHelper.record_query_response(
345+
ap=self.ap,
346+
query=query,
347+
bot_id=query.bot_uuid or 'unknown',
348+
bot_name=bot_name,
349+
pipeline_id=self.pipeline_entity.uuid,
350+
pipeline_name=pipeline_name,
351+
runner_name=runner_name,
352+
)
353+
except Exception as e:
354+
self.ap.logger.error(f'Failed to record query response: {e}')
355+
342356
except Exception as e:
343357
inst_name = query.current_stage_name if query.current_stage_name else 'unknown'
344358
self.ap.logger.error(f'Error processing query {query.query_id} stage={inst_name} : {e}')

web/src/app/home/bots/BotDetailDialog.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { Button } from '@/components/ui/button';
2222
import BotForm from '@/app/home/bots/components/bot-form/BotForm';
2323
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
24+
import BotSessionMonitor from '@/app/home/bots/components/bot-session/BotSessionMonitor';
2425
import { useTranslation } from 'react-i18next';
2526
import { z } from 'zod';
2627
import { httpClient } from '@/app/infra/http/HttpClient';
@@ -82,6 +83,19 @@ export default function BotDetailDialog({
8283
</svg>
8384
),
8485
},
86+
{
87+
key: 'sessions',
88+
label: t('bots.sessionMonitor.title'),
89+
icon: (
90+
<svg
91+
xmlns="http://www.w3.org/2000/svg"
92+
viewBox="0 0 24 24"
93+
fill="currentColor"
94+
>
95+
<path d="M2 22C2 17.5817 5.58172 14 10 14C14.4183 14 18 17.5817 18 22H16C16 18.6863 13.3137 16 10 16C6.68629 16 4 18.6863 4 22H2ZM10 13C6.685 13 4 10.315 4 7C4 3.685 6.685 1 10 1C13.315 1 16 3.685 16 7C16 10.315 13.315 13 10 13ZM10 11C12.21 11 14 9.21 14 7C14 4.79 12.21 3 10 3C7.79 3 6 4.79 6 7C6 9.21 7.79 11 10 11ZM18.2837 14.7028C21.0644 15.9561 23 18.752 23 22H21C21 19.564 19.5483 17.4671 17.4628 16.5271L18.2837 14.7028ZM17.5962 3.41321C19.5944 4.23703 21 6.20361 21 8.5C21 11.3702 18.8042 13.7252 16 13.9776V11.9646C17.6967 11.7222 19 10.264 19 8.5C19 7.11935 18.2016 5.92603 17.041 5.35635L17.5962 3.41321Z"></path>
96+
</svg>
97+
),
98+
},
8599
];
86100

87101
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -155,7 +169,7 @@ export default function BotDetailDialog({
155169
return (
156170
<>
157171
<Dialog open={open} onOpenChange={onOpenChange}>
158-
<DialogContent className="overflow-hidden p-0 !max-w-[50rem] max-h-[75vh] flex">
172+
<DialogContent className="overflow-hidden p-0 !max-w-[70rem] max-h-[75vh] flex">
159173
<SidebarProvider className="items-start w-full flex">
160174
<Sidebar
161175
collapsible="none"
@@ -189,10 +203,12 @@ export default function BotDetailDialog({
189203
<DialogTitle>
190204
{activeMenu === 'config'
191205
? t('bots.editBot')
192-
: t('bots.botLogTitle')}
206+
: activeMenu === 'logs'
207+
? t('bots.botLogTitle')
208+
: t('bots.sessionMonitor.title')}
193209
</DialogTitle>
194210
</DialogHeader>
195-
<div className="flex-1 overflow-y-auto px-6 pb-6">
211+
<div className={activeMenu === 'sessions' ? 'flex-1 min-h-0' : 'flex-1 overflow-y-auto px-6 pb-6'}>
196212
{activeMenu === 'config' && (
197213
<BotForm
198214
initBotId={botId}
@@ -204,6 +220,9 @@ export default function BotDetailDialog({
204220
{activeMenu === 'logs' && botId && (
205221
<BotLogListComponent botId={botId} />
206222
)}
223+
{activeMenu === 'sessions' && botId && (
224+
<BotSessionMonitor botId={botId} />
225+
)}
207226
</div>
208227
{activeMenu === 'config' && (
209228
<DialogFooter className="px-6 py-4 border-t shrink-0">

0 commit comments

Comments
 (0)