mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-20 23:45:22 +08:00
refactor: remove beta diagnostics collection and reporting (#2616)
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
@@ -94,7 +92,6 @@ class InteractionManager:
|
||||
self.ap = ap
|
||||
self.store = store or InteractionStore(ap.persistence_mgr.get_db_engine())
|
||||
|
||||
@diagnostics.observe('interaction', 'interaction.request', source='agent', stage='execute')
|
||||
async def handle_result(
|
||||
self,
|
||||
*,
|
||||
@@ -208,7 +205,6 @@ class InteractionManager:
|
||||
except Exception as exc:
|
||||
await self.store.mark_delivery_failed(run_id, request.interaction_id, str(exc))
|
||||
raise
|
||||
diagnostics.set_outcome('waiting', reason_code='waiting')
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
@@ -255,14 +251,12 @@ class InteractionManager:
|
||||
except pydantic.ValidationError:
|
||||
return
|
||||
|
||||
@diagnostics.observe('interaction', 'interaction.acknowledge', source='platform', stage='ack')
|
||||
async def acknowledge_submission(self, record: dict[str, typing.Any], adapter: typing.Any) -> None:
|
||||
"""Best-effort transition of submitted controls into a read-only state."""
|
||||
delivery_result = record.get('delivery_result')
|
||||
if not isinstance(delivery_result, dict) or not self._supports_platform_api(
|
||||
adapter, INTERACTION_ACKNOWLEDGE_API
|
||||
):
|
||||
diagnostics.set_outcome('skipped')
|
||||
return
|
||||
try:
|
||||
delivery_result = await adapter.call_platform_api(
|
||||
@@ -280,8 +274,6 @@ class InteractionManager:
|
||||
delivery_result,
|
||||
)
|
||||
except Exception as exc:
|
||||
diagnostics.set_outcome('failed', reason_code='response_error')
|
||||
diagnostics.annotate(error=exc)
|
||||
self._warning(f'Failed to acknowledge interaction submission: {exc}')
|
||||
|
||||
async def _find_update_target(
|
||||
@@ -332,7 +324,6 @@ class InteractionManager:
|
||||
f'interaction field {field.id} has duplicate option values',
|
||||
)
|
||||
|
||||
@diagnostics.observe('interaction', 'interaction.consume', source='platform', stage='dispatch')
|
||||
async def consume_callback(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -10,8 +10,6 @@ from langbot_plugin.api.entities.builtin.provider import message as provider_mes
|
||||
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
|
||||
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
|
||||
from ...telemetry import diagnostics as diagnostics
|
||||
from .reply_stream import ReplyStreamSession
|
||||
from ...core import app
|
||||
from ...api.http.context import ExecutionContext
|
||||
@@ -80,7 +78,6 @@ class AgentRunOrchestrator:
|
||||
self.journal = AgentRunJournal(ap)
|
||||
self._session_registry = get_session_registry()
|
||||
|
||||
@diagnostics.observe('run', 'runner.run', source='agent', stage='prepare')
|
||||
async def run(
|
||||
self,
|
||||
event: AgentEventEnvelope,
|
||||
@@ -106,8 +103,6 @@ class AgentRunOrchestrator:
|
||||
runner_id,
|
||||
bound_plugins,
|
||||
)
|
||||
|
||||
diagnostics.runner_metadata(self.ap, descriptor, binding.processor_type)
|
||||
usage = 'event' if binding.processor_type == 'event_processor' else 'agent'
|
||||
if usage not in descriptor.usages:
|
||||
raise ValueError(f'The selected Runner does not support {usage} usage')
|
||||
@@ -177,7 +172,6 @@ class AgentRunOrchestrator:
|
||||
|
||||
state_context = build_state_context(event, binding, descriptor)
|
||||
run_id = context['run_id']
|
||||
diagnostics.annotate(run_id=run_id, stage='execute')
|
||||
context['context']['available_apis']['reply_stream'] = hasattr(PluginToRuntimeAction, 'REPLY_STREAM') and any(
|
||||
tool.get('tool_name') == 'event_reply' and tool.get('tool_type') == 'platform'
|
||||
for tool in resources.get('tools', [])
|
||||
@@ -188,7 +182,6 @@ class AgentRunOrchestrator:
|
||||
source=(adapter_context or {}).get('_platform_event')
|
||||
or getattr((adapter_context or {}).get('_query'), 'message_event', None),
|
||||
)
|
||||
reply_streams.diagnostics = getattr(self.ap, 'diagnostics', None)
|
||||
available_apis = context.get('context', {}).get('available_apis')
|
||||
run_authorization = {
|
||||
'runner_id': descriptor.id,
|
||||
@@ -390,12 +383,6 @@ class AgentRunOrchestrator:
|
||||
terminal_status = 'cancelled'
|
||||
terminal_reason = run_snapshot.get('status_reason') or 'cancel_requested'
|
||||
break
|
||||
diagnostics.set_outcome(
|
||||
{'completed': 'succeeded', 'failed': 'failed', 'cancelled': 'cancelled'}.get(
|
||||
terminal_status, 'succeeded'
|
||||
),
|
||||
reason_code='runner_failed' if terminal_status == 'failed' else '',
|
||||
)
|
||||
await self.journal.finalize_run(
|
||||
run_id=run_id,
|
||||
status=terminal_status or 'completed',
|
||||
@@ -403,7 +390,6 @@ class AgentRunOrchestrator:
|
||||
usage=terminal_usage,
|
||||
)
|
||||
except Exception as exc:
|
||||
diagnostics.set_outcome('timeout' if self._is_deadline_exhausted(context) else 'failed')
|
||||
failed_usage = terminal_usage
|
||||
await self.journal.finalize_run(
|
||||
run_id=run_id,
|
||||
@@ -432,7 +418,6 @@ class AgentRunOrchestrator:
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@diagnostics.observe('lifecycle', 'runner.query_prepare', source='pipeline', stage='prepare')
|
||||
async def run_from_query(
|
||||
self,
|
||||
query: pipeline_query.Query,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...telemetry import diagnostics
|
||||
|
||||
import copy
|
||||
import fnmatch
|
||||
import typing
|
||||
@@ -669,7 +667,6 @@ def resolve_platform_api_call(session, bot_uuid, action, params, context_tool=No
|
||||
raise ValueError(f'Platform API {action} or its target is not authorized for this run')
|
||||
|
||||
|
||||
@diagnostics.observe('api', 'host.platform_tool', source='agent')
|
||||
async def execute_platform_tool(
|
||||
ap: typing.Any,
|
||||
execution_context: typing.Any,
|
||||
@@ -689,7 +686,6 @@ async def execute_platform_tool(
|
||||
normalized = _event_params(definition, context, normalized)
|
||||
# This flag is frozen by the Host from the synthetic debug envelope, not tool arguments.
|
||||
if delivery.get('surface') == 'webui' and (delivery.get('platform_capabilities') or {}).get('debug_mock') is True:
|
||||
diagnostics.annotate(source='webui_debug', attributes={'synthetic': True})
|
||||
result = _execute_mock_platform_tool(definition, context, normalized)
|
||||
if message_chain is not None:
|
||||
result['parameters']['message'] = message_chain.model_dump(mode='json')
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
import asyncio
|
||||
|
||||
@@ -138,9 +136,6 @@ class RunnerRegistry:
|
||||
permissions=typed_manifest.permissions,
|
||||
raw_manifest=manifest,
|
||||
)
|
||||
manager = getattr(self.ap, 'diagnostics', None)
|
||||
if isinstance(manager, diagnostics.DiagnosticsManager) and manager.enabled:
|
||||
diagnostics.declare_runner(descriptor)
|
||||
return descriptor
|
||||
|
||||
async def refresh(self, context: TenantContext) -> None:
|
||||
|
||||
@@ -7,8 +7,6 @@ from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from ...telemetry import diagnostics
|
||||
|
||||
import pydantic
|
||||
from langbot_plugin.api.entities.builtin.platform import events, message
|
||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
||||
@@ -56,9 +54,6 @@ class ReplyStreamSession:
|
||||
self._closed = False
|
||||
self._active: set[asyncio.Task] = set()
|
||||
|
||||
@diagnostics.observe(
|
||||
'delivery', 'reply_stream.apply', source='agent', fields=lambda b: {'attributes': {'stream': True}}
|
||||
)
|
||||
async def apply(self, request: ReplyStreamRequest) -> dict:
|
||||
task = asyncio.create_task(self._apply(request))
|
||||
self._active.add(task)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import typing
|
||||
@@ -172,7 +170,6 @@ class AgentRunSessionRegistry:
|
||||
'query_id': query_id,
|
||||
'execution_query': execution_query,
|
||||
'reply_streams': reply_streams,
|
||||
'_diagnostic_context': diagnostics.capture_context(),
|
||||
'plugin_identity': plugin_identity,
|
||||
'authorization': authorization,
|
||||
'status': {
|
||||
|
||||
@@ -28,7 +28,6 @@ from ..authz import (
|
||||
)
|
||||
from ..context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
|
||||
from ....cloud.support_admin import SupportAdminSessionError
|
||||
from ... import management_diagnostics as diagnostics
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ....core.app import Application
|
||||
@@ -238,7 +237,6 @@ class RouterGroup(abc.ABC):
|
||||
observed_at = time.monotonic()
|
||||
try:
|
||||
if request_context is not None:
|
||||
diagnostics.workspace(request_context)
|
||||
with bounded_executor.blocking_work_scope(request_context.workspace_uuid):
|
||||
persistence_mgr = getattr(
|
||||
self.ap,
|
||||
@@ -321,14 +319,7 @@ class RouterGroup(abc.ABC):
|
||||
)
|
||||
return self.internal_error_response(request_id)
|
||||
|
||||
# Observe outside authentication, using the registered Core handler
|
||||
# identity rather than the URL (which can contain user identifiers).
|
||||
new_f = diagnostics.observe(
|
||||
diagnostics.operation_id('http', f, rule=rule, methods=options.get('methods')),
|
||||
source='http',
|
||||
ap=self.ap,
|
||||
http=True,
|
||||
)(handler_error)
|
||||
new_f = handler_error
|
||||
# Quart/Flask requires a unique endpoint name even when the same URL
|
||||
# intentionally has separate handlers for different HTTP methods.
|
||||
# Include the method set so CRUD routes can declare distinct
|
||||
@@ -735,8 +726,6 @@ class RouterGroup(abc.ABC):
|
||||
|
||||
def fail(self, code: int | str, msg: str) -> quart.Response:
|
||||
"""Return an error response"""
|
||||
|
||||
diagnostics.outcome('failed')
|
||||
return quart.jsonify(
|
||||
{
|
||||
'code': code,
|
||||
|
||||
@@ -7,7 +7,6 @@ import contextlib
|
||||
import json
|
||||
|
||||
import quart
|
||||
from .... import management_diagnostics as diagnostics
|
||||
|
||||
from .....agent.runner.errors import (
|
||||
RunnerError,
|
||||
@@ -43,9 +42,6 @@ def debug_stream_response(service, context, agent_uuid: str, payload: dict) -> q
|
||||
)
|
||||
await queue.put({'kind': 'completed', 'data': result})
|
||||
except Exception as exc:
|
||||
# The stream still uses HTTP 200 when execution returns an
|
||||
# error frame. Mark the inherited request, not its contents.
|
||||
diagnostics.outcome('failed')
|
||||
if isinstance(exc, RunnerExecutionError):
|
||||
code, message = exc.error_code or 'runner_execution_failed', exc.message
|
||||
elif isinstance(exc, RunnerNotFoundError):
|
||||
|
||||
@@ -21,7 +21,6 @@ import httpx
|
||||
import quart
|
||||
|
||||
from ... import group
|
||||
from ..... import management_diagnostics as diagnostics
|
||||
from ......utils import httpclient, paths
|
||||
from ......platform.sources.websocket_manager import WebSocketScope, is_valid_session_id, ws_connection_manager
|
||||
from .websocket_chat import create_scoped_duplex_tasks, wait_for_duplex_tasks
|
||||
@@ -330,24 +329,20 @@ class EmbedRouterGroup(group.RouterGroup):
|
||||
# -- Embed WebSocket endpoint ----------------------------------------
|
||||
|
||||
@self.quart_app.websocket(self.path + '/<bot_uuid>/ws/connect')
|
||||
@diagnostics.observe('websocket.embed.session', source='websocket', ap=self.ap)
|
||||
async def embed_websocket_connect(bot_uuid: str):
|
||||
"""WebSocket connection for embed widget, keyed by bot_uuid."""
|
||||
await quart.websocket.accept()
|
||||
if not _is_valid_uuid(bot_uuid):
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Invalid bot_uuid format'}))
|
||||
diagnostics.outcome('rejected')
|
||||
return
|
||||
|
||||
runtime_bot, pipeline_uuid = await self._resolve_bot(bot_uuid)
|
||||
if runtime_bot is None:
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Bot not found or not available'}))
|
||||
diagnostics.outcome('rejected')
|
||||
return
|
||||
|
||||
session_type = quart.websocket.args.get('session_type', 'person')
|
||||
if session_type not in ['person', 'group']:
|
||||
diagnostics.outcome('rejected')
|
||||
await quart.websocket.send(
|
||||
json.dumps({'type': 'error', 'message': 'session_type must be person or group'})
|
||||
)
|
||||
@@ -356,7 +351,6 @@ class EmbedRouterGroup(group.RouterGroup):
|
||||
session_id = quart.websocket.args.get('session_id', '')
|
||||
if not is_valid_session_id(session_id):
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Valid session_id is required'}))
|
||||
diagnostics.outcome('rejected')
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -364,16 +358,12 @@ class EmbedRouterGroup(group.RouterGroup):
|
||||
await self._assert_execution_active(runtime_bot)
|
||||
except Exception:
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Unauthorized'}))
|
||||
diagnostics.outcome('rejected')
|
||||
return
|
||||
|
||||
diagnostics.workspace(runtime_bot.execution_context)
|
||||
try:
|
||||
proxy_bot = await self.ap.platform_mgr.get_websocket_proxy_bot(runtime_bot.execution_context)
|
||||
websocket_adapter = proxy_bot.adapter
|
||||
if not websocket_adapter:
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'WebSocket adapter not found'}))
|
||||
diagnostics.outcome('rejected')
|
||||
return
|
||||
|
||||
connection = await ws_connection_manager.add_connection(
|
||||
@@ -431,13 +421,11 @@ class EmbedRouterGroup(group.RouterGroup):
|
||||
try:
|
||||
await wait_for_duplex_tasks(receive_task, send_task)
|
||||
except Exception as e:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error(f'Embed WebSocket task error: {e}')
|
||||
finally:
|
||||
await ws_connection_manager.remove_connection(connection.connection_id)
|
||||
|
||||
except Exception as e:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error(f'Embed WebSocket connection error: {e}', exc_info=True)
|
||||
try:
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Internal server error'}))
|
||||
@@ -451,36 +439,30 @@ class EmbedRouterGroup(group.RouterGroup):
|
||||
while connection.is_active:
|
||||
message = await quart.websocket.receive()
|
||||
await ws_connection_manager.update_activity(connection.connection_id)
|
||||
try:
|
||||
data = await asyncio.to_thread(json.loads, message)
|
||||
message_type = data.get('type', 'message')
|
||||
|
||||
with diagnostics.scope(self.ap, 'websocket.embed.message', source='websocket'):
|
||||
diagnostics.workspace(getattr(owner_bot, 'execution_context', None))
|
||||
try:
|
||||
data = await asyncio.to_thread(json.loads, message)
|
||||
message_type = data.get('type', 'message')
|
||||
|
||||
if message_type == 'ping':
|
||||
await connection.send_queue.put(
|
||||
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
|
||||
)
|
||||
elif message_type == 'message':
|
||||
try:
|
||||
current_bot = await self._resolve_connected_bot(owner_bot, pipeline_uuid)
|
||||
except Exception:
|
||||
diagnostics.outcome('rejected')
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Bot is unavailable'})
|
||||
break
|
||||
await websocket_adapter.handle_websocket_message(connection, data, owner_bot=current_bot)
|
||||
elif message_type == 'disconnect':
|
||||
if message_type == 'ping':
|
||||
await connection.send_queue.put(
|
||||
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
|
||||
)
|
||||
elif message_type == 'message':
|
||||
try:
|
||||
current_bot = await self._resolve_connected_bot(owner_bot, pipeline_uuid)
|
||||
except Exception:
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Bot is unavailable'})
|
||||
break
|
||||
else:
|
||||
diagnostics.outcome('skipped')
|
||||
await websocket_adapter.handle_websocket_message(connection, data, owner_bot=current_bot)
|
||||
elif message_type == 'disconnect':
|
||||
break
|
||||
else:
|
||||
pass
|
||||
|
||||
except json.JSONDecodeError:
|
||||
diagnostics.outcome('rejected')
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
|
||||
except json.JSONDecodeError:
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
|
||||
|
||||
except Exception as e:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error(f'Embed receive error: {e}', exc_info=True)
|
||||
finally:
|
||||
connection.is_active = False
|
||||
@@ -496,13 +478,11 @@ class EmbedRouterGroup(group.RouterGroup):
|
||||
message = await asyncio.wait_for(connection.send_queue.get(), timeout=1.0)
|
||||
if message is None:
|
||||
break
|
||||
with diagnostics.scope(self.ap, 'websocket.embed.send', source='websocket'):
|
||||
encoded = await asyncio.to_thread(json.dumps, message)
|
||||
await quart.websocket.send(encoded)
|
||||
encoded = await asyncio.to_thread(json.dumps, message)
|
||||
await quart.websocket.send(encoded)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
except Exception as e:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error(f'Embed send error: {e}', exc_info=True)
|
||||
finally:
|
||||
connection.is_active = False
|
||||
|
||||
@@ -14,7 +14,6 @@ import quart
|
||||
from ....authz import Permission, permissions_for_role, require_permission
|
||||
from ....context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
|
||||
from ... import group
|
||||
from ..... import management_diagnostics as diagnostics
|
||||
from ......core.task_boundary import run_in_workspace_uow
|
||||
from ......platform.sources.websocket_manager import WebSocketScope, ws_connection_manager
|
||||
from ......utils import bounded_executor
|
||||
@@ -211,7 +210,6 @@ class WebSocketChatRouterGroup(group.RouterGroup):
|
||||
|
||||
async def initialize(self) -> None:
|
||||
@self.quart_app.websocket(self.path + '/connect')
|
||||
@diagnostics.observe('websocket.dashboard.session', source='websocket', ap=self.ap)
|
||||
async def websocket_connect(pipeline_uuid: str):
|
||||
"""Open one authenticated dashboard debug connection."""
|
||||
|
||||
@@ -219,14 +217,10 @@ class WebSocketChatRouterGroup(group.RouterGroup):
|
||||
try:
|
||||
request_context, token = await self._authenticate_websocket()
|
||||
except Exception:
|
||||
diagnostics.outcome('rejected')
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Unauthorized'}))
|
||||
return
|
||||
|
||||
diagnostics.workspace(request_context)
|
||||
session_type = quart.websocket.args.get('session_type', 'person')
|
||||
if session_type not in ['person', 'group']:
|
||||
diagnostics.outcome('rejected')
|
||||
await quart.websocket.send(
|
||||
json.dumps({'type': 'error', 'message': 'session_type must be person or group'})
|
||||
)
|
||||
@@ -235,7 +229,6 @@ class WebSocketChatRouterGroup(group.RouterGroup):
|
||||
try:
|
||||
websocket_adapter = await self._get_scoped_adapter(request_context, pipeline_uuid)
|
||||
if websocket_adapter is None:
|
||||
diagnostics.outcome('rejected')
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Pipeline not found'}))
|
||||
return
|
||||
|
||||
@@ -294,13 +287,11 @@ class WebSocketChatRouterGroup(group.RouterGroup):
|
||||
try:
|
||||
await wait_for_duplex_tasks(receive_task, send_task)
|
||||
except Exception as exc:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error(f'WebSocket task execution error: {exc}')
|
||||
finally:
|
||||
await ws_connection_manager.remove_connection(connection.connection_id)
|
||||
|
||||
except Exception:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error('Dashboard WebSocket connection error', exc_info=True)
|
||||
try:
|
||||
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Internal server error'}))
|
||||
@@ -417,35 +408,28 @@ class WebSocketChatRouterGroup(group.RouterGroup):
|
||||
while connection.is_active:
|
||||
message = await quart.websocket.receive()
|
||||
await ws_connection_manager.update_activity(connection.connection_id)
|
||||
|
||||
with diagnostics.scope(self.ap, 'websocket.dashboard.message', source='websocket'):
|
||||
diagnostics.workspace(request_context)
|
||||
try:
|
||||
data = await asyncio.to_thread(json.loads, message)
|
||||
message_type = data.get('type', 'message')
|
||||
if message_type == 'ping':
|
||||
await connection.send_queue.put(
|
||||
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
|
||||
)
|
||||
elif message_type == 'message':
|
||||
try:
|
||||
request_context = await self._revalidate_websocket_authorization(request_context, token)
|
||||
except Exception:
|
||||
diagnostics.outcome('rejected')
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Unauthorized'})
|
||||
break
|
||||
await websocket_adapter.handle_websocket_message(connection, data)
|
||||
elif message_type == 'disconnect':
|
||||
try:
|
||||
data = await asyncio.to_thread(json.loads, message)
|
||||
message_type = data.get('type', 'message')
|
||||
if message_type == 'ping':
|
||||
await connection.send_queue.put(
|
||||
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
|
||||
)
|
||||
elif message_type == 'message':
|
||||
try:
|
||||
request_context = await self._revalidate_websocket_authorization(request_context, token)
|
||||
except Exception:
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Unauthorized'})
|
||||
break
|
||||
else:
|
||||
diagnostics.outcome('skipped')
|
||||
logger.warning(f'Unknown WebSocket message type: {message_type}')
|
||||
except json.JSONDecodeError:
|
||||
diagnostics.outcome('rejected')
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
|
||||
await websocket_adapter.handle_websocket_message(connection, data)
|
||||
elif message_type == 'disconnect':
|
||||
break
|
||||
else:
|
||||
logger.warning(f'Unknown WebSocket message type: {message_type}')
|
||||
except json.JSONDecodeError:
|
||||
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
|
||||
|
||||
except Exception:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error('Dashboard WebSocket receive error', exc_info=True)
|
||||
finally:
|
||||
connection.is_active = False
|
||||
@@ -461,13 +445,11 @@ class WebSocketChatRouterGroup(group.RouterGroup):
|
||||
message = await asyncio.wait_for(connection.send_queue.get(), timeout=1.0)
|
||||
if message is None:
|
||||
break
|
||||
with diagnostics.scope(self.ap, 'websocket.dashboard.send', source='websocket'):
|
||||
encoded = await asyncio.to_thread(json.dumps, message)
|
||||
await quart.websocket.send(encoded)
|
||||
encoded = await asyncio.to_thread(json.dumps, message)
|
||||
await quart.websocket.send(encoded)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
except Exception:
|
||||
diagnostics.outcome('failed')
|
||||
logger.error('Dashboard WebSocket send error', exc_info=True)
|
||||
finally:
|
||||
connection.is_active = False
|
||||
|
||||
@@ -200,11 +200,14 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
if invitation_delivery_service is None:
|
||||
invitation_delivery_service = InvitationDeliveryService(self.ap)
|
||||
|
||||
# Detect beta flag from telemetry diagnostics manager
|
||||
is_beta = False
|
||||
diagnostics = getattr(self.ap, 'diagnostics', None)
|
||||
if diagnostics is not None:
|
||||
is_beta = getattr(diagnostics, 'beta', False)
|
||||
# Release channel is independent of diagnostics or usage telemetry.
|
||||
from packaging.version import Version, InvalidVersion
|
||||
|
||||
try:
|
||||
release = Version(constants.semantic_version)
|
||||
is_beta = bool(release.pre and release.pre[0] == 'b' and not release.local)
|
||||
except InvalidVersion:
|
||||
is_beta = False
|
||||
|
||||
# Detect Cloud deployment mode
|
||||
is_cloud = getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud'
|
||||
|
||||
@@ -6,7 +6,6 @@ import fnmatch
|
||||
import time
|
||||
import uuid
|
||||
import typing
|
||||
from ... import management_diagnostics as diagnostics
|
||||
|
||||
import sqlalchemy
|
||||
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
|
||||
@@ -135,7 +134,6 @@ class AgentService:
|
||||
|
||||
return None
|
||||
|
||||
@diagnostics.observe('http.agent.debug_agent', source='webui_debug')
|
||||
async def debug_agent(
|
||||
self,
|
||||
context: RequestContext,
|
||||
@@ -150,7 +148,6 @@ class AgentService:
|
||||
delivers outputs to a real platform, and supports both message and
|
||||
non-message event envelopes.
|
||||
"""
|
||||
diagnostics.workspace(context)
|
||||
agent = await self.get_agent(context, agent_uuid)
|
||||
if agent is None or agent.get('kind') not in {AGENT_KIND_AGENT, AGENT_KIND_EVENT_PROCESSOR}:
|
||||
raise ValueError('Agent not found')
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
"""Content-free management boundaries. Diagnostics never participate in I/O.
|
||||
|
||||
Only source-code identities and status categories enter these helpers. Never
|
||||
pass a URL, request, frame, token, tool argument, or serialized result to them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import contextvars
|
||||
import functools
|
||||
import re
|
||||
|
||||
import quart
|
||||
from quart.wrappers.response import IterableBody, ResponseBody
|
||||
|
||||
from ..telemetry import diagnostics as d
|
||||
from ..telemetry import diagnostic_privacy as privacy
|
||||
|
||||
_ACTIVE_BOUNDARY = contextvars.ContextVar('management_diagnostic_boundary', default=None)
|
||||
|
||||
|
||||
def operation_id(source, fn, *, rule='', methods=()):
|
||||
"""Called at registration with a Core function, never a client tool name."""
|
||||
module = fn.__module__.split('.groups.', 1)[-1]
|
||||
if source != 'http':
|
||||
module = ''
|
||||
name = fn.__name__
|
||||
if source == 'http' and name == '_':
|
||||
# Many Core routes use the anonymous function name `_`. Disambiguate
|
||||
# using ONLY the source-declared template and methods at registration.
|
||||
# This is never quart.request.path, url_rule, endpoint or request.method.
|
||||
template = re.sub(r'[^A-Za-z0-9_.:-]+', '.', rule).strip('.') or 'root'
|
||||
name = '.'.join((*sorted(methods or ('GET',)), template))
|
||||
return '.'.join(part for part in (source, module, name) if part)[:128]
|
||||
|
||||
|
||||
def outcome(value, reason_code='response_error'):
|
||||
try:
|
||||
d.set_outcome(value, reason_code=reason_code)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def workspace(context):
|
||||
"""Annotate only this boundary's owned span with a matching trusted context."""
|
||||
try:
|
||||
from .http.context import ExecutionContext, RequestContext
|
||||
|
||||
if not isinstance(context, (ExecutionContext, RequestContext)):
|
||||
return
|
||||
boundary = _ACTIVE_BOUNDARY.get()
|
||||
span = d.current_span()
|
||||
if boundary is None or span is None or span is not boundary.span:
|
||||
return
|
||||
if d._manager(boundary.ap) is not span.manager:
|
||||
return
|
||||
instance = getattr(
|
||||
getattr(boundary.ap, 'workspace_service', None),
|
||||
'instance_uuid',
|
||||
getattr(span.manager, 'instance_id', None),
|
||||
)
|
||||
# Structural recorders need not expose instance metadata. Real managers
|
||||
# do, and must never receive another instance's Workspace annotation.
|
||||
if instance is not None and instance != context.instance_uuid:
|
||||
return
|
||||
d.annotate(workspace_uuid=context.workspace_uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class Boundary:
|
||||
def __init__(self, ap, operation, source):
|
||||
self.ap = ap
|
||||
self.span = None
|
||||
try:
|
||||
manager = d._manager(ap)
|
||||
if manager is not None:
|
||||
fields = {'source': source, 'stage': 'execute'}
|
||||
if source == 'webui_debug':
|
||||
fields['attributes'] = {'synthetic': True}
|
||||
self.span = d.Span(manager, 'api', operation, fields)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@contextlib.contextmanager
|
||||
def activate(self):
|
||||
# None is an explicit inheritance barrier, not a no-op: disabled or
|
||||
# broken B work must not borrow enabled A's span through ContextVars.
|
||||
token = d._CURRENT.set(self.span)
|
||||
boundary_token = _ACTIVE_BOUNDARY.set(self)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_ACTIVE_BOUNDARY.reset(boundary_token)
|
||||
d._CURRENT.reset(token)
|
||||
|
||||
def finish(self, error=None):
|
||||
if self.span is not None:
|
||||
try:
|
||||
self.span.finish(error)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class _ObservedBody(ResponseBody):
|
||||
"""Carry the HTTP parent through iteration, not generator suspension."""
|
||||
|
||||
def __init__(self, body, boundary):
|
||||
self.body = body
|
||||
self.boundary = boundary
|
||||
self.iterator = None
|
||||
self.exhausted = False
|
||||
|
||||
async def __aenter__(self):
|
||||
try:
|
||||
with self.boundary.activate():
|
||||
entered = await self.body.__aenter__()
|
||||
self.iterator = entered.__aiter__()
|
||||
return self
|
||||
except BaseException as exc:
|
||||
self.boundary.finish(exc)
|
||||
raise
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, tb):
|
||||
try:
|
||||
with self.boundary.activate():
|
||||
result = await self.body.__aexit__(exc_type, exc_value, tb)
|
||||
except BaseException as exc:
|
||||
self.boundary.finish(exc)
|
||||
raise
|
||||
finally:
|
||||
self.boundary.finish(exc_value if exc_value is not None else (None if self.exhausted else GeneratorExit()))
|
||||
return result
|
||||
|
||||
async def __aiter__(self):
|
||||
try:
|
||||
if self.iterator is None:
|
||||
with self.boundary.activate():
|
||||
self.iterator = self.body.__aiter__()
|
||||
while True:
|
||||
try:
|
||||
with self.boundary.activate():
|
||||
value = await anext(self.iterator)
|
||||
except StopAsyncIteration:
|
||||
self.exhausted = True
|
||||
self.boundary.finish()
|
||||
return
|
||||
yield value
|
||||
except BaseException as exc:
|
||||
self.boundary.finish(exc)
|
||||
raise
|
||||
|
||||
|
||||
def _response_status(value):
|
||||
"""Inspect response metadata only; never read or deserialize a body."""
|
||||
response = value[0] if isinstance(value, tuple) else value
|
||||
status = response.status_code if isinstance(response, quart.Response) else 200
|
||||
if isinstance(value, tuple) and len(value) > 1 and type(value[1]) is int:
|
||||
status = value[1]
|
||||
if status in (401, 403):
|
||||
outcome('rejected')
|
||||
elif status >= 400:
|
||||
outcome('failed')
|
||||
elif isinstance(response, dict) and 'code' in response and response['code'] != 0:
|
||||
outcome('failed')
|
||||
return response
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def scope(ap, operation, *, source):
|
||||
"""A fixed operation around an existing non-generator statement block."""
|
||||
try:
|
||||
privacy.code_value('operation', operation)
|
||||
except Exception:
|
||||
pass
|
||||
boundary = Boundary(ap, operation, source)
|
||||
try:
|
||||
with boundary.activate():
|
||||
yield
|
||||
except BaseException as exc:
|
||||
boundary.finish(exc)
|
||||
raise
|
||||
finally:
|
||||
boundary.finish()
|
||||
|
||||
|
||||
def observe(operation, *, source, ap=None, http=False):
|
||||
# The operation is supplied by Core registration code, not request data.
|
||||
try:
|
||||
privacy.code_value('operation', operation)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def decorator(fn):
|
||||
@functools.wraps(fn)
|
||||
async def wrapped(*args, **kwargs):
|
||||
try:
|
||||
owner = (ap() if callable(ap) else ap) if ap is not None else getattr(args[0], 'ap', None)
|
||||
except Exception:
|
||||
owner = None
|
||||
boundary = Boundary(owner, operation, source)
|
||||
streaming = False
|
||||
try:
|
||||
with boundary.activate():
|
||||
value = await fn(*args, **kwargs)
|
||||
if http:
|
||||
try:
|
||||
response = _response_status(value)
|
||||
if isinstance(response, quart.Response) and isinstance(response.response, IterableBody):
|
||||
response.response = _ObservedBody(response.response, boundary)
|
||||
streaming = True
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
except BaseException as exc:
|
||||
boundary.finish(exc)
|
||||
raise
|
||||
finally:
|
||||
if not streaming:
|
||||
boundary.finish()
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
@@ -24,7 +24,6 @@ import uuid
|
||||
from ..http.context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
|
||||
from .context import bind_request_context, reset_request_context
|
||||
from .server import LangBotMCPServer
|
||||
from .. import management_diagnostics as diagnostics
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ...core import app as app_module
|
||||
@@ -86,7 +85,6 @@ class MCPMount:
|
||||
authenticate_api_key = self.ap.apikey_service.authenticate_api_key
|
||||
is_mcp_path = self._is_mcp_path
|
||||
|
||||
@diagnostics.observe('mcp.request', source='mcp', ap=self.ap)
|
||||
async def dispatch_mcp(scope, receive, send):
|
||||
# Authenticate MCP HTTP requests with a LangBot API key.
|
||||
api_key = _extract_api_key(scope.get('headers', []))
|
||||
@@ -96,7 +94,6 @@ class MCPMount:
|
||||
identity = await authenticate_api_key(api_key)
|
||||
|
||||
if identity is None:
|
||||
diagnostics.outcome('rejected')
|
||||
await send(
|
||||
{
|
||||
'type': 'http.response.start',
|
||||
@@ -123,7 +120,6 @@ class MCPMount:
|
||||
entitlement = await resolver.resolve(identity.workspace_uuid)
|
||||
entitlement_revision = entitlement.entitlement_revision
|
||||
except Exception:
|
||||
diagnostics.outcome('rejected')
|
||||
await send(
|
||||
{
|
||||
'type': 'http.response.start',
|
||||
@@ -151,7 +147,6 @@ class MCPMount:
|
||||
),
|
||||
entitlement_revision=entitlement_revision,
|
||||
)
|
||||
diagnostics.workspace(request_context)
|
||||
tenant_scope = getattr(self.ap.persistence_mgr, 'tenant_scope', None)
|
||||
if not callable(tenant_scope):
|
||||
raise RuntimeError('MCP request persistence scope is unavailable')
|
||||
|
||||
@@ -24,7 +24,6 @@ from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from ..http.authz import Permission, require_permission
|
||||
from .context import get_request_context
|
||||
from .. import management_diagnostics as diagnostics
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ...core import app as app_module
|
||||
@@ -53,7 +52,6 @@ def _dump(value: typing.Any) -> str:
|
||||
def _authorized(permission: Permission):
|
||||
context = get_request_context()
|
||||
require_permission(context, permission)
|
||||
diagnostics.workspace(context)
|
||||
return context
|
||||
|
||||
|
||||
@@ -443,7 +441,7 @@ class LangBotMCPServer:
|
||||
"""Register a tool boundary before its authorization and service call."""
|
||||
|
||||
def register(fn):
|
||||
observed = diagnostics.observe(diagnostics.operation_id('mcp', fn), source='mcp', ap=self.ap)(fn)
|
||||
observed = fn
|
||||
return self.mcp.tool(**options)(observed)
|
||||
|
||||
return register
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
@@ -164,7 +162,6 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
||||
def _uses_websocket(self) -> bool:
|
||||
return self.uses_websocket()
|
||||
|
||||
@diagnostics.observe('lifecycle', 'box.initialize', source='runtime', stage='execute')
|
||||
async def initialize(self) -> None:
|
||||
async with self._lifecycle_lock:
|
||||
if self._closing:
|
||||
|
||||
@@ -642,10 +642,6 @@ class Application:
|
||||
if self.telemetry is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self.telemetry.shutdown()
|
||||
diagnostics_manager = getattr(self, 'diagnostics', None)
|
||||
if diagnostics_manager is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await diagnostics_manager.shutdown()
|
||||
if self.vector_db_mgr is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self.vector_db_mgr.shutdown()
|
||||
|
||||
@@ -7,7 +7,6 @@ import os
|
||||
|
||||
from . import app
|
||||
from . import stage
|
||||
from ..telemetry import diagnostics
|
||||
from ..utils import constants, importutil
|
||||
|
||||
# Import startup stage implementation to register
|
||||
@@ -40,33 +39,7 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
|
||||
stage_cls = stage.preregistered_stages[stage_name]
|
||||
stage_inst = stage_cls()
|
||||
|
||||
if stage_name == 'GenKeysStage':
|
||||
# Optional diagnostics must not make startup depend on package
|
||||
# metadata, session markers, or its background transport.
|
||||
ap.diagnostics = None
|
||||
space_config = ap.instance_config.data.get('space', {})
|
||||
if not space_config.get('disable_telemetry', False) and not space_config.get(
|
||||
'disable_beta_diagnostics', False
|
||||
):
|
||||
manager = None
|
||||
try:
|
||||
manager = diagnostics.DiagnosticsManager(ap, marker_path='data/labels/beta_diagnostics_session')
|
||||
await manager.start_session()
|
||||
manager.start()
|
||||
ap.diagnostics = manager
|
||||
except BaseException as exc:
|
||||
if manager is not None:
|
||||
# Cleanup faults cannot replace the startup fault.
|
||||
try:
|
||||
await manager.shutdown(drain_timeout=0)
|
||||
except asyncio.CancelledError:
|
||||
if not isinstance(exc, asyncio.CancelledError):
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
if not isinstance(exc, Exception):
|
||||
raise
|
||||
await diagnostics.observe('lifecycle', 'startup.' + stage_name, source='startup', ap=ap)(stage_inst.run)(ap)
|
||||
await stage_inst.run(ap)
|
||||
|
||||
await ap.initialize()
|
||||
except BaseException:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import datetime
|
||||
import enum
|
||||
import sqlite3
|
||||
@@ -162,7 +160,6 @@ class PersistenceManager:
|
||||
default=None,
|
||||
)
|
||||
|
||||
@diagnostics.observe('lifecycle', 'persistence.initialize', source='startup', stage='execute')
|
||||
async def initialize(self):
|
||||
database_type = self.ap.instance_config.data.get('database', {}).get('use', 'sqlite')
|
||||
self.ap.logger.info(f'Initializing database type: {database_type}...')
|
||||
@@ -1658,7 +1655,6 @@ class PersistenceManager:
|
||||
|
||||
# =================================
|
||||
|
||||
@diagnostics.observe('lifecycle', 'persistence.run_alembic_migrations', source='startup', stage='execute')
|
||||
async def _run_alembic_migrations(self, target_revision: str = 'head'):
|
||||
"""Run the supported Alembic-based 4.x migrations."""
|
||||
from . import alembic_runner
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..telemetry import diagnostics
|
||||
|
||||
import dataclasses
|
||||
import typing
|
||||
import traceback
|
||||
@@ -180,7 +178,6 @@ class RuntimePipeline:
|
||||
raise WorkspaceInvariantError('RuntimePipeline instance does not match the active Workspace binding')
|
||||
return execution_context
|
||||
|
||||
@diagnostics.observe('run', 'pipeline.run', source='pipeline', fields=lambda b: {'processor_type': 'pipeline'})
|
||||
async def run(self, query: pipeline_query.Query):
|
||||
if (
|
||||
query.instance_uuid != self.execution_context.instance_uuid
|
||||
@@ -257,7 +254,6 @@ class RuntimePipeline:
|
||||
if result.console_notice:
|
||||
self.ap.logger.info(result.console_notice)
|
||||
if result.error_notice:
|
||||
diagnostics.set_outcome('failed', reason_code='response_error')
|
||||
self.ap.logger.error(result.error_notice)
|
||||
# Mark query as having error
|
||||
query.variables['_monitoring_has_error'] = True
|
||||
@@ -430,7 +426,6 @@ class RuntimePipeline:
|
||||
await self._assert_execution_active(query)
|
||||
|
||||
if event_ctx.is_prevented_default():
|
||||
diagnostics.set_outcome('skipped', reason_code='discarded')
|
||||
self.ap.logger.debug(
|
||||
f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}'
|
||||
)
|
||||
@@ -477,11 +472,8 @@ class RuntimePipeline:
|
||||
self.ap.logger.error(f'Failed to record query response: {e}')
|
||||
|
||||
except WorkspaceError as e:
|
||||
diagnostics.set_outcome('rejected', reason_code='processor_incompatible')
|
||||
self.ap.logger.info(f'Dropped query {query.query_id} because its Workspace execution binding is stale: {e}')
|
||||
except Exception as e:
|
||||
diagnostics.set_outcome('failed', reason_code='response_error')
|
||||
diagnostics.annotate(error=e)
|
||||
inst_name = query.current_stage_name if query.current_stage_name else 'unknown'
|
||||
self.ap.logger.error(f'Error processing query {query.query_id} stage={inst_name} : {e}')
|
||||
self.ap.logger.error(f'Traceback: {traceback.format_exc()}')
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import inspect
|
||||
@@ -310,7 +308,6 @@ class QueryPool:
|
||||
)
|
||||
object.__setattr__(query, 'query_uuid', query_uuid)
|
||||
object.__setattr__(query, '_execution_context', execution_context)
|
||||
object.__setattr__(query, '_diagnostic_context', diagnostics.capture_context())
|
||||
|
||||
self.queries.append(query)
|
||||
self.cached_queries[(execution_context.workspace_uuid, query_uuid)] = query
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
import typing
|
||||
@@ -96,7 +94,6 @@ class AiocqhttpAdapter(AiocqhttpAPIMixin, abstract_platform_adapter.AbstractPlat
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
@@ -149,7 +146,6 @@ class AiocqhttpAdapter(AiocqhttpAPIMixin, abstract_platform_adapter.AbstractPlat
|
||||
await self.logger.info(f'WebSocket connection established, bot id: {self.bot_account_id}')
|
||||
await self._dispatch_native_event(event)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: aiocqhttp.Event):
|
||||
self.bot_account_id = str(getattr(event, 'self_id', '') or self.bot_account_id)
|
||||
if getattr(event, 'type', None) == 'message' and str(getattr(event, 'user_id', '')) == self.bot_account_id:
|
||||
@@ -167,14 +163,12 @@ class AiocqhttpAdapter(AiocqhttpAPIMixin, abstract_platform_adapter.AbstractPlat
|
||||
except Exception:
|
||||
await self.logger.error(f'Error in aiocqhttp native event: {traceback.format_exc()}')
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _dispatch_native_event(self, event: aiocqhttp.Event):
|
||||
eba_event = await self.event_converter.target2yiri(event, self.bot, self.bot_account_id, self._lookup)
|
||||
if eba_event:
|
||||
await self._dispatch_eba_event(eba_event)
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
import aiocqhttp
|
||||
@@ -17,7 +15,6 @@ from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedErro
|
||||
class AiocqhttpAPIMixin:
|
||||
bot: aiocqhttp.CQHttp
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -38,7 +35,6 @@ class AiocqhttpAPIMixin:
|
||||
raise ValueError(f'Unsupported aiocqhttp target_type: {target_type}')
|
||||
return platform_events.MessageResult(message_id=(raw or {}).get('message_id'), raw=raw or {})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -53,7 +49,6 @@ class AiocqhttpAPIMixin:
|
||||
raw = await self.bot.send(message_source.source_platform_object, aiocq_msg)
|
||||
return platform_events.MessageResult(message_id=(raw or {}).get('message_id'), raw=raw or {})
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -62,7 +57,6 @@ class AiocqhttpAPIMixin:
|
||||
) -> None:
|
||||
await self.bot.delete_msg(message_id=int(message_id))
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -81,7 +75,6 @@ class AiocqhttpAPIMixin:
|
||||
raise ValueError(f'Unsupported aiocqhttp to_chat_type: {to_chat_type}')
|
||||
return platform_events.MessageResult(message_id=(raw or {}).get('message_id'), raw=raw or {})
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -107,7 +100,6 @@ class AiocqhttpAPIMixin:
|
||||
)
|
||||
return await AiocqhttpEventConverter.message_to_eba(event, self.bot)
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
raw = await self.bot.get_group_info(group_id=int(group_id))
|
||||
return platform_entities.UserGroup(
|
||||
@@ -116,7 +108,6 @@ class AiocqhttpAPIMixin:
|
||||
member_count=raw.get('member_count'),
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_group_list', source='platform', stage='accepted')
|
||||
async def get_group_list(self) -> list[platform_entities.UserGroup]:
|
||||
raw_list = await self.bot.get_group_list()
|
||||
return [
|
||||
@@ -128,7 +119,6 @@ class AiocqhttpAPIMixin:
|
||||
for item in raw_list
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -136,7 +126,6 @@ class AiocqhttpAPIMixin:
|
||||
raw_list = await self.bot.get_group_member_list(group_id=int(group_id))
|
||||
return [self._member_to_entity(item, group_id) for item in raw_list]
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -145,11 +134,9 @@ class AiocqhttpAPIMixin:
|
||||
raw = await self.bot.get_group_member_info(group_id=int(group_id), user_id=int(user_id), no_cache=True)
|
||||
return self._member_to_entity(raw, group_id)
|
||||
|
||||
@diagnostics.observe('api', 'set_group_name', source='platform', stage='accepted')
|
||||
async def set_group_name(self, group_id: typing.Union[int, str], name: str) -> None:
|
||||
await self.bot.set_group_name(group_id=int(group_id), group_name=name)
|
||||
|
||||
@diagnostics.observe('api', 'mute_member', source='platform', stage='accepted')
|
||||
async def mute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -158,19 +145,15 @@ class AiocqhttpAPIMixin:
|
||||
) -> None:
|
||||
await self.bot.set_group_ban(group_id=int(group_id), user_id=int(user_id), duration=int(duration))
|
||||
|
||||
@diagnostics.observe('api', 'unmute_member', source='platform', stage='accepted')
|
||||
async def unmute_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]) -> None:
|
||||
await self.bot.set_group_ban(group_id=int(group_id), user_id=int(user_id), duration=0)
|
||||
|
||||
@diagnostics.observe('api', 'kick_member', source='platform', stage='accepted')
|
||||
async def kick_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]) -> None:
|
||||
await self.bot.set_group_kick(group_id=int(group_id), user_id=int(user_id), reject_add_request=False)
|
||||
|
||||
@diagnostics.observe('api', 'leave_group', source='platform', stage='accepted')
|
||||
async def leave_group(self, group_id: typing.Union[int, str]) -> None:
|
||||
await self.bot.set_group_leave(group_id=int(group_id), is_dismiss=False)
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
raw = await self.bot.get_stranger_info(user_id=int(user_id), no_cache=True)
|
||||
return platform_entities.User(
|
||||
@@ -179,7 +162,6 @@ class AiocqhttpAPIMixin:
|
||||
avatar_url=raw.get('avatar_url'),
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
raw_list = await self.bot.get_friend_list()
|
||||
return [
|
||||
@@ -191,7 +173,6 @@ class AiocqhttpAPIMixin:
|
||||
for item in raw_list
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'approve_friend_request', source='platform', stage='accepted')
|
||||
async def approve_friend_request(
|
||||
self,
|
||||
request_id: typing.Union[int, str],
|
||||
@@ -200,15 +181,12 @@ class AiocqhttpAPIMixin:
|
||||
) -> None:
|
||||
await self.bot.set_friend_add_request(flag=str(request_id), approve=approve, remark=remark or '')
|
||||
|
||||
@diagnostics.observe('api', 'approve_group_invite', source='platform', stage='accepted')
|
||||
async def approve_group_invite(self, request_id: typing.Union[int, str], approve: bool = True) -> None:
|
||||
await self.bot.set_group_add_request(flag=str(request_id), sub_type='invite', approve=approve, reason='')
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
import aiocqhttp
|
||||
@@ -23,7 +21,6 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(
|
||||
event: aiocqhttp.Event,
|
||||
bot: aiocqhttp.CQHttp | None = None,
|
||||
@@ -42,7 +39,6 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
event: aiocqhttp.Event,
|
||||
bot: aiocqhttp.CQHttp | None = None,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import traceback
|
||||
import typing
|
||||
|
||||
@@ -135,7 +133,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
def _plain_message(text: str) -> platform_message.MessageChain:
|
||||
return platform_message.MessageChain([platform_message.Plain(text=text)])
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -152,7 +149,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
raise ValueError(f'Unsupported dingtalk target_type: {target_type}')
|
||||
return platform_events.MessageResult(raw=raw if isinstance(raw, dict) else {'result': raw})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -169,7 +165,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
raw=raw if isinstance(raw, dict) else {'result': raw},
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'reply_message_chunk', source='platform', stage='accepted')
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -193,7 +188,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
if is_final and bot_message.tool_calls is None:
|
||||
self.card_instance_id_dict.pop(message_id)
|
||||
|
||||
@diagnostics.observe('api', 'create_message_card', source='platform', stage='accepted')
|
||||
async def create_message_card(self, message_id, event):
|
||||
while len(self.card_instance_id_dict) >= 1000:
|
||||
self.card_instance_id_dict.pop(next(iter(self.card_instance_id_dict)), None)
|
||||
@@ -211,7 +205,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
return bool(self.config.get('enable-stream-reply', False))
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
if action == 'interaction.request' and action in self.get_supported_apis():
|
||||
return await send_interaction(self, params)
|
||||
@@ -267,7 +260,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
DingTalkCardCallbackHandler(self),
|
||||
)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: DingTalkEvent):
|
||||
try:
|
||||
interaction_event = interaction_event_from_native(event, self.interaction_callback_contexts)
|
||||
@@ -293,7 +285,6 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
await self.logger.error(f'Error in dingtalk native event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.libs.dingtalk_api.api import DingTalkClient
|
||||
@@ -16,7 +14,6 @@ class DingTalkAPIMixin:
|
||||
_user_cache: dict[str, platform_entities.User]
|
||||
_group_cache: dict[str, platform_entities.UserGroup]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -28,22 +25,18 @@ class DingTalkAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
return self._group_cache.get(str(group_id)) or platform_entities.UserGroup(id=group_id, name='')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_list', source='platform', stage='accepted')
|
||||
async def get_group_list(self) -> list[platform_entities.UserGroup]:
|
||||
return list(self._group_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[platform_entities.UserGroupMember]:
|
||||
raise NotSupportedError('get_group_member_list')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -59,18 +52,14 @@ class DingTalkAPIMixin:
|
||||
display_name=user.nickname,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
return self._user_cache.get(str(user_id)) or platform_entities.User(id=user_id, nickname='')
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
return await self.bot.get_file_url(file_id)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.libs.dingtalk_api.dingtalkevent import DingTalkEvent
|
||||
@@ -18,7 +16,6 @@ class DingTalkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(event: DingTalkEvent, bot_name: str) -> platform_events.Event | None:
|
||||
if event.conversation in {'FriendMessage', 'GroupMessage'}:
|
||||
return await DingTalkEventConverter.message_to_eba(event, bot_name)
|
||||
@@ -30,7 +27,6 @@ class DingTalkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return DingTalkEventConverter.platform_specific(event, f'message.{event.conversation or "unknown"}')
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
event: DingTalkEvent,
|
||||
bot_name: str,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
@@ -47,30 +45,10 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
adapter_self = self
|
||||
|
||||
class LangBotDiscordClient(discord.Client):
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_ready(self: discord.Client):
|
||||
adapter_self.bot_account_id = str(self.user.id) if self.user else ''
|
||||
await adapter_self.logger.info(f'Discord adapter running as {self.user}')
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_message(self: discord.Client, message: discord.Message):
|
||||
if self.user and message.author.id == self.user.id:
|
||||
return
|
||||
@@ -94,16 +72,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
except Exception:
|
||||
await adapter_self.logger.error(f'Error in discord on_message: {traceback.format_exc()}')
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_interaction(self: discord.Client, interaction: discord.Interaction):
|
||||
custom_id = (interaction.data or {}).get('custom_id') if isinstance(interaction.data, dict) else None
|
||||
try:
|
||||
@@ -123,46 +91,16 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
except Exception:
|
||||
await adapter_self.logger.error(f'Error in Discord interaction callback: {traceback.format_exc()}')
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_message_edit(self: discord.Client, before: discord.Message, after: discord.Message):
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'message_edit', (before, after), self.user.id if self.user else None
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_message_delete(self: discord.Client, message: discord.Message):
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'message_delete', message, self.user.id if self.user else None
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_raw_message_delete(self: discord.Client, payload: discord.RawMessageDeleteEvent):
|
||||
await adapter_self._dispatch_gateway_tuple(
|
||||
'raw_message_delete',
|
||||
@@ -170,16 +108,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
self.user.id if self.user else None,
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_reaction_add(
|
||||
self: discord.Client, reaction: discord.Reaction, user: discord.User | discord.Member
|
||||
):
|
||||
@@ -189,16 +117,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
'reaction_add', (reaction, user), self.user.id if self.user else None
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_reaction_remove(
|
||||
self: discord.Client, reaction: discord.Reaction, user: discord.User | discord.Member
|
||||
):
|
||||
@@ -208,16 +126,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
'reaction_remove', (reaction, user), self.user.id if self.user else None
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_raw_reaction_add(self: discord.Client, payload: discord.RawReactionActionEvent):
|
||||
if self.user and payload.user_id == self.user.id:
|
||||
return
|
||||
@@ -227,16 +135,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
self.user.id if self.user else None,
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_raw_reaction_remove(self: discord.Client, payload: discord.RawReactionActionEvent):
|
||||
if self.user and payload.user_id == self.user.id:
|
||||
return
|
||||
@@ -246,55 +144,15 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
self.user.id if self.user else None,
|
||||
)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_member_join(self: discord.Client, member: discord.Member):
|
||||
await adapter_self._dispatch_gateway_tuple('member_join', member, self.user.id if self.user else None)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_member_remove(self: discord.Client, member: discord.Member):
|
||||
await adapter_self._dispatch_gateway_tuple('member_remove', member, self.user.id if self.user else None)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_guild_join(self: discord.Client, guild: discord.Guild):
|
||||
await adapter_self._dispatch_gateway_tuple('guild_join', guild, self.user.id if self.user else None)
|
||||
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_guild_remove(self: discord.Client, guild: discord.Guild):
|
||||
await adapter_self._dispatch_gateway_tuple('guild_remove', guild, self.user.id if self.user else None)
|
||||
|
||||
@@ -352,7 +210,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
def get_interaction_capabilities(self) -> dict[str, typing.Any]:
|
||||
return interaction_delivery_capabilities()
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||
content, files = await self.message_converter.yiri2target(message)
|
||||
channel = await self._get_channel(target_id)
|
||||
@@ -362,7 +219,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
sent = await channel.send(**kwargs)
|
||||
return platform_events.MessageResult(message_id=sent.id, raw={'message_id': sent.id})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -380,7 +236,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
sent = await message_source.source_platform_object.channel.send(**kwargs)
|
||||
return platform_events.MessageResult(message_id=sent.id, raw={'message_id': sent.id})
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _dispatch_gateway_tuple(self, kind: str, payload, bot_user_id: int | None):
|
||||
try:
|
||||
event = await self.event_converter.target2yiri((kind, payload), bot_user_id)
|
||||
@@ -390,7 +245,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
await self.logger.error(f'Error in discord {kind}: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
@@ -415,7 +269,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
):
|
||||
self.listeners.pop(event_type, None)
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
if action == 'interaction.request':
|
||||
return await send_interaction(self, params)
|
||||
@@ -437,7 +290,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
return True
|
||||
|
||||
@diagnostics.observe('api', 'create_message_card', source='platform', stage='accepted')
|
||||
async def create_message_card(self, message_id: str, event: platform_events.MessageEvent) -> bool:
|
||||
"""Set up a stream context for progressive editing.
|
||||
|
||||
@@ -457,7 +309,6 @@ class DiscordAdapter(DiscordAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
}
|
||||
return True
|
||||
|
||||
@diagnostics.observe('api', 'reply_message_chunk', source='platform', stage='accepted')
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
@@ -17,7 +15,6 @@ from langbot_plugin.api.entities.builtin.platform import message as platform_mes
|
||||
class DiscordAPIMixin:
|
||||
bot: discord.Client
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -34,7 +31,6 @@ class DiscordAPIMixin:
|
||||
return
|
||||
await message.edit(content=content)
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -45,7 +41,6 @@ class DiscordAPIMixin:
|
||||
message = await channel.fetch_message(int(message_id))
|
||||
await message.delete()
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -61,12 +56,10 @@ class DiscordAPIMixin:
|
||||
sent = await to_channel.send(content=message.content, files=files)
|
||||
return platform_events.MessageResult(message_id=sent.id, raw={'message_id': sent.id})
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
guild = await self._get_guild(group_id)
|
||||
return DiscordEventConverter.group_from_guild(guild)
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -75,7 +68,6 @@ class DiscordAPIMixin:
|
||||
members = guild.members or [member async for member in guild.fetch_members(limit=None)]
|
||||
return [self._member_to_entity(member) for member in members]
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -85,22 +77,18 @@ class DiscordAPIMixin:
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
return self._member_to_entity(member)
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
user = self.bot.get_user(int(user_id)) or await self.bot.fetch_user(int(user_id))
|
||||
return DiscordEventConverter.user_from_author(user)
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
|
||||
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
return file_id
|
||||
|
||||
@diagnostics.observe('api', 'mute_member', source='platform', stage='accepted')
|
||||
async def mute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -114,7 +102,6 @@ class DiscordAPIMixin:
|
||||
until = datetime.datetime.now(datetime.UTC) + datetime.timedelta(seconds=duration)
|
||||
await member.timeout(until, reason='LangBot Omni mute_member')
|
||||
|
||||
@diagnostics.observe('api', 'unmute_member', source='platform', stage='accepted')
|
||||
async def unmute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -124,7 +111,6 @@ class DiscordAPIMixin:
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
await member.timeout(None, reason='LangBot Omni unmute_member')
|
||||
|
||||
@diagnostics.observe('api', 'kick_member', source='platform', stage='accepted')
|
||||
async def kick_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -134,7 +120,6 @@ class DiscordAPIMixin:
|
||||
member = guild.get_member(int(user_id)) or await guild.fetch_member(int(user_id))
|
||||
await member.kick(reason='LangBot Omni kick_member')
|
||||
|
||||
@diagnostics.observe('api', 'leave_group', source='platform', stage='accepted')
|
||||
async def leave_group(self, group_id: typing.Union[int, str]) -> None:
|
||||
guild = await self._get_guild(group_id)
|
||||
await guild.leave()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
import discord
|
||||
@@ -18,7 +16,6 @@ class DiscordEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(event: typing.Any, bot_user_id: int | None = None) -> platform_events.Event | None:
|
||||
if isinstance(event, discord.Message):
|
||||
return await DiscordEventConverter.message_to_eba(event)
|
||||
@@ -241,7 +238,6 @@ class DiscordEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(message: discord.Message) -> platform_events.FriendMessage | platform_events.GroupMessage:
|
||||
message_chain = await DiscordMessageConverter.target2yiri(message)
|
||||
if isinstance(message.channel, discord.DMChannel):
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
from langbot.pkg.platform.sources.kook import _decode_gateway_message
|
||||
|
||||
import asyncio
|
||||
@@ -101,7 +99,6 @@ class KookAdapter(KookAPIMixin, BasePlatformAdapter):
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
@@ -171,7 +168,6 @@ class KookAdapter(KookAPIMixin, BasePlatformAdapter):
|
||||
self.session_id = str(data.get('session_id') or '')
|
||||
await self.logger.info(f'KOOK WebSocket HELLO received, session_id: {self.session_id}')
|
||||
|
||||
@diagnostics.observe('event', 'platform.receive', source='platform', stage='accepted')
|
||||
async def _handle_event(self, data: dict, sn: int):
|
||||
self.current_sn = max(self.current_sn, sn)
|
||||
|
||||
@@ -196,13 +192,10 @@ class KookAdapter(KookAPIMixin, BasePlatformAdapter):
|
||||
if eba_event:
|
||||
self._cache_event(eba_event)
|
||||
await self._dispatch_eba_event(eba_event)
|
||||
except Exception as exc:
|
||||
diagnostics.annotate(error=exc)
|
||||
diagnostics.set_outcome('failed')
|
||||
except Exception:
|
||||
await self.logger.error(f'Error handling KOOK event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.pkg.platform.adapters.kook.message_converter import KookMessageConverter
|
||||
@@ -16,7 +14,6 @@ class KookAPIMixin:
|
||||
_user_cache: dict[str, platform_entities.User]
|
||||
_group_cache: dict[str, platform_entities.UserGroup]
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -37,7 +34,6 @@ class KookAPIMixin:
|
||||
data = raw.get('data') or {}
|
||||
return platform_events.MessageResult(message_id=data.get('msg_id'), raw=raw)
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -77,7 +73,6 @@ class KookAPIMixin:
|
||||
data = raw.get('data') or {}
|
||||
return platform_events.MessageResult(message_id=data.get('msg_id'), raw=raw)
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -89,7 +84,6 @@ class KookAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
cached = self._group_cache.get(str(group_id))
|
||||
if cached:
|
||||
@@ -102,18 +96,15 @@ class KookAPIMixin:
|
||||
member_count=data.get('user_count'),
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_group_list', source='platform', stage='accepted')
|
||||
async def get_group_list(self) -> list[platform_entities.UserGroup]:
|
||||
return list(self._group_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[platform_entities.UserGroupMember]:
|
||||
raise NotSupportedError('get_group_member_list')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -137,7 +128,6 @@ class KookAPIMixin:
|
||||
display_name=user.nickname,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
cached = self._user_cache.get(str(user_id))
|
||||
if cached:
|
||||
@@ -152,22 +142,18 @@ class KookAPIMixin:
|
||||
is_bot=bool(data.get('bot', False)),
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
data = {'file': file_data}
|
||||
raw = await self._request('POST', '/asset/create', data=data, filename=filename)
|
||||
result = raw.get('data') or {}
|
||||
return str(result.get('url') or result.get('id') or '')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
return file_id
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -177,7 +163,6 @@ class KookAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -187,7 +172,6 @@ class KookAPIMixin:
|
||||
endpoint = '/message/delete' if str(chat_type).lower() in {'group', 'channel'} else '/direct-message/delete'
|
||||
await self._request('POST', endpoint, json={'msg_id': str(message_id)})
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -201,7 +185,6 @@ class KookAPIMixin:
|
||||
raise NotSupportedError('forward_message:message_not_cached')
|
||||
return await self.send_message(to_chat_type, str(to_chat_id), cached.message_chain)
|
||||
|
||||
@diagnostics.observe('api', 'mute_member', source='platform', stage='accepted')
|
||||
async def mute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -210,7 +193,6 @@ class KookAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('mute_member')
|
||||
|
||||
@diagnostics.observe('api', 'unmute_member', source='platform', stage='accepted')
|
||||
async def unmute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -218,7 +200,6 @@ class KookAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('unmute_member')
|
||||
|
||||
@diagnostics.observe('api', 'kick_member', source='platform', stage='accepted')
|
||||
async def kick_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -226,6 +207,5 @@ class KookAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('kick_member')
|
||||
|
||||
@diagnostics.observe('api', 'leave_group', source='platform', stage='accepted')
|
||||
async def leave_group(self, group_id: typing.Union[int, str]) -> None:
|
||||
raise NotSupportedError('leave_group')
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import time
|
||||
|
||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||
@@ -18,7 +16,6 @@ class KookEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(kook_event: dict, bot_account_id: str = '') -> platform_events.Event | None:
|
||||
event_type = int(kook_event.get('type', 0) or 0)
|
||||
channel_type = kook_event.get('channel_type')
|
||||
@@ -58,7 +55,6 @@ class KookEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
kook_event: dict, bot_account_id: str = ''
|
||||
) -> platform_events.FriendMessage | platform_events.GroupMessage:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
from langbot.pkg.platform.sources.lark import (
|
||||
LarkAdapter as LegacyLarkAdapter,
|
||||
NonBlockingLarkWSClient,
|
||||
@@ -188,16 +186,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
self.request_app_ticket()
|
||||
|
||||
def _build_event_handler(self):
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
||||
await self._handle_message_event(event)
|
||||
|
||||
@@ -338,7 +326,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
self.request_tenant_access_token(tenant_key)
|
||||
return self.tenant_access_tokens.get(tenant_key, {}).get('token')
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -372,7 +359,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
message_id=message_ids[-1] if message_ids else '', raw={'message_ids': message_ids}
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -449,7 +435,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
while len(self.pending_monitoring_msg) > 1000:
|
||||
self.pending_monitoring_msg.pop(next(iter(self.pending_monitoring_msg)), None)
|
||||
|
||||
@diagnostics.observe('api', 'create_message_card', source='platform', stage='accepted')
|
||||
async def create_message_card(self, message_id, event) -> bool:
|
||||
card_id = await self.create_card_id(message_id)
|
||||
content = {'type': 'card', 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}}
|
||||
@@ -554,7 +539,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
raise RuntimeError(f'Lark card update failed: {response.code} {response.msg}')
|
||||
self.closed_streaming_cards.add(card_id)
|
||||
|
||||
@diagnostics.observe('api', 'reply_message_chunk', source='platform', stage='accepted')
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -605,7 +589,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
self.card_last_update_dict.pop(card_id, None)
|
||||
self.closed_streaming_cards.discard(card_id)
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
if action == 'interaction.request':
|
||||
return await send_interaction(self, params)
|
||||
@@ -750,7 +733,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
async def is_muted(self, group_id: int | None = None) -> bool:
|
||||
return False
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_message_event(self, event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
||||
try:
|
||||
if platform_events.FriendMessage in self.listeners or platform_events.GroupMessage in self.listeners:
|
||||
@@ -765,7 +747,6 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
||||
await self.logger.error(f'Error in lark message event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.Event):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from lark_oapi.api.im.v1 import GetChatRequest, GetMessageRequest
|
||||
@@ -19,7 +17,6 @@ class LarkAPIMixin:
|
||||
_user_cache: dict[str, platform_entities.User]
|
||||
_group_cache: dict[str, platform_entities.UserGroup]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -55,7 +52,6 @@ class LarkAPIMixin:
|
||||
self._message_cache[str(message_id)] = event
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
cached = self._group_cache.get(str(group_id))
|
||||
if cached:
|
||||
@@ -75,7 +71,6 @@ class LarkAPIMixin:
|
||||
self._group_cache[str(group.id)] = group
|
||||
return group
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -84,14 +79,12 @@ class LarkAPIMixin:
|
||||
user = self._user_cache.get(str(user_id)) or platform_entities.User(id=user_id)
|
||||
return platform_entities.UserGroupMember(user=user, group_id=group_id, role=platform_entities.MemberRole.MEMBER)
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
cached = self._user_cache.get(str(user_id))
|
||||
if cached:
|
||||
return cached
|
||||
return platform_entities.User(id=user_id)
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
if str(file_id).startswith('file://'):
|
||||
return str(file_id)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import time
|
||||
import typing
|
||||
|
||||
@@ -26,7 +24,6 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(
|
||||
event: lark_oapi.im.v1.P2ImMessageReceiveV1,
|
||||
api_client: lark_oapi.Client,
|
||||
@@ -34,7 +31,6 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return await LarkEventConverter.message_to_eba(event, api_client)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
event: lark_oapi.im.v1.P2ImMessageReceiveV1,
|
||||
api_client: lark_oapi.Client,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
import typing
|
||||
@@ -96,7 +94,6 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -105,7 +102,6 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
|
||||
) -> platform_events.MessageResult:
|
||||
raise NotSupportedError('send_message:official_account_requires_inbound_webhook_reply')
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -122,7 +118,6 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
|
||||
await self.bot.set_message(source.message_id, content)
|
||||
return platform_events.MessageResult(message_id=source.message_id, raw={'queued': True})
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
@@ -175,7 +170,6 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
|
||||
for msg_type in ('text', 'image', 'voice', 'event'):
|
||||
self.bot.on_message(msg_type)(self._handle_native_event)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: OAEvent):
|
||||
self.bot_account_id = event.receiver_id or self.bot_account_id
|
||||
try:
|
||||
@@ -192,7 +186,6 @@ class OfficialAccountAdapter(OfficialAccountAPIMixin, abstract_platform_adapter.
|
||||
await self.logger.error(f'Error in officialaccount native event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
@@ -14,7 +12,6 @@ class OfficialAccountAPIMixin:
|
||||
_message_cache: dict[str, platform_events.MessageReceivedEvent]
|
||||
_user_cache: dict[str, platform_entities.User]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -26,18 +23,15 @@ class OfficialAccountAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
user = self._user_cache.get(str(user_id))
|
||||
if user is None:
|
||||
raise NotSupportedError('get_user_info:not_cached')
|
||||
return user
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -47,7 +41,6 @@ class OfficialAccountAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -56,7 +49,6 @@ class OfficialAccountAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('delete_message')
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -67,30 +59,24 @@ class OfficialAccountAPIMixin:
|
||||
) -> platform_events.MessageResult:
|
||||
raise NotSupportedError('forward_message')
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
raise NotSupportedError('get_group_info')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_list', source='platform', stage='accepted')
|
||||
async def get_group_list(self) -> list[platform_entities.UserGroup]:
|
||||
raise NotSupportedError('get_group_list')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[platform_entities.UserGroupMember]:
|
||||
raise NotSupportedError('get_group_member_list')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import time
|
||||
import typing
|
||||
|
||||
@@ -18,7 +16,6 @@ class OfficialAccountEventConverter(abstract_platform_adapter.AbstractEventConve
|
||||
async def yiri2target(event: platform_events.Event) -> typing.Any:
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(self, event: OAEvent) -> platform_events.FriendMessage | None:
|
||||
eba_event = await self.target2yiri(event)
|
||||
if not isinstance(eba_event, platform_events.MessageReceivedEvent):
|
||||
@@ -34,7 +31,6 @@ class OfficialAccountEventConverter(abstract_platform_adapter.AbstractEventConve
|
||||
source_platform_object=event,
|
||||
)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(self, event: OAEvent) -> platform_events.Event | None:
|
||||
if event.type in {'text', 'image', 'voice'}:
|
||||
return await self.message_to_eba(event)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
@@ -134,7 +132,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
def _plain_message(text: str) -> platform_message.MessageChain:
|
||||
return platform_message.MessageChain([platform_message.Plain(text=text)])
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -146,7 +143,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
)
|
||||
return platform_events.MessageResult(raw={'results': raw})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -165,7 +161,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
)
|
||||
return platform_events.MessageResult(message_id=source.d_id or source.id, raw={'results': raw})
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
if action == 'interaction.request':
|
||||
return await send_interaction(self, params)
|
||||
@@ -263,7 +258,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
return bool(self.config.get('enable-stream-reply') or self.config.get('enable_stream_reply'))
|
||||
|
||||
@diagnostics.observe('api', 'create_message_card', source='platform', stage='accepted')
|
||||
async def create_message_card(self, message_id: str, event: platform_events.MessageEvent) -> bool:
|
||||
source = event.source_platform_object
|
||||
if not isinstance(source, QQOfficialEvent) or source.t != 'C2C_MESSAGE_CREATE':
|
||||
@@ -283,7 +277,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
self._stream_ctx_ts[message_id] = time.time()
|
||||
return True
|
||||
|
||||
@diagnostics.observe('api', 'reply_message_chunk', source='platform', stage='accepted')
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -371,7 +364,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
if event is not None:
|
||||
await self._dispatch_eba_event(event)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: QQOfficialEvent):
|
||||
self.bot_account_id = self.config.get('appid', self.bot_account_id)
|
||||
try:
|
||||
@@ -388,7 +380,6 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
||||
await self.logger.error(f'Error in qqofficial native event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.pkg.platform.adapters.qqofficial.errors import NotSupportedError
|
||||
@@ -16,7 +14,6 @@ class QQOfficialAPIMixin:
|
||||
_group_cache: dict[str, platform_entities.UserGroup]
|
||||
_member_cache: dict[tuple[str, str], platform_entities.UserGroupMember]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -28,25 +25,21 @@ class QQOfficialAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
user = self._user_cache.get(str(user_id))
|
||||
if user is None:
|
||||
raise NotSupportedError('get_user_info:not_cached')
|
||||
return user
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
group = self._group_cache.get(str(group_id))
|
||||
if group is None:
|
||||
raise NotSupportedError('get_group_info:not_cached')
|
||||
return group
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -57,7 +50,6 @@ class QQOfficialAPIMixin:
|
||||
raise NotSupportedError('get_group_member_info:not_cached')
|
||||
return member
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -66,7 +58,6 @@ class QQOfficialAPIMixin:
|
||||
member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id)
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -76,7 +67,6 @@ class QQOfficialAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -85,7 +75,6 @@ class QQOfficialAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('delete_message')
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -96,26 +85,20 @@ class QQOfficialAPIMixin:
|
||||
) -> platform_events.MessageResult:
|
||||
raise NotSupportedError('forward_message')
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
@diagnostics.observe('api', 'mute_member', source='platform', stage='accepted')
|
||||
async def mute_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str], duration: int = 0):
|
||||
raise NotSupportedError('mute_member')
|
||||
|
||||
@diagnostics.observe('api', 'unmute_member', source='platform', stage='accepted')
|
||||
async def unmute_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]):
|
||||
raise NotSupportedError('unmute_member')
|
||||
|
||||
@diagnostics.observe('api', 'kick_member', source='platform', stage='accepted')
|
||||
async def kick_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]):
|
||||
raise NotSupportedError('kick_member')
|
||||
|
||||
@diagnostics.observe('api', 'leave_group', source='platform', stage='accepted')
|
||||
async def leave_group(self, group_id: typing.Union[int, str]):
|
||||
raise NotSupportedError('leave_group')
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import typing
|
||||
@@ -33,7 +31,6 @@ class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter)
|
||||
async def yiri2target(event: platform_events.Event) -> typing.Any:
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
self, event: QQOfficialEvent
|
||||
) -> platform_events.FriendMessage | platform_events.GroupMessage | None:
|
||||
@@ -68,7 +65,6 @@ class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter)
|
||||
source_platform_object=event,
|
||||
)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(self, event: QQOfficialEvent) -> platform_events.Event:
|
||||
if event.t in MESSAGE_EVENT_TYPES:
|
||||
return await self.message_to_eba(event)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
import typing
|
||||
@@ -92,7 +90,6 @@ class SlackAdapter(SlackAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -103,7 +100,6 @@ class SlackAdapter(SlackAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
raw = await self._send_text(str(target_type), str(target_id), content)
|
||||
return platform_events.MessageResult(raw=raw)
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -118,7 +114,6 @@ class SlackAdapter(SlackAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
raw = await self._send_text(target_type, target_id, await SlackMessageConverter.yiri2target(message))
|
||||
return platform_events.MessageResult(message_id=source.message_id, raw=raw)
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
@@ -167,7 +162,6 @@ class SlackAdapter(SlackAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
for msg_type in ('im', 'channel'):
|
||||
self.bot.on_message(msg_type)(self._handle_native_event)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: SlackEvent):
|
||||
try:
|
||||
if platform_events.FriendMessage in self.listeners or platform_events.GroupMessage in self.listeners:
|
||||
@@ -183,7 +177,6 @@ class SlackAdapter(SlackAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
await self.logger.error(f'Error in slack native event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.pkg.platform.adapters.slack.errors import NotSupportedError
|
||||
@@ -16,7 +14,6 @@ class SlackAPIMixin:
|
||||
_group_cache: dict[str, platform_entities.UserGroup]
|
||||
_member_cache: dict[tuple[str, str], platform_entities.UserGroupMember]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -28,29 +25,24 @@ class SlackAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
user = self._user_cache.get(str(user_id))
|
||||
if user is None:
|
||||
raise NotSupportedError('get_user_info:not_cached')
|
||||
return user
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
group = self._group_cache.get(str(group_id))
|
||||
if group is None:
|
||||
raise NotSupportedError('get_group_info:not_cached')
|
||||
return group
|
||||
|
||||
@diagnostics.observe('api', 'get_group_list', source='platform', stage='accepted')
|
||||
async def get_group_list(self) -> list[platform_entities.UserGroup]:
|
||||
return list(self._group_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -59,7 +51,6 @@ class SlackAPIMixin:
|
||||
member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id)
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -70,7 +61,6 @@ class SlackAPIMixin:
|
||||
raise NotSupportedError('get_group_member_info:not_cached')
|
||||
return member
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -80,7 +70,6 @@ class SlackAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -89,7 +78,6 @@ class SlackAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('delete_message')
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -100,10 +88,8 @@ class SlackAPIMixin:
|
||||
) -> platform_events.MessageResult:
|
||||
raise NotSupportedError('forward_message')
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import time
|
||||
import typing
|
||||
|
||||
@@ -21,7 +19,6 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
async def yiri2target(event: platform_events.Event) -> typing.Any:
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
self, event: SlackEvent
|
||||
) -> platform_events.FriendMessage | platform_events.GroupMessage | None:
|
||||
@@ -56,7 +53,6 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
source_platform_object=event,
|
||||
)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(self, event: SlackEvent) -> platform_events.Event:
|
||||
if event.type in {'im', 'channel'}:
|
||||
return await self.message_to_eba(event)
|
||||
|
||||
@@ -6,9 +6,6 @@ Preserves all existing functionality (messaging, streaming output, markdown card
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
from langbot.pkg.telemetry.adapter_diagnostics import record_api_result
|
||||
|
||||
import typing
|
||||
import traceback
|
||||
|
||||
@@ -71,16 +68,6 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger):
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.native_callback',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
ap=lambda: getattr(logger, 'ap', None),
|
||||
fields=lambda b: {
|
||||
'workspace_uuid': getattr(getattr(logger, 'execution_context', None), 'workspace_uuid', '')
|
||||
},
|
||||
)
|
||||
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if (
|
||||
not update.message
|
||||
@@ -219,7 +206,6 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
|
||||
# ---- Message Send / Reply (preserving original logic) ----
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||
components = await TelegramMessageConverter.yiri2target(message, self.bot)
|
||||
|
||||
@@ -239,22 +225,21 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
text = telegramify_markdown.markdownify(content=text)
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
args['text'] = text
|
||||
record_api_result(await self.bot.send_message(**args))
|
||||
await self.bot.send_message(**args)
|
||||
elif component_type == 'photo':
|
||||
photo = component.get('photo')
|
||||
if photo is None:
|
||||
continue
|
||||
args['photo'] = telegram.InputFile(photo)
|
||||
record_api_result(await self.bot.send_photo(**args))
|
||||
await self.bot.send_photo(**args)
|
||||
elif component_type == 'document':
|
||||
doc = component.get('document')
|
||||
if doc is None:
|
||||
continue
|
||||
filename = component.get('filename', 'file')
|
||||
args['document'] = telegram.InputFile(doc, filename=filename)
|
||||
record_api_result(await self.bot.send_document(**args))
|
||||
await self.bot.send_document(**args)
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -286,20 +271,20 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
if self.config['markdown_card'] is True:
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
args['text'] = content
|
||||
record_api_result(await self.bot.send_message(**args))
|
||||
await self.bot.send_message(**args)
|
||||
elif component_type == 'photo':
|
||||
photo = component.get('photo')
|
||||
if photo is None:
|
||||
continue
|
||||
args['photo'] = telegram.InputFile(photo)
|
||||
record_api_result(await self.bot.send_photo(**args))
|
||||
await self.bot.send_photo(**args)
|
||||
elif component_type == 'document':
|
||||
doc = component.get('document')
|
||||
if doc is None:
|
||||
continue
|
||||
filename = component.get('filename', 'file')
|
||||
args['document'] = telegram.InputFile(doc, filename=filename)
|
||||
record_api_result(await self.bot.send_document(**args))
|
||||
await self.bot.send_document(**args)
|
||||
|
||||
# ---- Streaming Output (preserving original logic) ----
|
||||
|
||||
@@ -332,7 +317,6 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
cleaned = text.replace('\u200b', '').replace('\u200c', '').replace('\u200d', '').replace('\ufeff', '').strip()
|
||||
return cleaned == ''
|
||||
|
||||
@diagnostics.observe('api', 'create_message_card', source='platform', stage='accepted')
|
||||
async def create_message_card(self, message_id, event):
|
||||
assert isinstance(event.source_platform_object, Update)
|
||||
update = event.source_platform_object
|
||||
@@ -347,7 +331,6 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
|
||||
return True
|
||||
|
||||
@diagnostics.observe('api', 'reply_message_chunk', source='platform', stage='accepted')
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -483,7 +466,6 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
"""Dispatch once, preferring the most specific registered listener."""
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
@@ -510,7 +492,6 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
|
||||
# ---- Pass-through API ----
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(
|
||||
self,
|
||||
action: str,
|
||||
|
||||
@@ -5,9 +5,6 @@ Implements optional API methods defined in AbstractPlatformAdapter.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
from langbot.pkg.telemetry.adapter_diagnostics import record_api_result
|
||||
|
||||
import typing
|
||||
|
||||
import telegram
|
||||
@@ -28,7 +25,6 @@ class TelegramAPIMixin:
|
||||
|
||||
bot: telegram.Bot
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -53,10 +49,9 @@ class TelegramAPIMixin:
|
||||
}
|
||||
if self.config.get('markdown_card', False):
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
record_api_result(await self.bot.edit_message_text(**args), edited_content=new_content)
|
||||
await self.bot.edit_message_text(**args)
|
||||
return
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -64,9 +59,8 @@ class TelegramAPIMixin:
|
||||
message_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""Delete / recall a message."""
|
||||
record_api_result(await self.bot.delete_message(chat_id=chat_id, message_id=message_id))
|
||||
await self.bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -86,7 +80,6 @@ class TelegramAPIMixin:
|
||||
raw={'message_id': result.message_id},
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -107,7 +100,6 @@ class TelegramAPIMixin:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -142,7 +134,6 @@ class TelegramAPIMixin:
|
||||
)
|
||||
return members
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -169,7 +160,6 @@ class TelegramAPIMixin:
|
||||
display_name=member.custom_title if hasattr(member, 'custom_title') else None,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(
|
||||
self,
|
||||
user_id: typing.Union[int, str],
|
||||
@@ -182,7 +172,6 @@ class TelegramAPIMixin:
|
||||
username=chat.username,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(
|
||||
self,
|
||||
file_data: bytes,
|
||||
@@ -197,7 +186,6 @@ class TelegramAPIMixin:
|
||||
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(
|
||||
self,
|
||||
file_id: str,
|
||||
@@ -206,7 +194,6 @@ class TelegramAPIMixin:
|
||||
file = await self.bot.get_file(file_id)
|
||||
return file.file_path
|
||||
|
||||
@diagnostics.observe('api', 'mute_member', source='platform', stage='accepted')
|
||||
async def mute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -224,9 +211,8 @@ class TelegramAPIMixin:
|
||||
}
|
||||
if duration > 0:
|
||||
kwargs['until_date'] = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=duration)
|
||||
record_api_result(await self.bot.restrict_chat_member(**kwargs))
|
||||
await self.bot.restrict_chat_member(**kwargs)
|
||||
|
||||
@diagnostics.observe('api', 'unmute_member', source='platform', stage='accepted')
|
||||
async def unmute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -244,27 +230,23 @@ class TelegramAPIMixin:
|
||||
can_send_video_notes=True,
|
||||
can_send_voice_notes=True,
|
||||
)
|
||||
record_api_result(
|
||||
await self.bot.restrict_chat_member(
|
||||
chat_id=group_id,
|
||||
user_id=user_id,
|
||||
permissions=permissions,
|
||||
)
|
||||
await self.bot.restrict_chat_member(
|
||||
chat_id=group_id,
|
||||
user_id=user_id,
|
||||
permissions=permissions,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'kick_member', source='platform', stage='accepted')
|
||||
async def kick_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""Kick a member from the group."""
|
||||
record_api_result(await self.bot.ban_chat_member(chat_id=group_id, user_id=user_id))
|
||||
await self.bot.ban_chat_member(chat_id=group_id, user_id=user_id)
|
||||
|
||||
@diagnostics.observe('api', 'leave_group', source='platform', stage='accepted')
|
||||
async def leave_group(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""Make the bot leave a group."""
|
||||
record_api_result(await self.bot.leave_chat(chat_id=group_id))
|
||||
await self.bot.leave_chat(chat_id=group_id)
|
||||
|
||||
@@ -5,8 +5,6 @@ Converts all Telegram Update types to unified EBA events, not just messages.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
import telegram
|
||||
@@ -56,7 +54,6 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(
|
||||
update: Update,
|
||||
bot: telegram.Bot,
|
||||
@@ -387,13 +384,6 @@ class LegacyEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return event.source_platform_object
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe(
|
||||
'event',
|
||||
'platform.target2yiri',
|
||||
source='platform',
|
||||
stage='convert',
|
||||
fields=lambda _: {'attributes': {'adapter_evidence': False}},
|
||||
)
|
||||
async def target2yiri(event: Update, bot: telegram.Bot, bot_account_id: str):
|
||||
"""Convert to legacy format (FriendMessage / GroupMessage)."""
|
||||
import langbot_plugin.api.entities.builtin.platform.events as legacy_events
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
import typing
|
||||
@@ -92,7 +90,6 @@ class WecomAdapter(WecomAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -109,7 +106,6 @@ class WecomAdapter(WecomAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
raw_results.append(await self._send_content(user_id, agent_id, content))
|
||||
return platform_events.MessageResult(raw={'results': raw_results})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -125,7 +121,6 @@ class WecomAdapter(WecomAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
raw_results.append(await self._send_content(wecom_event.user_id, int(wecom_event.agent_id), content))
|
||||
return platform_events.MessageResult(message_id=wecom_event.message_id, raw={'results': raw_results})
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
@@ -179,7 +174,6 @@ class WecomAdapter(WecomAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
self.bot.on_message('text')(on_message)
|
||||
self.bot.on_message('image')(on_message)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: WecomEvent):
|
||||
self.bot_account_id = event.receiver_id or self.bot_account_id
|
||||
try:
|
||||
@@ -198,7 +192,6 @@ class WecomAdapter(WecomAPIMixin, abstract_platform_adapter.AbstractPlatformAdap
|
||||
await self.logger.error(f'Error in wecom native event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.libs.wecom_api.api import WecomClient
|
||||
@@ -16,7 +14,6 @@ class WecomAPIMixin:
|
||||
_message_cache: dict[str, platform_events.MessageReceivedEvent]
|
||||
_user_cache: dict[str, platform_entities.User]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -28,7 +25,6 @@ class WecomAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
cached = self._user_cache.get(str(user_id))
|
||||
if cached is not None:
|
||||
@@ -40,30 +36,24 @@ class WecomAPIMixin:
|
||||
username=info.get('alias') or info.get('userid') or None,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
raise NotSupportedError('get_group_info')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[platform_entities.UserGroupMember]:
|
||||
raise NotSupportedError('get_group_member_list')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -71,7 +61,6 @@ class WecomAPIMixin:
|
||||
) -> platform_entities.UserGroupMember:
|
||||
raise NotSupportedError('get_group_member_info')
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -81,7 +70,6 @@ class WecomAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.libs.wecom_api.api import WecomClient
|
||||
@@ -19,7 +17,6 @@ class WecomEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(event: WecomEvent, bot: WecomClient | None = None) -> platform_events.FriendMessage | None:
|
||||
eba_event = await WecomEventConverter.target2yiri(event, bot)
|
||||
if hasattr(eba_event, 'to_legacy_event'):
|
||||
@@ -39,7 +36,6 @@ class WecomEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(event: WecomEvent, bot: WecomClient | None = None) -> platform_events.Event | None:
|
||||
if event.type in {'text', 'image'}:
|
||||
return await WecomEventConverter.message_to_eba(event, bot)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
from langbot.pkg.platform.sources.wecombot import WecomBotAdapter as LegacyWecomBotAdapter
|
||||
|
||||
import asyncio
|
||||
@@ -135,7 +133,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
_iter_media_components = staticmethod(LegacyWecomBotAdapter._iter_media_components)
|
||||
_send_media = staticmethod(LegacyWecomBotAdapter._send_media)
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -151,7 +148,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
raw = await self.bot.send_message(str(target_id), content)
|
||||
return platform_events.MessageResult(raw={'result': raw})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -173,7 +169,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
raw = await self.bot.set_message(event.message_id, content)
|
||||
return platform_events.MessageResult(message_id=event.message_id, raw={'result': raw})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message_chunk', source='platform', stage='accepted')
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -201,7 +196,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
return self.config.get('enable-stream-reply', True)
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
if action == 'interaction.request' and 'interaction.request' in self.get_supported_apis():
|
||||
return await send_interaction(self, params)
|
||||
@@ -277,7 +271,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
self.bot.on_message('event')(self._handle_native_event)
|
||||
self.bot.on_message('template_card_event')(self._handle_interaction_event)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_interaction_event(self, event: WecomBotEvent):
|
||||
try:
|
||||
interaction_event = interaction_event_from_native(event)
|
||||
@@ -286,7 +279,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
except Exception:
|
||||
await self.logger.error(f'Error in WeComBot interaction callback: {traceback.format_exc()}')
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: WecomBotEvent):
|
||||
try:
|
||||
if platform_events.FriendMessage in self.listeners or platform_events.GroupMessage in self.listeners:
|
||||
@@ -312,7 +304,6 @@ class WecomBotAdapter(WecomBotAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
await self.logger.error(f'Error in wecombot feedback event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
@@ -16,7 +14,6 @@ class WecomBotAPIMixin:
|
||||
_group_cache: dict[str, platform_entities.UserGroup]
|
||||
_member_cache: dict[tuple[str, str], platform_entities.UserGroupMember]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -28,25 +25,21 @@ class WecomBotAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
cached = self._user_cache.get(str(user_id))
|
||||
if cached is None:
|
||||
raise NotSupportedError('get_user_info:not_cached')
|
||||
return cached
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
cached = self._group_cache.get(str(group_id))
|
||||
if cached is None:
|
||||
raise NotSupportedError('get_group_info:not_cached')
|
||||
return cached
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -57,7 +50,6 @@ class WecomBotAPIMixin:
|
||||
raise NotSupportedError('get_group_member_info:not_cached')
|
||||
return cached
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -66,15 +58,12 @@ class WecomBotAPIMixin:
|
||||
member for (cached_group_id, _), member in self._member_cache.items() if cached_group_id == str(group_id)
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -84,7 +73,6 @@ class WecomBotAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -93,7 +81,6 @@ class WecomBotAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('delete_message')
|
||||
|
||||
@diagnostics.observe('api', 'forward_message', source='platform', stage='accepted')
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
@@ -104,18 +91,14 @@ class WecomBotAPIMixin:
|
||||
) -> platform_events.MessageResult:
|
||||
raise NotSupportedError('forward_message')
|
||||
|
||||
@diagnostics.observe('api', 'mute_member', source='platform', stage='accepted')
|
||||
async def mute_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str], duration: int = 0):
|
||||
raise NotSupportedError('mute_member')
|
||||
|
||||
@diagnostics.observe('api', 'unmute_member', source='platform', stage='accepted')
|
||||
async def unmute_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]):
|
||||
raise NotSupportedError('unmute_member')
|
||||
|
||||
@diagnostics.observe('api', 'kick_member', source='platform', stage='accepted')
|
||||
async def kick_member(self, group_id: typing.Union[int, str], user_id: typing.Union[int, str]):
|
||||
raise NotSupportedError('kick_member')
|
||||
|
||||
@diagnostics.observe('api', 'leave_group', source='platform', stage='accepted')
|
||||
async def leave_group(self, group_id: typing.Union[int, str]):
|
||||
raise NotSupportedError('leave_group')
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import time
|
||||
import typing
|
||||
|
||||
@@ -21,7 +19,6 @@ class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
async def yiri2target(event: platform_events.Event) -> typing.Any:
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
self, event: WecomBotEvent
|
||||
) -> platform_events.FriendMessage | platform_events.GroupMessage | None:
|
||||
@@ -52,7 +49,6 @@ class WecomBotEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
source_platform_object=event,
|
||||
)
|
||||
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(self, event: WecomBotEvent) -> platform_events.Event:
|
||||
if event.type in {'single', 'group'} and event.msgtype != 'event':
|
||||
return await self.message_to_eba(event)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
@@ -94,7 +92,6 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
'call_platform_api',
|
||||
]
|
||||
|
||||
@diagnostics.observe('api', 'send_message', source='platform', stage='accepted')
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str,
|
||||
@@ -113,7 +110,6 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
)
|
||||
return platform_events.MessageResult(raw={'results': raw_results})
|
||||
|
||||
@diagnostics.observe('api', 'reply_message', source='platform', stage='accepted')
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -136,7 +132,6 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
)
|
||||
return platform_events.MessageResult(message_id=wecom_event.message_id, raw={'results': raw_results})
|
||||
|
||||
@diagnostics.observe('api', 'call_platform_api', source='platform', stage='accepted')
|
||||
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
||||
handler = PLATFORM_API_MAP.get(action)
|
||||
if handler is None:
|
||||
@@ -191,7 +186,6 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
for msg_type in ('text', 'image', 'file', 'voice'):
|
||||
self.bot.on_message(msg_type)(on_message)
|
||||
|
||||
@diagnostics.observe('event', 'platform.native_receive', source='platform', stage='convert')
|
||||
async def _handle_native_event(self, event: WecomCSEvent):
|
||||
self.bot_account_id = event.receiver_id or self.bot_account_id
|
||||
try:
|
||||
@@ -214,7 +208,6 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
await self.logger.error(f'Error in wecomcs native event: {traceback.format_exc()}')
|
||||
|
||||
async def _dispatch_eba_event(self, event: platform_events.EBAEvent):
|
||||
diagnostics.adapter_event_received(self, event)
|
||||
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
||||
callback = self.listeners.get(event_type)
|
||||
if callback:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.libs.wecom_customer_service_api.api import WecomCSClient
|
||||
@@ -16,7 +14,6 @@ class WecomCSAPIMixin:
|
||||
_message_cache: dict[str, platform_events.MessageReceivedEvent]
|
||||
_user_cache: dict[str, platform_entities.User]
|
||||
|
||||
@diagnostics.observe('api', 'get_message', source='platform', stage='accepted')
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -28,7 +25,6 @@ class WecomCSAPIMixin:
|
||||
raise NotSupportedError('get_message:message_not_cached')
|
||||
return event
|
||||
|
||||
@diagnostics.observe('api', 'get_user_info', source='platform', stage='accepted')
|
||||
async def get_user_info(self, user_id: typing.Union[int, str]) -> platform_entities.User:
|
||||
cached = self._user_cache.get(str(user_id))
|
||||
if cached is not None:
|
||||
@@ -43,30 +39,24 @@ class WecomCSAPIMixin:
|
||||
username=info.get('external_userid') or None,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'get_friend_list', source='platform', stage='accepted')
|
||||
async def get_friend_list(self) -> list[platform_entities.User]:
|
||||
return list(self._user_cache.values())
|
||||
|
||||
@diagnostics.observe('api', 'upload_file', source='platform', stage='accepted')
|
||||
async def upload_file(self, file_data: bytes, filename: str) -> str:
|
||||
raise NotSupportedError('upload_file')
|
||||
|
||||
@diagnostics.observe('api', 'get_file_url', source='platform', stage='accepted')
|
||||
async def get_file_url(self, file_id: str) -> str:
|
||||
raise NotSupportedError('get_file_url')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_info', source='platform', stage='accepted')
|
||||
async def get_group_info(self, group_id: typing.Union[int, str]) -> platform_entities.UserGroup:
|
||||
raise NotSupportedError('get_group_info')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_list', source='platform', stage='accepted')
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[platform_entities.UserGroupMember]:
|
||||
raise NotSupportedError('get_group_member_list')
|
||||
|
||||
@diagnostics.observe('api', 'get_group_member_info', source='platform', stage='accepted')
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
@@ -74,7 +64,6 @@ class WecomCSAPIMixin:
|
||||
) -> platform_entities.UserGroupMember:
|
||||
raise NotSupportedError('get_group_member_info')
|
||||
|
||||
@diagnostics.observe('api', 'edit_message', source='platform', stage='accepted')
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
@@ -84,7 +73,6 @@ class WecomCSAPIMixin:
|
||||
) -> None:
|
||||
raise NotSupportedError('edit_message')
|
||||
|
||||
@diagnostics.observe('api', 'delete_message', source='platform', stage='accepted')
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
|
||||
from langbot.libs.wecom_customer_service_api.api import WecomCSClient
|
||||
@@ -19,7 +17,6 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return getattr(event, 'source_platform_object', None)
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2legacy', source='platform', stage='convert')
|
||||
async def target2legacy(
|
||||
event: WecomCSEvent, bot: WecomCSClient | None = None
|
||||
) -> platform_events.FriendMessage | None:
|
||||
@@ -29,7 +26,6 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@diagnostics.observe('event', 'platform.target2yiri', source='platform', stage='convert')
|
||||
async def target2yiri(event: WecomCSEvent, bot: WecomCSClient | None = None) -> platform_events.Event | None:
|
||||
if event.type in {'text', 'image', 'file', 'voice'}:
|
||||
return await WecomCSEventConverter.message_to_eba(event, bot)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import dataclasses
|
||||
@@ -370,22 +368,6 @@ class RuntimeBot:
|
||||
) -> dict[str, typing.Any]:
|
||||
"""Record structured event routing state while preserving the human log."""
|
||||
binding = binding or {}
|
||||
diagnostics.event(
|
||||
self,
|
||||
'route',
|
||||
'route.primary',
|
||||
{
|
||||
'not_matched': 'skipped',
|
||||
'discarded': 'skipped',
|
||||
'matched': 'started',
|
||||
'delivered': 'succeeded',
|
||||
'failed': 'failed',
|
||||
}.get(status, 'unknown'),
|
||||
stage='dispatch',
|
||||
platform_event_type=event_type,
|
||||
processor_type=target_type or binding.get('target_type', ''),
|
||||
reason_code=failure_code or status,
|
||||
)
|
||||
metadata = {
|
||||
'kind': 'event_route_trace',
|
||||
'event_type': event_type,
|
||||
@@ -398,16 +380,6 @@ class RuntimeBot:
|
||||
'reason': reason or text,
|
||||
'run_id': run_id,
|
||||
}
|
||||
diagnostics.set_outcome(
|
||||
{
|
||||
'not_matched': 'skipped',
|
||||
'discarded': 'skipped',
|
||||
'matched': 'started',
|
||||
'delivered': 'succeeded',
|
||||
'failed': 'failed',
|
||||
}.get(status, 'unknown'),
|
||||
reason_code=failure_code or status,
|
||||
)
|
||||
log_method = getattr(self.logger, level, self.logger.info)
|
||||
await log_method(text, metadata=metadata)
|
||||
return metadata
|
||||
@@ -863,7 +835,6 @@ class RuntimeBot:
|
||||
processor_id=agent.get('uuid'),
|
||||
)
|
||||
|
||||
@diagnostics.observe('event', 'platform.receive', source='platform', stage='dispatch')
|
||||
async def _handle_platform_event(
|
||||
self,
|
||||
event: platform_events.EBAEvent,
|
||||
@@ -893,7 +864,6 @@ class RuntimeBot:
|
||||
if isinstance(result, BaseException):
|
||||
await self.logger.error(f'Event delivery failed: {result}')
|
||||
|
||||
@diagnostics.observe('route', 'route.subscription', source='platform', stage='dispatch')
|
||||
async def _dispatch_plugin_subscription(self, event, adapter, processor_uuid):
|
||||
event_type = event.type
|
||||
event_binding = {
|
||||
@@ -912,7 +882,6 @@ class RuntimeBot:
|
||||
# Resolve the installed declaration each time, including after plugin updates.
|
||||
patterns = descriptor.supported_event_patterns
|
||||
if not patterns or not self._agent_supports_event_type(patterns, event_type):
|
||||
diagnostics.set_outcome('skipped', reason_code='not_matched')
|
||||
return
|
||||
agent = {**agent, 'supported_event_patterns': patterns}
|
||||
return await self._dispatch_eba_event_to_processor(event, adapter, event_binding, agent)
|
||||
@@ -929,7 +898,6 @@ class RuntimeBot:
|
||||
text=f'Plugin processor {processor_uuid} failed: {exc}',
|
||||
)
|
||||
|
||||
@diagnostics.observe('route', 'route.dispatch', source='platform', stage='dispatch')
|
||||
async def _dispatch_eba_event_to_processor(
|
||||
self,
|
||||
event: platform_events.EBAEvent,
|
||||
@@ -1200,7 +1168,6 @@ class RuntimeBot:
|
||||
except Exception as e:
|
||||
await self.logger.error(f'Failed to record discarded message: {e}')
|
||||
|
||||
@diagnostics.observe('event', 'platform.legacy_receive', source='platform', stage='dispatch')
|
||||
async def _handle_legacy_message_event(
|
||||
self,
|
||||
event: platform_events.FriendMessage | platform_events.GroupMessage,
|
||||
@@ -1286,7 +1253,6 @@ class RuntimeBot:
|
||||
execution_context=self.execution_context,
|
||||
)
|
||||
|
||||
@diagnostics.observe('interaction', 'interaction.submit', source='platform', stage='dispatch')
|
||||
async def _handle_interaction_submission(
|
||||
self,
|
||||
event: platform_events.PlatformSpecificEvent,
|
||||
@@ -1395,7 +1361,6 @@ class RuntimeBot:
|
||||
execution_context=self.execution_context,
|
||||
)
|
||||
|
||||
@diagnostics.observe('interaction', 'interaction.resume', source='platform', stage='resume')
|
||||
async def _resume_agent_interaction(
|
||||
self,
|
||||
record: dict[str, typing.Any],
|
||||
@@ -1550,8 +1515,6 @@ class RuntimeBot:
|
||||
return
|
||||
await self._handle_platform_event(self._legacy_message_to_eba_event(event, adapter), adapter)
|
||||
|
||||
from ..telemetry.diagnostic_catalog import snapshot_bot
|
||||
|
||||
get_supported_events = getattr(self.adapter, 'get_supported_events', None)
|
||||
supported_events: list[str] = []
|
||||
if callable(get_supported_events):
|
||||
@@ -1623,8 +1586,6 @@ class RuntimeBot:
|
||||
platform_events.EBAEvent,
|
||||
tenant_scoped_listener(on_eba_event),
|
||||
)
|
||||
# Registration is evidence of an installed listener, not a live connection.
|
||||
snapshot_bot(self, listener_registered=True)
|
||||
|
||||
async def run(self):
|
||||
async def exception_wrapper():
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..telemetry import diagnostics
|
||||
|
||||
from typing import Any, Union
|
||||
import json
|
||||
import time
|
||||
@@ -303,8 +301,6 @@ async def _validate_agent_run_session(
|
||||
)
|
||||
if not available_apis.get(api_capability, False) and not has_admin_permission:
|
||||
return None, handler.ActionResponse.error(message=f'{api_name} access not authorized')
|
||||
|
||||
diagnostics.link_context(session.get('_diagnostic_context'))
|
||||
return session, None
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# For connect to plugin runtime.
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import contextvars
|
||||
@@ -720,7 +718,6 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
}
|
||||
)
|
||||
|
||||
@diagnostics.observe('lifecycle', 'runtime.prepare_connected_runtime', source='runtime', stage='execute')
|
||||
async def _prepare_connected_runtime(self) -> None:
|
||||
"""Handshake follow-up: pin OSS compatibility, then replay authority."""
|
||||
|
||||
@@ -906,7 +903,6 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
self.schedule_reconnect()
|
||||
failures = 0
|
||||
|
||||
@diagnostics.observe('lifecycle', 'runtime.initialize', source='runtime', stage='execute')
|
||||
async def initialize(self):
|
||||
if not self.is_enable_plugin:
|
||||
self.ap.logger.info('Plugin system is disabled.')
|
||||
@@ -1817,7 +1813,6 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
}
|
||||
return {**install_info, '_certification': certification_info}, verified
|
||||
|
||||
@diagnostics.observe('lifecycle', 'runtime.install_plugin', source='runtime', stage='execute')
|
||||
async def install_plugin(
|
||||
self,
|
||||
install_source: PluginInstallSource,
|
||||
@@ -1984,7 +1979,6 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
task_context.set_current_action('plugin updated' if operation == 'upgrade' else 'plugin installed')
|
||||
task_context.metadata['progress_percent'] = 100
|
||||
|
||||
@diagnostics.observe('lifecycle', 'runtime.upgrade_plugin', source='runtime', stage='execute')
|
||||
async def upgrade_plugin(
|
||||
self,
|
||||
plugin_author: str,
|
||||
@@ -2011,7 +2005,6 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
)
|
||||
return {}
|
||||
|
||||
@diagnostics.observe('lifecycle', 'runtime.delete_plugin', source='runtime', stage='execute')
|
||||
async def delete_plugin(
|
||||
self,
|
||||
plugin_author: str,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..telemetry import diagnostics
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import typing
|
||||
@@ -646,8 +644,6 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
payload fields can never select the scope. A transaction-free boundary
|
||||
avoids reserving one pooled connection across provider and network waits.
|
||||
"""
|
||||
|
||||
diagnostics.annotate(workspace_uuid=action_context.workspace_uuid)
|
||||
persistence_mgr = getattr(self.ap, 'persistence_mgr', None)
|
||||
if persistence_mgr is None:
|
||||
yield
|
||||
@@ -700,9 +696,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
async for response in responses:
|
||||
yield response
|
||||
|
||||
self.actions[action_name] = diagnostics.observe(
|
||||
'api', 'host.' + action_name, source='plugin', ap=self.ap
|
||||
)(secured_stream_action)
|
||||
self.actions[action_name] = secured_stream_action
|
||||
continue
|
||||
|
||||
async def secured_action(
|
||||
@@ -731,9 +725,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
response = await response
|
||||
return response
|
||||
|
||||
self.actions[action_name] = diagnostics.observe('api', 'host.' + action_name, source='plugin', ap=self.ap)(
|
||||
secured_action
|
||||
)
|
||||
self.actions[action_name] = secured_action
|
||||
|
||||
async def _get_plugin_setting(
|
||||
self,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import abc
|
||||
import typing
|
||||
import time
|
||||
@@ -100,7 +98,6 @@ class RuntimeProvider:
|
||||
raise WorkspaceInvariantError('LLM invocation requires an ExecutionContext when query is absent')
|
||||
return execution_context
|
||||
|
||||
@diagnostics.observe('api', 'model.invoke_llm', source='agent')
|
||||
async def invoke_llm(
|
||||
self,
|
||||
query: pipeline_query.Query | None,
|
||||
@@ -185,7 +182,6 @@ class RuntimeProvider:
|
||||
except Exception as monitor_err:
|
||||
self.requester.ap.logger.error(f'[Monitoring] Failed to record LLM call: {monitor_err}')
|
||||
|
||||
@diagnostics.observe('api', 'model.invoke_llm_stream', source='agent')
|
||||
async def invoke_llm_stream(
|
||||
self,
|
||||
query: pipeline_query.Query | None,
|
||||
@@ -268,7 +264,6 @@ class RuntimeProvider:
|
||||
except Exception as monitor_err:
|
||||
self.requester.ap.logger.error(f'[Monitoring] Failed to record LLM stream call: {monitor_err}')
|
||||
|
||||
@diagnostics.observe('api', 'model.invoke_embedding', source='agent')
|
||||
async def invoke_embedding(
|
||||
self,
|
||||
model: RuntimeEmbeddingModel,
|
||||
@@ -336,7 +331,6 @@ class RuntimeProvider:
|
||||
except Exception as monitor_err:
|
||||
self.requester.ap.logger.error(f'[Monitoring] Failed to record embedding call: {monitor_err}')
|
||||
|
||||
@diagnostics.observe('api', 'model.invoke_rerank', source='agent')
|
||||
async def invoke_rerank(
|
||||
self,
|
||||
model: RuntimeRerankModel,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import typing
|
||||
import time
|
||||
import inspect
|
||||
@@ -475,7 +473,6 @@ class ToolManager:
|
||||
)
|
||||
return result
|
||||
|
||||
@diagnostics.observe('api', 'tool.execute', source='agent', stage='execute')
|
||||
async def execute_func_call(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.telemetry import diagnostics
|
||||
|
||||
import posixpath
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -73,7 +71,6 @@ class RAGRuntimeService:
|
||||
raise WorkspaceNotFoundError('Knowledge base not found')
|
||||
return kb_uuid
|
||||
|
||||
@diagnostics.observe('api', 'knowledge.vector_upsert', source='agent')
|
||||
async def vector_upsert(
|
||||
self,
|
||||
execution_context: ExecutionContext,
|
||||
@@ -101,7 +98,6 @@ class RAGRuntimeService:
|
||||
documents=documents,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'knowledge.vector_search', source='agent')
|
||||
async def vector_search(
|
||||
self,
|
||||
execution_context: ExecutionContext,
|
||||
@@ -126,7 +122,6 @@ class RAGRuntimeService:
|
||||
vector_weight=vector_weight,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'knowledge.vector_delete', source='agent')
|
||||
async def vector_delete(
|
||||
self,
|
||||
execution_context: ExecutionContext,
|
||||
@@ -163,7 +158,6 @@ class RAGRuntimeService:
|
||||
)
|
||||
return count
|
||||
|
||||
@diagnostics.observe('api', 'knowledge.vector_list', source='agent')
|
||||
async def vector_list(
|
||||
self,
|
||||
execution_context: ExecutionContext,
|
||||
@@ -192,7 +186,6 @@ class RAGRuntimeService:
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
@diagnostics.observe('api', 'knowledge.get_file_stream', source='agent')
|
||||
async def get_file_stream(
|
||||
self,
|
||||
execution_context: ExecutionContext,
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
"""Finite adapter acceptance evidence, without message contents or target IDs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .diagnostic_catalog import catalog
|
||||
|
||||
|
||||
def boundary_fields(module, kind, operation, bound, parent):
|
||||
"""Only the actual adapter API / EBA conversion boundary counts as a test."""
|
||||
entry = next((v for k, v in catalog().items() if k in module.split('.')), None)
|
||||
if entry is None or '.platform.adapters.' not in module:
|
||||
return {}
|
||||
evidence = kind == 'event' and operation == 'platform.target2yiri'
|
||||
resolved = operation
|
||||
if kind == 'api':
|
||||
evidence = True
|
||||
if operation == 'call_platform_api':
|
||||
action = bound.get('action')
|
||||
if isinstance(action, str) and 'platform_api.' + action in entry['specific_apis']:
|
||||
resolved = 'platform_api.' + action
|
||||
elif action in entry['apis'] and action != 'call_platform_api':
|
||||
resolved = action
|
||||
else:
|
||||
# Keep the generic boundary for investigation, not a false named test.
|
||||
evidence = False
|
||||
if (
|
||||
entry['adapter'] == 'lark-omni'
|
||||
and resolved
|
||||
in {
|
||||
'platform_api.check_tenant_access_token',
|
||||
'platform_api.refresh_app_access_token',
|
||||
'platform_api.refresh_tenant_access_token',
|
||||
}
|
||||
and getattr(bound.get('self'), 'config', {}).get('app_type', 'self') != 'isv'
|
||||
):
|
||||
# Self-built apps return {'ok': True} without checking/refreshing a token.
|
||||
evidence = False
|
||||
# A forwarding API may call another decorated method. Count the outer call.
|
||||
if parent and parent.adapter_api_active:
|
||||
evidence = False
|
||||
if not evidence:
|
||||
return {'_adapter_api_active': kind == 'api'}
|
||||
return {
|
||||
'_adapter_api_active': kind == 'api',
|
||||
'_adapter_void_ack': completed_void_api(module, resolved),
|
||||
'_adapter_read_ack': completed_read_api(module, resolved),
|
||||
'operation': resolved,
|
||||
'attributes': {'adapter_evidence': True, **message_scenario(bound)},
|
||||
}
|
||||
|
||||
|
||||
def message_scenario(bound):
|
||||
"""Read typed routing/media categories only; never serialize a payload."""
|
||||
from langbot_plugin.api.entities.builtin.platform.events import MessageReceivedEvent
|
||||
from langbot_plugin.api.entities.builtin.platform.message import MessageChain
|
||||
|
||||
event = bound.get('event') or bound.get('message_source')
|
||||
target = bound.get('target_type') or bound.get('chat_type')
|
||||
message = bound.get('message') or bound.get('new_content')
|
||||
if isinstance(event, MessageReceivedEvent):
|
||||
target = event.chat_type
|
||||
message = event.message_chain
|
||||
result = {}
|
||||
if target is not None:
|
||||
target = getattr(target, 'value', target)
|
||||
if target == 'private':
|
||||
target = 'person'
|
||||
result['chat_type'] = target if target in ('person', 'group') else 'unknown'
|
||||
if isinstance(message, MessageChain):
|
||||
types = set()
|
||||
for item in message:
|
||||
name = type(item).__name__
|
||||
types.add(
|
||||
{
|
||||
'Plain': 'text',
|
||||
'Image': 'image',
|
||||
'Voice': 'audio',
|
||||
'Audio': 'audio',
|
||||
'Video': 'video',
|
||||
'File': 'file',
|
||||
}.get(name, 'other')
|
||||
)
|
||||
result['content_type'] = next(iter(types)) if len(types) == 1 else 'mixed' if types else 'unknown'
|
||||
return result
|
||||
|
||||
|
||||
# These exact API implementations always await an exception-raising SDK call,
|
||||
# including SDKs whose successful acknowledgement has no return payload.
|
||||
# Do not extend this to methods with conditional no-op or queued work.
|
||||
VOID_ACKNOWLEDGED_APIS = {
|
||||
'aiocqhttp': frozenset(
|
||||
{
|
||||
'delete_message',
|
||||
'set_group_name',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
'approve_friend_request',
|
||||
'approve_group_invite',
|
||||
}
|
||||
),
|
||||
'discord': frozenset(
|
||||
{
|
||||
'edit_message',
|
||||
'delete_message',
|
||||
'mute_member',
|
||||
'unmute_member',
|
||||
'kick_member',
|
||||
'leave_group',
|
||||
}
|
||||
),
|
||||
'kook': frozenset({'delete_message'}),
|
||||
}
|
||||
|
||||
|
||||
def completed_void_api(module, operation):
|
||||
return module.endswith('.api_impl') and any(
|
||||
directory in module.split('.') and operation in operations
|
||||
for directory, operations in VOID_ACKNOWLEDGED_APIS.items()
|
||||
)
|
||||
|
||||
|
||||
# These read methods return actual fetched/cached data or raise. Exclude the
|
||||
# known identity-only placeholders and file-ID passthroughs: a typed result alone
|
||||
# is not evidence that a lookup worked. Unsupported methods still raise normally.
|
||||
_READ_APIS = frozenset(
|
||||
{
|
||||
'get_message',
|
||||
'get_group_info',
|
||||
'get_group_list',
|
||||
'get_group_member_list',
|
||||
'get_group_member_info',
|
||||
'get_user_info',
|
||||
'get_friend_list',
|
||||
'get_file_url',
|
||||
}
|
||||
)
|
||||
READ_ACKNOWLEDGED_APIS = {
|
||||
'aiocqhttp': _READ_APIS,
|
||||
'telegram': _READ_APIS,
|
||||
'discord': _READ_APIS - {'get_file_url'},
|
||||
'dingtalk': _READ_APIS - {'get_group_info', 'get_user_info'},
|
||||
'kook': _READ_APIS - {'get_file_url'},
|
||||
'lark': _READ_APIS - {'get_user_info', 'get_group_member_info', 'get_file_url'},
|
||||
'officialaccount': _READ_APIS,
|
||||
'qqofficial': _READ_APIS,
|
||||
'slack': _READ_APIS,
|
||||
'wecom': _READ_APIS - {'get_user_info'},
|
||||
'wecombot': _READ_APIS,
|
||||
'wecomcs': _READ_APIS,
|
||||
}
|
||||
|
||||
|
||||
def completed_read_api(module, operation):
|
||||
return module.endswith('.api_impl') and any(
|
||||
directory in module.split('.') and operation in operations
|
||||
for directory, operations in READ_ACKNOWLEDGED_APIS.items()
|
||||
)
|
||||
|
||||
|
||||
def response_outcome(value, adapter=None):
|
||||
"""Finite acknowledgement contracts; never inspect or export message content.
|
||||
|
||||
A MessageResult may contain an *inbound* message ID even when nothing was
|
||||
sent. Only its actual raw acknowledgement is eligible. Empty/queued/unknown
|
||||
returns are unconfirmed, not failures. Unknown SDK shapes need an explicit
|
||||
confirmation at their operation boundary, not a truthiness fallback.
|
||||
"""
|
||||
from langbot_plugin.api.entities.builtin.platform.events import MessageResult
|
||||
|
||||
message_result = isinstance(value, MessageResult)
|
||||
if message_result:
|
||||
value = value.raw
|
||||
if not isinstance(value, dict):
|
||||
return 'skipped'
|
||||
if (
|
||||
value.get('ok') is False
|
||||
or value.get('status') == 'failed'
|
||||
or any(type(value.get(key)) is int and value[key] != 0 for key in ('retcode', 'errcode', 'code'))
|
||||
):
|
||||
return 'failed'
|
||||
if value.get('queued') is True or value.get('status') == 'async':
|
||||
return 'skipped'
|
||||
# The wrappers that batch sends retain the real responses in these fields.
|
||||
if 'results' in value:
|
||||
results = value['results']
|
||||
if not isinstance(results, list) or not results:
|
||||
return 'skipped'
|
||||
outcomes = [response_outcome(item, adapter) for item in results]
|
||||
return 'failed' if 'failed' in outcomes else 'skipped' if 'skipped' in outcomes else 'succeeded'
|
||||
for key in ('result', 'raw'):
|
||||
if key in value:
|
||||
return response_outcome(value[key], adapter)
|
||||
if (
|
||||
value.get('ok') is True
|
||||
or value.get('status') == 'ok'
|
||||
or any(type(value.get(key)) is int and value[key] == 0 for key in ('retcode', 'errcode', 'code'))
|
||||
):
|
||||
return 'succeeded'
|
||||
if message_result:
|
||||
# These wrappers put only SDK-returned IDs in raw (never the source ID).
|
||||
if adapter in {'aiocqhttp-omni', 'discord-omni', 'telegram-omni'}:
|
||||
message_id = value.get('message_id')
|
||||
if type(message_id) in (str, int) and message_id:
|
||||
return 'succeeded'
|
||||
if adapter == 'lark-omni':
|
||||
ids = value.get('message_ids')
|
||||
if isinstance(ids, list) and ids and all(isinstance(item, str) and item for item in ids):
|
||||
return 'succeeded'
|
||||
return 'skipped'
|
||||
|
||||
|
||||
def record_api_result(value, *, edited_content=None):
|
||||
"""Record Telegram's Message/True acknowledgement and return it unchanged.
|
||||
|
||||
This runs after the SDK await, not after conversion or local stream setup.
|
||||
No payload is retained; exceptions and the adapter's public return stay intact.
|
||||
"""
|
||||
try:
|
||||
from .diagnostics import current_span
|
||||
|
||||
span = current_span()
|
||||
if span is not None and span.kind == 'api' and span.fields.get('attributes', {}).get('adapter_evidence'):
|
||||
from telegram import Message
|
||||
|
||||
outcome = (
|
||||
'succeeded'
|
||||
if value is True or isinstance(value, Message)
|
||||
else 'failed'
|
||||
if value is False
|
||||
else 'skipped'
|
||||
)
|
||||
if outcome == 'succeeded' and edited_content is not None:
|
||||
from langbot_plugin.api.entities.builtin.platform.message import Plain
|
||||
|
||||
if any(not isinstance(item, Plain) for item in edited_content):
|
||||
outcome = 'partial'
|
||||
# One unacknowledged component must not be hidden by a later success.
|
||||
prior = span.adapter_api_result
|
||||
span.adapter_api_result = (
|
||||
'failed'
|
||||
if 'failed' in (prior, outcome)
|
||||
else 'skipped'
|
||||
if 'skipped' in (prior, outcome)
|
||||
else 'partial'
|
||||
if 'partial' in (prior, outcome)
|
||||
else outcome
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
@@ -1,93 +0,0 @@
|
||||
"""Packaged capability catalog and projections from existing runtime declarations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from . import diagnostic_privacy as privacy
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def catalog():
|
||||
result = {}
|
||||
base = Path(__file__).resolve().parents[1] / 'platform'
|
||||
for manifest in sorted((base / 'adapters').glob('*/manifest.yaml')):
|
||||
data = yaml.safe_load(manifest.read_text(encoding='utf-8'))
|
||||
spec = data.get('spec', {})
|
||||
name = data['metadata']['name']
|
||||
privacy.code_value('adapter', name)
|
||||
events = spec.get('supported_events', [])
|
||||
apis = spec.get('supported_apis', {})
|
||||
apis = apis if isinstance(apis, list) else [v for items in apis.values() for v in items]
|
||||
for event in events:
|
||||
privacy.code_value('platform_event_type', event)
|
||||
for operation in apis:
|
||||
privacy.code_value('operation', operation)
|
||||
specific_apis = []
|
||||
for api in spec.get('platform_specific_apis', []):
|
||||
privacy.code_value('operation', api['action'])
|
||||
specific_apis.append(privacy.code_value('operation', 'platform_api.' + api['action']))
|
||||
result[manifest.parent.name] = {
|
||||
'adapter': name,
|
||||
'events': events,
|
||||
'apis': apis,
|
||||
'specific_apis': specific_apis,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def adapter_fields(adapter):
|
||||
module = type(adapter).__module__.split('.')
|
||||
entry = next((entry for directory, entry in catalog().items() if directory in module), None)
|
||||
return {'adapter': entry['adapter']} if entry else {}
|
||||
|
||||
|
||||
def _snapshot_bot(bot, *, listener_registered=False):
|
||||
from . import diagnostics
|
||||
|
||||
manager = getattr(bot.ap, 'diagnostics', None)
|
||||
if not isinstance(manager, diagnostics.DiagnosticsManager) or not manager.enabled:
|
||||
return
|
||||
fields = adapter_fields(bot.adapter)
|
||||
if not fields:
|
||||
return
|
||||
fields['workspace_uuid'] = bot.execution_context.workspace_uuid
|
||||
entry = next(e for e in catalog().values() if e['adapter'] == fields['adapter'])
|
||||
for capability_type, method, declared in (
|
||||
('event', 'get_supported_events', entry['events']),
|
||||
('api', 'get_supported_apis', entry['apis'] + entry['specific_apis']),
|
||||
):
|
||||
try:
|
||||
supported = set(getattr(bot.adapter, method)() or [])
|
||||
except Exception:
|
||||
supported = set()
|
||||
for name in declared:
|
||||
if name == 'call_platform_api':
|
||||
continue
|
||||
manager.emit(
|
||||
'capability',
|
||||
name if capability_type == 'api' else 'platform.receive',
|
||||
'succeeded',
|
||||
source='platform',
|
||||
stage='snapshot',
|
||||
**fields,
|
||||
platform_event_type=name if capability_type == 'event' else '',
|
||||
attributes={
|
||||
'capability_type': capability_type,
|
||||
'capability_name': name,
|
||||
'supported': name in supported
|
||||
or (name in entry['specific_apis'] and 'call_platform_api' in supported),
|
||||
'configured': True,
|
||||
**({'listener_registered': listener_registered} if capability_type == 'event' else {}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def snapshot_bot(bot, *, listener_registered=False):
|
||||
try:
|
||||
_snapshot_bot(bot, listener_registered=listener_registered)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,117 +0,0 @@
|
||||
"""Closed, content-free projection for Beta diagnostics; never serialize payloads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import hashlib
|
||||
import math
|
||||
import re
|
||||
from uuid import UUID
|
||||
|
||||
KINDS = frozenset('lifecycle event route run api delivery interaction capability transport summary'.split())
|
||||
SOURCES = frozenset(
|
||||
'platform pipeline agent event_processor webui_debug http websocket mcp plugin runtime startup internal synthetic'.split()
|
||||
)
|
||||
OUTCOMES = frozenset('started succeeded failed cancelled timeout skipped rejected waiting partial unknown'.split())
|
||||
PROCESSORS = frozenset(('', 'pipeline', 'agent', 'event_processor'))
|
||||
# These sets are populated ONLY by source-code decorators / packaged manifests,
|
||||
# never by requests, installed third-party plugin declarations or configuration.
|
||||
VOCABULARY: dict[str, set[str]] = {
|
||||
'operation': {'diagnostics.transport', 'startup', 'route.primary', 'route.subscription', 'runner.run'},
|
||||
'stage': {'execute', 'prepare', 'convert', 'dispatch', 'accepted', 'ack', 'resume', 'shutdown', 'snapshot'},
|
||||
'adapter': set(),
|
||||
'platform_event_type': set(),
|
||||
'reason_code': set(
|
||||
'runner_failed response_error route_not_found processor_incompatible processor_not_found discarded not_matched matched delivered waiting interaction_rejected generator_closed transport_loss'.split()
|
||||
),
|
||||
'capability_type': {'event', 'api', 'processor'},
|
||||
'tool_category': {'native', 'plugin', 'mcp', 'skill', 'platform', 'unknown'},
|
||||
'transport': {'stdio', 'websocket', 'http', 'unknown'},
|
||||
'runner_usage': {'agent', 'event'},
|
||||
'os': {'linux', 'darwin', 'windows'},
|
||||
'arch': {'x86_64', 'aarch64', 'arm64', 'amd64'},
|
||||
'database': {'sqlite', 'postgresql'},
|
||||
'edition': {'community', 'cloud', 'enterprise'},
|
||||
'chat_type': {'person', 'group', 'unknown'},
|
||||
'content_type': {'text', 'image', 'audio', 'video', 'file', 'mixed', 'other', 'unknown'},
|
||||
}
|
||||
BOOLS = frozenset(
|
||||
'stream synthetic configured available previous_session_unclean recovered supported adapter_evidence listener_registered'.split()
|
||||
)
|
||||
NUMBERS = frozenset(
|
||||
'attempts successes failures cancellations timeouts partial unknown generated queued acked dropped retried failed queue_size capacity result_count input_tokens output_tokens'.split()
|
||||
)
|
||||
VERSIONS = frozenset('sdk_version plugin_version runner_version python_version'.split())
|
||||
|
||||
|
||||
def code_value(field: str, value: str) -> str:
|
||||
"""Register a literal from trusted Core source, not a runtime string."""
|
||||
VOCABULARY.setdefault(field, set()).add(value)
|
||||
return value
|
||||
|
||||
|
||||
def category(field: str, value) -> str:
|
||||
return value if type(value) is str and value in VOCABULARY.get(field, ()) else ''
|
||||
|
||||
|
||||
def opaque(value) -> str:
|
||||
if not isinstance(value, str):
|
||||
return ''
|
||||
try:
|
||||
return str(UUID(value))
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
return ''
|
||||
|
||||
|
||||
def version_string(value) -> str:
|
||||
# Public package versions only; do not permit arbitrary PEP440 local labels.
|
||||
return value if isinstance(value, str) and re.fullmatch(r'[0-9][0-9.abrcdevpost-]{0,47}', value) else ''
|
||||
|
||||
|
||||
def attributes(values) -> dict:
|
||||
if not isinstance(values, dict):
|
||||
return {}
|
||||
result = {}
|
||||
for key, value in values.items():
|
||||
if key in BOOLS and type(value) is bool:
|
||||
result[key] = value
|
||||
elif key in NUMBERS and type(value) is int and math.isfinite(value) and 0 <= value <= 1_000_000:
|
||||
result[key] = value
|
||||
elif key in VERSIONS and version_string(value):
|
||||
result[key] = value
|
||||
elif key in VOCABULARY and key not in {'operation', 'stage', 'adapter', 'platform_event_type', 'reason_code'}:
|
||||
if category(key, value):
|
||||
result[key] = value
|
||||
elif key == 'capability_name' and any(category(f, value) for f in ('operation', 'platform_event_type')):
|
||||
result[key] = value
|
||||
elif key == 'capability_name' and value in PROCESSORS - {''}:
|
||||
result[key] = value
|
||||
elif key in ('event_types', 'api_operations', 'processor_types') and isinstance(value, (tuple, list)):
|
||||
allowed = (
|
||||
VOCABULARY['platform_event_type']
|
||||
if key == 'event_types'
|
||||
else VOCABULARY['operation']
|
||||
if key == 'api_operations'
|
||||
else PROCESSORS
|
||||
)
|
||||
result[key] = [v for v in value[:128] if type(v) is str and v in allowed]
|
||||
elif (
|
||||
key in ('source_revision', 'target_revision')
|
||||
and isinstance(value, str)
|
||||
and re.fullmatch('[a-f0-9]{40}', value)
|
||||
):
|
||||
result[key] = value
|
||||
elif key == 'latency_buckets' and isinstance(value, (list, tuple)):
|
||||
result[key] = [v for v in value[:128] if type(v) is int and 0 <= v <= 1_000_000]
|
||||
return result
|
||||
|
||||
|
||||
def error_fields(error: BaseException, operation: str, stage: str) -> dict:
|
||||
cls = type(error)
|
||||
# Third party exception names may be dynamically constructed from input.
|
||||
name = cls.__name__ if getattr(builtins, cls.__name__, None) is cls else 'Exception'
|
||||
if isinstance(error, TimeoutError):
|
||||
name = 'TimeoutError'
|
||||
# No message, traceback, filename, line number, or locals are inspected.
|
||||
fingerprint = hashlib.sha256(f'{operation}|{stage}|{cls.__module__}|{cls.__qualname__}'.encode()).hexdigest()
|
||||
return {'error_type': name, 'error_fingerprint': fingerprint}
|
||||
@@ -1,399 +0,0 @@
|
||||
"""Bounded, memory-only Beta diagnostics transport, isolated from usage telemetry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from packaging.version import Version, InvalidVersion
|
||||
|
||||
from . import diagnostic_privacy as privacy
|
||||
|
||||
|
||||
class DiagnosticsManager:
|
||||
"""All producers are synchronous; only this owned worker performs I/O.
|
||||
|
||||
Retention: 2048 events / 15 minutes, 5 attempts, 8 KiB per event. An in-flight
|
||||
event stays in the same bounded queue until explicitly ACKed. No disk spool
|
||||
means abrupt process loss cannot be counted after restart (documented).
|
||||
"""
|
||||
|
||||
def __init__(self, ap, *, version=None, instance_id=None, capacity=2048, marker_path=None):
|
||||
from ..utils import constants
|
||||
from .diagnostic_catalog import catalog
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
catalog()
|
||||
self.ap = ap
|
||||
self.marker_path = Path(marker_path) if marker_path is not None else None
|
||||
self._session_id = str(uuid4())
|
||||
self._marker_active = False
|
||||
raw_version = version or importlib.metadata.version('langbot')
|
||||
try:
|
||||
parsed = Version(raw_version)
|
||||
self.version = str(parsed)
|
||||
self.beta = bool(parsed.pre and parsed.pre[0] == 'b' and not parsed.local)
|
||||
except InvalidVersion:
|
||||
self.version, self.beta = '', False
|
||||
self.instance_id = instance_id or constants.instance_id
|
||||
try:
|
||||
from langbot._build_info import CORE_REVISION
|
||||
except ImportError:
|
||||
CORE_REVISION = ''
|
||||
candidate = os.getenv('LANGBOT_BUILD_REVISION', CORE_REVISION)
|
||||
self.revision = candidate.lower() if re.fullmatch('[a-fA-F0-9]{40}', candidate or '') else ''
|
||||
try:
|
||||
self.sdk_version = privacy.version_string(importlib.metadata.version('langbot-plugin'))
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
self.sdk_version = ''
|
||||
self.capacity = max(1, min(capacity, 2048))
|
||||
self.pending: list[dict] = []
|
||||
self.counters = Counter()
|
||||
self._reported = Counter()
|
||||
self._last_retry = False
|
||||
self.max_attempts = 5
|
||||
self.retention_seconds = 900
|
||||
self.request_timeout = 10
|
||||
self.client: httpx.AsyncClient | None = None
|
||||
self._attempts: dict[str, int] = {}
|
||||
self._born: dict[str, float] = {}
|
||||
self._worker = None
|
||||
self._request = None
|
||||
self._closing = False
|
||||
self._wake = asyncio.Event()
|
||||
self._flush_lock = asyncio.Lock()
|
||||
|
||||
async def start_session(self):
|
||||
"""One bounded, content-free marker per process; never per-event disk I/O."""
|
||||
if not self.enabled:
|
||||
await self._clear_marker()
|
||||
return
|
||||
previous = False
|
||||
if self.marker_path is not None:
|
||||
|
||||
def mark():
|
||||
previous = self.marker_path.exists()
|
||||
self.marker_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.marker_path.write_text(self._session_id)
|
||||
return previous
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
previous = await asyncio.to_thread(mark)
|
||||
self._marker_active = True
|
||||
privacy.code_value('operation', 'startup.session')
|
||||
self.emit(
|
||||
'lifecycle',
|
||||
'startup.session',
|
||||
'started',
|
||||
source='startup',
|
||||
stage='snapshot',
|
||||
attributes={
|
||||
'previous_session_unclean': previous,
|
||||
'os': platform.system().lower(),
|
||||
'arch': platform.machine().lower(),
|
||||
'python_version': '.'.join(map(str, sys.version_info[:3])),
|
||||
'database': self.ap.instance_config.data.get('database', {}).get('use', ''),
|
||||
'edition': self.ap.instance_config.data.get('system', {}).get('edition', ''),
|
||||
},
|
||||
)
|
||||
|
||||
async def _clear_marker(self):
|
||||
if self.marker_path is None:
|
||||
return
|
||||
|
||||
def remove():
|
||||
if self.marker_path.exists():
|
||||
with self.marker_path.open() as handle:
|
||||
owner = handle.read(64)
|
||||
if owner == self._session_id or not self._marker_active:
|
||||
self.marker_path.unlink(missing_ok=True)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
await asyncio.to_thread(remove)
|
||||
self._marker_active = False
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
config = getattr(getattr(self.ap, 'instance_config', None), 'data', {}).get('space', {})
|
||||
return (
|
||||
self.beta
|
||||
and not self._closing
|
||||
and not config.get('disable_telemetry', False)
|
||||
and not config.get('disable_beta_diagnostics', False)
|
||||
and bool(config.get('url'))
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
self.pending.clear()
|
||||
self._attempts.clear()
|
||||
self._born.clear()
|
||||
if self._request is not None and not self._request.done():
|
||||
self._request.cancel()
|
||||
|
||||
def emit(
|
||||
self,
|
||||
kind,
|
||||
operation,
|
||||
outcome,
|
||||
*,
|
||||
source='internal',
|
||||
workspace_uuid='',
|
||||
attributes=None,
|
||||
stage='execute',
|
||||
adapter='',
|
||||
processor_type='',
|
||||
platform_event_type='',
|
||||
reason_code='',
|
||||
duration_ms=0,
|
||||
error=None,
|
||||
trace_id='',
|
||||
span_id='',
|
||||
parent_span_id='',
|
||||
run_id='',
|
||||
**ignored,
|
||||
):
|
||||
"""Project known scalar fields; failures in diagnostics never escape."""
|
||||
try:
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
return
|
||||
if kind not in privacy.KINDS or outcome not in privacy.OUTCOMES:
|
||||
return
|
||||
self.counters['generated'] += 1
|
||||
event = {
|
||||
'schema_version': 1,
|
||||
'event_id': str(uuid4()),
|
||||
'kind': kind,
|
||||
'instance_id': self.instance_id,
|
||||
'workspace_uuid': privacy.opaque(workspace_uuid),
|
||||
'core_version': self.version,
|
||||
'core_revision': self.revision,
|
||||
'sdk_version': self.sdk_version,
|
||||
'release_channel': 'beta',
|
||||
'source': source if source in privacy.SOURCES else 'internal',
|
||||
'operation': privacy.category('operation', operation),
|
||||
'stage': privacy.category('stage', stage),
|
||||
'outcome': outcome,
|
||||
'reason_code': privacy.category('reason_code', reason_code),
|
||||
'adapter': privacy.category('adapter', adapter),
|
||||
'processor_type': processor_type if processor_type in privacy.PROCESSORS else '',
|
||||
'platform_event_type': privacy.category('platform_event_type', platform_event_type),
|
||||
'occurred_at': datetime.now(timezone.utc).isoformat(),
|
||||
'count': 1,
|
||||
'sample_rate': 1,
|
||||
'duration_ms': max(0, min(float(duration_ms), 86400000)),
|
||||
'attributes': privacy.attributes(attributes),
|
||||
}
|
||||
for key, value in (
|
||||
('trace_id', trace_id),
|
||||
('span_id', span_id),
|
||||
('parent_span_id', parent_span_id),
|
||||
('run_id', run_id),
|
||||
):
|
||||
if privacy.opaque(value):
|
||||
event[key] = privacy.opaque(value)
|
||||
if error is not None:
|
||||
event.update(privacy.error_fields(error, event['operation'], event['stage']))
|
||||
encoded = json.dumps(event, allow_nan=False).encode()
|
||||
if (
|
||||
not isinstance(self.instance_id, str)
|
||||
or not 0 < len(self.instance_id) <= 128
|
||||
or len(encoded) > 8192
|
||||
or len(self.pending) >= self.capacity
|
||||
):
|
||||
self.counters['dropped'] += 1
|
||||
return
|
||||
self.pending.append(event)
|
||||
self._born[event['event_id']] = time.monotonic()
|
||||
self.counters['queued'] += 1
|
||||
self._wake.set()
|
||||
except Exception:
|
||||
self.counters['dropped'] += 1
|
||||
|
||||
async def credentials(self, workspace_uuid):
|
||||
# Resolve each Workspace independently in the background. No OSS secret.
|
||||
token = os.getenv('LANGBOT_TELEMETRY_INGEST_TOKEN', '').strip()
|
||||
if token:
|
||||
return {'X-LangBot-Telemetry-Token': token}
|
||||
if not workspace_uuid:
|
||||
return {}
|
||||
users = getattr(self.ap, 'user_service', None)
|
||||
space = getattr(self.ap, 'space_service', None)
|
||||
if users is None or space is None:
|
||||
return {}
|
||||
owner = await users.get_workspace_owner(workspace_uuid)
|
||||
email = getattr(owner, 'user', None)
|
||||
token = await space.get_valid_access_token(email) if email else None
|
||||
return {'Authorization': f'Bearer {token}'} if token else {}
|
||||
|
||||
def _remove(self, ids):
|
||||
self.pending[:] = [e for e in self.pending if e['event_id'] not in ids]
|
||||
for event_id in ids:
|
||||
self._attempts.pop(event_id, None)
|
||||
self._born.pop(event_id, None)
|
||||
|
||||
async def flush_once(self):
|
||||
async with self._flush_lock:
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
return
|
||||
expired = {
|
||||
e['event_id']
|
||||
for e in self.pending
|
||||
if time.monotonic() - self._born.get(e['event_id'], 0) > self.retention_seconds
|
||||
}
|
||||
self.counters['dropped'] += len(expired)
|
||||
self._remove(expired)
|
||||
if not self.pending:
|
||||
return
|
||||
# Do not authenticate a mixed-Workspace batch with one owner's token.
|
||||
workspace = self.pending[0]['workspace_uuid']
|
||||
batch, size = [], 64
|
||||
for event in self.pending:
|
||||
n = len(json.dumps(event).encode()) + 2
|
||||
if event['workspace_uuid'] != workspace:
|
||||
continue
|
||||
if len(batch) == 50 or size + n > 256 * 1024:
|
||||
break
|
||||
batch.append(event)
|
||||
size += n
|
||||
ids = {e['event_id'] for e in batch}
|
||||
for event_id in ids:
|
||||
self._attempts[event_id] = self._attempts.get(event_id, 0) + 1
|
||||
acked, rejected = set(), set()
|
||||
permanent = False
|
||||
try:
|
||||
async with asyncio.timeout(self.request_timeout):
|
||||
headers = {}
|
||||
try:
|
||||
async with asyncio.timeout(min(1.0, self.request_timeout / 3)):
|
||||
headers = await self.credentials(workspace)
|
||||
except Exception:
|
||||
pass
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
return
|
||||
if self.client is None:
|
||||
self.client = httpx.AsyncClient(timeout=self.request_timeout, follow_redirects=False)
|
||||
url = (
|
||||
self.ap.instance_config.data['space']['url'].rstrip('/') + '/api/v1/telemetry/diagnostics/batch'
|
||||
)
|
||||
# Stream the response to bound malicious/old server responses too.
|
||||
async with self.client.stream(
|
||||
'POST', url, json={'schema_version': 1, 'events': batch}, headers=headers
|
||||
) as response:
|
||||
body = bytearray()
|
||||
async for chunk in response.aiter_bytes():
|
||||
body.extend(chunk)
|
||||
if len(body) > 65536:
|
||||
raise ValueError('diagnostics_response_limit')
|
||||
if response.status_code == 200:
|
||||
payload = json.loads(body)
|
||||
if payload.get('code') == 200 and isinstance(payload.get('data'), dict):
|
||||
data = payload['data']
|
||||
acked = {x for x in data.get('accepted_event_ids', []) if isinstance(x, str)} & ids
|
||||
rejected = {
|
||||
x.get('event_id')
|
||||
for x in data.get('rejected', [])
|
||||
if isinstance(x, dict) and x.get('code') == 'invalid_event'
|
||||
} & ids
|
||||
elif isinstance(payload.get('code'), int):
|
||||
permanent = 400 <= payload['code'] < 500 and payload['code'] != 429
|
||||
else:
|
||||
permanent = 400 <= response.status_code < 500 and response.status_code != 429
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
return
|
||||
rejected -= acked
|
||||
remaining = ids - acked - rejected
|
||||
if remaining:
|
||||
self.counters['failed'] += 1
|
||||
exhausted = {i for i in remaining if permanent or self._attempts.get(i, 0) >= self.max_attempts}
|
||||
self.counters['acked'] += len(acked)
|
||||
self.counters['dropped'] += len(rejected | exhausted)
|
||||
self._last_retry = bool(remaining - exhausted)
|
||||
self.counters['retried'] += len(remaining - exhausted)
|
||||
self._remove(acked | rejected | exhausted)
|
||||
|
||||
def report_transport(self, stage='snapshot'):
|
||||
# Snapshot interval deltas before enqueue; retries retain this event ID.
|
||||
snapshot = self.counters.copy()
|
||||
delta = snapshot - self._reported
|
||||
before = self.counters['queued']
|
||||
self.emit(
|
||||
'transport',
|
||||
'diagnostics.transport',
|
||||
'partial' if delta['dropped'] else 'succeeded',
|
||||
stage=stage,
|
||||
attributes={**dict(delta), 'queue_size': len(self.pending), 'capacity': self.capacity},
|
||||
)
|
||||
if self.counters['queued'] > before:
|
||||
self._reported = snapshot
|
||||
|
||||
def start(self):
|
||||
if self.beta and self._worker is None:
|
||||
self._worker = asyncio.create_task(self._loop(), name='beta-diagnostics')
|
||||
|
||||
async def _loop(self):
|
||||
next_send, next_summary, backoff = 0.0, time.monotonic() + 60, 0.0
|
||||
try:
|
||||
while not self._closing:
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
if self._marker_active:
|
||||
await self._clear_marker()
|
||||
elif time.monotonic() >= next_summary:
|
||||
self.report_transport()
|
||||
next_summary = time.monotonic() + 60
|
||||
if self._request is not None and self._request.done():
|
||||
with contextlib.suppress(asyncio.CancelledError, Exception):
|
||||
self._request.result()
|
||||
self._request = None
|
||||
backoff = min(30, max(1, backoff * 2)) if self._last_retry else 0
|
||||
next_send = time.monotonic() + random.uniform(0.5, 1.5) * backoff
|
||||
if self.enabled and self.pending and self._request is None and time.monotonic() >= next_send:
|
||||
self._request = asyncio.create_task(self.flush_once(), name='beta-diagnostics-batch')
|
||||
self._wake.clear()
|
||||
with contextlib.suppress(TimeoutError):
|
||||
await asyncio.wait_for(self._wake.wait(), 0.1)
|
||||
finally:
|
||||
if self._request is not None:
|
||||
self._request.cancel()
|
||||
await asyncio.gather(self._request, return_exceptions=True)
|
||||
|
||||
async def shutdown(self, drain_timeout=2):
|
||||
if self._worker is not None:
|
||||
self._worker.cancel()
|
||||
await asyncio.gather(self._worker, return_exceptions=True)
|
||||
self._worker = None
|
||||
if self.enabled and drain_timeout > 0:
|
||||
self.report_transport(stage='shutdown')
|
||||
with contextlib.suppress(Exception):
|
||||
async with asyncio.timeout(drain_timeout):
|
||||
while self.pending:
|
||||
await self.flush_once()
|
||||
if self.pending:
|
||||
await asyncio.sleep(0.1)
|
||||
self.counters['dropped'] += len(self.pending)
|
||||
self._closing = True
|
||||
self.clear()
|
||||
await self._clear_marker()
|
||||
if self.client is not None:
|
||||
await self.client.aclose()
|
||||
@@ -1,546 +0,0 @@
|
||||
"""Explicit diagnostic boundaries, preserving coroutine and generator semantics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import contextvars
|
||||
import functools
|
||||
import inspect
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
from .diagnostic_transport import DiagnosticsManager # noqa: F401
|
||||
from . import diagnostic_privacy as privacy
|
||||
|
||||
_CURRENT = contextvars.ContextVar('beta_diagnostic_span', default=None)
|
||||
|
||||
|
||||
def current_span():
|
||||
return _CURRENT.get()
|
||||
|
||||
|
||||
def set_outcome(outcome, *, reason_code=''):
|
||||
span = current_span()
|
||||
if span is not None:
|
||||
span.outcome = outcome if outcome in privacy.OUTCOMES else 'unknown'
|
||||
span.fields['reason_code'] = privacy.category('reason_code', reason_code)
|
||||
|
||||
|
||||
def annotate(**fields):
|
||||
"""Trusted hook metadata is still projected at the transport boundary."""
|
||||
span = current_span()
|
||||
if span is not None:
|
||||
span.fields.update(fields)
|
||||
|
||||
|
||||
def adapter_event_received(owner, event):
|
||||
"""Record one converted event before dispatch, including native callback paths."""
|
||||
with contextlib.suppress(Exception):
|
||||
manager = _manager(owner)
|
||||
if manager is None:
|
||||
return
|
||||
from langbot_plugin.api.entities.builtin.platform.events import EBAEvent
|
||||
from .adapter_diagnostics import message_scenario
|
||||
|
||||
if not isinstance(event, EBAEvent) or not _context_matches(manager, owner, _owner_context(owner)):
|
||||
return
|
||||
fields = _context_fields(owner, {'event': event})
|
||||
fields['attributes'] = {
|
||||
**fields.get('attributes', {}),
|
||||
'adapter_evidence': True,
|
||||
**message_scenario({'event': event}),
|
||||
}
|
||||
parent = current_span()
|
||||
if (
|
||||
parent
|
||||
and parent.manager is manager
|
||||
and not (
|
||||
fields.get('workspace_uuid')
|
||||
and parent.fields.get('workspace_uuid')
|
||||
and fields['workspace_uuid'] != parent.fields['workspace_uuid']
|
||||
)
|
||||
):
|
||||
fields['trace_id'] = parent.fields['trace_id']
|
||||
fields['parent_span_id'] = parent.fields['span_id']
|
||||
if parent.fields.get('source') in ('webui_debug', 'synthetic'):
|
||||
fields['source'] = parent.fields['source']
|
||||
fields['attributes']['synthetic'] = True
|
||||
fields.setdefault('source', 'platform')
|
||||
privacy.code_value('operation', 'platform.adapter_event')
|
||||
manager.emit('event', 'platform.adapter_event', 'succeeded', stage='accepted', **fields)
|
||||
|
||||
|
||||
def _owner_app(owner):
|
||||
"""An explicit owner (even absent/disabled) is an inheritance barrier."""
|
||||
if owner is None:
|
||||
return None, False
|
||||
if hasattr(owner, 'ap'):
|
||||
return owner.ap, True
|
||||
if hasattr(owner, 'diagnostics') or hasattr(owner, 'instance_config'):
|
||||
return owner, True
|
||||
for name in ('requester', 'logger', 'adapter'):
|
||||
nested = getattr(owner, name, None)
|
||||
if nested is not None and nested is not owner:
|
||||
app, explicit = _owner_app(nested)
|
||||
if explicit:
|
||||
return app, True
|
||||
return None, False
|
||||
|
||||
|
||||
def _manager(owner):
|
||||
app, _ = _owner_app(owner)
|
||||
manager = getattr(app, 'diagnostics', None)
|
||||
if manager is None or not callable(getattr(manager, 'emit', None)) or not getattr(manager, 'enabled', False):
|
||||
return None
|
||||
# Honor the shared Span/management producer interface without inheriting a
|
||||
# different producer just because this application's producer is absent.
|
||||
config = getattr(getattr(app, 'instance_config', None), 'data', {}).get('space', {})
|
||||
if config.get('disable_telemetry', False) or config.get('disable_beta_diagnostics', False):
|
||||
return None
|
||||
# Direct manager holders (e.g. ReplyStreamSession) are intentional. An app
|
||||
# must never borrow another app's manager, policy, or credential resolver.
|
||||
if hasattr(app, 'instance_config') and getattr(manager, 'ap', app) is not app:
|
||||
return None
|
||||
return manager
|
||||
|
||||
|
||||
def _owner_context(owner):
|
||||
context = getattr(owner, 'execution_context', None)
|
||||
if context is not None:
|
||||
return context
|
||||
for name in ('requester', 'logger', 'adapter'):
|
||||
nested = getattr(owner, name, None)
|
||||
if nested is not None and nested is not owner:
|
||||
context = _owner_context(nested)
|
||||
if context is not None:
|
||||
return context
|
||||
return None
|
||||
|
||||
|
||||
def _execution_context(owner, bound):
|
||||
context = bound.get('execution_context') or _owner_context(owner)
|
||||
adapter_context = bound.get('adapter_context')
|
||||
if isinstance(adapter_context, dict):
|
||||
context = adapter_context.get('_execution_context') or context
|
||||
query = bound.get('query')
|
||||
if query is not None:
|
||||
context = getattr(query, '_execution_context', None) or context
|
||||
return context
|
||||
|
||||
|
||||
def _context_matches(manager, owner, context):
|
||||
from ..api.http.context import ExecutionContext
|
||||
|
||||
if not isinstance(context, ExecutionContext):
|
||||
return True
|
||||
instance = getattr(getattr(manager.ap, 'workspace_service', None), 'instance_uuid', manager.instance_id)
|
||||
if instance != context.instance_uuid:
|
||||
return False
|
||||
owned = _owner_context(owner)
|
||||
return not isinstance(owned, ExecutionContext) or (
|
||||
owned.instance_uuid,
|
||||
owned.workspace_uuid,
|
||||
owned.placement_generation,
|
||||
) == (context.instance_uuid, context.workspace_uuid, context.placement_generation)
|
||||
|
||||
|
||||
def _context_fields(owner, bound):
|
||||
context = _execution_context(owner, bound)
|
||||
query = bound.get('query')
|
||||
from .diagnostic_catalog import adapter_fields
|
||||
|
||||
adapter = getattr(owner, 'adapter', None) or owner
|
||||
fields = adapter_fields(adapter)
|
||||
# ExecutionContext is constructed/validated by the existing auth boundary;
|
||||
# do not infer Workspace identity from arbitrary payload dicts or event IDs.
|
||||
if context is not None:
|
||||
from ..api.http.context import ExecutionContext
|
||||
|
||||
if isinstance(context, ExecutionContext):
|
||||
fields['workspace_uuid'] = context.workspace_uuid
|
||||
saved = getattr(query, '_diagnostic_context', None) if query is not None else None
|
||||
if isinstance(saved, dict) and saved.get('workspace_uuid') == context.workspace_uuid:
|
||||
fields.update(saved)
|
||||
binding = bound.get('binding')
|
||||
if binding is not None:
|
||||
fields['processor_type'] = getattr(binding, 'processor_type', '')
|
||||
event = bound.get('event')
|
||||
if event is not None:
|
||||
fields['platform_event_type'] = getattr(event, 'event_type', None) or getattr(event, 'type', '')
|
||||
delivery = getattr(event, 'delivery', None)
|
||||
if getattr(delivery, 'surface', None) == 'webui':
|
||||
fields['source'] = 'webui_debug'
|
||||
fields['attributes'] = {'synthetic': True}
|
||||
if getattr(owner, 'mock', False) is True:
|
||||
fields['source'] = 'webui_debug'
|
||||
fields['attributes'] = {'synthetic': True}
|
||||
return fields
|
||||
|
||||
|
||||
class Span:
|
||||
def __init__(self, manager, kind, operation, fields):
|
||||
parent = current_span()
|
||||
self.manager = manager
|
||||
self.kind = kind
|
||||
self.operation = operation
|
||||
self.fields = dict(fields)
|
||||
self.adapter_api_active = bool(self.fields.pop('_adapter_api_active', False))
|
||||
self.adapter_void_ack = bool(self.fields.pop('_adapter_void_ack', False))
|
||||
self.adapter_read_ack = bool(self.fields.pop('_adapter_read_ack', False))
|
||||
self.adapter_api_result = None
|
||||
if (
|
||||
parent
|
||||
and parent.fields.get('workspace_uuid')
|
||||
and self.fields.get('workspace_uuid')
|
||||
and parent.fields['workspace_uuid'] != self.fields['workspace_uuid']
|
||||
):
|
||||
parent = None
|
||||
if self.fields.get('operation'):
|
||||
self.operation = privacy.category('operation', self.fields.pop('operation')) or operation
|
||||
self.outcome = None
|
||||
self.finished = False
|
||||
self.started = time.monotonic()
|
||||
self.fields['trace_id'] = (
|
||||
parent.fields['trace_id']
|
||||
if parent and parent.manager is manager
|
||||
else (privacy.opaque(self.fields.get('trace_id')) or str(uuid4()))
|
||||
)
|
||||
self.fields['span_id'] = str(uuid4())
|
||||
if parent and parent.manager is manager:
|
||||
self.adapter_api_active = self.adapter_api_active or parent.adapter_api_active
|
||||
self.fields['parent_span_id'] = parent.fields['span_id']
|
||||
for key in ('workspace_uuid', 'adapter', 'processor_type', 'platform_event_type', 'run_id'):
|
||||
if not self.fields.get(key) and parent.fields.get(key):
|
||||
self.fields[key] = parent.fields[key]
|
||||
if parent.fields.get('source') in ('webui_debug', 'synthetic'):
|
||||
self.fields['source'] = parent.fields['source']
|
||||
self.fields['attributes'] = {**self.fields.get('attributes', {}), 'synthetic': True}
|
||||
self.emit('started')
|
||||
|
||||
def emit(self, outcome, **extra):
|
||||
if self.manager is not None:
|
||||
self.manager.emit(self.kind, self.operation, outcome, **{**self.fields, **extra})
|
||||
|
||||
@contextlib.contextmanager
|
||||
def activate(self):
|
||||
token = _CURRENT.set(self)
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
_CURRENT.reset(token)
|
||||
|
||||
def finish(self, error=None):
|
||||
if self.finished:
|
||||
return
|
||||
self.finished = True
|
||||
error = error if error is not None else self.fields.pop('error', None)
|
||||
if isinstance(error, (asyncio.CancelledError, GeneratorExit)):
|
||||
outcome = 'cancelled'
|
||||
elif isinstance(error, TimeoutError):
|
||||
outcome = 'timeout'
|
||||
elif error is not None:
|
||||
outcome = self.outcome if self.outcome in ('timeout', 'partial', 'rejected') else 'failed'
|
||||
else:
|
||||
outcome = self.outcome or 'succeeded'
|
||||
self.emit(outcome, error=error, duration_ms=(time.monotonic() - self.started) * 1000)
|
||||
|
||||
|
||||
def result_outcome(value):
|
||||
"""Inspect only the SDK response's status, not arbitrary result contents."""
|
||||
from langbot_plugin.api.entities.builtin.platform.events import EBAEvent
|
||||
from langbot_plugin.runtime.io.handler import ActionResponse
|
||||
|
||||
span = current_span()
|
||||
if span is not None and span.fields.get('stage') == 'convert':
|
||||
if isinstance(value, EBAEvent):
|
||||
annotate(platform_event_type=value.type)
|
||||
if span.fields.get('attributes', {}).get('adapter_evidence'):
|
||||
from .adapter_diagnostics import message_scenario
|
||||
|
||||
annotate(attributes={**span.fields['attributes'], **message_scenario({'event': value})})
|
||||
# Successful conversion is counted once at dispatch, which also
|
||||
# covers adapters constructing EBA events in native callbacks.
|
||||
span.fields['attributes']['adapter_evidence'] = False
|
||||
elif value is None:
|
||||
set_outcome('skipped', reason_code='not_matched')
|
||||
if not isinstance(value, EBAEvent) and span.fields.get('attributes', {}).get('adapter_evidence'):
|
||||
span.fields['attributes']['adapter_evidence'] = False
|
||||
if span is not None and span.kind == 'api' and span.fields.get('attributes', {}).get('adapter_evidence'):
|
||||
from .adapter_diagnostics import response_outcome
|
||||
|
||||
# A normal return is not an acknowledgement. Fail closed for acceptance
|
||||
# (but never for the business call) even if result inspection raises.
|
||||
if span.outcome is None:
|
||||
set_outcome('skipped')
|
||||
outcome = span.adapter_api_result
|
||||
if outcome is None:
|
||||
if (value is None and span.adapter_void_ack) or (
|
||||
span.adapter_read_ack and value is not None and value != ''
|
||||
):
|
||||
outcome = 'succeeded'
|
||||
else:
|
||||
outcome = response_outcome(value, span.fields.get('adapter'))
|
||||
set_outcome(outcome, **({'reason_code': 'response_error'} if outcome == 'failed' else {}))
|
||||
if isinstance(value, ActionResponse):
|
||||
if value.code != 0:
|
||||
set_outcome('failed', reason_code='response_error')
|
||||
|
||||
|
||||
def observe(kind, operation, *, source='internal', stage='execute', ap=None, fields=None):
|
||||
"""Explicit boundary with a stable/off fast path and transparent generators."""
|
||||
privacy.code_value('operation', operation)
|
||||
privacy.code_value('stage', stage)
|
||||
|
||||
def decorate(fn):
|
||||
signature = inspect.signature(fn)
|
||||
|
||||
def span_for(args, kwargs):
|
||||
try:
|
||||
bound = signature.bind_partial(*args, **kwargs).arguments
|
||||
owner = bound.get(next(iter(signature.parameters), ''))
|
||||
manager_owner = (ap() if callable(ap) else ap) if ap is not None else owner
|
||||
manager = _manager(manager_owner)
|
||||
_, explicit = _owner_app(manager_owner)
|
||||
context = _execution_context(owner, bound)
|
||||
parent = current_span()
|
||||
if manager is None and not explicit and ap is None and parent is not None:
|
||||
# Stateless converters may inherit, but a different explicit
|
||||
# Workspace must not select a parent's credentials/policy.
|
||||
if context is not None and getattr(context, 'workspace_uuid', None) != parent.fields.get(
|
||||
'workspace_uuid'
|
||||
):
|
||||
return None
|
||||
manager = parent.manager
|
||||
if manager is None or not manager.enabled:
|
||||
return None
|
||||
if not _context_matches(manager, owner, context):
|
||||
return None
|
||||
metadata = {'source': source, 'stage': stage}
|
||||
metadata.update(_context_fields(owner, bound))
|
||||
from .diagnostic_catalog import catalog
|
||||
|
||||
for directory, entry in catalog().items():
|
||||
if directory in fn.__module__.split('.') and '.platform.' in fn.__module__:
|
||||
metadata['adapter'] = entry['adapter']
|
||||
break
|
||||
if fields:
|
||||
extra = fields(bound)
|
||||
extra['attributes'] = {**metadata.get('attributes', {}), **extra.get('attributes', {})}
|
||||
metadata.update(extra)
|
||||
from .adapter_diagnostics import boundary_fields
|
||||
|
||||
if parent and (
|
||||
parent.manager is not manager
|
||||
or (
|
||||
metadata.get('workspace_uuid')
|
||||
and parent.fields.get('workspace_uuid')
|
||||
and metadata['workspace_uuid'] != parent.fields['workspace_uuid']
|
||||
)
|
||||
):
|
||||
parent = None
|
||||
evidence = (
|
||||
boundary_fields(fn.__module__, kind, operation, bound, parent)
|
||||
if metadata.get('attributes', {}).get('adapter_evidence') is not False
|
||||
else {}
|
||||
)
|
||||
metadata['attributes'] = {**metadata.get('attributes', {}), **evidence.pop('attributes', {})}
|
||||
metadata.update(evidence)
|
||||
return Span(manager, kind, operation, metadata)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def inspect_result(value):
|
||||
with contextlib.suppress(Exception):
|
||||
result_outcome(value)
|
||||
|
||||
def finish(span, error=None):
|
||||
if span is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
span.finish(error)
|
||||
|
||||
if inspect.isasyncgenfunction(fn):
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
class ObservedGenerator(AsyncGenerator):
|
||||
"""Delegate each native protocol operation, without extra close.
|
||||
|
||||
A yield-based proxy cannot distinguish athrow(GeneratorExit)
|
||||
(which may yield) from aclose() (which must reject a yield).
|
||||
Let the native generator implement that distinction and retain
|
||||
its own primary/cleanup exception and cancellation semantics.
|
||||
"""
|
||||
|
||||
def __init__(self, args, kwargs):
|
||||
self.gen = fn(*args, **kwargs)
|
||||
self.args, self.kwargs = args, kwargs
|
||||
self.span = None
|
||||
self.started = False
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.gen, name)
|
||||
|
||||
async def _advance(self, method, *values):
|
||||
if not self.started:
|
||||
self.started = True
|
||||
self.span = span_for(self.args, self.kwargs)
|
||||
token = _CURRENT.set(self.span)
|
||||
try:
|
||||
value = await method(*values)
|
||||
if self.span is not None and method != self.gen.aclose:
|
||||
inspect_result(value)
|
||||
if method == self.gen.aclose:
|
||||
finish(self.span, GeneratorExit())
|
||||
return value
|
||||
except StopAsyncIteration:
|
||||
finish(self.span)
|
||||
raise
|
||||
except BaseException as exc:
|
||||
# Rejected protocol calls (e.g. concurrent asend, a
|
||||
# yielded GeneratorExit) need not terminate the stream.
|
||||
if self.gen.ag_frame is None:
|
||||
finish(self.span, exc)
|
||||
raise
|
||||
finally:
|
||||
_CURRENT.reset(token)
|
||||
|
||||
def __anext__(self):
|
||||
return self._advance(self.gen.__anext__)
|
||||
|
||||
def asend(self, value):
|
||||
return self._advance(self.gen.asend, value)
|
||||
|
||||
def athrow(self, *values):
|
||||
return self._advance(self.gen.athrow, *values)
|
||||
|
||||
def aclose(self):
|
||||
return self._advance(self.gen.aclose)
|
||||
|
||||
@functools.wraps(fn)
|
||||
def stream(*args, **kwargs):
|
||||
return ObservedGenerator(args, kwargs)
|
||||
|
||||
return stream
|
||||
|
||||
@functools.wraps(fn)
|
||||
async def call(*args, **kwargs):
|
||||
span = span_for(args, kwargs)
|
||||
if span is None:
|
||||
token = _CURRENT.set(None)
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
finally:
|
||||
_CURRENT.reset(token)
|
||||
try:
|
||||
with span.activate():
|
||||
value = await fn(*args, **kwargs)
|
||||
inspect_result(value)
|
||||
finish(span)
|
||||
return value
|
||||
except BaseException as exc:
|
||||
finish(span, exc)
|
||||
raise
|
||||
|
||||
return call
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def event(owner, kind, operation, outcome, **fields):
|
||||
"""Emit a point-in-time fact from an existing state transition."""
|
||||
try:
|
||||
manager = _manager(owner)
|
||||
_, explicit = _owner_app(owner)
|
||||
parent = current_span()
|
||||
context = _owner_context(owner)
|
||||
if manager is None and not explicit and parent:
|
||||
if context is not None and getattr(context, 'workspace_uuid', None) != parent.fields.get('workspace_uuid'):
|
||||
return
|
||||
manager = parent.manager
|
||||
if manager is not None and _context_matches(manager, owner, context):
|
||||
inherited = dict(parent.fields) if parent and parent.manager is manager else {}
|
||||
if context is not None and getattr(context, 'workspace_uuid', None) != inherited.get('workspace_uuid'):
|
||||
inherited = _context_fields(owner, {})
|
||||
inherited.update(fields)
|
||||
manager.emit(kind, operation, outcome, **inherited)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def capture_context():
|
||||
span = current_span()
|
||||
if span is None or not span.manager.enabled:
|
||||
return None
|
||||
result = {
|
||||
k: span.fields[k]
|
||||
for k in ('trace_id', 'workspace_uuid', 'run_id', 'adapter', 'processor_type', 'platform_event_type', 'source')
|
||||
if k in span.fields
|
||||
}
|
||||
result['parent_span_id'] = span.fields['span_id']
|
||||
return result
|
||||
|
||||
|
||||
def link_context(saved):
|
||||
"""Link only after the existing Host run/installation validator accepted it."""
|
||||
span = current_span()
|
||||
if span is None or not isinstance(saved, dict):
|
||||
return
|
||||
if span.fields.get('workspace_uuid') and span.fields['workspace_uuid'] != saved.get('workspace_uuid'):
|
||||
return
|
||||
for key in ('trace_id', 'parent_span_id', 'workspace_uuid', 'run_id'):
|
||||
if privacy.opaque(saved.get(key)):
|
||||
span.fields[key] = saved[key]
|
||||
for key in ('adapter', 'platform_event_type'):
|
||||
if privacy.category(key, saved.get(key)):
|
||||
span.fields[key] = saved[key]
|
||||
if saved.get('processor_type') in privacy.PROCESSORS:
|
||||
span.fields['processor_type'] = saved['processor_type']
|
||||
if saved.get('source') in ('webui_debug', 'synthetic'):
|
||||
span.fields['source'] = saved['source']
|
||||
span.fields['attributes'] = {**span.fields.get('attributes', {}), 'synthetic': True}
|
||||
|
||||
|
||||
def declare_runner(descriptor):
|
||||
"""Allow only identifiers from a validated installed public Runner manifest."""
|
||||
import re
|
||||
|
||||
pairs = {'plugin_id': descriptor.get_plugin_id(), 'runner_id': descriptor.id}
|
||||
for field, value in pairs.items():
|
||||
allowed = privacy.VOCABULARY.setdefault(field, set())
|
||||
if (
|
||||
isinstance(value, str)
|
||||
and len(value) <= 128
|
||||
and len(allowed) < 1024
|
||||
and re.fullmatch(r'(?:plugin:)?[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?', value)
|
||||
):
|
||||
allowed.add(value)
|
||||
|
||||
|
||||
def runner_metadata(owner, descriptor, processor_type):
|
||||
manager = _manager(owner)
|
||||
if manager is None or not manager.enabled:
|
||||
return
|
||||
metadata = {
|
||||
'plugin_id': descriptor.get_plugin_id(),
|
||||
'runner_id': descriptor.id,
|
||||
'plugin_version': getattr(descriptor, 'plugin_version', ''),
|
||||
'runner_usage': 'event' if processor_type == 'event_processor' else 'agent',
|
||||
}
|
||||
annotate(attributes=metadata)
|
||||
event(
|
||||
owner,
|
||||
'capability',
|
||||
'runner.run',
|
||||
'succeeded',
|
||||
source='runtime',
|
||||
stage='snapshot',
|
||||
processor_type=processor_type,
|
||||
attributes={
|
||||
**metadata,
|
||||
'capability_type': 'processor',
|
||||
'capability_name': processor_type,
|
||||
'supported': True,
|
||||
'configured': True,
|
||||
'available': True,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user