mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-26 18:35:38 +08:00
fix(ci): align SDK dependency and validate acceptance migrations
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user