mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
add conversation expire config & user query text to dingtalk card (#2147)
* add conversation expire config * add user query text to card * fix(pipeline): move session limit to AI config * test(pipeline): cover AI session limit config * refactor(pipeline): merge session expire-time into AI runner stage Move the session validity duration field out of the standalone session-limit stage into the runner stage so it actually renders in the AI tab (the tab only shows the runner stage and the stage matching the selected runner — any other stage is filtered out). Read path, default config, metadata description, and tests updated accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pipeline): expire conversations from last update time * fix(n8n): sync generated conversation id into payload --------- Co-authored-by: RockChinQ <rockchinq@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
106
tests/unit_tests/pipeline/test_chat_session_limit.py
Normal file
106
tests/unit_tests/pipeline/test_chat_session_limit.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
|
||||
def _preproc_module():
|
||||
# Import pipelinemgr first so pipeline stages are registered without tripping
|
||||
# the stage <-> core.app circular import during isolated test collection.
|
||||
import_module('langbot.pkg.pipeline.pipelinemgr')
|
||||
return import_module('langbot.pkg.pipeline.preproc.preproc')
|
||||
|
||||
|
||||
def _entities_module():
|
||||
return import_module('langbot.pkg.pipeline.entities')
|
||||
|
||||
|
||||
def _conversation(created_at: datetime, updated_at: datetime | None = None):
|
||||
prompt = Mock()
|
||||
prompt.messages = []
|
||||
prompt.copy = Mock(return_value=Mock(messages=[]))
|
||||
|
||||
return SimpleNamespace(
|
||||
uuid='existing-conversation-uuid',
|
||||
create_time=created_at,
|
||||
update_time=updated_at,
|
||||
prompt=prompt,
|
||||
messages=[],
|
||||
)
|
||||
|
||||
|
||||
def _prompt_preprocessing_context(default_prompt=None, prompt=None):
|
||||
ctx = Mock()
|
||||
ctx.event.default_prompt = default_prompt or []
|
||||
ctx.event.prompt = prompt or []
|
||||
return ctx
|
||||
|
||||
|
||||
async def _run_preprocessor(mock_app, sample_query, conversation):
|
||||
session = SimpleNamespace(launcher_type=sample_query.launcher_type, launcher_id=sample_query.launcher_id)
|
||||
mock_app.sess_mgr.get_session = AsyncMock(return_value=session)
|
||||
mock_app.sess_mgr.get_conversation = AsyncMock(return_value=conversation)
|
||||
mock_app.plugin_connector.emit_event = AsyncMock(return_value=_prompt_preprocessing_context())
|
||||
|
||||
sample_query.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent', 'expire-time': 60},
|
||||
'local-agent': {'model': {'primary': '', 'fallbacks': []}, 'prompt': []},
|
||||
},
|
||||
'trigger': {'misc': {'combine-quote-message': False}},
|
||||
'output': {'misc': {'exception-handling': 'show-hint'}},
|
||||
}
|
||||
|
||||
return await _preproc_module().PreProcessor(mock_app).process(sample_query, 'PreProcessor')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preprocessor_expires_conversation_from_last_update_time(mock_app, sample_query):
|
||||
conversation = _conversation(
|
||||
created_at=datetime.now() - timedelta(seconds=10),
|
||||
updated_at=datetime.now() - timedelta(seconds=120),
|
||||
)
|
||||
|
||||
result = await _run_preprocessor(mock_app, sample_query, conversation)
|
||||
|
||||
assert result.result_type == _entities_module().ResultType.CONTINUE
|
||||
assert conversation.uuid is None
|
||||
assert conversation.update_time > datetime.now() - timedelta(seconds=5)
|
||||
assert result.new_query.variables['conversation_id'] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preprocessor_keeps_conversation_when_last_update_is_not_expired(mock_app, sample_query):
|
||||
conversation = _conversation(
|
||||
created_at=datetime.now() - timedelta(seconds=120),
|
||||
updated_at=datetime.now() - timedelta(seconds=30),
|
||||
)
|
||||
|
||||
result = await _run_preprocessor(mock_app, sample_query, conversation)
|
||||
|
||||
assert result.result_type == _entities_module().ResultType.CONTINUE
|
||||
assert conversation.uuid == 'existing-conversation-uuid'
|
||||
assert conversation.update_time > datetime.now() - timedelta(seconds=5)
|
||||
assert result.new_query.variables['conversation_id'] == 'existing-conversation-uuid'
|
||||
|
||||
|
||||
def test_expire_time_metadata_lives_under_ai_runner_not_safety():
|
||||
metadata_dir = Path('src/langbot/templates/metadata/pipeline')
|
||||
|
||||
ai_meta = yaml.safe_load((metadata_dir / 'ai.yaml').read_text())
|
||||
safety_meta = yaml.safe_load((metadata_dir / 'safety.yaml').read_text())
|
||||
|
||||
ai_stage_names = [stage['name'] for stage in ai_meta['stages']]
|
||||
assert 'session-limit' not in ai_stage_names
|
||||
assert 'session-limit' not in [stage['name'] for stage in safety_meta['stages']]
|
||||
|
||||
runner_stage = next(stage for stage in ai_meta['stages'] if stage['name'] == 'runner')
|
||||
expire_time = next(item for item in runner_stage['config'] if item['name'] == 'expire-time')
|
||||
assert 'Conversation expire time' in expire_time['label']['en_US']
|
||||
assert 'Session validity' not in expire_time['label']['en_US']
|
||||
Reference in New Issue
Block a user