mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-20 23:45:22 +08:00
feat: reset bot session context while retaining monitoring history
This commit is contained in:
@@ -209,3 +209,7 @@ recommendation only when LangBot Models is enabled and the Workspace owner is
|
||||
bound to a LangBot Account. It returns a nullable `uuid`.
|
||||
New processor LLM selector defaults use this preference; existing configurations
|
||||
are not rewritten. Embedding and rerank models are not eligible.
|
||||
|
||||
### Reset a bot session context
|
||||
|
||||
Use `reset_session_context(bot_id, session_id)` only when a user asks to start a session afresh. It requires `resource.manage`, preserves monitoring records, and refuses sessions with active tasks. It clears conversation runner state and excludes earlier transcript entries from subsequent model context. Files and long-term memory are not removed.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Scoped context reset markers, independent of retained monitoring history."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from ...entity.persistence.runner_state import RunnerState
|
||||
|
||||
HOST_RESET_RUNNER = '__langbot_context_reset__'
|
||||
|
||||
|
||||
def reset_scope(workspace, bot, conversation):
|
||||
return 'host-reset:' + hashlib.sha256(json.dumps([workspace, bot, conversation]).encode()).hexdigest()
|
||||
|
||||
|
||||
async def get_reset_generation(engine, workspace, bot, conversation):
|
||||
if not all((workspace, bot, conversation)):
|
||||
return None
|
||||
async with engine.connect() as connection:
|
||||
value = (
|
||||
await connection.execute(
|
||||
sa.select(RunnerState.value_json).where(
|
||||
RunnerState.scope_key == reset_scope(workspace, bot, conversation),
|
||||
RunnerState.state_key == 'generation',
|
||||
RunnerState.runner_id == HOST_RESET_RUNNER,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
return json.loads(value) if value else None
|
||||
|
||||
|
||||
def active_history_condition(transcript):
|
||||
cutoff = (
|
||||
sa.select(sa.func.max(RunnerState.created_at))
|
||||
.where(
|
||||
RunnerState.runner_id == HOST_RESET_RUNNER,
|
||||
RunnerState.state_key == 'generation',
|
||||
RunnerState.workspace_id == transcript.workspace_id,
|
||||
RunnerState.bot_id == transcript.bot_id,
|
||||
RunnerState.conversation_id == transcript.conversation_id,
|
||||
)
|
||||
.correlate(transcript)
|
||||
.scalar_subquery()
|
||||
)
|
||||
return sa.or_(cutoff.is_(None), transcript.created_at > cutoff)
|
||||
@@ -191,7 +191,9 @@ class PersistentStateStore:
|
||||
# Seed external.conversation_id from event.conversation_id if not set
|
||||
if self._check_scope_enabled('conversation', binding) and event.conversation_id:
|
||||
if 'external.conversation_id' not in snapshot['conversation']:
|
||||
snapshot['conversation']['external.conversation_id'] = event.conversation_id
|
||||
from .context_reset import get_reset_generation
|
||||
generation = await get_reset_generation(self._db_engine, event.workspace_id, event.bot_id, event.conversation_id)
|
||||
snapshot['conversation']['external.conversation_id'] = generation or event.conversation_id
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
@@ -335,6 +335,8 @@ class TranscriptStore:
|
||||
thread_id: str | None,
|
||||
strict_thread: bool,
|
||||
) -> typing.Any:
|
||||
from .context_reset import active_history_condition
|
||||
query = query.where(active_history_condition(Transcript))
|
||||
if bot_id is not None:
|
||||
query = query.where(Transcript.bot_id == bot_id)
|
||||
if workspace_id is not None:
|
||||
|
||||
@@ -545,6 +545,24 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/sessions/reset-context', methods=['POST'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, permission=Permission.RESOURCE_MANAGE)
|
||||
async def reset_session_context(request_context: RequestContext):
|
||||
body = await quart.request.get_json()
|
||||
if not isinstance(body, dict):
|
||||
return self.http_status(400, 'invalid_request', 'Expected a JSON object')
|
||||
try:
|
||||
result = await self.ap.monitoring_service.reset_session_context(
|
||||
request_context, body.get('bot_id'), body.get('session_id'),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, 'invalid_request', str(exc))
|
||||
except LookupError as exc:
|
||||
return self.http_status(404, 'resource_not_found', str(exc))
|
||||
except RuntimeError as exc:
|
||||
return self.http_status(409, 'session_busy', str(exc))
|
||||
return self.success(data=result)
|
||||
|
||||
@self.route(
|
||||
'/sessions/<session_id>/analysis',
|
||||
methods=['GET'],
|
||||
|
||||
@@ -2193,6 +2193,60 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
total,
|
||||
)
|
||||
|
||||
@_workspace_transaction
|
||||
async def reset_session_context(self, context: TenantContext, bot_id: str, session_id: str) -> dict:
|
||||
"""Reset model context without deleting monitoring history or other sessions."""
|
||||
from ....agent.runner.context_reset import HOST_RESET_RUNNER, reset_scope
|
||||
from ....entity.persistence.runner_state import RunnerState
|
||||
|
||||
if not isinstance(bot_id, str) or not bot_id or not isinstance(session_id, str) or not session_id:
|
||||
raise ValueError('bot_id and session_id are required')
|
||||
workspace = require_workspace_uuid(context)
|
||||
execute = self.ap.persistence_mgr.execute_async
|
||||
source = self._session_message_source(workspace, [bot_id])
|
||||
exists = (await execute(sqlalchemy.select(source.c.session_id).where(
|
||||
source.c.session_id == session_id,
|
||||
).limit(1))).first()
|
||||
if exists is None:
|
||||
session = persistence_monitoring.MonitoringSession
|
||||
exists = (await execute(sqlalchemy.select(session.session_id).where(
|
||||
session.workspace_uuid == workspace, session.bot_id == bot_id, session.session_id == session_id,
|
||||
).limit(1))).first()
|
||||
if exists is None:
|
||||
raise LookupError('Session not found')
|
||||
# Legacy sessions use uppercase PERSON/GROUP; envelope IDs use lowercase.
|
||||
conversation = re.sub(r'^(PERSON|GROUP)_', lambda match: match[1].lower() + '_', session_id)
|
||||
run = persistence_agent_run.AgentRun
|
||||
busy = (await execute(sqlalchemy.select(run.run_id).where(
|
||||
run.workspace_id == workspace, run.bot_id == bot_id, run.conversation_id == conversation,
|
||||
run.status.in_(['created', 'queued', 'claimed', 'running']),
|
||||
).limit(1))).first()
|
||||
message = persistence_monitoring.MonitoringMessage
|
||||
pending = (await execute(sqlalchemy.select(message.id).where(
|
||||
message.workspace_uuid == workspace, message.bot_id == bot_id,
|
||||
message.session_id.in_([session_id, conversation]), message.status == 'pending',
|
||||
).limit(1))).first()
|
||||
interaction = persistence_agent_interaction.AgentInteraction
|
||||
waiting = (await execute(sqlalchemy.select(interaction.id).where(
|
||||
interaction.workspace_id == workspace, interaction.bot_id == bot_id,
|
||||
interaction.conversation_id == conversation, interaction.status.in_(['pending', 'submitted']),
|
||||
sqlalchemy.or_(interaction.expires_at.is_(None), interaction.expires_at > datetime.datetime.utcnow()),
|
||||
).limit(1))).first()
|
||||
if busy or pending or waiting:
|
||||
raise RuntimeError('Session is processing a task; wait until it finishes')
|
||||
await execute(sqlalchemy.delete(RunnerState).where(
|
||||
RunnerState.workspace_id == workspace, RunnerState.bot_id == bot_id,
|
||||
RunnerState.conversation_id == conversation, RunnerState.scope == 'conversation',
|
||||
))
|
||||
generation = str(uuid.uuid4())
|
||||
await execute(sqlalchemy.insert(RunnerState).values(
|
||||
runner_id=HOST_RESET_RUNNER, binding_identity=HOST_RESET_RUNNER, scope='conversation',
|
||||
scope_key=reset_scope(workspace, bot_id, conversation), state_key='generation',
|
||||
value_json=json.dumps(generation), workspace_id=workspace, bot_id=bot_id,
|
||||
conversation_id=conversation, created_at=datetime.datetime.utcnow(),
|
||||
))
|
||||
return {'reset': True, 'session_id': session_id}
|
||||
|
||||
async def get_sessions(
|
||||
self,
|
||||
context: TenantContext,
|
||||
|
||||
@@ -211,6 +211,12 @@ class LangBotMCPServer:
|
||||
async def get_inflight_executions() -> str:
|
||||
return _dump(await ap.monitoring_service.get_inflight_snapshot(_authorized(Permission.RESOURCE_VIEW)))
|
||||
|
||||
@mcp.tool(description='Reset a bot session model context, preserving monitoring history. Refuses busy sessions.')
|
||||
async def reset_session_context(bot_id: str, session_id: str) -> str:
|
||||
return _dump(await ap.monitoring_service.reset_session_context(
|
||||
_authorized(Permission.RESOURCE_MANAGE), bot_id, session_id,
|
||||
))
|
||||
|
||||
@mcp.tool(
|
||||
description='List Workspace executions, including events that did not start a processor. The legacy pipeline_ids filter accepts Agent, Pipeline and event processor IDs. Returns bounded rows and summary metrics.'
|
||||
)
|
||||
|
||||
@@ -271,8 +271,19 @@ class SessionManager:
|
||||
async def get_session(self, query: pipeline_query.Query) -> provider_session.Session:
|
||||
"""获取会话"""
|
||||
session_key, execution_context = _query_session_key(query)
|
||||
from ...agent.runner.context_reset import get_reset_generation
|
||||
generation = None
|
||||
persistence = getattr(self.ap, 'persistence_mgr', None)
|
||||
if persistence is not None:
|
||||
generation = await get_reset_generation(
|
||||
persistence.get_db_engine(), execution_context.workspace_uuid, query.bot_uuid,
|
||||
f'{str(query.launcher_type.value).lower()}_{query.launcher_id}',
|
||||
)
|
||||
now = time.monotonic()
|
||||
session = self._session_index.get(session_key)
|
||||
if session is not None and getattr(session, '_context_generation', None) != generation:
|
||||
self._remove_session(session)
|
||||
session = None
|
||||
if session is not None:
|
||||
self._touch_session(session, session_key, now)
|
||||
return session
|
||||
@@ -319,6 +330,7 @@ class SessionManager:
|
||||
object.__setattr__(session, 'bot_uuid', query.bot_uuid)
|
||||
object.__setattr__(session, '_execution_context', session_context)
|
||||
object.__setattr__(session, '_langbot_session_key', session_key)
|
||||
object.__setattr__(session, '_context_generation', generation)
|
||||
object.__setattr__(session, '_langbot_session_concurrency', session_concurrency)
|
||||
session._semaphore = asyncio.Semaphore(session_concurrency)
|
||||
self._session_index[session_key] = session
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import contextlib
|
||||
import datetime
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot.pkg.api.http.service.monitoring import MonitoringService
|
||||
from langbot.pkg.agent.runner.context_reset import get_reset_generation
|
||||
from langbot.pkg.agent.runner.transcript_store import TranscriptStore
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.entity.persistence.monitoring import MonitoringSession
|
||||
from langbot.pkg.entity.persistence.runner_state import RunnerState
|
||||
from langbot.pkg.entity.persistence.agent_run import AgentRun
|
||||
from langbot.pkg.entity.persistence.transcript import Transcript
|
||||
|
||||
|
||||
class Persistence:
|
||||
def __init__(self, engine):
|
||||
self.engine = engine
|
||||
|
||||
def get_db_engine(self):
|
||||
return self.engine
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def tenant_uow(self, workspace):
|
||||
async with self.engine.begin() as conn:
|
||||
self.conn = conn
|
||||
yield
|
||||
|
||||
async def execute_async(self, statement):
|
||||
return await self.conn.execute(statement)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def context_service(tmp_path):
|
||||
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "reset.db"}')
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await conn.execute(
|
||||
sa.insert(MonitoringSession).values(
|
||||
workspace_uuid='w',
|
||||
bot_id='b',
|
||||
session_id='person_u',
|
||||
bot_name='Bot',
|
||||
pipeline_id='p',
|
||||
pipeline_name='Pipeline',
|
||||
start_time=datetime.datetime.utcnow(),
|
||||
last_activity=datetime.datetime.utcnow(),
|
||||
)
|
||||
)
|
||||
for workspace, bot, conversation, scope in [
|
||||
('w', 'b', 'person_u', 'conversation'),
|
||||
('w', 'b2', 'person_u', 'conversation'),
|
||||
('w2', 'b', 'person_u', 'conversation'),
|
||||
('w', 'b', 'person_other', 'conversation'),
|
||||
('w', 'b', 'person_u', 'actor'),
|
||||
]:
|
||||
await conn.execute(
|
||||
sa.insert(RunnerState).values(
|
||||
workspace_id=workspace,
|
||||
bot_id=bot,
|
||||
conversation_id=conversation,
|
||||
scope=scope,
|
||||
scope_key=f'{workspace}:{bot}:{conversation}:{scope}',
|
||||
state_key='checkpoint',
|
||||
runner_id='runner',
|
||||
binding_identity='binding',
|
||||
value_json=json.dumps('old'),
|
||||
)
|
||||
)
|
||||
yield MonitoringService(SimpleNamespace(persistence_mgr=Persistence(engine))), engine
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_preserves_logs_and_isolates_context(context_service):
|
||||
service, engine = context_service
|
||||
store = TranscriptStore(engine)
|
||||
for bot in ['b', 'b2']:
|
||||
await store.append_transcript(None, 'event', 'person_u', 'user', bot_id=bot, workspace_id='w', content='old')
|
||||
await service.reset_session_context('w', 'b', 'person_u')
|
||||
assert await get_reset_generation(engine, 'w', 'b', 'person_u')
|
||||
async with engine.connect() as conn:
|
||||
assert (await conn.execute(sa.select(sa.func.count()).select_from(Transcript))).scalar() == 2
|
||||
assert (await conn.execute(sa.select(sa.func.count()).select_from(MonitoringSession))).scalar() == 1
|
||||
assert (
|
||||
await conn.execute(
|
||||
sa.select(sa.func.count()).select_from(RunnerState).where(RunnerState.runner_id == 'runner')
|
||||
)
|
||||
).scalar() == 4
|
||||
page = await store.page_transcript('person_u', bot_id='b', workspace_id='w')
|
||||
assert not page[0]
|
||||
assert (await store.page_transcript('person_u', bot_id='b2', workspace_id='w'))[0]
|
||||
await store.append_transcript(None, 'new', 'person_u', 'user', bot_id='b', workspace_id='w', content='fresh')
|
||||
assert len((await store.page_transcript('person_u', bot_id='b', workspace_id='w'))[0]) == 1
|
||||
await service.reset_session_context('w', 'b', 'person_u')
|
||||
assert not (await store.page_transcript('person_u', bot_id='b', workspace_id='w'))[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_refuses_wrong_scope_and_running_tasks(context_service):
|
||||
service, engine = context_service
|
||||
for workspace, bot in [('other', 'b'), ('w', 'other')]:
|
||||
with pytest.raises(LookupError):
|
||||
await service.reset_session_context(workspace, bot, 'person_u')
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(
|
||||
sa.insert(AgentRun).values(
|
||||
run_id='run',
|
||||
event_id='evt',
|
||||
agent_id='agent',
|
||||
binding_id='binding',
|
||||
runner_id='runner',
|
||||
workspace_id='w',
|
||||
bot_id='b',
|
||||
conversation_id='person_u',
|
||||
status='running',
|
||||
)
|
||||
)
|
||||
with pytest.raises(RuntimeError):
|
||||
await service.reset_session_context('w', 'b', 'person_u')
|
||||
assert await get_reset_generation(engine, 'w', 'b', 'person_u') is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_starts_new_external_runner_conversation(context_service):
|
||||
from unit_tests.agent.test_state_store import FakeBinding, FakeEventEnvelope, make_descriptor
|
||||
from langbot.pkg.agent.runner.persistent_state_store import PersistentStateStore
|
||||
|
||||
service, engine = context_service
|
||||
store = PersistentStateStore(engine)
|
||||
event = FakeEventEnvelope(workspace_id='w', bot_id='b', conversation_id='person_u')
|
||||
binding = FakeBinding()
|
||||
descriptor = make_descriptor()
|
||||
await store.apply_update_from_event(
|
||||
event, binding, descriptor, 'conversation', 'external.conversation_id', 'remote-old', None
|
||||
)
|
||||
assert (await store.build_snapshot_from_event(event, binding, descriptor))['conversation'][
|
||||
'external.conversation_id'
|
||||
] == 'remote-old'
|
||||
await service.reset_session_context('w', 'b', 'person_u')
|
||||
snapshot = await store.build_snapshot_from_event(event, binding, descriptor)
|
||||
assert snapshot['conversation']['external.conversation_id'] == await get_reset_generation(
|
||||
engine, 'w', 'b', 'person_u'
|
||||
)
|
||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
from importlib import import_module
|
||||
|
||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||
@@ -26,6 +26,13 @@ from langbot.pkg.pipeline.pool import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_generation(monkeypatch):
|
||||
lookup = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr('langbot.pkg.agent.runner.context_reset.get_reset_generation', lookup)
|
||||
return lookup
|
||||
|
||||
|
||||
TEST_CONTEXT = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid='workspace-test',
|
||||
@@ -447,7 +454,7 @@ class TestSessionManagerWorkspaceIsolation:
|
||||
sessions.append(await manager.get_session(query))
|
||||
|
||||
assert len(manager.session_list) == 2
|
||||
assert sessions[0] not in manager.session_list
|
||||
assert all(session is not sessions[0] for session in manager.session_list)
|
||||
assert sessions[1:] == manager.session_list
|
||||
assert await manager.get_session(queries[-1]) is manager.session_list[-1]
|
||||
|
||||
@@ -556,3 +563,18 @@ class TestSessionManagerWorkspaceIsolation:
|
||||
content = conversation.messages[0].content
|
||||
assert content[1].image_base64 is None
|
||||
assert content[2].file_base64 is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_generation_replaces_cached_session(reset_generation):
|
||||
app = Mock()
|
||||
app.instance_config.data = {'concurrency': {'session': 5}}
|
||||
manager = get_session_module().SessionManager(app)
|
||||
query = scoped_query()
|
||||
previous = await manager.get_session(query)
|
||||
assert await manager.get_session(query) is previous
|
||||
reset_generation.return_value = 'new-context'
|
||||
fresh = await manager.get_session(query)
|
||||
assert fresh is not previous
|
||||
assert not fresh.conversations
|
||||
assert await manager.get_session(query) is fresh
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useCurrentWorkspace } from '@/app/infra/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import LoadErrorState from '@/components/LoadErrorState';
|
||||
import React, {
|
||||
useState,
|
||||
@@ -13,6 +16,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
RotateCcw,
|
||||
Ban,
|
||||
Bot,
|
||||
Copy,
|
||||
@@ -143,6 +147,23 @@ const BotSessionMonitor = forwardRef<
|
||||
BotSessionMonitorProps
|
||||
>(function BotSessionMonitor({ botId }, ref) {
|
||||
const { t } = useTranslation();
|
||||
const workspace = useCurrentWorkspace();
|
||||
const canReset = workspace?.permissions.includes('resource.manage') ?? false;
|
||||
const [resetTarget, setResetTarget] = useState<string | null>(null);
|
||||
const [resetting, setResetting] = useState(false);
|
||||
const resetContext = async () => {
|
||||
if (!resetTarget || resetting) return;
|
||||
setResetting(true);
|
||||
try {
|
||||
await httpClient.resetSessionContext(botId, resetTarget);
|
||||
toast.success(t('bots.sessionMonitor.resetSuccess'));
|
||||
setResetTarget(null);
|
||||
} catch {
|
||||
toast.error(t('bots.sessionMonitor.resetError'));
|
||||
} finally {
|
||||
setResetting(false);
|
||||
}
|
||||
};
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
const [sessionTotal, setSessionTotal] = useState(0);
|
||||
const [sessionPage, setSessionPage] = useState(0);
|
||||
@@ -686,7 +707,7 @@ const BotSessionMonitor = forwardRef<
|
||||
{/* Left Panel: Session List */}
|
||||
<div className="max-h-48 md:max-h-none md:w-60 flex-shrink-0 border-b md:border-b-0 md:border-r flex flex-col min-h-0">
|
||||
{/* Admin header */}
|
||||
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between">
|
||||
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium hover:text-foreground transition-colors"
|
||||
@@ -860,7 +881,7 @@ const BotSessionMonitor = forwardRef<
|
||||
selectedSession?.user_id ||
|
||||
selectedSessionId.slice(0, 20)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
|
||||
{parseSessionType(selectedSessionId) && (
|
||||
<span>{parseSessionType(selectedSessionId)}</span>
|
||||
)}
|
||||
@@ -923,6 +944,13 @@ const BotSessionMonitor = forwardRef<
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{canReset && (
|
||||
<Button variant="outline" size="sm" className="h-7 shrink-0 gap-1.5"
|
||||
disabled={resetting} onClick={() => setResetTarget(selectedSessionId)}>
|
||||
<RotateCcw className="size-3.5" />
|
||||
{t('bots.sessionMonitor.resetContext')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
@@ -1194,6 +1222,21 @@ const BotSessionMonitor = forwardRef<
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={resetTarget !== null} onOpenChange={(open) => { if (!open && !resetting) setResetTarget(null); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('bots.sessionMonitor.resetContext')}</DialogTitle>
|
||||
<DialogDescription>{t('bots.sessionMonitor.resetDescription')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" disabled={resetting} onClick={() => setResetTarget(null)}>{t('common.cancel')}</Button>
|
||||
<Button disabled={resetting} onClick={resetContext}>
|
||||
<RotateCcw className={cn('size-4', resetting && 'animate-spin motion-reduce:animate-none')} />
|
||||
{t('bots.sessionMonitor.resetContext')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<BotAdminsDialog
|
||||
botId={botId}
|
||||
open={adminsDialogOpen}
|
||||
|
||||
@@ -822,6 +822,12 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.get(`/api/v1/monitoring/sessions?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
public resetSessionContext(botId: string, sessionId: string): Promise<{ reset: boolean }> {
|
||||
return this.post('/api/v1/monitoring/sessions/reset-context', {
|
||||
bot_id: botId, session_id: sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
public getSessionAnalysis<T>(
|
||||
sessionId: string,
|
||||
botId: string,
|
||||
|
||||
@@ -1172,6 +1172,11 @@ const enUS = {
|
||||
imagesAttached: 'image(s) attached',
|
||||
noLogs: 'No logs yet',
|
||||
sessionMonitor: {
|
||||
resetContext: "Reset context",
|
||||
resetDescription: "New messages will start with an empty context. Monitoring history is kept; files and long-term memory are unchanged. Wait for active tasks to finish before resetting.",
|
||||
resetSuccess: "Context reset. New messages will start a fresh conversation.",
|
||||
resetError: "Could not reset context. Wait for active tasks to finish and try again.",
|
||||
|
||||
title: 'Sessions',
|
||||
sessions: 'Sessions',
|
||||
noSessions: 'No sessions found',
|
||||
|
||||
@@ -1194,6 +1194,11 @@ const esES = {
|
||||
imagesAttached: 'imagen(es) adjunta(s)',
|
||||
noLogs: 'Aún no hay registros',
|
||||
sessionMonitor: {
|
||||
resetContext: "Restablecer contexto",
|
||||
resetDescription: "Los mensajes nuevos comenzarán sin contexto previo. Se conservan el historial de monitoreo, los archivos y la memoria a largo plazo. Espere a que terminen las tareas activas.",
|
||||
resetSuccess: "Contexto restablecido.",
|
||||
resetError: "No se pudo restablecer. Espere a que terminen las tareas activas y vuelva a intentarlo.",
|
||||
|
||||
title: 'Sesiones',
|
||||
sessions: 'Sesiones',
|
||||
noSessions: 'No se encontraron sesiones',
|
||||
|
||||
@@ -1182,6 +1182,11 @@ const jaJP = {
|
||||
imagesAttached: '枚の画像が添付されています',
|
||||
noLogs: 'ログはまだありません',
|
||||
sessionMonitor: {
|
||||
resetContext: "コンテキストをリセット",
|
||||
resetDescription: "新しいメッセージは空のコンテキストから開始します。監視履歴、ファイル、長期記憶は保持されます。実行中のタスクが完了してからリセットしてください。",
|
||||
resetSuccess: "コンテキストをリセットしました。",
|
||||
resetError: "リセットできませんでした。実行中のタスクが完了してから再試行してください。",
|
||||
|
||||
title: 'セッション監視',
|
||||
sessions: 'セッション一覧',
|
||||
noSessions: 'セッションが見つかりません',
|
||||
|
||||
@@ -1192,6 +1192,11 @@ const ruRU = {
|
||||
imagesAttached: 'изображений прикреплено',
|
||||
noLogs: 'Журналов пока нет',
|
||||
sessionMonitor: {
|
||||
resetContext: "Сбросить контекст",
|
||||
resetDescription: "Новые сообщения начнутся с пустого контекста. История мониторинга, файлы и долговременная память сохранятся. Дождитесь завершения активных задач.",
|
||||
resetSuccess: "Контекст сброшен.",
|
||||
resetError: "Не удалось сбросить контекст. Дождитесь завершения активных задач и повторите попытку.",
|
||||
|
||||
title: 'Сессии',
|
||||
sessions: 'Сессии',
|
||||
noSessions: 'Сессии не найдены',
|
||||
|
||||
@@ -1152,6 +1152,11 @@ const thTH = {
|
||||
imagesAttached: 'รูปภาพที่แนบ',
|
||||
noLogs: 'ยังไม่มีบันทึก',
|
||||
sessionMonitor: {
|
||||
resetContext: "รีเซ็ตบริบท",
|
||||
resetDescription: "ข้อความใหม่จะเริ่มด้วยบริบทว่าง ประวัติการตรวจสอบ ไฟล์ และความจำระยะยาวจะยังคงอยู่ โปรดรอให้งานที่กำลังทำอยู่เสร็จก่อน",
|
||||
resetSuccess: "รีเซ็ตบริบทแล้ว",
|
||||
resetError: "รีเซ็ตไม่สำเร็จ โปรดรอให้งานที่กำลังทำอยู่เสร็จแล้วลองอีกครั้ง",
|
||||
|
||||
title: 'เซสชัน',
|
||||
sessions: 'เซสชัน',
|
||||
noSessions: 'ไม่พบเซสชัน',
|
||||
|
||||
@@ -1170,6 +1170,11 @@ const viVN = {
|
||||
imagesAttached: 'hình ảnh đính kèm',
|
||||
noLogs: 'Chưa có nhật ký',
|
||||
sessionMonitor: {
|
||||
resetContext: "Đặt lại ngữ cảnh",
|
||||
resetDescription: "Tin nhắn mới sẽ bắt đầu với ngữ cảnh trống. Lịch sử giám sát, tệp và bộ nhớ dài hạn được giữ lại. Hãy đợi các tác vụ đang chạy hoàn tất.",
|
||||
resetSuccess: "Đã đặt lại ngữ cảnh.",
|
||||
resetError: "Không thể đặt lại. Hãy đợi các tác vụ đang chạy hoàn tất rồi thử lại.",
|
||||
|
||||
title: 'Phiên',
|
||||
sessions: 'Phiên',
|
||||
noSessions: 'Không tìm thấy phiên nào',
|
||||
|
||||
@@ -1108,6 +1108,11 @@ const zhHans = {
|
||||
imagesAttached: '张图片',
|
||||
noLogs: '暂无日志',
|
||||
sessionMonitor: {
|
||||
resetContext: "重置上下文",
|
||||
resetDescription: "重置后,新消息将从空上下文开始。监控历史会保留,文件和长期记忆不受影响。请等待当前任务结束后再重置。",
|
||||
resetSuccess: "上下文已重置,新消息将开启新的对话。",
|
||||
resetError: "重置失败,请等待当前任务结束后重试。",
|
||||
|
||||
title: '会话监控',
|
||||
sessions: '会话列表',
|
||||
noSessions: '暂无会话',
|
||||
|
||||
@@ -1107,6 +1107,11 @@ const zhHant = {
|
||||
imagesAttached: '張圖片已附加',
|
||||
noLogs: '暫無日誌',
|
||||
sessionMonitor: {
|
||||
resetContext: "重設上下文",
|
||||
resetDescription: "重設後,新訊息將從空上下文開始。監控歷史會保留,檔案和長期記憶不受影響。請等待目前任務結束後再重設。",
|
||||
resetSuccess: "上下文已重設,新訊息將開始新的對話。",
|
||||
resetError: "重設失敗,請等待目前任務結束後重試。",
|
||||
|
||||
title: '會話監控',
|
||||
sessions: '會話列表',
|
||||
noSessions: '暫無會話',
|
||||
|
||||
Reference in New Issue
Block a user