mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 01:15:24 +08:00
fix(ci): align SDK dependency and validate acceptance migrations
This commit is contained in:
@@ -192,7 +192,10 @@ class PersistentStateStore:
|
||||
if self._check_scope_enabled('conversation', binding) and event.conversation_id:
|
||||
if 'external.conversation_id' not in snapshot['conversation']:
|
||||
from .context_reset import get_reset_generation
|
||||
generation = await get_reset_generation(self._db_engine, event.workspace_id, event.bot_id, event.conversation_id)
|
||||
|
||||
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
|
||||
|
||||
@@ -742,6 +742,7 @@ async def execute_platform_tool(
|
||||
raise ValueError(f'Platform API {definition.api} is no longer supported by bot {bot_id}')
|
||||
api_func = getattr(bot.adapter, definition.api, None)
|
||||
if reply_via_source:
|
||||
|
||||
async def api_func(target_type, target_id, message):
|
||||
source = await bot.adapter.get_message(
|
||||
chat_type=target_type,
|
||||
@@ -749,14 +750,17 @@ async def execute_platform_tool(
|
||||
message_id=delivery['reply_target']['message_id'],
|
||||
)
|
||||
return await bot.adapter.reply_message(source, message)
|
||||
|
||||
# Some platforms require a one-time event credential instead of an
|
||||
# ordinary conversation send. Only pass the Host-frozen event context.
|
||||
event_reply_handler = getattr(bot.adapter, 'reply_to_event', None)
|
||||
if definition.name == 'event_reply' and callable(event_reply_handler):
|
||||
|
||||
async def api_func(target_type, target_id, message):
|
||||
return await event_reply_handler(
|
||||
context=context, target_type=target_type, target_id=target_id, message=message
|
||||
)
|
||||
|
||||
if not callable(api_func):
|
||||
raise ValueError(f'Platform API {definition.api} is declared but not implemented')
|
||||
if definition.api == 'send_message':
|
||||
|
||||
@@ -336,6 +336,7 @@ class TranscriptStore:
|
||||
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)
|
||||
|
||||
@@ -32,6 +32,7 @@ def validate_monitoring_window(start_time: datetime.datetime | None, end_time: d
|
||||
if start_time >= end or end - start_time > datetime.timedelta(days=365):
|
||||
quart.abort(400, description='Monitoring time range must be positive and no longer than 365 days')
|
||||
|
||||
|
||||
@group.group_class('monitoring', '/api/v1/monitoring')
|
||||
class MonitoringRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@@ -545,15 +546,21 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/sessions/reset-context', methods=['POST'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, permission=Permission.RESOURCE_MANAGE)
|
||||
@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'),
|
||||
request_context,
|
||||
body.get('bot_id'),
|
||||
body.get('session_id'),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, 'invalid_request', str(exc))
|
||||
|
||||
@@ -162,23 +162,43 @@ class AdaptersRouterGroup(group.RouterGroup):
|
||||
|
||||
slack_setup = get_slack_setup(self.ap)
|
||||
|
||||
@self.route('/slack/setup', methods=['POST'], permission=Permission.RESOURCE_MANAGE,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
@self.route(
|
||||
'/slack/setup',
|
||||
methods=['POST'],
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
)
|
||||
async def slack_start(request_context: RequestContext):
|
||||
data = await quart.request.get_json()
|
||||
if not isinstance(data, dict):
|
||||
return self.http_status(400, -1, 'invalid_input')
|
||||
try:
|
||||
result = await slack_setup.start(request_context, **{
|
||||
key: data[key] for key in ('bot_uuid', 'access_token', 'refresh_token', 'webhook_url',
|
||||
'name', 'socket_mode', 'redirect_url') if key in data
|
||||
})
|
||||
result = await slack_setup.start(
|
||||
request_context,
|
||||
**{
|
||||
key: data[key]
|
||||
for key in (
|
||||
'bot_uuid',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'webhook_url',
|
||||
'name',
|
||||
'socket_mode',
|
||||
'redirect_url',
|
||||
)
|
||||
if key in data
|
||||
},
|
||||
)
|
||||
return self.success(data=result)
|
||||
except (SlackSetupError, TypeError) as exc:
|
||||
return self.http_status(400, -1, str(exc) if isinstance(exc, SlackSetupError) else 'invalid_input')
|
||||
|
||||
@self.route('/slack/setup/<session_id>', methods=['GET', 'DELETE'],
|
||||
permission=Permission.RESOURCE_MANAGE, auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
@self.route(
|
||||
'/slack/setup/<session_id>',
|
||||
methods=['GET', 'DELETE'],
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
)
|
||||
async def slack_status(session_id: str, request_context: RequestContext):
|
||||
try:
|
||||
slack_setup.owned(request_context, session_id)
|
||||
@@ -194,15 +214,22 @@ class AdaptersRouterGroup(group.RouterGroup):
|
||||
@self.route('/slack/setup/callback', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||
async def slack_callback():
|
||||
try:
|
||||
await slack_setup.finish(quart.request.args.get('state', ''), quart.request.args.get('code', ''),
|
||||
quart.request.args.get('error'))
|
||||
await slack_setup.finish(
|
||||
quart.request.args.get('state', ''),
|
||||
quart.request.args.get('code', ''),
|
||||
quart.request.args.get('error'),
|
||||
)
|
||||
message = 'Slack authorization completed. Return to LangBot to finish setup. / Slack 授权完成,请返回 LangBot 完成配置。'
|
||||
status = 200
|
||||
except SlackSetupError:
|
||||
message = 'Slack authorization failed or expired. Return to LangBot for details. / Slack 授权失败或已过期,请返回 LangBot 查看详情。'
|
||||
status = 400
|
||||
return quart.Response(message, status=status, content_type='text/plain; charset=utf-8',
|
||||
headers={'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer'})
|
||||
return quart.Response(
|
||||
message,
|
||||
status=status,
|
||||
content_type='text/plain; charset=utf-8',
|
||||
headers={'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer'},
|
||||
)
|
||||
|
||||
@self.route('', methods=['GET'])
|
||||
async def _() -> str:
|
||||
|
||||
@@ -452,7 +452,9 @@ class AgentService:
|
||||
if runner_id:
|
||||
descriptor = await self.ap.runner_registry.get(context, runner_id)
|
||||
config['runner_config'][runner_id] = await self.ap.llm_model_service.apply_default_model(
|
||||
context, descriptor.config_schema, config['runner_config'][runner_id],
|
||||
context,
|
||||
descriptor.config_schema,
|
||||
config['runner_config'][runner_id],
|
||||
)
|
||||
patterns = agent_data.get('supported_event_patterns', AGENT_DEFAULT_EVENT_PATTERNS)
|
||||
new_uuid = str(uuid.uuid4())
|
||||
@@ -566,7 +568,9 @@ class AgentService:
|
||||
if parameters is None:
|
||||
parameters = self.ap.pipeline_service._get_default_values_from_schema(descriptor.config_schema)
|
||||
if draft:
|
||||
parameters = await self.ap.llm_model_service.apply_default_model(context, descriptor.config_schema, parameters)
|
||||
parameters = await self.ap.llm_model_service.apply_default_model(
|
||||
context, descriptor.config_schema, parameters
|
||||
)
|
||||
if not isinstance(parameters, dict):
|
||||
raise ValueError('Processor parameters must be an object')
|
||||
for field in descriptor.config_schema:
|
||||
|
||||
@@ -198,6 +198,7 @@ class LLMModelsService:
|
||||
|
||||
async def get_starred_model(self, context: TenantContext) -> str | None:
|
||||
from ....entity.persistence.metadata import WorkspaceMetadata
|
||||
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(WorkspaceMetadata.value).where(
|
||||
WorkspaceMetadata.workspace_uuid == require_workspace_uuid(context),
|
||||
@@ -212,6 +213,7 @@ class LLMModelsService:
|
||||
|
||||
async def set_starred_model(self, context: TenantContext, model_uuid: str | None) -> None:
|
||||
from ....entity.persistence.metadata import WorkspaceMetadata
|
||||
|
||||
workspace_uuid = require_workspace_uuid(context)
|
||||
if model_uuid and await self.get_llm_model(context, model_uuid) is None:
|
||||
raise ValueError('Model not found in this Workspace')
|
||||
@@ -222,11 +224,16 @@ class LLMModelsService:
|
||||
else:
|
||||
from sqlalchemy.dialects.sqlite import insert
|
||||
statement = insert(WorkspaceMetadata).values(
|
||||
workspace_uuid=workspace_uuid, key='starred_llm_model', value=model_uuid or '',
|
||||
workspace_uuid=workspace_uuid,
|
||||
key='starred_llm_model',
|
||||
value=model_uuid or '',
|
||||
)
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
statement.on_conflict_do_update(
|
||||
index_elements=['workspace_uuid', 'key'],
|
||||
set_={'value': model_uuid or ''},
|
||||
)
|
||||
)
|
||||
await self.ap.persistence_mgr.execute_async(statement.on_conflict_do_update(
|
||||
index_elements=['workspace_uuid', 'key'], set_={'value': model_uuid or ''},
|
||||
))
|
||||
|
||||
async def get_default_model(self, context: TenantContext) -> str | None:
|
||||
starred = await self.get_starred_model(context)
|
||||
@@ -245,9 +252,18 @@ class LLMModelsService:
|
||||
|
||||
async def apply_default_model(self, context: TenantContext, schema: list[dict], parameters: dict) -> dict:
|
||||
import copy
|
||||
fields = [field for field in schema if field.get('type') in (
|
||||
'llm-model-selector', 'select-llm-model', 'model-fallback-selector',
|
||||
) and field.get('name')]
|
||||
|
||||
fields = [
|
||||
field
|
||||
for field in schema
|
||||
if field.get('type')
|
||||
in (
|
||||
'llm-model-selector',
|
||||
'select-llm-model',
|
||||
'model-fallback-selector',
|
||||
)
|
||||
and field.get('name')
|
||||
]
|
||||
if not fields:
|
||||
return parameters
|
||||
model_uuid = await self.get_default_model(context)
|
||||
@@ -264,6 +280,7 @@ class LLMModelsService:
|
||||
elif not result.get(name):
|
||||
result[name] = model_uuid
|
||||
return result
|
||||
|
||||
async def get_llm_models(self, context: TenantContext, include_secret: bool = False) -> list[dict]:
|
||||
"""Get all LLM models with provider info"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
|
||||
@@ -132,6 +132,7 @@ def _workspace_transaction(method):
|
||||
result = await method(self, context, *args, **kwargs)
|
||||
if method.__name__ in {'record_message', 'update_message_status'}:
|
||||
from ....utils.inflight import inflight_hub
|
||||
|
||||
inflight_hub.notify(workspace_uuid)
|
||||
return result
|
||||
|
||||
@@ -1430,10 +1431,13 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
pipeline_timing = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(AgentRun.started_at, AgentRun.finished_at)
|
||||
.select_from(Message)
|
||||
.join(AgentRun, sqlalchemy.and_(
|
||||
AgentRun.workspace_id == workspace_uuid,
|
||||
AgentRun.run_id == Message.run_id,
|
||||
))
|
||||
.join(
|
||||
AgentRun,
|
||||
sqlalchemy.and_(
|
||||
AgentRun.workspace_id == workspace_uuid,
|
||||
AgentRun.run_id == Message.run_id,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
*pipeline_conditions,
|
||||
Message.status == 'success',
|
||||
@@ -1443,10 +1447,7 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
.order_by(Message.timestamp.desc())
|
||||
.limit(self._detail_limit())
|
||||
)
|
||||
durations.extend(
|
||||
(row[1] - row[0]).total_seconds() * 1000
|
||||
for row in pipeline_timing.all() if row[0] and row[1]
|
||||
)
|
||||
durations.extend((row[1] - row[0]).total_seconds() * 1000 for row in pipeline_timing.all() if row[0] and row[1])
|
||||
durations.sort()
|
||||
pipeline_status_result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(
|
||||
@@ -1679,21 +1680,39 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
workspace = require_workspace_uuid(context)
|
||||
active = await self.get_executions(context, statuses=['running', 'queued'], limit=100, include_summary=False)
|
||||
recent = await self.get_executions(
|
||||
context, start_time=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=30),
|
||||
limit=100, include_summary=False,
|
||||
context,
|
||||
start_time=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=30),
|
||||
limit=100,
|
||||
include_summary=False,
|
||||
)
|
||||
items = {(r['source'], r['id']): r for r in recent['items'] + active['items'] if r['source'] != 'event'}
|
||||
# Keep the actual terminal state of older runs that just left the active set.
|
||||
for kind, model, column, scope in [
|
||||
('agent', persistence_agent_run.AgentRun, persistence_agent_run.AgentRun.run_id, persistence_agent_run.AgentRun.workspace_id),
|
||||
('pipeline', persistence_monitoring.MonitoringMessage, persistence_monitoring.MonitoringMessage.id, persistence_monitoring.MonitoringMessage.workspace_uuid),
|
||||
(
|
||||
'agent',
|
||||
persistence_agent_run.AgentRun,
|
||||
persistence_agent_run.AgentRun.run_id,
|
||||
persistence_agent_run.AgentRun.workspace_id,
|
||||
),
|
||||
(
|
||||
'pipeline',
|
||||
persistence_monitoring.MonitoringMessage,
|
||||
persistence_monitoring.MonitoringMessage.id,
|
||||
persistence_monitoring.MonitoringMessage.workspace_uuid,
|
||||
),
|
||||
]:
|
||||
missing = [r['id'] for r in (previous or [])[:100] if r['source'] == kind and (kind, r['id']) not in items]
|
||||
if missing:
|
||||
records = await self._execution_rows(workspace, sqlalchemy.select(model).where(scope == workspace, column.in_(missing)))
|
||||
records = await self._execution_rows(
|
||||
workspace, sqlalchemy.select(model).where(scope == workspace, column.in_(missing))
|
||||
)
|
||||
names = await self._agent_names(workspace, records) if kind == 'agent' else {}
|
||||
for record in records:
|
||||
row = self._serialize_agent_execution(record, names) if kind == 'agent' else self._serialize_pipeline_execution(record)
|
||||
row = (
|
||||
self._serialize_agent_execution(record, names)
|
||||
if kind == 'agent'
|
||||
else self._serialize_pipeline_execution(record)
|
||||
)
|
||||
items[(kind, row['id'])] = row
|
||||
rows = list(items.values())
|
||||
Run = persistence_agent_run.AgentRun
|
||||
@@ -1701,18 +1720,28 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
agent_ids = [r['id'] for r in rows if r['source'] == 'agent']
|
||||
pipeline_ids = [r['id'] for r in rows if r['source'] == 'pipeline']
|
||||
linked = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(Run.run_id, Message.id.label('message_id')).outerjoin(
|
||||
Message, sqlalchemy.and_(Message.workspace_uuid == workspace, Message.run_id == Run.run_id)
|
||||
).where(Run.workspace_id == workspace, sqlalchemy.or_(Run.run_id.in_(agent_ids), Message.id.in_(pipeline_ids)))
|
||||
sqlalchemy.select(Run.run_id, Message.id.label('message_id'))
|
||||
.outerjoin(Message, sqlalchemy.and_(Message.workspace_uuid == workspace, Message.run_id == Run.run_id))
|
||||
.where(
|
||||
Run.workspace_id == workspace, sqlalchemy.or_(Run.run_id.in_(agent_ids), Message.id.in_(pipeline_ids))
|
||||
)
|
||||
)
|
||||
identities = {r.run_id: ('pipeline', r.message_id) if r.message_id in pipeline_ids else ('agent', r.run_id) for r in linked.all()}
|
||||
identities = {
|
||||
r.run_id: ('pipeline', r.message_id) if r.message_id in pipeline_ids else ('agent', r.run_id)
|
||||
for r in linked.all()
|
||||
}
|
||||
inflight_hub.track(workspace, identities)
|
||||
Event = persistence_agent_run.AgentRunEvent
|
||||
latest = sqlalchemy.select(Event.run_id, sqlalchemy.func.max(Event.sequence).label('seq')).where(
|
||||
Event.run_id.in_(identities)
|
||||
).group_by(Event.run_id).subquery()
|
||||
latest = (
|
||||
sqlalchemy.select(Event.run_id, sqlalchemy.func.max(Event.sequence).label('seq'))
|
||||
.where(Event.run_id.in_(identities))
|
||||
.group_by(Event.run_id)
|
||||
.subquery()
|
||||
)
|
||||
phases = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(Event.run_id, Event.type).join(latest, sqlalchemy.and_(Event.run_id == latest.c.run_id, Event.sequence == latest.c.seq))
|
||||
sqlalchemy.select(Event.run_id, Event.type).join(
|
||||
latest, sqlalchemy.and_(Event.run_id == latest.c.run_id, Event.sequence == latest.c.seq)
|
||||
)
|
||||
)
|
||||
for run_id, event_type in phases.all():
|
||||
row = items.get(identities[run_id])
|
||||
@@ -1951,7 +1980,10 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
if end_time:
|
||||
query = query.where(source.c.timestamp <= end_time)
|
||||
return await self._session_projection_page(
|
||||
workspace_uuid, query.order_by(source.c.timestamp.desc(), source.c.id.desc()), limit, offset,
|
||||
workspace_uuid,
|
||||
query.order_by(source.c.timestamp.desc(), source.c.id.desc()),
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
conditions = [persistence_monitoring.MonitoringMessage.workspace_uuid == workspace_uuid]
|
||||
|
||||
@@ -2204,47 +2236,99 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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',
|
||||
))
|
||||
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(),
|
||||
))
|
||||
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(
|
||||
@@ -2266,7 +2350,14 @@ class MonitoringService(ExecutionDetailsMixin):
|
||||
workspace_uuid = require_workspace_uuid(context)
|
||||
if bot_ids and not pipeline_ids and not execution_statuses and mode == 'all':
|
||||
return await self.get_bot_conversation_sessions(
|
||||
workspace_uuid, bot_ids, start_time, end_time, user_query, is_active, limit, offset,
|
||||
workspace_uuid,
|
||||
bot_ids,
|
||||
start_time,
|
||||
end_time,
|
||||
user_query,
|
||||
is_active,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
conditions = [persistence_monitoring.MonitoringSession.workspace_uuid == workspace_uuid]
|
||||
|
||||
|
||||
@@ -104,16 +104,24 @@ class ExecutionDetailsMixin:
|
||||
message = models.MonitoringMessage
|
||||
conversation = sa.func.coalesce(
|
||||
EventLog.conversation_id,
|
||||
sa.select(AgentRun.conversation_id).where(
|
||||
sa.select(AgentRun.conversation_id)
|
||||
.where(
|
||||
AgentRun.workspace_id == workspace,
|
||||
AgentRun.bot_id == EventLog.bot_id,
|
||||
AgentRun.event_id == EventLog.event_id,
|
||||
).order_by(AgentRun.id).limit(1).scalar_subquery(),
|
||||
)
|
||||
.order_by(AgentRun.id)
|
||||
.limit(1)
|
||||
.scalar_subquery(),
|
||||
sa.case(
|
||||
(self._execution_json_text(EventLog.input_json, 'chat_type') == 'group',
|
||||
sa.literal('group_') + self._execution_json_text(EventLog.input_json, 'chat_id')),
|
||||
(self._execution_json_text(EventLog.input_json, 'chat_type') == 'private',
|
||||
sa.literal('person_') + self._execution_json_text(EventLog.input_json, ('sender', 'id'))),
|
||||
(
|
||||
self._execution_json_text(EventLog.input_json, 'chat_type') == 'group',
|
||||
sa.literal('group_') + self._execution_json_text(EventLog.input_json, 'chat_id'),
|
||||
),
|
||||
(
|
||||
self._execution_json_text(EventLog.input_json, 'chat_type') == 'private',
|
||||
sa.literal('person_') + self._execution_json_text(EventLog.input_json, ('sender', 'id')),
|
||||
),
|
||||
),
|
||||
)
|
||||
values = {
|
||||
@@ -122,8 +130,12 @@ class ExecutionDetailsMixin:
|
||||
'timestamp': sa.func.coalesce(EventLog.event_time, EventLog.created_at),
|
||||
'bot_id': EventLog.bot_id,
|
||||
'bot_name': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, 'bot_name'), ''),
|
||||
'pipeline_id': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_uuid')), ''),
|
||||
'pipeline_name': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), ''),
|
||||
'pipeline_id': sa.func.coalesce(
|
||||
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_uuid')), ''
|
||||
),
|
||||
'pipeline_name': sa.func.coalesce(
|
||||
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), ''
|
||||
),
|
||||
'message_content': sa.func.coalesce(EventLog.input_json, EventLog.input_summary, ''),
|
||||
'session_id': conversation,
|
||||
'status': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, 'status'), 'success'),
|
||||
@@ -137,45 +149,62 @@ class ExecutionDetailsMixin:
|
||||
}
|
||||
columns = list(message.__table__.columns)
|
||||
stored = sa.select(*columns).where(message.workspace_uuid == workspace, message.bot_id.in_(bot_ids))
|
||||
incoming = sa.select(*[
|
||||
values.get(column.name, sa.literal(None)).label(column.name) for column in columns
|
||||
]).where(
|
||||
incoming = sa.select(
|
||||
*[values.get(column.name, sa.literal(None)).label(column.name) for column in columns]
|
||||
).where(
|
||||
EventLog.workspace_id == workspace,
|
||||
EventLog.bot_id.in_(bot_ids),
|
||||
EventLog.source == 'platform',
|
||||
EventLog.event_type == 'message.received',
|
||||
conversation.is_not(None),
|
||||
conversation != '',
|
||||
~sa.exists(sa.select(message.id).where(
|
||||
message.workspace_uuid == workspace,
|
||||
message.bot_id == EventLog.bot_id,
|
||||
sa.func.coalesce(message.role, 'user') != 'assistant',
|
||||
sa.or_(message.event_id == EventLog.event_id, message.id == EventLog.event_id),
|
||||
)),
|
||||
~sa.exists(
|
||||
sa.select(message.id).where(
|
||||
message.workspace_uuid == workspace,
|
||||
message.bot_id == EventLog.bot_id,
|
||||
sa.func.coalesce(message.role, 'user') != 'assistant',
|
||||
sa.or_(message.event_id == EventLog.event_id, message.id == EventLog.event_id),
|
||||
)
|
||||
),
|
||||
)
|
||||
return sa.union_all(stored, incoming).subquery()
|
||||
|
||||
async def _session_projection_page(self, workspace, statement, limit, offset):
|
||||
count = (await self._execution_query(workspace, sa.select(sa.func.count()).select_from(statement.subquery()))).scalar_one()
|
||||
count = (
|
||||
await self._execution_query(workspace, sa.select(sa.func.count()).select_from(statement.subquery()))
|
||||
).scalar_one()
|
||||
rows = (await self._execution_query(workspace, statement.limit(limit).offset(offset))).mappings().all()
|
||||
return [
|
||||
{key: value.isoformat() if isinstance(value, datetime.datetime) else value for key, value in row.items()}
|
||||
for row in rows
|
||||
], count
|
||||
|
||||
async def get_bot_conversation_sessions(self, workspace, bot_ids, start_time, end_time, user_query, is_active, limit, offset):
|
||||
async def get_bot_conversation_sessions(
|
||||
self, workspace, bot_ids, start_time, end_time, user_query, is_active, limit, offset
|
||||
):
|
||||
"""Read session summaries across processor types without mutating history."""
|
||||
messages = self._session_message_source(workspace, bot_ids)
|
||||
key = [messages.c.workspace_uuid, messages.c.bot_id, messages.c.session_id]
|
||||
ranked = sa.select(
|
||||
*messages.c,
|
||||
sa.func.row_number().over(partition_by=key, order_by=[
|
||||
sa.case((messages.c.role == 'user', 0), else_=1), messages.c.timestamp.desc(), messages.c.id.desc(),
|
||||
]).label('position'),
|
||||
sa.func.min(messages.c.timestamp).over(partition_by=key).label('first_seen'),
|
||||
sa.func.max(messages.c.timestamp).over(partition_by=key).label('last_seen'),
|
||||
sa.func.count().over(partition_by=key).label('count'),
|
||||
).where(messages.c.session_id != '').subquery()
|
||||
ranked = (
|
||||
sa.select(
|
||||
*messages.c,
|
||||
sa.func.row_number()
|
||||
.over(
|
||||
partition_by=key,
|
||||
order_by=[
|
||||
sa.case((messages.c.role == 'user', 0), else_=1),
|
||||
messages.c.timestamp.desc(),
|
||||
messages.c.id.desc(),
|
||||
],
|
||||
)
|
||||
.label('position'),
|
||||
sa.func.min(messages.c.timestamp).over(partition_by=key).label('first_seen'),
|
||||
sa.func.max(messages.c.timestamp).over(partition_by=key).label('last_seen'),
|
||||
sa.func.count().over(partition_by=key).label('count'),
|
||||
)
|
||||
.where(messages.c.session_id != '')
|
||||
.subquery()
|
||||
)
|
||||
session = models.MonitoringSession
|
||||
fields = list(session.__table__.columns)
|
||||
projected = {
|
||||
@@ -184,15 +213,21 @@ class ExecutionDetailsMixin:
|
||||
'message_count': ranked.c['count'],
|
||||
'is_active': sa.literal(True),
|
||||
}
|
||||
derived = sa.select(*[
|
||||
(projected[field.name] if field.name in projected else ranked.c[field.name]).label(field.name)
|
||||
for field in fields
|
||||
]).where(ranked.c.position == 1)
|
||||
derived = sa.select(
|
||||
*[
|
||||
(projected[field.name] if field.name in projected else ranked.c[field.name]).label(field.name)
|
||||
for field in fields
|
||||
]
|
||||
).where(ranked.c.position == 1)
|
||||
existing = sa.select(*fields).where(session.workspace_uuid == workspace, session.bot_id.in_(bot_ids))
|
||||
combined = sa.union_all(existing, derived).subquery()
|
||||
group = [combined.c.workspace_uuid, combined.c.bot_id, combined.c.session_id]
|
||||
merged = sa.select(
|
||||
*[combined.c[field.name] for field in fields if field.name not in {'message_count', 'start_time', 'last_activity'}],
|
||||
*[
|
||||
combined.c[field.name]
|
||||
for field in fields
|
||||
if field.name not in {'message_count', 'start_time', 'last_activity'}
|
||||
],
|
||||
sa.func.max(combined.c.message_count).over(partition_by=group).label('message_count'),
|
||||
sa.func.min(combined.c.start_time).over(partition_by=group).label('start_time'),
|
||||
sa.func.max(combined.c.last_activity).over(partition_by=group).label('last_activity'),
|
||||
@@ -209,7 +244,10 @@ class ExecutionDetailsMixin:
|
||||
if is_active is not None:
|
||||
query = query.where(merged.c.is_active == is_active)
|
||||
return await self._session_projection_page(
|
||||
workspace, query.order_by(merged.c.last_activity.desc(), merged.c.session_id), limit, offset,
|
||||
workspace,
|
||||
query.order_by(merged.c.last_activity.desc(), merged.c.session_id),
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
|
||||
def _execution_json_text(self, column, key):
|
||||
@@ -335,12 +373,10 @@ class ExecutionDetailsMixin:
|
||||
# Delivered pipeline ingress is not an unhandled execution, including
|
||||
# older queued/aggregated messages whose trace identity was lost.
|
||||
~sa.and_(
|
||||
sa.func.coalesce(
|
||||
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), ''
|
||||
) == 'pipeline',
|
||||
sa.func.coalesce(
|
||||
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'status')), ''
|
||||
) == 'delivered',
|
||||
sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), '')
|
||||
== 'pipeline',
|
||||
sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'status')), '')
|
||||
== 'delivered',
|
||||
),
|
||||
~sa.exists(
|
||||
sa.select(AgentRun.id).where(
|
||||
@@ -400,11 +436,13 @@ class ExecutionDetailsMixin:
|
||||
'runner_id': None,
|
||||
'debug': row.source == 'debug',
|
||||
'has_error': status == 'failed',
|
||||
'status_reason': metadata.get('reason') or '; '.join(
|
||||
'status_reason': metadata.get('reason')
|
||||
or '; '.join(
|
||||
route.get('reason') or route.get('failure_code') or 'failure_reason_not_recorded'
|
||||
for route in metadata.get('routes', [])
|
||||
if isinstance(route, dict) and route.get('status') == 'failed'
|
||||
) or None,
|
||||
)
|
||||
or None,
|
||||
}
|
||||
|
||||
async def enrich_execution_rows(self, workspace, items):
|
||||
@@ -415,10 +453,13 @@ class ExecutionDetailsMixin:
|
||||
linked = await self._execution_query(
|
||||
workspace,
|
||||
sa.select(models.MonitoringMessage.id, AgentRun)
|
||||
.join(AgentRun, sa.and_(
|
||||
AgentRun.workspace_id == workspace,
|
||||
AgentRun.run_id == models.MonitoringMessage.run_id,
|
||||
))
|
||||
.join(
|
||||
AgentRun,
|
||||
sa.and_(
|
||||
AgentRun.workspace_id == workspace,
|
||||
AgentRun.run_id == models.MonitoringMessage.run_id,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
models.MonitoringMessage.workspace_uuid == workspace,
|
||||
models.MonitoringMessage.id.in_(pipeline_items),
|
||||
@@ -426,10 +467,9 @@ class ExecutionDetailsMixin:
|
||||
)
|
||||
for message_id, run in linked.all():
|
||||
metrics = self._serialize_agent_execution(run)
|
||||
pipeline_items[message_id].update({
|
||||
key: metrics[key]
|
||||
for key in ('started_at_ms', 'finished_at_ms', 'duration_ms', 'usage', 'cost')
|
||||
})
|
||||
pipeline_items[message_id].update(
|
||||
{key: metrics[key] for key in ('started_at_ms', 'finished_at_ms', 'duration_ms', 'usage', 'cost')}
|
||||
)
|
||||
|
||||
calls = models.MonitoringLLMCall
|
||||
usage_rows = await self._execution_query(
|
||||
@@ -439,10 +479,12 @@ class ExecutionDetailsMixin:
|
||||
sa.func.sum(calls.input_tokens),
|
||||
sa.func.sum(calls.output_tokens),
|
||||
sa.func.sum(calls.total_tokens),
|
||||
).where(
|
||||
)
|
||||
.where(
|
||||
calls.workspace_uuid == workspace,
|
||||
calls.message_id.in_(pipeline_items),
|
||||
).group_by(calls.message_id),
|
||||
)
|
||||
.group_by(calls.message_id),
|
||||
)
|
||||
for message_id, input_tokens, output_tokens, total_tokens in usage_rows.all():
|
||||
if not pipeline_items[message_id].get('usage'):
|
||||
|
||||
@@ -69,7 +69,9 @@ class PipelineService:
|
||||
runner_config.setdefault('expire-time', 0)
|
||||
ai_config['runner_config'] = {selected.id: self._get_default_values_from_schema(selected.config_schema)}
|
||||
ai_config['runner_config'][selected.id] = await self.ap.llm_model_service.apply_default_model(
|
||||
context, selected.config_schema, ai_config['runner_config'][selected.id],
|
||||
context,
|
||||
selected.config_schema,
|
||||
ai_config['runner_config'][selected.id],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -207,35 +207,65 @@ class LangBotMCPServer:
|
||||
return _dump(await ap.monitoring_service.get_message_details(context, identifier))
|
||||
return _dump(await ap.monitoring_service.get_session_analysis(context, identifier))
|
||||
|
||||
@mcp.tool(description='Read a bounded snapshot of active and recently started Workspace executions, including the latest progress event. No percentage is estimated.')
|
||||
@mcp.tool(
|
||||
description='Read a bounded snapshot of active and recently started Workspace executions, including the latest progress event. No percentage is estimated.'
|
||||
)
|
||||
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.')
|
||||
@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,
|
||||
))
|
||||
return _dump(
|
||||
await ap.monitoring_service.reset_session_context(
|
||||
_authorized(Permission.RESOURCE_MANAGE),
|
||||
bot_id,
|
||||
session_id,
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool(description='Create/configure a Slack app for an existing Workspace bot draft. Returns a setup session; poll it for the installation authorization URL. The user must approve Slack installation. Configuration tokens stay in memory only.')
|
||||
async def start_slack_setup(bot_uuid: str, access_token: str, refresh_token: str,
|
||||
webhook_url: str = '', name: str = 'LangBot',
|
||||
socket_mode: bool = False, redirect_url: str = '') -> str:
|
||||
@mcp.tool(
|
||||
description='Create/configure a Slack app for an existing Workspace bot draft. Returns a setup session; poll it for the installation authorization URL. The user must approve Slack installation. Configuration tokens stay in memory only.'
|
||||
)
|
||||
async def start_slack_setup(
|
||||
bot_uuid: str,
|
||||
access_token: str,
|
||||
refresh_token: str,
|
||||
webhook_url: str = '',
|
||||
name: str = 'LangBot',
|
||||
socket_mode: bool = False,
|
||||
redirect_url: str = '',
|
||||
) -> str:
|
||||
from langbot.pkg.api.http.service.slack_setup import get_slack_setup
|
||||
return _dump(await get_slack_setup(ap).start(
|
||||
_authorized(Permission.RESOURCE_MANAGE), bot_uuid=bot_uuid, access_token=access_token,
|
||||
refresh_token=refresh_token, webhook_url=webhook_url, name=name,
|
||||
socket_mode=socket_mode, redirect_url=redirect_url,
|
||||
))
|
||||
|
||||
@mcp.tool(description='Read an owned Slack setup session. Success includes adapter credentials; save them with the existing bot update tool. Socket Mode additionally needs an App-Level Token with connections:write.')
|
||||
return _dump(
|
||||
await get_slack_setup(ap).start(
|
||||
_authorized(Permission.RESOURCE_MANAGE),
|
||||
bot_uuid=bot_uuid,
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
webhook_url=webhook_url,
|
||||
name=name,
|
||||
socket_mode=socket_mode,
|
||||
redirect_url=redirect_url,
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
description='Read an owned Slack setup session. Success includes adapter credentials; save them with the existing bot update tool. Socket Mode additionally needs an App-Level Token with connections:write.'
|
||||
)
|
||||
async def get_slack_setup_status(session_id: str) -> str:
|
||||
from langbot.pkg.api.http.service.slack_setup import get_slack_setup
|
||||
|
||||
return _dump(get_slack_setup(ap).status(_authorized(Permission.RESOURCE_MANAGE), session_id))
|
||||
|
||||
@mcp.tool(description='Discard an owned Slack setup session and its temporary credentials. Does not delete the Slack app.')
|
||||
@mcp.tool(
|
||||
description='Discard an owned Slack setup session and its temporary credentials. Does not delete the Slack app.'
|
||||
)
|
||||
async def cancel_slack_setup(session_id: str) -> str:
|
||||
from langbot.pkg.api.http.service.slack_setup import get_slack_setup
|
||||
|
||||
service = get_slack_setup(ap)
|
||||
service.owned(_authorized(Permission.RESOURCE_MANAGE), session_id)
|
||||
service.discard(session_id)
|
||||
@@ -463,13 +493,17 @@ class LangBotMCPServer:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump({'uuid': await ap.llm_model_service.get_starred_model(context)})
|
||||
|
||||
@mcp.tool(description='Star one LLM model for this Workspace, replacing the previous choice. Pass null to clear.')
|
||||
@mcp.tool(
|
||||
description='Star one LLM model for this Workspace, replacing the previous choice. Pass null to clear.'
|
||||
)
|
||||
async def set_starred_model(model_uuid: str | None = None) -> str:
|
||||
context = _authorized(Permission.PROVIDER_SECRET_MANAGE)
|
||||
await ap.llm_model_service.set_starred_model(context, model_uuid)
|
||||
return _dump({'uuid': model_uuid})
|
||||
|
||||
@mcp.tool(description='Resolve the default LLM model: starred model, then the available Space wizard recommendation.')
|
||||
@mcp.tool(
|
||||
description='Resolve the default LLM model: starred model, then the available Space wizard recommendation.'
|
||||
)
|
||||
async def get_default_model() -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
return _dump({'uuid': await ap.llm_model_service.get_default_model(context)})
|
||||
|
||||
@@ -359,6 +359,7 @@ class Application:
|
||||
async def run(self):
|
||||
self.event_loop_monitor.start()
|
||||
from ..utils.execution_lease import maintain_execution_leases
|
||||
|
||||
self.task_mgr.create_task(
|
||||
maintain_execution_leases(self),
|
||||
name='execution-lease-maintenance',
|
||||
|
||||
@@ -10,15 +10,32 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
for table in ('agent_run', 'monitoring_messages'):
|
||||
op.add_column(table, sa.Column('execution_owner_id', sa.String(36), nullable=True))
|
||||
op.add_column(table, sa.Column('execution_lease_expires_at', sa.DateTime(), nullable=True))
|
||||
for column in ('execution_owner_id', 'execution_lease_expires_at'):
|
||||
op.create_index(f'ix_{table}_{column}', table, [column])
|
||||
if not inspector.has_table(table):
|
||||
continue
|
||||
columns = {c['name'] for c in inspector.get_columns(table)}
|
||||
indexes = {i['name'] for i in inspector.get_indexes(table)}
|
||||
for column, column_type in (
|
||||
('execution_owner_id', sa.String(36)),
|
||||
('execution_lease_expires_at', sa.DateTime()),
|
||||
):
|
||||
if column not in columns:
|
||||
op.add_column(table, sa.Column(column, column_type, nullable=True))
|
||||
index = f'ix_{table}_{column}'
|
||||
if index not in indexes:
|
||||
op.create_index(index, table, [column])
|
||||
|
||||
|
||||
def downgrade():
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
for table in ('agent_run', 'monitoring_messages'):
|
||||
if not inspector.has_table(table):
|
||||
continue
|
||||
columns = {c['name'] for c in inspector.get_columns(table)}
|
||||
indexes = {i['name'] for i in inspector.get_indexes(table)}
|
||||
for column in ('execution_owner_id', 'execution_lease_expires_at'):
|
||||
op.drop_index(f'ix_{table}_{column}', table_name=table)
|
||||
op.drop_column(table, column)
|
||||
if f'ix_{table}_{column}' in indexes:
|
||||
op.drop_index(f'ix_{table}_{column}', table_name=table)
|
||||
if column in columns:
|
||||
op.drop_column(table, column)
|
||||
|
||||
@@ -489,8 +489,7 @@ def _validate_scoped_statement_call(args: tuple[typing.Any, ...], kwargs: dict[s
|
||||
and type(source.type) is sqlalchemy.Text
|
||||
and getattr(getattr(source, 'table', None), 'name', None) in {'event_log', 'agent_run'}
|
||||
and (
|
||||
source.name == 'metadata_json'
|
||||
or (source.table.name == 'event_log' and source.name == 'input_json')
|
||||
source.name == 'metadata_json' or (source.table.name == 'event_log' and source.name == 'input_json')
|
||||
)
|
||||
)
|
||||
if not (pipeline_json_cas or monitoring_metadata):
|
||||
|
||||
@@ -449,7 +449,9 @@ class RuntimePipeline:
|
||||
try:
|
||||
from ..platform.processing_indicator import processing_indicator
|
||||
|
||||
async with processing_indicator(query.adapter, query.launcher_type.value.lower(), query.launcher_id):
|
||||
async with processing_indicator(
|
||||
query.adapter, query.launcher_type.value.lower(), query.launcher_id
|
||||
):
|
||||
return await self._process_query(query)
|
||||
except asyncio.CancelledError:
|
||||
lane_outcome = 'cancelled'
|
||||
|
||||
@@ -166,9 +166,9 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
assert isinstance(message_source.source_platform_object, DingTalkEvent)
|
||||
incoming_message = message_source.source_platform_object.incoming_message
|
||||
markdown_enabled = self.config.get('markdown_card', False)
|
||||
text_message = platform_message.MessageChain([
|
||||
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
|
||||
])
|
||||
text_message = platform_message.MessageChain(
|
||||
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
|
||||
)
|
||||
content, at = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
|
||||
raw = await self.bot.send_message(content, incoming_message, at)
|
||||
await self._send_attachments(message, incoming_message)
|
||||
@@ -206,9 +206,9 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
last_updates[message_id] = now
|
||||
|
||||
markdown_enabled = self.config.get('markdown_card', False)
|
||||
text_message = platform_message.MessageChain([
|
||||
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
|
||||
])
|
||||
text_message = platform_message.MessageChain(
|
||||
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
|
||||
)
|
||||
content, _ = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
|
||||
card_instance, card_instance_id = self.card_instance_id_dict[message_id]
|
||||
if not content and bot_message.content:
|
||||
|
||||
@@ -14,14 +14,23 @@ def subscription_events(message):
|
||||
headers = message.headers
|
||||
action = headers.event_type or data.get('EventType') or data.get('eventType') or ''
|
||||
raw = {'headers': vars(headers).copy(), 'data': data}
|
||||
stamp = headers.event_born_time or data.get('TimeStamp') or data.get('EventTime') or data.get('timestamp') or headers.time or 0
|
||||
stamp = (
|
||||
headers.event_born_time
|
||||
or data.get('TimeStamp')
|
||||
or data.get('EventTime')
|
||||
or data.get('timestamp')
|
||||
or headers.time
|
||||
or 0
|
||||
)
|
||||
stamp = float(stamp)
|
||||
common = dict(
|
||||
adapter_name=ADAPTER_NAME,
|
||||
timestamp=stamp / 1000 if stamp > 10_000_000_000 else stamp,
|
||||
source_platform_object=raw,
|
||||
)
|
||||
group_id = data.get('OpenConversationId') or data.get('openConversationId') or data.get('chatId') or data.get('ChatId')
|
||||
group_id = (
|
||||
data.get('OpenConversationId') or data.get('openConversationId') or data.get('chatId') or data.get('ChatId')
|
||||
)
|
||||
group = entities.UserGroup(id=str(group_id or ''), name=str(data.get('Title') or data.get('title') or ''))
|
||||
owner_id = data.get('Owner') or data.get('owner')
|
||||
if owner_id:
|
||||
@@ -38,13 +47,18 @@ def subscription_events(message):
|
||||
yield events.MemberJoinedEvent(group=group, member=member, inviter=operator, **common)
|
||||
else:
|
||||
yield events.MemberLeftEvent(
|
||||
group=group, member=member, operator=operator,
|
||||
is_kicked=action == 'chat_remove_member', **common,
|
||||
group=group,
|
||||
member=member,
|
||||
operator=operator,
|
||||
is_kicked=action == 'chat_remove_member',
|
||||
**common,
|
||||
)
|
||||
elif group_id and action in {'chat_update_title', 'chat_update_owner'}:
|
||||
yield events.GroupInfoUpdatedEvent(
|
||||
group=group, operator=operator,
|
||||
changed_fields=['name' if action == 'chat_update_title' else 'owner_id'], **common,
|
||||
group=group,
|
||||
operator=operator,
|
||||
changed_fields=['name' if action == 'chat_update_title' else 'owner_id'],
|
||||
**common,
|
||||
)
|
||||
else:
|
||||
yield events.PlatformSpecificEvent(action=action or 'unknown', data=raw, **common)
|
||||
|
||||
@@ -570,7 +570,9 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
ContentCardElementRequest.builder()
|
||||
.card_id(card_id)
|
||||
.element_id('streaming_txt')
|
||||
.request_body(ContentCardElementRequestBody.builder().content(streaming_text(content)).sequence(sequence).build())
|
||||
.request_body(
|
||||
ContentCardElementRequestBody.builder().content(streaming_text(content)).sequence(sequence).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
response: ContentCardElementResponse = await self.api_client.cardkit.v1.card_element.acontent(
|
||||
@@ -578,7 +580,9 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
)
|
||||
if not response.success():
|
||||
if self._streaming_mode_closed(response):
|
||||
await self._replace_streaming_card(card_id, content, finished=is_final and not bot_message.tool_calls)
|
||||
await self._replace_streaming_card(
|
||||
card_id, content, finished=is_final and not bot_message.tool_calls
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f'Lark card_element update failed: {response.code} {response.msg}')
|
||||
self.card_last_update_dict[card_id] = now
|
||||
|
||||
@@ -46,9 +46,7 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
operator = cls._event_user(data.get('operator_id'))
|
||||
if event_type == 'im.chat.member.bot.deleted_v1':
|
||||
return [platform_events.BotRemovedFromGroupEvent(group=group, operator=operator, **common)]
|
||||
if event_type in {
|
||||
'im.chat.member.user.added_v1', 'im.chat.member.user.deleted_v1'
|
||||
}:
|
||||
if event_type in {'im.chat.member.user.added_v1', 'im.chat.member.user.deleted_v1'}:
|
||||
events = []
|
||||
for user in data.get('users') or []:
|
||||
member = cls._event_user(user.get('user_id'), user.get('name') or '')
|
||||
@@ -56,15 +54,25 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
continue
|
||||
if event_type == 'im.chat.member.user.added_v1':
|
||||
inviter = operator if operator and operator.id != member.id else None
|
||||
events.append(platform_events.MemberJoinedEvent(
|
||||
group=group, member=member, inviter=inviter,
|
||||
join_type='invite' if inviter else None, **common,
|
||||
))
|
||||
events.append(
|
||||
platform_events.MemberJoinedEvent(
|
||||
group=group,
|
||||
member=member,
|
||||
inviter=inviter,
|
||||
join_type='invite' if inviter else None,
|
||||
**common,
|
||||
)
|
||||
)
|
||||
else:
|
||||
events.append(platform_events.MemberLeftEvent(
|
||||
group=group, member=member, operator=operator,
|
||||
is_kicked=operator is not None and operator.id != member.id, **common,
|
||||
))
|
||||
events.append(
|
||||
platform_events.MemberLeftEvent(
|
||||
group=group,
|
||||
member=member,
|
||||
operator=operator,
|
||||
is_kicked=operator is not None and operator.id != member.id,
|
||||
**common,
|
||||
)
|
||||
)
|
||||
return events
|
||||
if event_type == 'im.chat.updated_v1':
|
||||
after = data.get('after_change') or {}
|
||||
@@ -77,9 +85,14 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
group.avatar_url = after.get('avatar')
|
||||
owner = cls._event_user(after.get('owner_id'))
|
||||
group.owner_id = owner.id if owner else None
|
||||
return [platform_events.GroupInfoUpdatedEvent(
|
||||
group=group, operator=operator, changed_fields=sorted(changed), **common,
|
||||
)]
|
||||
return [
|
||||
platform_events.GroupInfoUpdatedEvent(
|
||||
group=group,
|
||||
operator=operator,
|
||||
changed_fields=sorted(changed),
|
||||
**common,
|
||||
)
|
||||
]
|
||||
context = message_context or {}
|
||||
if event_type == 'im.message.recalled_v1':
|
||||
return [platform_events.MessageDeletedEvent(message_id=data['message_id'], **context, **common)]
|
||||
@@ -87,11 +100,16 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
user = cls._event_user(data.get('user_id'))
|
||||
if user is None:
|
||||
user = platform_entities.User(id=data.get('app_id') or '', is_bot=data.get('operator_type') == 'app')
|
||||
return [platform_events.MessageReactionEvent(
|
||||
message_id=data['message_id'], user=user,
|
||||
reaction=(data.get('reaction_type') or {}).get('emoji_type') or '',
|
||||
is_add=event_type == 'im.message.reaction.created_v1', **context, **common,
|
||||
)]
|
||||
return [
|
||||
platform_events.MessageReactionEvent(
|
||||
message_id=data['message_id'],
|
||||
user=user,
|
||||
reaction=(data.get('reaction_type') or {}).get('emoji_type') or '',
|
||||
is_add=event_type == 'im.message.reaction.created_v1',
|
||||
**context,
|
||||
**common,
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
_processed_thread_quote_cache: typing.ClassVar[dict[str, float]] = {}
|
||||
|
||||
@@ -7,7 +7,7 @@ def split_content(content: str) -> tuple[str, str]:
|
||||
reasoning, answer = [], []
|
||||
inside, offset = False, 0
|
||||
for marker in re.finditer(r'</?think>', content, re.IGNORECASE):
|
||||
(reasoning if inside else answer).append(content[offset:marker.start()])
|
||||
(reasoning if inside else answer).append(content[offset : marker.start()])
|
||||
inside = marker.group().lower() == '<think>'
|
||||
offset = marker.end()
|
||||
tail = content[offset:]
|
||||
@@ -38,32 +38,62 @@ def build_card(content: str = '', *, streaming: bool = False, finished: bool = T
|
||||
elements.append({'tag': 'markdown', 'content': streaming_text(content), 'element_id': 'streaming_txt'})
|
||||
else:
|
||||
if reasoning:
|
||||
elements.append({
|
||||
'tag': 'collapsible_panel', 'expanded': not finished,
|
||||
'header': {'title': {'tag': 'plain_text', 'content': '思考过程 / Thinking'}},
|
||||
'elements': [{'tag': 'markdown', 'content': reasoning}],
|
||||
})
|
||||
elements.append(
|
||||
{
|
||||
'tag': 'collapsible_panel',
|
||||
'expanded': not finished,
|
||||
'header': {'title': {'tag': 'plain_text', 'content': '思考过程 / Thinking'}},
|
||||
'elements': [{'tag': 'markdown', 'content': reasoning}],
|
||||
}
|
||||
)
|
||||
if answer:
|
||||
elements.append({'tag': 'markdown', 'content': answer})
|
||||
elements.append({
|
||||
'tag': 'column_set', 'horizontal_spacing': '8px',
|
||||
'columns': [
|
||||
{'tag': 'column', 'width': 'weighted', 'weight': 1, 'vertical_align': 'center',
|
||||
'elements': [{'tag': 'markdown', 'content': '内容由 AI 生成 / AI-generated',
|
||||
'text_size': 'notation'}]},
|
||||
{'tag': 'column', 'width': 'auto', 'vertical_align': 'center',
|
||||
'elements': [{'tag': 'markdown', 'content': '[LangBot](https://langbot.app?utm_source=feishu&utm_medium=bot_card&utm_campaign=langbot)', 'text_size': 'notation',
|
||||
'text_align': 'right',
|
||||
'icon': {'tag': 'custom_icon',
|
||||
'img_key': 'img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg'}}]},
|
||||
],
|
||||
})
|
||||
elements.append(
|
||||
{
|
||||
'tag': 'column_set',
|
||||
'horizontal_spacing': '8px',
|
||||
'columns': [
|
||||
{
|
||||
'tag': 'column',
|
||||
'width': 'weighted',
|
||||
'weight': 1,
|
||||
'vertical_align': 'center',
|
||||
'elements': [
|
||||
{'tag': 'markdown', 'content': '内容由 AI 生成 / AI-generated', 'text_size': 'notation'}
|
||||
],
|
||||
},
|
||||
{
|
||||
'tag': 'column',
|
||||
'width': 'auto',
|
||||
'vertical_align': 'center',
|
||||
'elements': [
|
||||
{
|
||||
'tag': 'markdown',
|
||||
'content': '[LangBot](https://langbot.app?utm_source=feishu&utm_medium=bot_card&utm_campaign=langbot)',
|
||||
'text_size': 'notation',
|
||||
'text_align': 'right',
|
||||
'icon': {
|
||||
'tag': 'custom_icon',
|
||||
'img_key': 'img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
config = {'update_multi': True}
|
||||
if streaming:
|
||||
config.update(streaming_mode=True, streaming_config={
|
||||
'print_step': {'default': 3}, 'print_frequency_ms': {'default': 40}, 'print_strategy': 'fast',
|
||||
})
|
||||
config.update(
|
||||
streaming_mode=True,
|
||||
streaming_config={
|
||||
'print_step': {'default': 3},
|
||||
'print_frequency_ms': {'default': 40},
|
||||
'print_strategy': 'fast',
|
||||
},
|
||||
)
|
||||
return {
|
||||
'schema': '2.0', 'config': config,
|
||||
'schema': '2.0',
|
||||
'config': config,
|
||||
'body': {'direction': 'vertical', 'padding': '12px 12px 12px 12px', 'elements': elements},
|
||||
}
|
||||
|
||||
@@ -55,7 +55,9 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
}
|
||||
event_class = event_classes.get(event_type)
|
||||
if event_class:
|
||||
fields = {key: value for key, value in data.items() if key in event_class.model_fields and value is not None}
|
||||
fields = {
|
||||
key: value for key, value in data.items() if key in event_class.model_fields and value is not None
|
||||
}
|
||||
fields.update(common)
|
||||
fields['data'] = dict(data)
|
||||
actor_id = data.get('servicer_userid') or customer_id
|
||||
|
||||
@@ -613,9 +613,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
incoming_message = event.incoming_message
|
||||
|
||||
markdown_enabled = self.config.get('markdown_card', False)
|
||||
text_message = platform_message.MessageChain([
|
||||
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
|
||||
])
|
||||
text_message = platform_message.MessageChain(
|
||||
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
|
||||
)
|
||||
content, at = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
|
||||
await self.bot.send_message(content, incoming_message, at)
|
||||
for part in message:
|
||||
@@ -647,9 +647,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
|
||||
if (msg_seq - 1) % 8 == 0 or is_final:
|
||||
markdown_enabled = self.config.get('markdown_card', False)
|
||||
text_message = platform_message.MessageChain([
|
||||
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
|
||||
])
|
||||
text_message = platform_message.MessageChain(
|
||||
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
|
||||
)
|
||||
content, at = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
|
||||
if not content and bot_message.content:
|
||||
content = bot_message.content # 兼容直接传入content的情况
|
||||
|
||||
@@ -66,7 +66,6 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
yiri_chain = await OmniMessageConverter.target2yiri(event, bot.bot_token)
|
||||
|
||||
if event.type == 'channel':
|
||||
|
||||
sender = platform_entities.GroupMember(
|
||||
id=event.user_id,
|
||||
member_name=str(event.sender_name),
|
||||
@@ -108,7 +107,10 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
||||
|
||||
bot = SlackClient(
|
||||
bot_token=config['bot_token'], signing_secret=config.get('signing_secret', ''), logger=logger, unified_mode=True
|
||||
bot_token=config['bot_token'],
|
||||
signing_secret=config.get('signing_secret', ''),
|
||||
logger=logger,
|
||||
unified_mode=True,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
@@ -174,6 +176,7 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
if self.config.get('socket_mode', False):
|
||||
await self.bot.run_socket(self.config['app_token'])
|
||||
return
|
||||
|
||||
# 统一 webhook 模式下,不启动独立的 Quart 应用
|
||||
# 保持运行但不启动独立端口
|
||||
async def keep_alive():
|
||||
|
||||
@@ -272,11 +272,14 @@ class SessionManager:
|
||||
"""获取会话"""
|
||||
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,
|
||||
persistence.get_db_engine(),
|
||||
execution_context.workspace_uuid,
|
||||
query.bot_uuid,
|
||||
f'{str(query.launcher_type.value).lower()}_{query.launcher_id}',
|
||||
)
|
||||
now = time.monotonic()
|
||||
|
||||
@@ -41,44 +41,87 @@ async def reconcile_execution_leases(engine, workspace_uuid, *, now=None, owner_
|
||||
conditions.extend([AgentRun.queue_name.is_(None), AgentRun.claimed_by_runtime_id.is_(None)])
|
||||
async with engine.begin() as connection:
|
||||
if engine.dialect.name == 'postgresql':
|
||||
await connection.execute(sa.text("SELECT set_config('langbot.workspace_uuid', :workspace, true)"),
|
||||
{'workspace': workspace_uuid})
|
||||
await connection.execute(sa.update(model).where(
|
||||
*conditions, model.execution_owner_id == owner_id,
|
||||
model.execution_lease_expires_at > now,
|
||||
).values(execution_lease_expires_at=now + datetime.timedelta(seconds=LEASE_SECONDS)))
|
||||
await connection.execute(
|
||||
sa.text("SELECT set_config('langbot.workspace_uuid', :workspace, true)"),
|
||||
{'workspace': workspace_uuid},
|
||||
)
|
||||
await connection.execute(
|
||||
sa.update(model)
|
||||
.where(
|
||||
*conditions,
|
||||
model.execution_owner_id == owner_id,
|
||||
model.execution_lease_expires_at > now,
|
||||
)
|
||||
.values(execution_lease_expires_at=now + datetime.timedelta(seconds=LEASE_SECONDS))
|
||||
)
|
||||
expired = [*conditions, model.execution_lease_expires_at <= now]
|
||||
rows = (await connection.execute(sa.select(identity, scope).where(*expired)
|
||||
.order_by(model.execution_lease_expires_at).limit(500))).all()
|
||||
rows = (
|
||||
await connection.execute(
|
||||
sa.select(identity, scope).where(*expired).order_by(model.execution_lease_expires_at).limit(500)
|
||||
)
|
||||
).all()
|
||||
if not rows:
|
||||
continue
|
||||
values = {'status': 'failed', 'status_reason': INTERRUPTED_REASON,
|
||||
'finished_at': now, 'updated_at': now} if model is AgentRun else {
|
||||
'status': 'error', 'level': 'error',
|
||||
}
|
||||
values = (
|
||||
{'status': 'failed', 'status_reason': INTERRUPTED_REASON, 'finished_at': now, 'updated_at': now}
|
||||
if model is AgentRun
|
||||
else {
|
||||
'status': 'error',
|
||||
'level': 'error',
|
||||
}
|
||||
)
|
||||
if model is MonitoringMessage:
|
||||
messages = (await connection.execute(sa.select(model).where(
|
||||
model.id.in_([row[0] for row in rows]),
|
||||
))).mappings().all()
|
||||
messages = (
|
||||
(
|
||||
await connection.execute(
|
||||
sa.select(model).where(
|
||||
model.id.in_([row[0] for row in rows]),
|
||||
)
|
||||
)
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
for message in messages:
|
||||
result = await connection.execute(sa.update(model).where(
|
||||
*expired, model.id == message['id'],
|
||||
).values(**values))
|
||||
result = await connection.execute(
|
||||
sa.update(model)
|
||||
.where(
|
||||
*expired,
|
||||
model.id == message['id'],
|
||||
)
|
||||
.values(**values)
|
||||
)
|
||||
changed += result.rowcount
|
||||
if result.rowcount:
|
||||
await connection.execute(sa.insert(MonitoringError).values(
|
||||
id=str(uuid.uuid4()), timestamp=now,
|
||||
error_type='ExecutionInterrupted', error_message=INTERRUPTED_REASON,
|
||||
message_id=message['id'],
|
||||
**{key: message[key] for key in (
|
||||
'workspace_uuid', 'bot_id', 'bot_name', 'pipeline_id',
|
||||
'pipeline_name', 'session_id',
|
||||
)},
|
||||
))
|
||||
await connection.execute(
|
||||
sa.insert(MonitoringError).values(
|
||||
id=str(uuid.uuid4()),
|
||||
timestamp=now,
|
||||
error_type='ExecutionInterrupted',
|
||||
error_message=INTERRUPTED_REASON,
|
||||
message_id=message['id'],
|
||||
**{
|
||||
key: message[key]
|
||||
for key in (
|
||||
'workspace_uuid',
|
||||
'bot_id',
|
||||
'bot_name',
|
||||
'pipeline_id',
|
||||
'pipeline_name',
|
||||
'session_id',
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = await connection.execute(sa.update(model).where(
|
||||
*expired, identity.in_([row[0] for row in rows]),
|
||||
).values(**values))
|
||||
result = await connection.execute(
|
||||
sa.update(model)
|
||||
.where(
|
||||
*expired,
|
||||
identity.in_([row[0] for row in rows]),
|
||||
)
|
||||
.values(**values)
|
||||
)
|
||||
changed += result.rowcount
|
||||
workspaces.update(row[1] for row in rows)
|
||||
for workspace in workspaces:
|
||||
@@ -92,7 +135,8 @@ async def maintain_execution_leases(ap):
|
||||
count = 0
|
||||
for binding in await ap.workspace_service.list_active_execution_bindings():
|
||||
count += await reconcile_execution_leases(
|
||||
ap.persistence_mgr.get_db_engine(), binding.workspace_uuid,
|
||||
ap.persistence_mgr.get_db_engine(),
|
||||
binding.workspace_uuid,
|
||||
)
|
||||
if count:
|
||||
ap.logger.info(f'Marked {count} interrupted executions as failed after host lease expiry')
|
||||
|
||||
Reference in New Issue
Block a user