mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 16:35:20 +08:00
perf(monitoring): load execution details on demand
- Load overview first and fetch secondary sections when expanded - Add a covering index and select conversation IDs before loading page payloads - Preserve full-detail API compatibility and add pagination and lazy-loading tests
This commit is contained in:
@@ -423,6 +423,99 @@ async def test_execution_detail_for_agent_and_pipeline(engine, service):
|
||||
await service.get_execution_detail(WORKSPACE, 'pipeline', 'missing-message')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detail_overview_does_not_query_collapsed_sections(engine, service, monkeypatch):
|
||||
await _seed(engine)
|
||||
page = service._execution_page
|
||||
converters = []
|
||||
|
||||
async def capture(workspace, statement, convert, offset, limit):
|
||||
converters.append(convert.__name__)
|
||||
return await page(workspace, statement, convert, offset, limit)
|
||||
|
||||
async def reject_history(*args, **kwargs):
|
||||
pytest.fail('Collapsed conversation history must not be loaded')
|
||||
|
||||
monkeypatch.setattr(service, '_execution_page', capture)
|
||||
monkeypatch.setattr(service, '_conversation_message_page', reject_history)
|
||||
detail = await service.get_execution_detail(WORKSPACE, 'pipeline', 'msg-user', section='overview')
|
||||
|
||||
assert set(detail['pages']) == {'inputs', 'outputs', 'deliveries', 'errors'}
|
||||
assert detail['pages']['inputs']['items'][0]['id'] == 'msg-user'
|
||||
assert len(converters) == 4
|
||||
assert 'events' not in detail
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conversation_pages_identities_from_covering_index_before_payloads(engine, service):
|
||||
import sqlalchemy as sa
|
||||
|
||||
await _seed(engine)
|
||||
message = persistence_monitoring.MonitoringMessage
|
||||
payload = 'historical inline media ' * 4096
|
||||
async with engine.begin() as connection:
|
||||
await connection.execute(
|
||||
sa.insert(message),
|
||||
[
|
||||
dict(
|
||||
id=f'history-{index:02d}',
|
||||
workspace_uuid=workspace,
|
||||
bot_id=bot,
|
||||
bot_name='Bot',
|
||||
pipeline_id='pipeline',
|
||||
pipeline_name='Pipeline',
|
||||
session_id='session-1',
|
||||
timestamp=_dt(12),
|
||||
role='user',
|
||||
message_content=payload,
|
||||
variables=payload,
|
||||
status='success',
|
||||
level='info',
|
||||
)
|
||||
for index, (workspace, bot) in enumerate(
|
||||
[(WORKSPACE, 'bot-1')] * 5 + [(WORKSPACE, 'other-bot'), ('other-workspace', 'bot-1')]
|
||||
)
|
||||
],
|
||||
)
|
||||
statements = []
|
||||
|
||||
def capture(connection, cursor, statement, parameters, context, executemany):
|
||||
if statement.startswith('SELECT'):
|
||||
statements.append((statement, parameters))
|
||||
|
||||
sa.event.listen(engine.sync_engine, 'before_cursor_execute', capture)
|
||||
try:
|
||||
first = await service.get_execution_detail(WORKSPACE, 'pipeline', 'msg-user', section='conversation', limit=2)
|
||||
second = await service.get_execution_detail(
|
||||
WORKSPACE, 'pipeline', 'msg-user', section='conversation', limit=2, offset=2
|
||||
)
|
||||
finally:
|
||||
sa.event.remove(engine.sync_engine, 'before_cursor_execute', capture)
|
||||
|
||||
page = first['pages']['conversation']
|
||||
assert [row['id'] for row in page['items']] == ['history-04', 'history-03']
|
||||
assert page['has_more'] and page['next_offset'] == 2
|
||||
assert all(row['content'] == payload for row in page['items'])
|
||||
assert [row['id'] for row in second['pages']['conversation']['items']] == ['history-02', 'history-01']
|
||||
identities = [(sql, params) for sql, params in statements if sql.startswith('SELECT monitoring_messages.id \n')]
|
||||
assert len(identities) == 2
|
||||
async with engine.connect() as connection:
|
||||
for sql, params in identities:
|
||||
assert 'message_content' not in sql and 'variables' not in sql
|
||||
plan = (await connection.exec_driver_sql('EXPLAIN QUERY PLAN ' + sql, params)).all()
|
||||
assert any('COVERING INDEX ix_monitoring_messages_conversation_page' in row[3] for row in plan), plan
|
||||
assert not any('TEMP B-TREE' in row[3] for row in plan), plan
|
||||
payload_queries = [(sql, params) for sql, params in statements if 'JOIN monitoring_messages ON' in sql]
|
||||
assert len(payload_queries) == 2
|
||||
assert {value for value in payload_queries[0][1] if str(value).startswith('history-')} == {
|
||||
'history-04',
|
||||
'history-03',
|
||||
}
|
||||
for sql, params in payload_queries:
|
||||
plan = (await connection.exec_driver_sql('EXPLAIN QUERY PLAN ' + sql, params)).all()
|
||||
assert any('sqlite_autoindex_monitoring_messages_1 (id=?)' in row[3] for row in plan), plan
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_deliveries_context_and_tenant_isolation(engine, service):
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Conversation paging index is safe for existing and fresh databases."""
|
||||
|
||||
import importlib
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
from langbot.pkg.entity.persistence.monitoring import MonitoringMessage
|
||||
|
||||
|
||||
def test_conversation_index_migration_preserves_history_and_matches_metadata():
|
||||
migration = importlib.import_module('langbot.pkg.persistence.alembic.versions.0040_conversation_page_index')
|
||||
engine = sa.create_engine('sqlite:///:memory:')
|
||||
metadata = sa.MetaData()
|
||||
table = sa.Table(
|
||||
'monitoring_messages',
|
||||
metadata,
|
||||
*[
|
||||
sa.Column(name, sa.String, primary_key=name == 'id')
|
||||
for name in ('id', 'workspace_uuid', 'bot_id', 'session_id', 'timestamp', 'message_content')
|
||||
],
|
||||
)
|
||||
sa.Index('existing_index', table.c.workspace_uuid)
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
metadata.create_all(connection)
|
||||
connection.execute(table.insert().values(id='history', message_content='inline media'))
|
||||
with Operations.context(MigrationContext.configure(connection)):
|
||||
migration.upgrade()
|
||||
migration.upgrade()
|
||||
indexes = {
|
||||
index['name']: index['column_names'] for index in sa.inspect(connection).get_indexes(table.name)
|
||||
}
|
||||
expected = next(index for index in MonitoringMessage.__table__.indexes if index.name == migration.INDEX)
|
||||
assert indexes[migration.INDEX] == [column.name for column in expected.columns]
|
||||
migration.downgrade()
|
||||
assert {index['name'] for index in sa.inspect(connection).get_indexes(table.name)} == {'existing_index'}
|
||||
assert connection.scalar(sa.select(table.c.message_content)) == 'inline media'
|
||||
migration.upgrade()
|
||||
finally:
|
||||
engine.dispose()
|
||||
Reference in New Issue
Block a user