mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
shit
This commit is contained in:
+1
-1
@@ -70,7 +70,7 @@ dependencies = [
|
|||||||
"chromadb>=1.0.0,<2.0.0",
|
"chromadb>=1.0.0,<2.0.0",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"pyseekdb==1.1.0.post3",
|
"pyseekdb==1.1.0.post3",
|
||||||
"langbot-plugin==0.3.11",
|
"langbot-plugin @ file:///home/typer/Desktop/langbot-plugin-sdk",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"matrix-nio>=0.25.2",
|
"matrix-nio>=0.25.2",
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from ....core import app
|
from ....core import app
|
||||||
@@ -14,13 +17,12 @@ from ....entity.persistence import workflow as persistence_workflow
|
|||||||
from ....workflow.entities import (
|
from ....workflow.entities import (
|
||||||
WorkflowDefinition,
|
WorkflowDefinition,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
ExecutionStatus,
|
|
||||||
NodeDefinition,
|
NodeDefinition,
|
||||||
EdgeDefinition,
|
EdgeDefinition,
|
||||||
Position,
|
Position,
|
||||||
MessageContext,
|
MessageContext,
|
||||||
NodeStatus,
|
|
||||||
)
|
)
|
||||||
|
from langbot_plugin.api.entities.builtin.workflow.enums import ExecutionStatus, NodeStatus
|
||||||
from ....workflow.executor import WorkflowExecutor
|
from ....workflow.executor import WorkflowExecutor
|
||||||
from ....workflow.registry import NodeTypeRegistry
|
from ....workflow.registry import NodeTypeRegistry
|
||||||
|
|
||||||
@@ -54,8 +56,9 @@ class WorkflowService:
|
|||||||
self.executor = WorkflowExecutor(ap)
|
self.executor = WorkflowExecutor(ap)
|
||||||
self.registry = NodeTypeRegistry.instance()
|
self.registry = NodeTypeRegistry.instance()
|
||||||
|
|
||||||
# Import workflow nodes to trigger registration
|
# Auto-discover and register workflow nodes using discovery engine
|
||||||
from ....workflow import nodes # noqa: F401
|
if hasattr(ap, 'discover') and ap.discover is not None:
|
||||||
|
self.registry.discover_nodes(ap.discover)
|
||||||
|
|
||||||
async def get_workflows(
|
async def get_workflows(
|
||||||
self, sort_by: str = 'created_at', sort_order: str = 'DESC', enabled_only: bool = False
|
self, sort_by: str = 'created_at', sort_order: str = 'DESC', enabled_only: bool = False
|
||||||
@@ -328,6 +331,17 @@ class WorkflowService:
|
|||||||
|
|
||||||
raw_trigger_data = trigger_data or {}
|
raw_trigger_data = trigger_data or {}
|
||||||
|
|
||||||
|
# Get bot name and workflow name for monitoring
|
||||||
|
bot_name = 'WebChat'
|
||||||
|
workflow_name = workflow_dict.get('name', 'Unknown')
|
||||||
|
if bot_id:
|
||||||
|
try:
|
||||||
|
bot = await self.ap.bot_service.get_bot(bot_id, include_secret=False)
|
||||||
|
if bot:
|
||||||
|
bot_name = bot.get('name', 'WebChat')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
context = ExecutionContext(
|
context = ExecutionContext(
|
||||||
execution_id=execution_uuid,
|
execution_id=execution_uuid,
|
||||||
@@ -382,6 +396,15 @@ class WorkflowService:
|
|||||||
raw_message=message_context_data.get('raw_message', {}),
|
raw_message=message_context_data.get('raw_message', {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Note: Frontend panel logging has been removed.
|
||||||
|
# A new solution will be implemented separately.
|
||||||
|
# Store workflow info in context for child nodes to reference
|
||||||
|
context.variables['_workflow_name'] = workflow_name
|
||||||
|
context.variables['_bot_name'] = bot_name
|
||||||
|
context.variables['_bot_id'] = bot_id or ''
|
||||||
|
context.variables['_session_id'] = session_id or ''
|
||||||
|
context.variables['_user_id'] = user_id
|
||||||
|
|
||||||
max_execution_time = self.DEFAULT_MAX_EXECUTION_TIME
|
max_execution_time = self.DEFAULT_MAX_EXECUTION_TIME
|
||||||
workflow_settings = definition.get('settings', {}) if isinstance(definition, dict) else {}
|
workflow_settings = definition.get('settings', {}) if isinstance(definition, dict) else {}
|
||||||
if isinstance(workflow_settings, dict):
|
if isinstance(workflow_settings, dict):
|
||||||
@@ -459,6 +482,8 @@ class WorkflowService:
|
|||||||
error=str(e),
|
error=str(e),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
raise WorkflowExecutionFailedError(
|
raise WorkflowExecutionFailedError(
|
||||||
execution_uuid,
|
execution_uuid,
|
||||||
str(e),
|
str(e),
|
||||||
@@ -528,8 +553,6 @@ class WorkflowService:
|
|||||||
|
|
||||||
async def get_node_types(self) -> list[dict]:
|
async def get_node_types(self) -> list[dict]:
|
||||||
"""Get all available node types"""
|
"""Get all available node types"""
|
||||||
# Process pending registrations
|
|
||||||
self.registry.process_pending_registrations()
|
|
||||||
node_types = self.registry.list_all()
|
node_types = self.registry.list_all()
|
||||||
|
|
||||||
# Enrich node schemas with pipeline config metadata
|
# Enrich node schemas with pipeline config metadata
|
||||||
@@ -537,7 +560,6 @@ class WorkflowService:
|
|||||||
|
|
||||||
async def get_node_types_by_category(self) -> dict[str, list[dict]]:
|
async def get_node_types_by_category(self) -> dict[str, list[dict]]:
|
||||||
"""Get node types organized by category"""
|
"""Get node types organized by category"""
|
||||||
self.registry.process_pending_registrations()
|
|
||||||
categories = self.registry.get_categories()
|
categories = self.registry.get_categories()
|
||||||
|
|
||||||
# Enrich node schemas with pipeline config metadata
|
# Enrich node schemas with pipeline config metadata
|
||||||
@@ -548,7 +570,6 @@ class WorkflowService:
|
|||||||
|
|
||||||
async def get_node_types_by_category_meta(self) -> list[dict]:
|
async def get_node_types_by_category_meta(self) -> list[dict]:
|
||||||
"""Get workflow node category metadata for the editor UI."""
|
"""Get workflow node category metadata for the editor UI."""
|
||||||
self.registry.process_pending_registrations()
|
|
||||||
categories = self.registry.get_categories()
|
categories = self.registry.get_categories()
|
||||||
|
|
||||||
ordered_categories = ['trigger', 'process', 'control', 'action', 'integration', 'misc']
|
ordered_categories = ['trigger', 'process', 'control', 'action', 'integration', 'misc']
|
||||||
@@ -1094,43 +1115,42 @@ class WorkflowService:
|
|||||||
async def get_execution_logs(
|
async def get_execution_logs(
|
||||||
self, workflow_uuid: str, execution_uuid: str, limit: int = 100, offset: int = 0
|
self, workflow_uuid: str, execution_uuid: str, limit: int = 100, offset: int = 0
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Get execution logs for a workflow execution"""
|
"""Get execution logs for a workflow execution from monitoring_messages table"""
|
||||||
execution = await self.get_execution(execution_uuid)
|
execution = await self.get_execution(execution_uuid)
|
||||||
if execution is None:
|
if execution is None:
|
||||||
raise ValueError(f'Execution {execution_uuid} not found')
|
raise ValueError(f'Execution {execution_uuid} not found')
|
||||||
if execution.get('workflow_uuid') != workflow_uuid:
|
if execution.get('workflow_uuid') != workflow_uuid:
|
||||||
raise ValueError(f'Execution {execution_uuid} not found in workflow {workflow_uuid}')
|
raise ValueError(f'Execution {execution_uuid} not found in workflow {workflow_uuid}')
|
||||||
|
|
||||||
|
# Get logs from monitoring_messages table
|
||||||
|
from ....entity.persistence import monitoring as persistence_monitoring
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
sqlalchemy.select(persistence_workflow.WorkflowNodeExecution)
|
sqlalchemy.select(persistence_monitoring.MonitoringMessage)
|
||||||
.where(persistence_workflow.WorkflowNodeExecution.execution_uuid == execution_uuid)
|
.where(persistence_monitoring.MonitoringMessage.pipeline_id == workflow_uuid)
|
||||||
.order_by(persistence_workflow.WorkflowNodeExecution.id.asc())
|
.order_by(persistence_monitoring.MonitoringMessage.timestamp.asc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(query)
|
result = await self.ap.persistence_mgr.execute_async(query)
|
||||||
node_executions = result.all()
|
messages = result.all()
|
||||||
|
|
||||||
logs = []
|
logs = []
|
||||||
for node_exec in node_executions:
|
for msg in messages:
|
||||||
serialized = self._serialize_node_execution(node_exec)
|
serialized = self.ap.persistence_mgr.serialize_model(persistence_monitoring.MonitoringMessage, msg)
|
||||||
timestamp = serialized.get('completed_at') or serialized.get('started_at') or execution.get('started_at')
|
|
||||||
level = 'error' if serialized.get('status') == 'failed' else 'info'
|
|
||||||
message = f'{serialized.get("node_type")}::{serialized.get("node_id")} - {serialized.get("status")}'
|
|
||||||
if serialized.get('error'):
|
|
||||||
message = f'{message} - {serialized.get("error")}'
|
|
||||||
logs.append(
|
logs.append(
|
||||||
{
|
{
|
||||||
'id': str(serialized.get('id', serialized.get('node_id'))),
|
'id': serialized.get('id', ''),
|
||||||
'timestamp': timestamp,
|
'timestamp': serialized.get('timestamp'),
|
||||||
'level': level,
|
'level': serialized.get('level', 'info'),
|
||||||
'node_id': serialized.get('node_id'),
|
'node_id': serialized.get('runner_name', ''),
|
||||||
'message': message,
|
'message': serialized.get('message_content', ''),
|
||||||
'data': {
|
'data': {
|
||||||
'inputs': serialized.get('inputs'),
|
'role': serialized.get('role'),
|
||||||
'outputs': serialized.get('outputs'),
|
'platform': serialized.get('platform'),
|
||||||
'retry_count': serialized.get('retry_count'),
|
'user_id': serialized.get('user_id'),
|
||||||
|
'user_name': serialized.get('user_name'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -237,11 +237,9 @@ class LoadConfigStage(stage.BootingStage):
|
|||||||
for node_config in ap.workflow_node_configs.values():
|
for node_config in ap.workflow_node_configs.values():
|
||||||
workflow_registry.register_metadata(node_config, source=node_config.get('_source', 'core'))
|
workflow_registry.register_metadata(node_config, source=node_config.get('_source', 'core'))
|
||||||
|
|
||||||
# Import node modules after metadata registration so decorators can bind
|
# Auto-discover and register workflow nodes using discovery engine
|
||||||
# implementations to YAML-defined canonical node types.
|
if hasattr(ap, 'discover') and ap.discover is not None:
|
||||||
from langbot.pkg.workflow import nodes as workflow_nodes # noqa: F401
|
workflow_registry.discover_nodes(ap.discover)
|
||||||
|
|
||||||
workflow_registry.process_pending_registrations()
|
|
||||||
|
|
||||||
workflow_load_errors = workflow_metadata_loader.get_load_errors()
|
workflow_load_errors = workflow_metadata_loader.get_load_errors()
|
||||||
if workflow_load_errors:
|
if workflow_load_errors:
|
||||||
|
|||||||
@@ -304,3 +304,65 @@ class ComponentDiscoveryEngine:
|
|||||||
if component.kind == kind:
|
if component.kind == kind:
|
||||||
result.append(component)
|
result.append(component)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def discover_workflow_nodes(self, nodes_dir: str) -> typing.List[typing.Type]:
|
||||||
|
"""Discover workflow node classes from a directory of Python modules.
|
||||||
|
|
||||||
|
Scans all .py files in the given directory, imports them, and collects
|
||||||
|
classes that are subclasses of WorkflowNode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes_dir: Directory path like 'pkg/workflow/nodes/'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of WorkflowNode subclasses found
|
||||||
|
"""
|
||||||
|
from langbot.pkg.workflow.node import WorkflowNode
|
||||||
|
|
||||||
|
node_classes: typing.List[typing.Type[WorkflowNode]] = []
|
||||||
|
|
||||||
|
# Normalize path
|
||||||
|
if nodes_dir.endswith('/'):
|
||||||
|
nodes_dir = nodes_dir[:-1]
|
||||||
|
|
||||||
|
# Import the nodes package to trigger all module imports
|
||||||
|
module_path = nodes_dir.replace('/', '.').replace('\\', '.')
|
||||||
|
package_path = module_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the package __init__ to trigger submodule imports
|
||||||
|
importlib.import_module(f'langbot.{package_path}')
|
||||||
|
except ImportError:
|
||||||
|
self.ap.logger.warning(f'Failed to import workflow nodes package: langbot.{package_path}')
|
||||||
|
|
||||||
|
# Since workflow/__init__.py is empty, explicitly import all .py files in the nodes directory
|
||||||
|
import os
|
||||||
|
# engine.py is in langbot/pkg/discover/, nodes are in langbot/pkg/workflow/nodes/
|
||||||
|
nodes_abs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'workflow', 'nodes'))
|
||||||
|
if os.path.isdir(nodes_abs_path):
|
||||||
|
for filename in os.listdir(nodes_abs_path):
|
||||||
|
if filename.endswith('.py') and not filename.startswith('_'):
|
||||||
|
module_name = filename[:-3]
|
||||||
|
try:
|
||||||
|
importlib.import_module(f'langbot.{package_path}.{module_name}')
|
||||||
|
except ImportError as e:
|
||||||
|
self.ap.logger.warning(f'Failed to import workflow node module: {module_name}: {e}')
|
||||||
|
|
||||||
|
# Now collect all WorkflowNode subclasses from sys.modules
|
||||||
|
import sys
|
||||||
|
prefix = f'langbot.{package_path}.'
|
||||||
|
for mod_name, mod in sys.modules.items():
|
||||||
|
if mod_name.startswith(prefix) and mod is not None:
|
||||||
|
for attr_name in dir(mod):
|
||||||
|
attr = getattr(mod, attr_name)
|
||||||
|
if (
|
||||||
|
isinstance(attr, type)
|
||||||
|
and issubclass(attr, WorkflowNode)
|
||||||
|
and attr is not WorkflowNode
|
||||||
|
and hasattr(attr, 'type_name')
|
||||||
|
and attr.type_name
|
||||||
|
):
|
||||||
|
if attr not in node_classes:
|
||||||
|
node_classes.append(attr)
|
||||||
|
|
||||||
|
return node_classes
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||||
import langbot_plugin.api.entities.events as events
|
import langbot_plugin.api.entities.events as events
|
||||||
from ..utils import importutil
|
from ..utils import importutil
|
||||||
from .config_coercion import coerce_pipeline_config
|
from .config import coerce_pipeline_config
|
||||||
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||||
@@ -284,9 +284,9 @@ class RuntimePipeline:
|
|||||||
# Record query start and store message_id
|
# Record query start and store message_id
|
||||||
message_id = ''
|
message_id = ''
|
||||||
try:
|
try:
|
||||||
from . import monitoring_helper
|
from . import monitor
|
||||||
|
|
||||||
message_id = await monitoring_helper.MonitoringHelper.record_query_start(
|
message_id = await monitor.MonitoringHelper.record_query_start(
|
||||||
ap=self.ap,
|
ap=self.ap,
|
||||||
query=query,
|
query=query,
|
||||||
bot_id=query.bot_uuid or 'unknown',
|
bot_id=query.bot_uuid or 'unknown',
|
||||||
@@ -338,7 +338,7 @@ class RuntimePipeline:
|
|||||||
# Record query success only if no error occurred during processing
|
# Record query success only if no error occurred during processing
|
||||||
if not query.variables.get('_monitoring_has_error', False):
|
if not query.variables.get('_monitoring_has_error', False):
|
||||||
try:
|
try:
|
||||||
await monitoring_helper.MonitoringHelper.record_query_success(
|
await monitor.MonitoringHelper.record_query_success(
|
||||||
ap=self.ap,
|
ap=self.ap,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
query=query,
|
query=query,
|
||||||
@@ -348,7 +348,7 @@ class RuntimePipeline:
|
|||||||
|
|
||||||
# Record bot response message
|
# Record bot response message
|
||||||
try:
|
try:
|
||||||
await monitoring_helper.MonitoringHelper.record_query_response(
|
await monitor.MonitoringHelper.record_query_response(
|
||||||
ap=self.ap,
|
ap=self.ap,
|
||||||
query=query,
|
query=query,
|
||||||
bot_id=query.bot_uuid or 'unknown',
|
bot_id=query.bot_uuid or 'unknown',
|
||||||
@@ -367,9 +367,9 @@ class RuntimePipeline:
|
|||||||
|
|
||||||
# Record query error
|
# Record query error
|
||||||
try:
|
try:
|
||||||
from . import monitoring_helper
|
from . import monitor
|
||||||
|
|
||||||
await monitoring_helper.MonitoringHelper.record_query_error(
|
await monitor.MonitoringHelper.record_query_error(
|
||||||
ap=self.ap,
|
ap=self.ap,
|
||||||
query=query,
|
query=query,
|
||||||
bot_id=query.bot_uuid or 'unknown',
|
bot_id=query.bot_uuid or 'unknown',
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class RuntimeProvider:
|
|||||||
|
|
||||||
# Import monitoring helper
|
# Import monitoring helper
|
||||||
try:
|
try:
|
||||||
from ...pipeline import monitoring_helper
|
from ...pipeline import monitor
|
||||||
|
|
||||||
# Get monitoring metadata from query variables
|
# Get monitoring metadata from query variables
|
||||||
if query.variables:
|
if query.variables:
|
||||||
@@ -96,7 +96,7 @@ class RuntimeProvider:
|
|||||||
pipeline_name = 'Unknown'
|
pipeline_name = 'Unknown'
|
||||||
message_id = None
|
message_id = None
|
||||||
|
|
||||||
await monitoring_helper.MonitoringHelper.record_llm_call(
|
await monitor.MonitoringHelper.record_llm_call(
|
||||||
ap=self.requester.ap,
|
ap=self.requester.ap,
|
||||||
query=query,
|
query=query,
|
||||||
bot_id=query.bot_uuid or 'unknown',
|
bot_id=query.bot_uuid or 'unknown',
|
||||||
@@ -154,7 +154,7 @@ class RuntimeProvider:
|
|||||||
|
|
||||||
# Import monitoring helper
|
# Import monitoring helper
|
||||||
try:
|
try:
|
||||||
from ...pipeline import monitoring_helper
|
from ...pipeline import monitor
|
||||||
|
|
||||||
# Get monitoring metadata from query variables
|
# Get monitoring metadata from query variables
|
||||||
if query.variables:
|
if query.variables:
|
||||||
@@ -166,7 +166,7 @@ class RuntimeProvider:
|
|||||||
pipeline_name = 'Unknown'
|
pipeline_name = 'Unknown'
|
||||||
message_id = None
|
message_id = None
|
||||||
|
|
||||||
await monitoring_helper.MonitoringHelper.record_llm_call(
|
await monitor.MonitoringHelper.record_llm_call(
|
||||||
ap=self.requester.ap,
|
ap=self.requester.ap,
|
||||||
query=query,
|
query=query,
|
||||||
bot_id=query.bot_uuid or 'unknown',
|
bot_id=query.bot_uuid or 'unknown',
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
"""Workflow package for LangBot
|
|
||||||
|
|
||||||
This package provides a visual workflow system for LangBot, including:
|
|
||||||
- Workflow definition models
|
|
||||||
- Execution engine
|
|
||||||
- Node types (trigger, process, control, action, integration)
|
|
||||||
- Trigger system for automation
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .entities import (
|
|
||||||
WorkflowDefinition,
|
|
||||||
NodeDefinition,
|
|
||||||
EdgeDefinition,
|
|
||||||
Position,
|
|
||||||
PortDefinition,
|
|
||||||
TriggerDefinition,
|
|
||||||
WorkflowSettings,
|
|
||||||
ExecutionContext,
|
|
||||||
NodeState,
|
|
||||||
ExecutionStatus,
|
|
||||||
NodeStatus,
|
|
||||||
)
|
|
||||||
|
|
||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .node import WorkflowNode, workflow_node
|
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> Any:
|
|
||||||
"""Lazily expose heavier workflow modules.
|
|
||||||
|
|
||||||
Loading workflow metadata should not import the executor or node modules as a
|
|
||||||
side effect. Node implementations are imported explicitly during boot after
|
|
||||||
YAML metadata has been registered.
|
|
||||||
"""
|
|
||||||
if name == 'NodeTypeRegistry':
|
|
||||||
from .registry import NodeTypeRegistry
|
|
||||||
|
|
||||||
return NodeTypeRegistry
|
|
||||||
|
|
||||||
if name == 'WorkflowExecutor':
|
|
||||||
from .executor import WorkflowExecutor
|
|
||||||
|
|
||||||
return WorkflowExecutor
|
|
||||||
|
|
||||||
if name in ('DebugWorkflowExecutor', 'DebugExecutionState', 'ExecutionLog'):
|
|
||||||
from . import debug
|
|
||||||
|
|
||||||
return getattr(debug, name)
|
|
||||||
|
|
||||||
if name == 'nodes':
|
|
||||||
return import_module('.nodes', __name__)
|
|
||||||
|
|
||||||
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# Entities
|
|
||||||
'WorkflowDefinition',
|
|
||||||
'NodeDefinition',
|
|
||||||
'EdgeDefinition',
|
|
||||||
'Position',
|
|
||||||
'PortDefinition',
|
|
||||||
'TriggerDefinition',
|
|
||||||
'WorkflowSettings',
|
|
||||||
'ExecutionContext',
|
|
||||||
'NodeState',
|
|
||||||
'ExecutionStatus',
|
|
||||||
'NodeStatus',
|
|
||||||
# Node
|
|
||||||
'WorkflowNode',
|
|
||||||
'workflow_node',
|
|
||||||
# Registry
|
|
||||||
'NodeTypeRegistry',
|
|
||||||
# Executor
|
|
||||||
'WorkflowExecutor',
|
|
||||||
# Debug
|
|
||||||
'DebugWorkflowExecutor',
|
|
||||||
'DebugExecutionState',
|
|
||||||
'ExecutionLog',
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -11,16 +11,18 @@ from typing import Any, Optional
|
|||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
# Import SDK entities for standard workflow protocol types
|
# Import SDK entities for standard workflow protocol types
|
||||||
from langbot_plugin.api.entities.builtin.workflow import (
|
from langbot_plugin.api.entities.builtin.workflow.entities import (
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
ExecutionStep,
|
ExecutionStep,
|
||||||
ExecutionStatus,
|
|
||||||
MessageContext,
|
MessageContext,
|
||||||
NodeDefinition,
|
NodeDefinition,
|
||||||
NodeState,
|
NodeState,
|
||||||
NodeStatus,
|
|
||||||
PortDefinition,
|
PortDefinition,
|
||||||
)
|
)
|
||||||
|
from langbot_plugin.api.entities.builtin.workflow.enums import (
|
||||||
|
ExecutionStatus,
|
||||||
|
NodeStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Position(pydantic.BaseModel):
|
class Position(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from .entities import (
|
|||||||
)
|
)
|
||||||
from ..entity.persistence import workflow as persistence_workflow
|
from ..entity.persistence import workflow as persistence_workflow
|
||||||
from .registry import NodeTypeRegistry
|
from .registry import NodeTypeRegistry
|
||||||
|
from . import monitor
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..core import app
|
from ..core import app
|
||||||
@@ -169,6 +170,10 @@ class WorkflowExecutor:
|
|||||||
context.status = ExecutionStatus.RUNNING
|
context.status = ExecutionStatus.RUNNING
|
||||||
context.start_time = datetime.now()
|
context.start_time = datetime.now()
|
||||||
|
|
||||||
|
# Note: Frontend panel logging has been removed.
|
||||||
|
# A new solution will be implemented separately.
|
||||||
|
monitoring_message_id = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build execution graph
|
# Build execution graph
|
||||||
node_map = {node.id: node for node in workflow.nodes}
|
node_map = {node.id: node for node in workflow.nodes}
|
||||||
@@ -227,9 +232,15 @@ class WorkflowExecutor:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Note: Frontend panel logging has been removed.
|
||||||
|
# A new solution will be implemented separately.
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
context.end_time = datetime.now()
|
context.end_time = datetime.now()
|
||||||
|
|
||||||
|
# Note: Frontend panel logging has been removed.
|
||||||
|
# A new solution will be implemented separately.
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
async def _execute_from_node(
|
async def _execute_from_node(
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Monitoring helper for recording events during workflow execution.
|
||||||
|
This module provides convenient methods to record monitoring data
|
||||||
|
without cluttering the main workflow code.
|
||||||
|
|
||||||
|
NOTE: All frontend panel logging functionality has been removed.
|
||||||
|
A new solution will be implemented separately.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import time
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from ..core import app
|
||||||
|
from langbot_plugin.api.entities.builtin.workflow.query import WorkflowQuery
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowMonitoringHelper:
|
||||||
|
"""Helper class for workflow monitoring operations"""
|
||||||
|
|
||||||
|
# All frontend panel logging methods have been removed.
|
||||||
|
# A new solution will be implemented separately.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LLMCallMonitor:
|
||||||
|
"""Context manager for monitoring LLM calls in workflow"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ap: app.Application,
|
||||||
|
query: WorkflowQuery,
|
||||||
|
bot_id: str,
|
||||||
|
bot_name: str,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_name: str,
|
||||||
|
node_name: str,
|
||||||
|
model_name: str,
|
||||||
|
):
|
||||||
|
self.ap = ap
|
||||||
|
self.query = query
|
||||||
|
self.bot_id = bot_id
|
||||||
|
self.bot_name = bot_name
|
||||||
|
self.workflow_id = workflow_id
|
||||||
|
self.workflow_name = workflow_name
|
||||||
|
self.node_name = node_name
|
||||||
|
self.model_name = model_name
|
||||||
|
self.start_time = None
|
||||||
|
self.input_tokens = 0
|
||||||
|
self.output_tokens = 0
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.start_time = time.time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
# LLM call monitoring has been removed.
|
||||||
|
# A new solution will be implemented separately.
|
||||||
|
return False
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
"""
|
||||||
|
Monitoring helper for recording events during workflow execution.
|
||||||
|
This module provides convenient methods to record monitoring data
|
||||||
|
without cluttering the main workflow code.
|
||||||
|
|
||||||
|
New logging scheme:
|
||||||
|
- Trigger log: adapter → workflow_name → local-workflow (with original message)
|
||||||
|
- LLM call log: adapter → workflow_name → local-workflow (with LLM info)
|
||||||
|
- LLM response log: adapter → workflow_name → local-workflow (with response message)
|
||||||
|
- Reply log: adapter → workflow_name → local-workflow (with reply content)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from ..core import app
|
||||||
|
from langbot_plugin.api.entities.builtin.workflow.query import WorkflowQuery
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowMonitoringHelper:
|
||||||
|
"""Helper class for workflow monitoring operations"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_adapter_name(query: WorkflowQuery) -> str:
|
||||||
|
"""Get adapter name from query"""
|
||||||
|
if query.adapter and hasattr(query.adapter, 'name'):
|
||||||
|
return query.adapter.name
|
||||||
|
if query.adapter and hasattr(query.adapter, 'adapter_name'):
|
||||||
|
return query.adapter.adapter_name
|
||||||
|
return 'WebChat'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_session_id(query: WorkflowQuery) -> str:
|
||||||
|
"""Build session_id from launcher info"""
|
||||||
|
launcher_type = query.launcher_type.value if query.launcher_type else 'unknown'
|
||||||
|
launcher_id = query.launcher_id or 'unknown'
|
||||||
|
return f'{launcher_type}_{launcher_id}'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def record_trigger_log(
|
||||||
|
ap: app.Application,
|
||||||
|
query: WorkflowQuery,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_name: str,
|
||||||
|
) -> str:
|
||||||
|
"""Record trigger node log
|
||||||
|
|
||||||
|
Format: adapter → workflow_name → local-workflow
|
||||||
|
Contains: original message content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
adapter_name = WorkflowMonitoringHelper._get_adapter_name(query)
|
||||||
|
session_id = WorkflowMonitoringHelper._get_session_id(query)
|
||||||
|
|
||||||
|
# Get message content
|
||||||
|
message_content = ''
|
||||||
|
if query.message_context and hasattr(query.message_context, 'message_content'):
|
||||||
|
message_content = query.message_context.message_content
|
||||||
|
elif query.message_chain and hasattr(query.message_chain, 'model_dump'):
|
||||||
|
message_content = json.dumps(query.message_chain.model_dump(), ensure_ascii=False)
|
||||||
|
|
||||||
|
# Build pipeline_name: workflow_name/local-workflow
|
||||||
|
pipeline_name = f'{workflow_name}/local-workflow' if workflow_name else 'local-workflow'
|
||||||
|
|
||||||
|
# Build log message: adapter → workflow_name → local-workflow
|
||||||
|
log_message = f'{adapter_name} → {workflow_name} → local-workflow'
|
||||||
|
if message_content:
|
||||||
|
log_message += f'\n{message_content}'
|
||||||
|
|
||||||
|
message_id = await ap.monitoring_service.record_message(
|
||||||
|
bot_id=query.bot_uuid or '',
|
||||||
|
bot_name=workflow_name or 'Workflow',
|
||||||
|
pipeline_id=workflow_id,
|
||||||
|
pipeline_name=pipeline_name,
|
||||||
|
message_content=log_message,
|
||||||
|
session_id=session_id,
|
||||||
|
status='success',
|
||||||
|
level='info',
|
||||||
|
platform='workflow',
|
||||||
|
user_id=query.sender_id,
|
||||||
|
user_name=query.sender_name,
|
||||||
|
role='user',
|
||||||
|
)
|
||||||
|
|
||||||
|
return message_id
|
||||||
|
except Exception as e:
|
||||||
|
ap.logger.error(f'Failed to record trigger log: {e}')
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def record_llm_call_log(
|
||||||
|
ap: app.Application,
|
||||||
|
query: WorkflowQuery,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_name: str,
|
||||||
|
node_name: str,
|
||||||
|
model_name: str,
|
||||||
|
input_tokens: int,
|
||||||
|
output_tokens: int,
|
||||||
|
duration_ms: int,
|
||||||
|
status: str = 'success',
|
||||||
|
error_message: str | None = None,
|
||||||
|
):
|
||||||
|
"""Record LLM call log (with LLM info)
|
||||||
|
|
||||||
|
Format: adapter → workflow_name → local-workflow
|
||||||
|
Contains: LLM call statistics
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
adapter_name = WorkflowMonitoringHelper._get_adapter_name(query)
|
||||||
|
session_id = WorkflowMonitoringHelper._get_session_id(query)
|
||||||
|
|
||||||
|
# Build pipeline_name: workflow_name/local-workflow
|
||||||
|
pipeline_name = f'{workflow_name}/local-workflow' if workflow_name else 'local-workflow'
|
||||||
|
|
||||||
|
# Build log message with LLM info
|
||||||
|
log_message = f'{adapter_name} → {workflow_name} → local-workflow\n'
|
||||||
|
log_message += f'LLM Call: {node_name}\n'
|
||||||
|
log_message += f'Model: {model_name}\n'
|
||||||
|
log_message += f'Status: {status}\n'
|
||||||
|
log_message += f'Duration: {duration_ms}ms\n'
|
||||||
|
log_message += f'Input Tokens: {input_tokens}\n'
|
||||||
|
log_message += f'Output Tokens: {output_tokens}\n'
|
||||||
|
log_message += f'Total Tokens: {input_tokens + output_tokens}'
|
||||||
|
|
||||||
|
if error_message:
|
||||||
|
log_message += f'\nError: {error_message}'
|
||||||
|
|
||||||
|
await ap.monitoring_service.record_llm_call(
|
||||||
|
bot_id=query.bot_uuid or '',
|
||||||
|
bot_name=workflow_name or 'Workflow',
|
||||||
|
pipeline_id=workflow_id,
|
||||||
|
pipeline_name=pipeline_name,
|
||||||
|
session_id=session_id,
|
||||||
|
model_name=model_name,
|
||||||
|
input_tokens=input_tokens,
|
||||||
|
output_tokens=output_tokens,
|
||||||
|
duration=duration_ms,
|
||||||
|
status=status,
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also record as message for display
|
||||||
|
await ap.monitoring_service.record_message(
|
||||||
|
bot_id=query.bot_uuid or '',
|
||||||
|
bot_name=workflow_name or 'Workflow',
|
||||||
|
pipeline_id=workflow_id,
|
||||||
|
pipeline_name=pipeline_name,
|
||||||
|
message_content=log_message,
|
||||||
|
session_id=session_id,
|
||||||
|
status=status,
|
||||||
|
level='info',
|
||||||
|
platform='workflow',
|
||||||
|
user_id=query.sender_id,
|
||||||
|
user_name=query.sender_name,
|
||||||
|
role='system',
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ap.logger.error(f'Failed to record LLM call log: {e}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def record_llm_response_log(
|
||||||
|
ap: app.Application,
|
||||||
|
query: WorkflowQuery,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_name: str,
|
||||||
|
node_name: str,
|
||||||
|
response_content: str,
|
||||||
|
):
|
||||||
|
"""Record LLM response log (without LLM info, with response message)
|
||||||
|
|
||||||
|
Format: adapter → workflow_name → local-workflow
|
||||||
|
Contains: response message content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
adapter_name = WorkflowMonitoringHelper._get_adapter_name(query)
|
||||||
|
session_id = WorkflowMonitoringHelper._get_session_id(query)
|
||||||
|
|
||||||
|
# Build pipeline_name: workflow_name/local-workflow
|
||||||
|
pipeline_name = f'{workflow_name}/local-workflow' if workflow_name else 'local-workflow'
|
||||||
|
|
||||||
|
# Build log message
|
||||||
|
log_message = f'{adapter_name} → {workflow_name} → local-workflow\n'
|
||||||
|
log_message += f'Node: {node_name}\n'
|
||||||
|
log_message += f'Response: {response_content[:500]}' # Limit length
|
||||||
|
|
||||||
|
await ap.monitoring_service.record_message(
|
||||||
|
bot_id=query.bot_uuid or '',
|
||||||
|
bot_name=workflow_name or 'Workflow',
|
||||||
|
pipeline_id=workflow_id,
|
||||||
|
pipeline_name=pipeline_name,
|
||||||
|
message_content=log_message,
|
||||||
|
session_id=session_id,
|
||||||
|
status='success',
|
||||||
|
level='info',
|
||||||
|
platform='workflow',
|
||||||
|
user_id=query.sender_id,
|
||||||
|
user_name=query.sender_name,
|
||||||
|
role='assistant',
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ap.logger.error(f'Failed to record LLM response log: {e}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def record_reply_log(
|
||||||
|
ap: app.Application,
|
||||||
|
query: WorkflowQuery,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_name: str,
|
||||||
|
node_name: str,
|
||||||
|
reply_content: str,
|
||||||
|
):
|
||||||
|
"""Record reply message log
|
||||||
|
|
||||||
|
Format: adapter → workflow_name → local-workflow
|
||||||
|
Contains: reply message content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
adapter_name = WorkflowMonitoringHelper._get_adapter_name(query)
|
||||||
|
session_id = WorkflowMonitoringHelper._get_session_id(query)
|
||||||
|
|
||||||
|
# Build pipeline_name: workflow_name/local-workflow
|
||||||
|
pipeline_name = f'{workflow_name}/local-workflow' if workflow_name else 'local-workflow'
|
||||||
|
|
||||||
|
# Build log message
|
||||||
|
log_message = f'{adapter_name} → {workflow_name} → local-workflow\n'
|
||||||
|
log_message += f'Node: {node_name}\n'
|
||||||
|
log_message += f'Reply: {reply_content[:500]}' # Limit length
|
||||||
|
|
||||||
|
await ap.monitoring_service.record_message(
|
||||||
|
bot_id=query.bot_uuid or '',
|
||||||
|
bot_name=workflow_name or 'Workflow',
|
||||||
|
pipeline_id=workflow_id,
|
||||||
|
pipeline_name=pipeline_name,
|
||||||
|
message_content=log_message,
|
||||||
|
session_id=session_id,
|
||||||
|
status='success',
|
||||||
|
level='info',
|
||||||
|
platform='workflow',
|
||||||
|
user_id=query.sender_id,
|
||||||
|
user_name=query.sender_name,
|
||||||
|
role='assistant',
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ap.logger.error(f'Failed to record reply log: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
class LLMCallMonitor:
|
||||||
|
"""Context manager for monitoring LLM calls in workflow"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ap: app.Application,
|
||||||
|
query: WorkflowQuery,
|
||||||
|
bot_id: str,
|
||||||
|
bot_name: str,
|
||||||
|
workflow_id: str,
|
||||||
|
workflow_name: str,
|
||||||
|
node_name: str,
|
||||||
|
model_name: str,
|
||||||
|
):
|
||||||
|
self.ap = ap
|
||||||
|
self.query = query
|
||||||
|
self.bot_id = bot_id
|
||||||
|
self.bot_name = bot_name
|
||||||
|
self.workflow_id = workflow_id
|
||||||
|
self.workflow_name = workflow_name
|
||||||
|
self.node_name = node_name
|
||||||
|
self.model_name = model_name
|
||||||
|
self.start_time = None
|
||||||
|
self.input_tokens = 0
|
||||||
|
self.output_tokens = 0
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.start_time = time.time()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
duration_ms = int((time.time() - self.start_time) * 1000) if self.start_time else 0
|
||||||
|
|
||||||
|
if exc_type is not None:
|
||||||
|
await WorkflowMonitoringHelper.record_llm_call_log(
|
||||||
|
ap=self.ap,
|
||||||
|
query=self.query,
|
||||||
|
workflow_id=self.workflow_id,
|
||||||
|
workflow_name=self.workflow_name,
|
||||||
|
node_name=self.node_name,
|
||||||
|
model_name=self.model_name,
|
||||||
|
input_tokens=self.input_tokens,
|
||||||
|
output_tokens=self.output_tokens,
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
status='error',
|
||||||
|
error_message=str(exc_val) if exc_val else None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await WorkflowMonitoringHelper.record_llm_call_log(
|
||||||
|
ap=self.ap,
|
||||||
|
query=self.query,
|
||||||
|
workflow_id=self.workflow_id,
|
||||||
|
workflow_name=self.workflow_name,
|
||||||
|
node_name=self.node_name,
|
||||||
|
model_name=self.model_name,
|
||||||
|
input_tokens=self.input_tokens,
|
||||||
|
output_tokens=self.output_tokens,
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
status='success',
|
||||||
|
)
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -142,34 +142,23 @@ class WorkflowNode(abc.ABC):
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Decorator and pending registration helpers
|
# Decorator for setting type_name attribute
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
_pending_registrations: list[tuple[str, type[WorkflowNode]]] = []
|
|
||||||
|
|
||||||
|
|
||||||
def workflow_node(type_name: str) -> Callable[[type[WorkflowNode]], type[WorkflowNode]]:
|
def workflow_node(type_name: str) -> Callable[[type[WorkflowNode]], type[WorkflowNode]]:
|
||||||
"""Decorator to register a workflow node type.
|
"""Decorator to set the type_name attribute on a workflow node class.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@workflow_node('llm_call')
|
@workflow_node('llm_call')
|
||||||
class LLMCallNode(WorkflowNode):
|
class LLMCallNode(WorkflowNode):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
The actual registration is now handled by the discovery engine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(cls: type[WorkflowNode]) -> type[WorkflowNode]:
|
def decorator(cls: type[WorkflowNode]) -> type[WorkflowNode]:
|
||||||
cls.type_name = type_name
|
cls.type_name = type_name
|
||||||
_pending_registrations.append((type_name, cls))
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def get_pending_registrations() -> list[tuple[str, type[WorkflowNode]]]:
|
|
||||||
"""Get pending node registrations"""
|
|
||||||
return _pending_registrations.copy()
|
|
||||||
|
|
||||||
|
|
||||||
def clear_pending_registrations():
|
|
||||||
"""Clear pending registrations after they're processed"""
|
|
||||||
_pending_registrations.clear()
|
|
||||||
|
|||||||
@@ -1,93 +1 @@
|
|||||||
"""Core workflow nodes package"""
|
|
||||||
|
|
||||||
# Import all node modules to trigger registration
|
|
||||||
# Trigger nodes
|
|
||||||
from . import message_trigger
|
|
||||||
from . import cron_trigger
|
|
||||||
from . import webhook_trigger
|
|
||||||
from . import event_trigger
|
|
||||||
|
|
||||||
# Process nodes
|
|
||||||
from . import llm_call
|
|
||||||
from . import code_executor
|
|
||||||
from . import http_request
|
|
||||||
from . import data_transform
|
|
||||||
from . import question_classifier
|
|
||||||
from . import parameter_extractor
|
|
||||||
from . import knowledge_retrieval
|
|
||||||
|
|
||||||
# Control nodes
|
|
||||||
from . import condition
|
|
||||||
from . import switch
|
|
||||||
from . import loop
|
|
||||||
from . import iterator
|
|
||||||
from . import parallel
|
|
||||||
from . import wait
|
|
||||||
from . import merge
|
|
||||||
from . import variable_aggregator
|
|
||||||
|
|
||||||
# Action nodes
|
|
||||||
from . import send_message
|
|
||||||
from . import reply_message
|
|
||||||
from . import call_pipeline
|
|
||||||
from . import call_workflow
|
|
||||||
from . import store_data
|
|
||||||
from . import set_variable
|
|
||||||
from . import opening_statement
|
|
||||||
from . import end
|
|
||||||
|
|
||||||
# Integration nodes
|
|
||||||
from . import database_query
|
|
||||||
from . import redis_operation
|
|
||||||
from . import mcp_tool
|
|
||||||
from . import memory_store
|
|
||||||
from . import dify_workflow
|
|
||||||
from . import dify_knowledge_query
|
|
||||||
from . import n8n_workflow
|
|
||||||
from . import langflow_flow
|
|
||||||
from . import coze_bot
|
|
||||||
# from . import plugin_call
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# Trigger nodes
|
|
||||||
'message_trigger',
|
|
||||||
'cron_trigger',
|
|
||||||
'webhook_trigger',
|
|
||||||
'event_trigger',
|
|
||||||
# Process nodes
|
|
||||||
'llm_call',
|
|
||||||
'code_executor',
|
|
||||||
'http_request',
|
|
||||||
'data_transform',
|
|
||||||
'question_classifier',
|
|
||||||
'parameter_extractor',
|
|
||||||
'knowledge_retrieval',
|
|
||||||
# Control nodes
|
|
||||||
'condition',
|
|
||||||
'switch',
|
|
||||||
'loop',
|
|
||||||
'iterator',
|
|
||||||
'parallel',
|
|
||||||
'wait',
|
|
||||||
'merge',
|
|
||||||
'variable_aggregator',
|
|
||||||
# Action nodes
|
|
||||||
'send_message',
|
|
||||||
'reply_message',
|
|
||||||
'call_pipeline',
|
|
||||||
'call_workflow',
|
|
||||||
'store_data',
|
|
||||||
'set_variable',
|
|
||||||
'opening_statement',
|
|
||||||
'end',
|
|
||||||
# Integration nodes
|
|
||||||
'database_query',
|
|
||||||
'redis_operation',
|
|
||||||
'mcp_tool',
|
|
||||||
'memory_store',
|
|
||||||
'dify_workflow',
|
|
||||||
'dify_knowledge_query',
|
|
||||||
'n8n_workflow',
|
|
||||||
'langflow_flow',
|
|
||||||
'coze_bot',
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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.platform.message as platform_message
|
||||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
|
|
||||||
@@ -154,6 +154,7 @@ class CallPipelineNode(WorkflowNode):
|
|||||||
if context.message_context and context.message_context.is_group:
|
if context.message_context and context.message_context.is_group:
|
||||||
group = platform_entities.Group(
|
group = platform_entities.Group(
|
||||||
id=context.message_context.group_id or context.session_id or 'workflow_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,
|
permission=platform_entities.Permission.Member,
|
||||||
)
|
)
|
||||||
sender = platform_entities.GroupMember(
|
sender = platform_entities.GroupMember(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
|
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import re
|
|||||||
import signal
|
import signal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
from ..safe_eval import safe_eval_with_vars
|
from ..safe_eval import safe_eval_with_vars
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('coze_bot')
|
@workflow_node('coze_bot')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('cron_trigger')
|
@workflow_node('cron_trigger')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
from ..safe_eval import safe_eval_with_vars
|
from ..safe_eval import safe_eval_with_vars
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('database_query')
|
@workflow_node('database_query')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('dify_knowledge_query')
|
@workflow_node('dify_knowledge_query')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('dify_workflow')
|
@workflow_node('dify_workflow')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('end')
|
@workflow_node('end')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('event_trigger')
|
@workflow_node('event_trigger')
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('iterator')
|
@workflow_node('iterator')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('knowledge_retrieval')
|
@workflow_node('knowledge_retrieval')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('langflow_flow')
|
@workflow_node('langflow_flow')
|
||||||
|
|||||||
@@ -12,13 +12,15 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from typing import Any, AsyncGenerator
|
from typing import Any, AsyncGenerator
|
||||||
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
||||||
import langbot_plugin.api.entities.builtin.rag.context as rag_context
|
import langbot_plugin.api.entities.builtin.rag.context as rag_context
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
from .. import monitoring_helper
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -383,7 +385,17 @@ Respond in the same language as the user's input.
|
|||||||
context.variables['_conversation_history'] = history
|
context.variables['_conversation_history'] = history
|
||||||
|
|
||||||
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
|
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
|
||||||
model_uuid = self.get_config('model', '')
|
# Support both new model_config format and legacy model + fallback_models format
|
||||||
|
model_config = self.get_config('model_config', None)
|
||||||
|
if model_config and isinstance(model_config, dict):
|
||||||
|
# New format: {primary: uuid, fallbacks: [uuid1, uuid2, ...]}
|
||||||
|
model_uuid = model_config.get('primary', '')
|
||||||
|
fallback_models = model_config.get('fallbacks', [])
|
||||||
|
else:
|
||||||
|
# Legacy format: separate model and fallback_models
|
||||||
|
model_uuid = self.get_config('model', '')
|
||||||
|
fallback_models = self.get_config('fallback_models', [])
|
||||||
|
|
||||||
if not model_uuid:
|
if not model_uuid:
|
||||||
raise ValueError('No model configured for LLM call node')
|
raise ValueError('No model configured for LLM call node')
|
||||||
|
|
||||||
@@ -399,8 +411,8 @@ Respond in the same language as the user's input.
|
|||||||
output_format = self.get_config('output_format', 'text')
|
output_format = self.get_config('output_format', 'text')
|
||||||
json_schema = self.get_config('json_schema', '')
|
json_schema = self.get_config('json_schema', '')
|
||||||
|
|
||||||
# Agent config: fallback models, knowledge bases, rerank, max_round
|
# Agent config: knowledge bases, rerank, max_round
|
||||||
fallback_models = self.get_config('fallback_models', [])
|
# (fallback_models already resolved above from model_config or fallback_models)
|
||||||
knowledge_bases = self.get_config('knowledge_bases', [])
|
knowledge_bases = self.get_config('knowledge_bases', [])
|
||||||
rerank_model = self.get_config('rerank_model', '')
|
rerank_model = self.get_config('rerank_model', '')
|
||||||
rerank_top_k = self.get_config('rerank_top_k', 5)
|
rerank_top_k = self.get_config('rerank_top_k', 5)
|
||||||
@@ -493,6 +505,9 @@ Respond in the same language as the user's input.
|
|||||||
if max_tokens and int(max_tokens) > 0:
|
if max_tokens and int(max_tokens) > 0:
|
||||||
extra_args['max_tokens'] = int(max_tokens)
|
extra_args['max_tokens'] = int(max_tokens)
|
||||||
|
|
||||||
|
# Track start time for duration calculation
|
||||||
|
self._llm_start_time = time.time()
|
||||||
|
|
||||||
# Invoke LLM with fallback
|
# Invoke LLM with fallback
|
||||||
try:
|
try:
|
||||||
result_message, used_model = await self._invoke_with_fallback(
|
result_message, used_model = await self._invoke_with_fallback(
|
||||||
@@ -563,18 +578,81 @@ Respond in the same language as the user's input.
|
|||||||
# Extract usage info
|
# Extract usage info
|
||||||
if hasattr(result_message, 'usage') and result_message.usage:
|
if hasattr(result_message, 'usage') and result_message.usage:
|
||||||
u = result_message.usage
|
u = result_message.usage
|
||||||
usage = {
|
# Handle both object and dict usage
|
||||||
'prompt_tokens': getattr(u, 'prompt_tokens', 0) or 0,
|
if isinstance(u, dict):
|
||||||
'completion_tokens': getattr(u, 'completion_tokens', 0) or 0,
|
usage = {
|
||||||
'total_tokens': getattr(u, 'total_tokens', 0) or 0,
|
'prompt_tokens': u.get('prompt_tokens', 0) or 0,
|
||||||
}
|
'completion_tokens': u.get('completion_tokens', 0) or 0,
|
||||||
|
'total_tokens': u.get('total_tokens', 0) or 0,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
usage = {
|
||||||
|
'prompt_tokens': getattr(u, 'prompt_tokens', 0) or 0,
|
||||||
|
'completion_tokens': getattr(u, 'completion_tokens', 0) or 0,
|
||||||
|
'total_tokens': getattr(u, 'total_tokens', 0) or 0,
|
||||||
|
}
|
||||||
elif hasattr(result_message, 'token_usage') and result_message.token_usage:
|
elif hasattr(result_message, 'token_usage') and result_message.token_usage:
|
||||||
u = result_message.token_usage
|
u = result_message.token_usage
|
||||||
usage = {
|
# Handle both object and dict token_usage
|
||||||
'prompt_tokens': getattr(u, 'prompt_tokens', 0) or 0,
|
if isinstance(u, dict):
|
||||||
'completion_tokens': getattr(u, 'completion_tokens', 0) or 0,
|
usage = {
|
||||||
'total_tokens': getattr(u, 'total_tokens', 0) or 0,
|
'prompt_tokens': u.get('prompt_tokens', 0) or 0,
|
||||||
}
|
'completion_tokens': u.get('completion_tokens', 0) or 0,
|
||||||
|
'total_tokens': u.get('total_tokens', 0) or 0,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
usage = {
|
||||||
|
'prompt_tokens': getattr(u, 'prompt_tokens', 0) or 0,
|
||||||
|
'completion_tokens': getattr(u, 'completion_tokens', 0) or 0,
|
||||||
|
'total_tokens': getattr(u, 'total_tokens', 0) or 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log successful response (matching Pipeline's cut_str behavior)
|
||||||
|
def _cut_str(s: str) -> str:
|
||||||
|
s0 = s.split('\n')[0]
|
||||||
|
if len(s0) > 20 or '\n' in s:
|
||||||
|
s0 = s0[:20] + '...'
|
||||||
|
return s0
|
||||||
|
logger.info(f'[LLM:{self.node_id}] Response: {_cut_str(response_text)}')
|
||||||
|
|
||||||
|
# Record LLM call log and response log
|
||||||
|
try:
|
||||||
|
if self.ap and context.query:
|
||||||
|
workflow_id = context.workflow_id or ''
|
||||||
|
workflow_name = context.variables.get('_workflow_name', 'Workflow')
|
||||||
|
node_name = self.get_config('name', self.node_id)
|
||||||
|
model_name = used_model.model_entity.name if used_model else 'unknown'
|
||||||
|
|
||||||
|
# Calculate duration
|
||||||
|
duration_ms = 0
|
||||||
|
if hasattr(self, '_llm_start_time'):
|
||||||
|
duration_ms = int((time.time() - self._llm_start_time) * 1000)
|
||||||
|
|
||||||
|
# Record LLM call log (with LLM info)
|
||||||
|
await monitoring_helper.WorkflowMonitoringHelper.record_llm_call_log(
|
||||||
|
ap=self.ap,
|
||||||
|
query=context.query,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
workflow_name=workflow_name,
|
||||||
|
node_name=node_name,
|
||||||
|
model_name=model_name,
|
||||||
|
input_tokens=usage.get('prompt_tokens', 0),
|
||||||
|
output_tokens=usage.get('completion_tokens', 0),
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
status='success',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record LLM response log (with response message)
|
||||||
|
await monitoring_helper.WorkflowMonitoringHelper.record_llm_response_log(
|
||||||
|
ap=self.ap,
|
||||||
|
query=context.query,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
workflow_name=workflow_name,
|
||||||
|
node_name=node_name,
|
||||||
|
response_content=response_text,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'[LLM:{self.node_id}] Failed to record LLM logs: {e}')
|
||||||
|
|
||||||
# Save to conversation history
|
# Save to conversation history
|
||||||
self._save_to_conversation_history(
|
self._save_to_conversation_history(
|
||||||
@@ -615,7 +693,13 @@ Respond in the same language as the user's input.
|
|||||||
Yields chunks of response text as they arrive.
|
Yields chunks of response text as they arrive.
|
||||||
Falls back to non-streaming if streaming is not available.
|
Falls back to non-streaming if streaming is not available.
|
||||||
"""
|
"""
|
||||||
model_uuid = self.get_config('model', '')
|
# Support both new model_config format and legacy model + fallback_models format
|
||||||
|
model_config = self.get_config('model_config', None)
|
||||||
|
if model_config and isinstance(model_config, dict):
|
||||||
|
model_uuid = model_config.get('primary', '')
|
||||||
|
else:
|
||||||
|
model_uuid = self.get_config('model', '')
|
||||||
|
|
||||||
if not model_uuid:
|
if not model_uuid:
|
||||||
raise ValueError('No model configured for LLM call node')
|
raise ValueError('No model configured for LLM call node')
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('loop')
|
@workflow_node('loop')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('mcp_tool')
|
@workflow_node('mcp_tool')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
class MemoryHelper:
|
class MemoryHelper:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('merge')
|
@workflow_node('merge')
|
||||||
|
|||||||
@@ -7,10 +7,15 @@ Node metadata (label, description, inputs, outputs, config) is loaded from:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
from .. import monitoring_helper
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@workflow_node('message_trigger')
|
@workflow_node('message_trigger')
|
||||||
class MessageTriggerNode(WorkflowNode):
|
class MessageTriggerNode(WorkflowNode):
|
||||||
@@ -21,6 +26,20 @@ class MessageTriggerNode(WorkflowNode):
|
|||||||
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
|
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
|
||||||
msg_ctx = context.message_context
|
msg_ctx = context.message_context
|
||||||
|
|
||||||
|
# Record trigger log
|
||||||
|
try:
|
||||||
|
if self.ap and context.query:
|
||||||
|
workflow_id = context.workflow_id or ''
|
||||||
|
workflow_name = context.variables.get('_workflow_name', 'Workflow')
|
||||||
|
await monitoring_helper.WorkflowMonitoringHelper.record_trigger_log(
|
||||||
|
ap=self.ap,
|
||||||
|
query=context.query,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
workflow_name=workflow_name,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'[MessageTrigger:{self.node_id}] Failed to record trigger log: {e}')
|
||||||
|
|
||||||
if msg_ctx:
|
if msg_ctx:
|
||||||
return {
|
return {
|
||||||
'message': msg_ctx.message_content,
|
'message': msg_ctx.message_content,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('n8n_workflow')
|
@workflow_node('n8n_workflow')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('opening_statement')
|
@workflow_node('opening_statement')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('parallel')
|
@workflow_node('parallel')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -88,6 +88,22 @@ class ParameterExtractorNode(WorkflowNode):
|
|||||||
extra_args={},
|
extra_args={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Log successful response (matching Pipeline's cut_str behavior)
|
||||||
|
response_preview = ''
|
||||||
|
if isinstance(result_message.content, str):
|
||||||
|
response_preview = result_message.content
|
||||||
|
elif isinstance(result_message.content, list):
|
||||||
|
for elem in result_message.content:
|
||||||
|
if hasattr(elem, 'text') and elem.text:
|
||||||
|
response_preview += elem.text
|
||||||
|
response_preview = response_preview.strip()
|
||||||
|
def _cut_str(s: str) -> str:
|
||||||
|
s0 = s.split('\n')[0]
|
||||||
|
if len(s0) > 20 or '\n' in s:
|
||||||
|
s0 = s0[:20] + '...'
|
||||||
|
return s0
|
||||||
|
logger.info(f'[ParameterExtractor:{self.node_id}] Response: {_cut_str(response_preview)}')
|
||||||
|
|
||||||
# Extract response text
|
# Extract response text
|
||||||
response_text = ''
|
response_text = ''
|
||||||
if isinstance(result_message.content, str):
|
if isinstance(result_message.content, str):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
# from typing import Any
|
# from typing import Any
|
||||||
|
|
||||||
# from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
# from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
# from ..node import WorkflowNode, workflow_node
|
# from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
# @workflow_node('plugin_call')
|
# @workflow_node('plugin_call')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -78,6 +78,22 @@ class QuestionClassifierNode(WorkflowNode):
|
|||||||
extra_args={},
|
extra_args={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Log successful response (matching Pipeline's cut_str behavior)
|
||||||
|
response_preview = ''
|
||||||
|
if isinstance(result_message.content, str):
|
||||||
|
response_preview = result_message.content
|
||||||
|
elif isinstance(result_message.content, list):
|
||||||
|
for elem in result_message.content:
|
||||||
|
if hasattr(elem, 'text') and elem.text:
|
||||||
|
response_preview += elem.text
|
||||||
|
response_preview = response_preview.strip()
|
||||||
|
def _cut_str(s: str) -> str:
|
||||||
|
s0 = s.split('\n')[0]
|
||||||
|
if len(s0) > 20 or '\n' in s:
|
||||||
|
s0 = s0[:20] + '...'
|
||||||
|
return s0
|
||||||
|
logger.info(f'[QuestionClassifier:{self.node_id}] Response: {_cut_str(response_preview)}')
|
||||||
|
|
||||||
# Extract response text
|
# Extract response text
|
||||||
response_text = ''
|
response_text = ''
|
||||||
if isinstance(result_message.content, str):
|
if isinstance(result_message.content, str):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('redis_operation')
|
@workflow_node('redis_operation')
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
from .. import monitoring_helper
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -80,7 +81,10 @@ class ReplyMessageNode(WorkflowNode):
|
|||||||
from langbot_plugin.api.entities.builtin.platform.message import MessageChain, Plain
|
from langbot_plugin.api.entities.builtin.platform.message import MessageChain, Plain
|
||||||
|
|
||||||
message_chain = MessageChain([Plain(text=message_str)])
|
message_chain = MessageChain([Plain(text=message_str)])
|
||||||
target_type = getattr(context, 'target_type', 'person') or 'person'
|
# 从 trigger_data 中获取 session_type,而不是从未设置的 context.target_type
|
||||||
|
target_type = 'person'
|
||||||
|
if context.trigger_data:
|
||||||
|
target_type = context.trigger_data.get('session_type', 'person') or 'person'
|
||||||
session_id = context.session_id or 'unknown'
|
session_id = context.session_id or 'unknown'
|
||||||
target_id = f'websocket_{session_id}'
|
target_id = f'websocket_{session_id}'
|
||||||
|
|
||||||
@@ -103,8 +107,26 @@ class ReplyMessageNode(WorkflowNode):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Record reply log
|
||||||
|
try:
|
||||||
|
if self.ap and context.query and send_success:
|
||||||
|
workflow_id = context.workflow_id or ''
|
||||||
|
workflow_name = context.variables.get('_workflow_name', 'Workflow')
|
||||||
|
node_name = self.get_config('name', self.node_id)
|
||||||
|
await monitoring_helper.WorkflowMonitoringHelper.record_reply_log(
|
||||||
|
ap=self.ap,
|
||||||
|
query=context.query,
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
workflow_name=workflow_name,
|
||||||
|
node_name=node_name,
|
||||||
|
reply_content=message_str,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'[ReplyMessage:{self.node_id}] Failed to record reply log: {e}')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'sent' if send_success else 'failed',
|
'status': 'sent' if send_success else 'failed',
|
||||||
|
'message_content': message_str,
|
||||||
'message_preview': message_str[:200],
|
'message_preview': message_str[:200],
|
||||||
'error': send_error,
|
'error': send_error,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -24,8 +24,15 @@ class SendMessageNode(WorkflowNode):
|
|||||||
message = inputs.get('message') or inputs.get('content') or inputs.get('input') or ''
|
message = inputs.get('message') or inputs.get('content') or inputs.get('input') or ''
|
||||||
message = str(message) if message is not None else ''
|
message = str(message) if message is not None else ''
|
||||||
|
|
||||||
# Get target configuration
|
# Get target configuration - fallback to session_type from context if not configured
|
||||||
target_type = self.get_config('target_type', 'person')
|
target_type = self.get_config('target_type', '')
|
||||||
|
if not target_type:
|
||||||
|
# Inherit from current session context
|
||||||
|
if context.trigger_data:
|
||||||
|
target_type = context.trigger_data.get('session_type', 'person') or 'person'
|
||||||
|
else:
|
||||||
|
target_type = 'person'
|
||||||
|
|
||||||
target_id = self.get_config('target_id', '')
|
target_id = self.get_config('target_id', '')
|
||||||
|
|
||||||
# If no target_id configured, use session_id from context
|
# If no target_id configured, use session_id from context
|
||||||
@@ -64,6 +71,7 @@ class SendMessageNode(WorkflowNode):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'sent' if send_success else 'failed',
|
'status': 'sent' if send_success else 'failed',
|
||||||
|
'message_content': message,
|
||||||
'message_preview': message[:200],
|
'message_preview': message[:200],
|
||||||
'target_type': target_type,
|
'target_type': target_type,
|
||||||
'target_id': target_id,
|
'target_id': target_id,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('set_variable')
|
@workflow_node('set_variable')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('store_data')
|
@workflow_node('store_data')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('switch')
|
@workflow_node('switch')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('variable_aggregator')
|
@workflow_node('variable_aggregator')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langbot_plugin.api.entities.builtin.workflow import ExecutionContext
|
from langbot_plugin.api.entities.builtin.workflow.entities import ExecutionContext
|
||||||
from ..node import WorkflowNode, workflow_node
|
from ..node import WorkflowNode, workflow_node
|
||||||
|
|
||||||
@workflow_node('webhook_trigger')
|
@workflow_node('webhook_trigger')
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from .metadata import build_node_type
|
from .metadata import build_node_type
|
||||||
from .node import WorkflowNode, clear_pending_registrations, get_pending_registrations
|
from .node import WorkflowNode
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langbot.pkg.discover.engine import ComponentDiscoveryEngine
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -116,13 +119,6 @@ class NodeTypeRegistry:
|
|||||||
canonical_type = self._resolve_registered_node_key(node_type)
|
canonical_type = self._resolve_registered_node_key(node_type)
|
||||||
if canonical_type:
|
if canonical_type:
|
||||||
return self._nodes[canonical_type]
|
return self._nodes[canonical_type]
|
||||||
|
|
||||||
if get_pending_registrations():
|
|
||||||
self.process_pending_registrations()
|
|
||||||
canonical_type = self._resolve_registered_node_key(node_type)
|
|
||||||
if canonical_type:
|
|
||||||
return self._nodes[canonical_type]
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_metadata(self, node_type: str) -> Optional[dict[str, Any]]:
|
def get_metadata(self, node_type: str) -> Optional[dict[str, Any]]:
|
||||||
@@ -203,11 +199,25 @@ class NodeTypeRegistry:
|
|||||||
"""Check whether a node has metadata or an implementation registered."""
|
"""Check whether a node has metadata or an implementation registered."""
|
||||||
return self.get_metadata(node_type) is not None or self.get(node_type) is not None
|
return self.get_metadata(node_type) is not None or self.get(node_type) is not None
|
||||||
|
|
||||||
def process_pending_registrations(self):
|
def discover_nodes(self, discover_engine: 'ComponentDiscoveryEngine', nodes_dir: str = 'pkg/workflow/nodes/'):
|
||||||
"""Process all pending node registrations from decorators."""
|
"""Discover and register workflow nodes from the discovery engine.
|
||||||
for node_type, node_class in get_pending_registrations():
|
|
||||||
self.register(node_type, node_class)
|
This method uses the ComponentDiscoveryEngine to find all WorkflowNode
|
||||||
clear_pending_registrations()
|
subclasses in the specified directory and registers them automatically,
|
||||||
|
replacing the old decorator-based registration mechanism.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
discover_engine: The ComponentDiscoveryEngine instance
|
||||||
|
nodes_dir: Directory path to scan for workflow nodes
|
||||||
|
"""
|
||||||
|
node_classes = discover_engine.discover_workflow_nodes(nodes_dir)
|
||||||
|
for node_class in node_classes:
|
||||||
|
type_name = getattr(node_class, 'type_name', '')
|
||||||
|
if type_name:
|
||||||
|
self.register(type_name, node_class)
|
||||||
|
logger.debug(f'Auto-registered workflow node: {type_name}')
|
||||||
|
else:
|
||||||
|
logger.warning(f'Workflow node class {node_class.__name__} missing type_name attribute')
|
||||||
|
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
"""Get total number of node types exposed by metadata or implementation."""
|
"""Get total number of node types exposed by metadata or implementation."""
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ spec:
|
|||||||
LLMAPIRequester:
|
LLMAPIRequester:
|
||||||
fromDirs:
|
fromDirs:
|
||||||
- path: pkg/provider/modelmgr/requesters/
|
- path: pkg/provider/modelmgr/requesters/
|
||||||
|
WorkflowNode:
|
||||||
|
fromDirs:
|
||||||
|
- path: pkg/workflow/nodes/
|
||||||
|
|||||||
@@ -60,26 +60,18 @@ outputs:
|
|||||||
zh_Hans: 解析后的输出(如果输出格式为 JSON)
|
zh_Hans: 解析后的输出(如果输出格式为 JSON)
|
||||||
|
|
||||||
config:
|
config:
|
||||||
- name: model
|
- name: model_config
|
||||||
type: llm-model-selector
|
|
||||||
required: true
|
|
||||||
label:
|
|
||||||
en_US: Model
|
|
||||||
zh_Hans: 模型
|
|
||||||
description:
|
|
||||||
en_US: Select the LLM model to use
|
|
||||||
zh_Hans: 选择要使用的 LLM 模型
|
|
||||||
|
|
||||||
- name: fallback_models
|
|
||||||
type: model-fallback-selector
|
type: model-fallback-selector
|
||||||
required: false
|
required: true
|
||||||
default: []
|
default:
|
||||||
|
primary: ''
|
||||||
|
fallbacks: []
|
||||||
label:
|
label:
|
||||||
en_US: Fallback Models
|
en_US: Model Configuration
|
||||||
zh_Hans: 备用模型
|
zh_Hans: 模型配置
|
||||||
description:
|
description:
|
||||||
en_US: List of fallback models to try if the primary model fails
|
en_US: Configure the primary model and optional fallback models
|
||||||
zh_Hans: 主模型失败时尝试的备用模型列表
|
zh_Hans: 配置主模型和可选的备用模型
|
||||||
|
|
||||||
- name: prompt
|
- name: prompt
|
||||||
label:
|
label:
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
"""Unit tests for config_coercion module"""
|
"""Unit tests for config module"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from langbot.pkg.pipeline.config_coercion import _coerce_value, coerce_pipeline_config
|
from langbot.pkg.pipeline.config import _coerce_value, coerce_pipeline_config
|
||||||
|
|
||||||
|
|
||||||
class TestCoerceValue:
|
class TestCoerceValue:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
@@ -425,7 +425,16 @@ function WorkflowEditorInner() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center: Flow Canvas */}
|
{/* Center: Flow Canvas */}
|
||||||
<div className="flex-1 relative">
|
<div
|
||||||
|
className="flex-1 relative"
|
||||||
|
style={{
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
touchAction: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={displayNodes}
|
nodes={displayNodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
@@ -444,7 +453,7 @@ function WorkflowEditorInner() {
|
|||||||
snapGrid={[15, 15]}
|
snapGrid={[15, 15]}
|
||||||
selectionMode={SelectionMode.Partial}
|
selectionMode={SelectionMode.Partial}
|
||||||
selectionOnDrag
|
selectionOnDrag
|
||||||
panOnDrag={[1, 2]} // Middle click and right click to pan
|
panOnDrag={[2]} // Right click to pan (left click is for node selection)
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
defaultEdgeOptions={{
|
defaultEdgeOptions={{
|
||||||
type: 'default',
|
type: 'default',
|
||||||
@@ -453,7 +462,7 @@ function WorkflowEditorInner() {
|
|||||||
type: MarkerType.ArrowClosed,
|
type: MarkerType.ArrowClosed,
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
color: 'hsl(var(--muted-foreground))',
|
color: 'oklch(from var(--muted-foreground) l c h / 1)',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
deleteKeyCode={null} // We handle delete manually
|
deleteKeyCode={null} // We handle delete manually
|
||||||
@@ -463,7 +472,7 @@ function WorkflowEditorInner() {
|
|||||||
gap={15}
|
gap={15}
|
||||||
size={1}
|
size={1}
|
||||||
variant={BackgroundVariant.Dots}
|
variant={BackgroundVariant.Dots}
|
||||||
color="hsl(var(--muted-foreground) / 0.3)"
|
color="oklch(from var(--muted-foreground) l c h / 0.9)"
|
||||||
/>
|
/>
|
||||||
<Controls
|
<Controls
|
||||||
showInteractive={false}
|
showInteractive={false}
|
||||||
@@ -571,7 +580,6 @@ function WorkflowEditorInner() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div className="w-px bg-border mx-0.5" />
|
|
||||||
|
|
||||||
{/* Zoom controls */}
|
{/* Zoom controls */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -1489,6 +1489,8 @@ const enUS = {
|
|||||||
inputOutputVariables: 'Input/Output Variables',
|
inputOutputVariables: 'Input/Output Variables',
|
||||||
inputs: 'Inputs',
|
inputs: 'Inputs',
|
||||||
outputs: 'Outputs',
|
outputs: 'Outputs',
|
||||||
|
autoLayout: 'Auto Layout',
|
||||||
|
autoLayoutSuccess: 'Nodes auto-layout complete',
|
||||||
availableVariables: 'Available Variables',
|
availableVariables: 'Available Variables',
|
||||||
globalVariables: 'Global Variables',
|
globalVariables: 'Global Variables',
|
||||||
messageContent: 'Message Content',
|
messageContent: 'Message Content',
|
||||||
|
|||||||
@@ -1427,6 +1427,8 @@ const zhHans = {
|
|||||||
inputOutputVariables: '输入/输出变量',
|
inputOutputVariables: '输入/输出变量',
|
||||||
inputs: '输入',
|
inputs: '输入',
|
||||||
outputs: '输出',
|
outputs: '输出',
|
||||||
|
autoLayout: '自动整理',
|
||||||
|
autoLayoutSuccess: '节点已自动整理',
|
||||||
availableVariables: '可用变量',
|
availableVariables: '可用变量',
|
||||||
globalVariables: '全局变量',
|
globalVariables: '全局变量',
|
||||||
messageContent: '消息内容',
|
messageContent: '消息内容',
|
||||||
|
|||||||
Reference in New Issue
Block a user