mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
264 lines
9.6 KiB
Python
264 lines
9.6 KiB
Python
"""Call Pipeline Node - invoke an existing pipeline
|
|
|
|
Node metadata is loaded from: ../../templates/metadata/nodes/call_pipeline.yaml
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
import pydantic
|
|
|
|
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_event_logger
|
|
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|
|
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
|
from ..node import WorkflowNode, workflow_node
|
|
|
|
|
|
class _NoOpEventLogger(abstract_event_logger.AbstractEventLogger):
|
|
"""No-op event logger for workflow pipeline adapter."""
|
|
|
|
async def info(
|
|
self,
|
|
text: str,
|
|
images: Optional[list[platform_message.Image]] = None,
|
|
message_session_id: Optional[str] = None,
|
|
no_throw: bool = True,
|
|
):
|
|
pass
|
|
|
|
async def debug(
|
|
self,
|
|
text: str,
|
|
images: Optional[list[platform_message.Image]] = None,
|
|
message_session_id: Optional[str] = None,
|
|
no_throw: bool = True,
|
|
):
|
|
pass
|
|
|
|
async def warning(
|
|
self,
|
|
text: str,
|
|
images: Optional[list[platform_message.Image]] = None,
|
|
message_session_id: Optional[str] = None,
|
|
no_throw: bool = True,
|
|
):
|
|
pass
|
|
|
|
async def error(
|
|
self,
|
|
text: str,
|
|
images: Optional[list[platform_message.Image]] = None,
|
|
message_session_id: Optional[str] = None,
|
|
no_throw: bool = True,
|
|
):
|
|
pass
|
|
|
|
@workflow_node('call_pipeline')
|
|
class CallPipelineNode(WorkflowNode):
|
|
"""Call pipeline node - invoke an existing pipeline"""
|
|
|
|
category = 'action'
|
|
|
|
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
|
|
if not self.ap:
|
|
raise RuntimeError('Application instance not available — cannot call pipeline')
|
|
|
|
raw_query = inputs.get('query', '')
|
|
query_text = str(raw_query or inputs.get('input') or '')
|
|
pipeline_ref = str(self.get_config('pipeline_uuid', '') or '').strip()
|
|
|
|
if not pipeline_ref:
|
|
raise ValueError('No pipeline configured for call pipeline node')
|
|
|
|
pipeline_data = await self.ap.pipeline_service.get_pipeline(pipeline_ref)
|
|
if pipeline_data is None:
|
|
pipeline_data = await self.ap.pipeline_service.get_pipeline_by_name(pipeline_ref)
|
|
if pipeline_data is None:
|
|
raise ValueError(f'Pipeline not found: {pipeline_ref}')
|
|
|
|
pipeline_uuid = str(pipeline_data.get('uuid', '') or '')
|
|
if not pipeline_uuid:
|
|
raise ValueError(f'Pipeline UUID missing for: {pipeline_ref}')
|
|
|
|
runtime_pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(pipeline_uuid)
|
|
if runtime_pipeline is None:
|
|
raise ValueError(f'Runtime pipeline not loaded: {pipeline_uuid}')
|
|
|
|
adapter = _WorkflowPipelineCaptureAdapter(context=context)
|
|
adapter.bot_account_id = 'workflow-call-pipeline'
|
|
|
|
message_event = self._build_message_event(query_text, context)
|
|
message_chain = message_event.message_chain
|
|
launcher_type = (
|
|
provider_session.LauncherTypes.GROUP
|
|
if context.message_context and context.message_context.is_group
|
|
else provider_session.LauncherTypes.PERSON
|
|
)
|
|
launcher_id = context.session_id or context.execution_id
|
|
sender_id = (
|
|
context.message_context.sender_id
|
|
if context.message_context and context.message_context.sender_id
|
|
else context.user_id or f'workflow_{context.execution_id}'
|
|
)
|
|
|
|
query = pipeline_query.Query(
|
|
bot_uuid=context.bot_id,
|
|
query_id=-1,
|
|
launcher_type=launcher_type,
|
|
launcher_id=launcher_id,
|
|
sender_id=sender_id,
|
|
message_event=message_event,
|
|
message_chain=message_chain,
|
|
variables={
|
|
'_called_from_workflow': True,
|
|
'_workflow_execution_id': context.execution_id,
|
|
'_workflow_id': context.workflow_id,
|
|
**dict(context.variables or {}),
|
|
},
|
|
resp_messages=[],
|
|
resp_message_chain=[],
|
|
adapter=adapter,
|
|
pipeline_uuid=pipeline_uuid,
|
|
)
|
|
|
|
await runtime_pipeline.run(query)
|
|
|
|
response_text = adapter.get_last_text_response()
|
|
result = {
|
|
'pipeline_uuid': pipeline_uuid,
|
|
'pipeline_name': pipeline_data.get('name', ''),
|
|
'responses': adapter.responses,
|
|
'query_text': query_text,
|
|
}
|
|
|
|
return {'response': response_text, 'result': result}
|
|
|
|
def _build_message_event(
|
|
self,
|
|
query_text: str,
|
|
context: ExecutionContext,
|
|
) -> platform_events.MessageEvent:
|
|
message_chain_data = context.trigger_data.get('message_chain') or context.trigger_data.get('message', [])
|
|
if isinstance(message_chain_data, list) and message_chain_data:
|
|
message_chain = platform_message.MessageChain.model_validate(message_chain_data)
|
|
else:
|
|
message_chain = platform_message.MessageChain([platform_message.Plain(text=query_text)])
|
|
|
|
if context.message_context and context.message_context.is_group:
|
|
group = platform_entities.Group(
|
|
id=context.message_context.group_id or context.session_id or 'workflow_group',
|
|
name=context.message_context.raw_message.get('group_name', 'Workflow Group') if context.message_context.raw_message else 'Workflow Group',
|
|
permission=platform_entities.Permission.Member,
|
|
)
|
|
sender = platform_entities.GroupMember(
|
|
id=context.message_context.sender_id,
|
|
member_name=context.message_context.sender_name or 'Workflow User',
|
|
permission=platform_entities.Permission.Member,
|
|
group=group,
|
|
)
|
|
return platform_events.GroupMessage(
|
|
sender=sender,
|
|
message_chain=message_chain,
|
|
time=context.message_context.raw_message.get('time') if context.message_context.raw_message else None,
|
|
)
|
|
|
|
sender = platform_entities.Friend(
|
|
id=context.message_context.sender_id if context.message_context else context.user_id or 'workflow_user',
|
|
nickname=context.message_context.sender_name if context.message_context else 'Workflow User',
|
|
remark=context.message_context.sender_name if context.message_context else 'Workflow User',
|
|
)
|
|
return platform_events.FriendMessage(
|
|
sender=sender,
|
|
message_chain=message_chain,
|
|
time=context.message_context.raw_message.get('time')
|
|
if context.message_context and context.message_context.raw_message
|
|
else None,
|
|
)
|
|
|
|
class _WorkflowPipelineCaptureAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
"""Adapter to capture pipeline responses for workflow execution."""
|
|
|
|
class Config:
|
|
arbitrary_types_allowed = True
|
|
|
|
responses: list[dict[str, Any]] = []
|
|
context: Optional[ExecutionContext] = pydantic.Field(default=None, exclude=True)
|
|
|
|
def __init__(self, context: ExecutionContext):
|
|
super().__init__(config={}, logger=_NoOpEventLogger(), context=context)
|
|
self.responses = []
|
|
|
|
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
|
payload = {
|
|
'type': 'send',
|
|
'target_type': target_type,
|
|
'target_id': target_id,
|
|
'content': str(message),
|
|
'message_chain': message.model_dump(),
|
|
}
|
|
self.responses.append(payload)
|
|
return payload
|
|
|
|
async def reply_message(
|
|
self,
|
|
message_source: platform_events.MessageEvent,
|
|
message: platform_message.MessageChain,
|
|
quote_origin: bool = False,
|
|
):
|
|
payload = {
|
|
'type': 'reply',
|
|
'content': str(message),
|
|
'message_chain': message.model_dump(),
|
|
'quote_origin': quote_origin,
|
|
}
|
|
self.responses.append(payload)
|
|
return payload
|
|
|
|
async def reply_message_chunk(
|
|
self,
|
|
message_source: platform_events.MessageEvent,
|
|
bot_message: dict,
|
|
message: platform_message.MessageChain,
|
|
quote_origin: bool = False,
|
|
is_final: bool = False,
|
|
):
|
|
payload = {
|
|
'type': 'reply_chunk',
|
|
'content': str(message),
|
|
'message_chain': message.model_dump(),
|
|
'quote_origin': quote_origin,
|
|
'is_final': is_final,
|
|
}
|
|
self.responses.append(payload)
|
|
return payload
|
|
|
|
async def create_message_card(self, message_id, event: platform_events.MessageEvent) -> bool:
|
|
return False
|
|
|
|
def register_listener(self, event_type, callback):
|
|
return None
|
|
|
|
def unregister_listener(self, event_type, callback):
|
|
return None
|
|
|
|
async def run_async(self):
|
|
return None
|
|
|
|
async def is_stream_output_supported(self) -> bool:
|
|
return False
|
|
|
|
async def kill(self) -> bool:
|
|
return True
|
|
|
|
def get_last_text_response(self) -> str:
|
|
if not self.responses:
|
|
return ''
|
|
return str(self.responses[-1].get('content', '') or '')
|