mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e0275ee08 | |||
| 3fa7389ba9 | |||
| 84a3b1f465 | |||
| 377d011da2 |
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.8.3"
|
version = "4.8.1"
|
||||||
description = "Production-grade platform for building agentic IM bots"
|
description = "Production-grade platform for building agentic IM bots"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -17,7 +17,7 @@ dependencies = [
|
|||||||
"certifi>=2025.4.26",
|
"certifi>=2025.4.26",
|
||||||
"colorlog~=6.6.0",
|
"colorlog~=6.6.0",
|
||||||
"cryptography>=44.0.3",
|
"cryptography>=44.0.3",
|
||||||
"dashscope>=1.25.10",
|
"dashscope>=1.23.2",
|
||||||
"dingtalk-stream>=0.24.0",
|
"dingtalk-stream>=0.24.0",
|
||||||
"discord-py>=2.5.2",
|
"discord-py>=2.5.2",
|
||||||
"pynacl>=1.5.0", # Required for Discord voice support
|
"pynacl>=1.5.0", # Required for Discord voice support
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LangBot - Production-grade platform for building agentic IM bots"""
|
"""LangBot - Production-grade platform for building agentic IM bots"""
|
||||||
|
|
||||||
__version__ = '4.8.3'
|
__version__ = '4.8.1'
|
||||||
|
|||||||
@@ -347,15 +347,10 @@ class DingTalkClient:
|
|||||||
raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
|
raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
|
||||||
|
|
||||||
async def create_and_card(
|
async def create_and_card(
|
||||||
self,
|
self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False
|
||||||
temp_card_id: str,
|
|
||||||
incoming_message: dingtalk_stream.ChatbotMessage,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
card_auto_layout: bool = False,
|
|
||||||
):
|
):
|
||||||
card_data = {}
|
content_key = 'content'
|
||||||
card_data['config'] = json.dumps({'autoLayout': card_auto_layout})
|
card_data = {content_key: ''}
|
||||||
card_data['content'] = ''
|
|
||||||
|
|
||||||
card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message)
|
card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message)
|
||||||
# print(card_instance)
|
# print(card_instance)
|
||||||
|
|||||||
@@ -64,9 +64,7 @@ class LLMModelsService:
|
|||||||
models = result.all()
|
models = result.all()
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, m) for m in models]
|
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, m) for m in models]
|
||||||
|
|
||||||
async def create_llm_model(
|
async def create_llm_model(self, model_data: dict, preserve_uuid: bool = False) -> str:
|
||||||
self, model_data: dict, preserve_uuid: bool = False, auto_set_to_default_pipeline: bool = True
|
|
||||||
) -> str:
|
|
||||||
"""Create a new LLM model"""
|
"""Create a new LLM model"""
|
||||||
if not preserve_uuid:
|
if not preserve_uuid:
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
model_data['uuid'] = str(uuid.uuid4())
|
||||||
@@ -97,19 +95,18 @@ class LLMModelsService:
|
|||||||
)
|
)
|
||||||
self.ap.model_mgr.llm_models.append(runtime_llm_model)
|
self.ap.model_mgr.llm_models.append(runtime_llm_model)
|
||||||
|
|
||||||
if auto_set_to_default_pipeline:
|
# set the default pipeline model to this model
|
||||||
# set the default pipeline model to this model
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
persistence_pipeline.LegacyPipeline.is_default == True
|
||||||
persistence_pipeline.LegacyPipeline.is_default == True
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
pipeline = result.first()
|
)
|
||||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
pipeline = result.first()
|
||||||
pipeline_config = pipeline.config
|
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
||||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
pipeline_config = pipeline.config
|
||||||
pipeline_data = {'config': pipeline_config}
|
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
pipeline_data = {'config': pipeline_config}
|
||||||
|
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
||||||
|
|
||||||
return model_data['uuid']
|
return model_data['uuid']
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('dingtalk_card_auto_layout', 41)
|
|
||||||
class DingTalkCardAutoLayoutMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
self.ap.platform_cfg.data['platform-adapters']['app']['dingtalk']['card_auto_layout'] = False
|
|
||||||
await self.ap.platform_cfg.dump_config()
|
|
||||||
@@ -231,10 +231,7 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
card_template_id = self.config['card_template_id']
|
card_template_id = self.config['card_template_id']
|
||||||
incoming_message = event.source_platform_object.incoming_message
|
incoming_message = event.source_platform_object.incoming_message
|
||||||
# message_id = incoming_message.message_id
|
# message_id = incoming_message.message_id
|
||||||
card_auto_layout = self.config.get('card_ auto_layout', False)
|
card_instance, card_instance_id = await self.bot.create_and_card(card_template_id, incoming_message)
|
||||||
card_instance, card_instance_id = await self.bot.create_and_card(
|
|
||||||
card_template_id, incoming_message, card_auto_layout=card_auto_layout
|
|
||||||
)
|
|
||||||
self.card_instance_id_dict[message_id] = (card_instance, card_instance_id)
|
self.card_instance_id_dict[message_id] = (card_instance, card_instance_id)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -56,13 +56,6 @@ spec:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: true
|
required: true
|
||||||
default: false
|
default: false
|
||||||
- name: card_auto_layout
|
|
||||||
label:
|
|
||||||
en_US: Card Auto Layout
|
|
||||||
zh_Hans: 卡片宽屏自动布局
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
- name: card_template_id
|
- name: card_template_id
|
||||||
label:
|
label:
|
||||||
en_US: card template id
|
en_US: card template id
|
||||||
|
|||||||
@@ -85,26 +85,6 @@ class TelegramMessageConverter(abstract_platform_adapter.AbstractMessageConverte
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if message.voice:
|
|
||||||
if message.caption:
|
|
||||||
message_components.extend(parse_message_text(message.caption))
|
|
||||||
|
|
||||||
file = await message.voice.get_file()
|
|
||||||
|
|
||||||
file_bytes = None
|
|
||||||
file_format = message.voice.mime_type or 'audio/ogg'
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
|
||||||
async with session.get(file.file_path) as response:
|
|
||||||
file_bytes = await response.read()
|
|
||||||
|
|
||||||
message_components.append(
|
|
||||||
platform_message.Voice(
|
|
||||||
base64=f'data:{file_format};base64,{base64.b64encode(file_bytes).decode("utf-8")}',
|
|
||||||
length=message.voice.duration,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return platform_message.MessageChain(message_components)
|
return platform_message.MessageChain(message_components)
|
||||||
|
|
||||||
|
|
||||||
@@ -179,9 +159,7 @@ class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
|
|
||||||
application = ApplicationBuilder().token(config['token']).build()
|
application = ApplicationBuilder().token(config['token']).build()
|
||||||
bot = application.bot
|
bot = application.bot
|
||||||
application.add_handler(
|
application.add_handler(MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback))
|
||||||
MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO | filters.VOICE, telegram_callback)
|
|
||||||
)
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
config=config,
|
config=config,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ class ModelManager:
|
|||||||
'prefered_ranking': space_model.featured_order,
|
'prefered_ranking': space_model.featured_order,
|
||||||
},
|
},
|
||||||
preserve_uuid=True,
|
preserve_uuid=True,
|
||||||
auto_set_to_default_pipeline=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif space_model.category == 'embedding':
|
elif space_model.category == 'embedding':
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
|
|||||||
# deepseek 不支持多模态,把content都转换成纯文字
|
# deepseek 不支持多模态,把content都转换成纯文字
|
||||||
for m in messages:
|
for m in messages:
|
||||||
if 'content' in m and isinstance(m['content'], list):
|
if 'content' in m and isinstance(m['content'], list):
|
||||||
m['content'] = ' '.join([c['text'] for c in m['content'] if 'text' in c])
|
m['content'] = ' '.join([c['text'] for c in m['content']])
|
||||||
|
|
||||||
args['messages'] = messages
|
args['messages'] = messages
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,24 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
|||||||
'timeout': 120,
|
'timeout': 120,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _add_thought_signature_to_messages(self, messages: list[dict]) -> list[dict]:
|
||||||
|
"""Add thought_signature to tool_calls in messages for Gemini API compatibility
|
||||||
|
|
||||||
|
Gemini API requires a thought_signature field in function call parts.
|
||||||
|
See: https://ai.google.dev/gemini-api/docs/thought-signatures
|
||||||
|
|
||||||
|
Note: This function modifies the dictionaries in the messages list in place.
|
||||||
|
"""
|
||||||
|
for msg in messages:
|
||||||
|
if 'tool_calls' in msg and msg['tool_calls']:
|
||||||
|
# Ensure we're working with a mutable copy of tool_calls
|
||||||
|
if not isinstance(msg['tool_calls'], list):
|
||||||
|
continue
|
||||||
|
for tool_call in msg['tool_calls']:
|
||||||
|
if isinstance(tool_call, dict) and 'thought_signature' not in tool_call:
|
||||||
|
tool_call['thought_signature'] = ''
|
||||||
|
return messages
|
||||||
|
|
||||||
async def _closure_stream(
|
async def _closure_stream(
|
||||||
self,
|
self,
|
||||||
query: pipeline_query.Query,
|
query: pipeline_query.Query,
|
||||||
@@ -42,6 +60,9 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
|||||||
# 设置此次请求中的messages
|
# 设置此次请求中的messages
|
||||||
messages = req_messages.copy()
|
messages = req_messages.copy()
|
||||||
|
|
||||||
|
# Add thought_signature to tool_calls for Gemini compatibility
|
||||||
|
messages = self._add_thought_signature_to_messages(messages)
|
||||||
|
|
||||||
# 检查vision
|
# 检查vision
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if 'content' in msg and isinstance(msg['content'], list):
|
if 'content' in msg and isinstance(msg['content'], list):
|
||||||
@@ -140,3 +161,29 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
|||||||
|
|
||||||
yield provider_message.MessageChunk(**chunk_data)
|
yield provider_message.MessageChunk(**chunk_data)
|
||||||
chunk_idx += 1
|
chunk_idx += 1
|
||||||
|
|
||||||
|
async def _closure(
|
||||||
|
self,
|
||||||
|
query: pipeline_query.Query,
|
||||||
|
req_messages: list[dict],
|
||||||
|
use_model: requester.RuntimeLLMModel,
|
||||||
|
use_funcs: list[resource_tool.LLMTool] = None,
|
||||||
|
extra_args: dict[str, typing.Any] = {},
|
||||||
|
remove_think: bool = False,
|
||||||
|
) -> tuple[provider_message.Message, dict]:
|
||||||
|
"""Override _closure to add thought_signature to messages"""
|
||||||
|
# Make a shallow copy to avoid mutating the caller's list
|
||||||
|
messages = req_messages.copy()
|
||||||
|
|
||||||
|
# Add thought_signature to tool_calls for Gemini compatibility
|
||||||
|
messages = self._add_thought_signature_to_messages(messages)
|
||||||
|
|
||||||
|
# Call parent implementation
|
||||||
|
return await super()._closure(
|
||||||
|
query=query,
|
||||||
|
req_messages=messages,
|
||||||
|
use_model=use_model,
|
||||||
|
use_funcs=use_funcs,
|
||||||
|
extra_args=extra_args,
|
||||||
|
remove_think=remove_think,
|
||||||
|
)
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ class DashScopeAPIRunner(runner.RequestRunner):
|
|||||||
stream=True, # 流式输出
|
stream=True, # 流式输出
|
||||||
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
|
incremental_output=True, # 增量输出,使用流式输出需要开启增量输出
|
||||||
session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话
|
session_id=query.session.using_conversation.uuid, # 会话ID用于,多轮对话
|
||||||
enable_thinking=has_thoughts,
|
|
||||||
has_thoughts=has_thoughts,
|
has_thoughts=has_thoughts,
|
||||||
# rag_options={ # 主要用于文件交互,暂不支持
|
# rag_options={ # 主要用于文件交互,暂不支持
|
||||||
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
|
# "session_file_ids": ["FILE_ID1"], # FILE_ID1 替换为实际的临时文件ID,逗号隔开多个
|
||||||
@@ -142,14 +141,14 @@ class DashScopeAPIRunner(runner.RequestRunner):
|
|||||||
# 获取流式传输的output
|
# 获取流式传输的output
|
||||||
stream_output = chunk.get('output', {})
|
stream_output = chunk.get('output', {})
|
||||||
stream_think = stream_output.get('thoughts', [])
|
stream_think = stream_output.get('thoughts', [])
|
||||||
if stream_think and stream_think[0].get('thought'):
|
if stream_think[0].get('thought'):
|
||||||
if not think_start:
|
if not think_start:
|
||||||
think_start = True
|
think_start = True
|
||||||
pending_content += f'<think>\n{stream_think[0].get("thought")}'
|
pending_content += f'<think>\n{stream_think[0].get("thought")}'
|
||||||
else:
|
else:
|
||||||
# 继续输出 reasoning_content
|
# 继续输出 reasoning_content
|
||||||
pending_content += stream_think[0].get('thought')
|
pending_content += stream_think[0].get('thought')
|
||||||
elif (not stream_think or stream_think[0].get('thought') == '') and not think_end:
|
elif stream_think[0].get('thought') == '' and not think_end:
|
||||||
think_end = True
|
think_end = True
|
||||||
pending_content += '\n</think>\n'
|
pending_content += '\n</think>\n'
|
||||||
if stream_output.get('text') is not None:
|
if stream_output.get('text') is not None:
|
||||||
|
|||||||
@@ -289,16 +289,12 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
yield msg
|
yield msg
|
||||||
if chunk['event'] == 'message_file':
|
if chunk['event'] == 'message_file':
|
||||||
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
|
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
|
||||||
# 检查URL是否已经是完整的连接
|
base_url = self.dify_client.base_url
|
||||||
if chunk['url'].startswith('http://') or chunk['url'].startswith('https://'):
|
|
||||||
image_url = chunk['url']
|
|
||||||
else:
|
|
||||||
base_url = self.dify_client.base_url
|
|
||||||
|
|
||||||
if base_url.endswith('/v1'):
|
if base_url.endswith('/v1'):
|
||||||
base_url = base_url[:-3]
|
base_url = base_url[:-3]
|
||||||
|
|
||||||
image_url = base_url + chunk['url']
|
image_url = base_url + chunk['url']
|
||||||
|
|
||||||
yield provider_message.Message(
|
yield provider_message.Message(
|
||||||
role='assistant',
|
role='assistant',
|
||||||
@@ -563,16 +559,12 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
if chunk['event'] == 'message_file':
|
if chunk['event'] == 'message_file':
|
||||||
message_idx += 1
|
message_idx += 1
|
||||||
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
|
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
|
||||||
# 检查URL是否已经是完整的连接
|
base_url = self.dify_client.base_url
|
||||||
if chunk['url'].startswith('http://') or chunk['url'].startswith('https://'):
|
|
||||||
image_url = chunk['url']
|
|
||||||
else:
|
|
||||||
base_url = self.dify_client.base_url
|
|
||||||
|
|
||||||
if base_url.endswith('/v1'):
|
if base_url.endswith('/v1'):
|
||||||
base_url = base_url[:-3]
|
base_url = base_url[:-3]
|
||||||
|
|
||||||
image_url = base_url + chunk['url']
|
image_url = base_url + chunk['url']
|
||||||
|
|
||||||
yield provider_message.MessageChunk(
|
yield provider_message.MessageChunk(
|
||||||
role='assistant',
|
role='assistant',
|
||||||
|
|||||||
Generated
-11210
File diff suppressed because it is too large
Load Diff
Generated
+664
-655
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,5 @@
|
|||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
import { LoadingPage } from '@/components/ui/loading-spinner';
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return <LoadingPage />;
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<LoadingSpinner size="lg" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
import PipelineFormComponent from './components/pipeline-form/PipelineFormComponent';
|
import PipelineFormComponent from './components/pipeline-form/PipelineFormComponent';
|
||||||
import DebugDialog from './components/debug-dialog/DebugDialog';
|
import DebugDialog from './components/debug-dialog/DebugDialog';
|
||||||
import PipelineExtension from './components/pipeline-extensions/PipelineExtension';
|
import PipelineExtension from './components/pipeline-extensions/PipelineExtension';
|
||||||
import PipelineMonitoringTab from './components/monitoring-tab/PipelineMonitoringTab';
|
|
||||||
|
|
||||||
interface PipelineDialogProps {
|
interface PipelineDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -34,7 +34,7 @@ interface PipelineDialogProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogMode = 'config' | 'debug' | 'extensions' | 'monitoring';
|
type DialogMode = 'config' | 'debug' | 'extensions';
|
||||||
|
|
||||||
export default function PipelineDialog({
|
export default function PipelineDialog({
|
||||||
open,
|
open,
|
||||||
@@ -111,19 +111,6 @@ export default function PipelineDialog({
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'monitoring',
|
|
||||||
label: t('pipelines.monitoring.title'),
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM4 5V19H20V5H4ZM6 7H18V9H6V7ZM6 11H18V13H6V11ZM6 15H12V17H6V15Z"></path>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const getDialogTitle = () => {
|
const getDialogTitle = () => {
|
||||||
@@ -135,9 +122,6 @@ export default function PipelineDialog({
|
|||||||
if (currentMode === 'extensions') {
|
if (currentMode === 'extensions') {
|
||||||
return t('pipelines.extensions.title');
|
return t('pipelines.extensions.title');
|
||||||
}
|
}
|
||||||
if (currentMode === 'monitoring') {
|
|
||||||
return t('pipelines.monitoring.title');
|
|
||||||
}
|
|
||||||
return t('pipelines.debugDialog.title');
|
return t('pipelines.debugDialog.title');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,23 +193,48 @@ export default function PipelineDialog({
|
|||||||
>
|
>
|
||||||
<DialogTitle>{getDialogTitle()}</DialogTitle>
|
<DialogTitle>{getDialogTitle()}</DialogTitle>
|
||||||
{currentMode === 'debug' && (
|
{currentMode === 'debug' && (
|
||||||
<div className="flex items-center gap-2 ml-2">
|
<>
|
||||||
<div
|
<div className="flex items-center gap-2 ml-2">
|
||||||
className={`w-2.5 h-2.5 rounded-full ${
|
<div
|
||||||
isWebSocketConnected ? 'bg-green-500' : 'bg-red-500'
|
className={`w-2.5 h-2.5 rounded-full ${
|
||||||
}`}
|
isWebSocketConnected ? 'bg-green-500' : 'bg-red-500'
|
||||||
title={
|
}`}
|
||||||
isWebSocketConnected
|
title={
|
||||||
|
isWebSocketConnected
|
||||||
|
? t('pipelines.debugDialog.connected')
|
||||||
|
: t('pipelines.debugDialog.disconnected')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{isWebSocketConnected
|
||||||
? t('pipelines.debugDialog.connected')
|
? t('pipelines.debugDialog.connected')
|
||||||
: t('pipelines.debugDialog.disconnected')
|
: t('pipelines.debugDialog.disconnected')}
|
||||||
}
|
</span>
|
||||||
/>
|
</div>
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="ml-auto mr-4">
|
||||||
{isWebSocketConnected
|
<Button
|
||||||
? t('pipelines.debugDialog.connected')
|
variant="outline"
|
||||||
: t('pipelines.debugDialog.disconnected')}
|
size="sm"
|
||||||
</span>
|
onClick={() => {
|
||||||
</div>
|
router.push(
|
||||||
|
`/home/monitoring?pipelineId=${pipelineId}`,
|
||||||
|
);
|
||||||
|
onOpenChange(false);
|
||||||
|
}}
|
||||||
|
className="bg-white dark:bg-[#2a2a2e]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-2"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM4 5V19H20V5H4ZM6 7H18V9H6V7ZM6 11H18V13H6V11ZM6 15H12V17H6V15Z"></path>
|
||||||
|
</svg>
|
||||||
|
{t('monitoring.viewMonitoring')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div
|
<div
|
||||||
@@ -259,16 +268,6 @@ export default function PipelineDialog({
|
|||||||
onConnectionStatusChange={setIsWebSocketConnected}
|
onConnectionStatusChange={setIsWebSocketConnected}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentMode === 'monitoring' && pipelineId && (
|
|
||||||
<PipelineMonitoringTab
|
|
||||||
pipelineId={pipelineId}
|
|
||||||
onNavigateToMonitoring={() => {
|
|
||||||
router.push(`/home/monitoring?pipelineId=${pipelineId}`);
|
|
||||||
onOpenChange(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
@@ -1,665 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { ChevronRight, ChevronDown, ExternalLink } from 'lucide-react';
|
|
||||||
import { useMonitoringData } from '@/app/home/monitoring/hooks/useMonitoringData';
|
|
||||||
import { MessageContentRenderer } from '@/app/home/monitoring/components/MessageContentRenderer';
|
|
||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
|
||||||
import { MessageDetails } from '@/app/home/monitoring/types/monitoring';
|
|
||||||
|
|
||||||
interface PipelineMonitoringTabProps {
|
|
||||||
pipelineId: string;
|
|
||||||
onNavigateToMonitoring?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RawMessageData {
|
|
||||||
id: string;
|
|
||||||
timestamp: string;
|
|
||||||
bot_id: string;
|
|
||||||
bot_name: string;
|
|
||||||
pipeline_id: string;
|
|
||||||
pipeline_name: string;
|
|
||||||
message_content: string;
|
|
||||||
session_id: string;
|
|
||||||
status: string;
|
|
||||||
level: string;
|
|
||||||
platform: string;
|
|
||||||
user_id: string;
|
|
||||||
runner_name: string;
|
|
||||||
variables: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RawLLMCallData {
|
|
||||||
id: string;
|
|
||||||
timestamp: string;
|
|
||||||
model_name: string;
|
|
||||||
status: string;
|
|
||||||
duration: number;
|
|
||||||
error_message: string | null;
|
|
||||||
input_tokens: number;
|
|
||||||
output_tokens: number;
|
|
||||||
total_tokens: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RawLLMStatsData {
|
|
||||||
total_calls: number;
|
|
||||||
total_input_tokens: number;
|
|
||||||
total_output_tokens: number;
|
|
||||||
total_tokens: number;
|
|
||||||
total_duration_ms: number;
|
|
||||||
average_duration_ms: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RawErrorData {
|
|
||||||
id: string;
|
|
||||||
timestamp: string;
|
|
||||||
error_type: string;
|
|
||||||
error_message: string;
|
|
||||||
stack_trace: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PipelineMonitoringTab({
|
|
||||||
pipelineId,
|
|
||||||
onNavigateToMonitoring,
|
|
||||||
}: PipelineMonitoringTabProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// Filter state - only show data for this pipeline, last 24 hours
|
|
||||||
const filterState = useMemo(
|
|
||||||
() => ({
|
|
||||||
selectedBots: [],
|
|
||||||
selectedPipelines: [pipelineId],
|
|
||||||
timeRange: 'last24Hours' as const,
|
|
||||||
customDateRange: null,
|
|
||||||
}),
|
|
||||||
[pipelineId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data, loading, refetch } = useMonitoringData(filterState);
|
|
||||||
|
|
||||||
const [expandedMessageId, setExpandedMessageId] = useState<string | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [messageDetails, setMessageDetails] = useState<
|
|
||||||
Record<string, MessageDetails>
|
|
||||||
>({});
|
|
||||||
const [loadingDetails, setLoadingDetails] = useState<Record<string, boolean>>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
|
|
||||||
const [activeTab, setActiveTab] = useState<string>('messages');
|
|
||||||
|
|
||||||
const toggleMessageExpand = async (messageId: string) => {
|
|
||||||
if (expandedMessageId === messageId) {
|
|
||||||
setExpandedMessageId(null);
|
|
||||||
} else {
|
|
||||||
setExpandedMessageId(messageId);
|
|
||||||
|
|
||||||
if (!messageDetails[messageId]) {
|
|
||||||
setLoadingDetails((prev) => ({ ...prev, [messageId]: true }));
|
|
||||||
try {
|
|
||||||
const result = await httpClient.get<{
|
|
||||||
message_id: string;
|
|
||||||
found: boolean;
|
|
||||||
message: RawMessageData | null;
|
|
||||||
llm_calls: RawLLMCallData[];
|
|
||||||
llm_stats: RawLLMStatsData;
|
|
||||||
errors: RawErrorData[];
|
|
||||||
}>(`/api/v1/monitoring/messages/${messageId}/details`);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
setMessageDetails((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[messageId]: {
|
|
||||||
messageId: result.message_id,
|
|
||||||
found: result.found,
|
|
||||||
message: result.message
|
|
||||||
? {
|
|
||||||
id: result.message.id,
|
|
||||||
timestamp: new Date(result.message.timestamp),
|
|
||||||
botId: result.message.bot_id,
|
|
||||||
botName: result.message.bot_name,
|
|
||||||
pipelineId: result.message.pipeline_id,
|
|
||||||
pipelineName: result.message.pipeline_name,
|
|
||||||
messageContent: result.message.message_content,
|
|
||||||
sessionId: result.message.session_id,
|
|
||||||
status: result.message.status,
|
|
||||||
level: result.message.level,
|
|
||||||
platform: result.message.platform,
|
|
||||||
userId: result.message.user_id,
|
|
||||||
runnerName: result.message.runner_name,
|
|
||||||
variables: result.message.variables,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
llmCalls: result.llm_calls.map((call: RawLLMCallData) => ({
|
|
||||||
id: call.id,
|
|
||||||
timestamp: new Date(call.timestamp),
|
|
||||||
modelName: call.model_name,
|
|
||||||
status: call.status,
|
|
||||||
duration: call.duration,
|
|
||||||
errorMessage: call.error_message,
|
|
||||||
tokens: {
|
|
||||||
input: call.input_tokens || 0,
|
|
||||||
output: call.output_tokens || 0,
|
|
||||||
total: call.total_tokens || 0,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
errors: result.errors.map((error: RawErrorData) => ({
|
|
||||||
id: error.id,
|
|
||||||
timestamp: new Date(error.timestamp),
|
|
||||||
errorType: error.error_type,
|
|
||||||
errorMessage: error.error_message,
|
|
||||||
stackTrace: error.stack_trace,
|
|
||||||
})),
|
|
||||||
llmStats: {
|
|
||||||
totalCalls: result.llm_stats.total_calls,
|
|
||||||
totalInputTokens: result.llm_stats.total_input_tokens,
|
|
||||||
totalOutputTokens: result.llm_stats.total_output_tokens,
|
|
||||||
totalTokens: result.llm_stats.total_tokens,
|
|
||||||
totalDurationMs: result.llm_stats.total_duration_ms,
|
|
||||||
averageDurationMs: result.llm_stats.average_duration_ms,
|
|
||||||
},
|
|
||||||
} as MessageDetails,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch message details:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingDetails((prev) => ({ ...prev, [messageId]: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleErrorExpand = (errorId: string) => {
|
|
||||||
if (expandedErrorId === errorId) {
|
|
||||||
setExpandedErrorId(null);
|
|
||||||
} else {
|
|
||||||
setExpandedErrorId(errorId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const jumpToMessage = async (messageId: string) => {
|
|
||||||
setActiveTab('messages');
|
|
||||||
// Small delay to ensure tab transition completes before expanding
|
|
||||||
setTimeout(() => {
|
|
||||||
toggleMessageExpand(messageId);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex flex-col">
|
|
||||||
{/* Header with refresh button */}
|
|
||||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{t('pipelines.monitoring.description')}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{onNavigateToMonitoring && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onNavigateToMonitoring}
|
|
||||||
className="bg-white dark:bg-[#2a2a2e] hover:bg-gray-50 dark:hover:bg-gray-800 border-gray-300 dark:border-gray-600"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 mr-2"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19V6.413L11.2071 14.2071L9.79289 12.7929L17.585 5H13V3H21Z"></path>
|
|
||||||
</svg>
|
|
||||||
{t('pipelines.monitoring.detailedLogs')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={refetch}
|
|
||||||
className="bg-white dark:bg-[#2a2a2e] hover:bg-gray-50 dark:hover:bg-gray-800 border-gray-300 dark:border-gray-600"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 mr-2"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path>
|
|
||||||
</svg>
|
|
||||||
{t('monitoring.refreshData')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Overview Stats */}
|
|
||||||
{data && (
|
|
||||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
|
||||||
<div className="bg-white dark:bg-[#2a2a2e] rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{t('monitoring.totalMessages')}
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">
|
|
||||||
{data.overview.totalMessages}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-[#2a2a2e] rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{t('monitoring.successRate')}
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-1">
|
|
||||||
{data.overview.successRate.toFixed(1)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-[#2a2a2e] rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{t('monitoring.tabs.errors')}
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-red-600 dark:text-red-400 mt-1">
|
|
||||||
{data.errors.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onValueChange={setActiveTab}
|
|
||||||
className="flex-1 flex flex-col min-h-0"
|
|
||||||
>
|
|
||||||
<TabsList className="bg-gray-100 dark:bg-[#1a1a1e] h-10 p-1 mb-4">
|
|
||||||
<TabsTrigger
|
|
||||||
value="messages"
|
|
||||||
className="px-4 py-1.5 text-sm font-medium cursor-pointer data-[state=active]:bg-white dark:data-[state=active]:bg-[#2a2a2e] data-[state=active]:shadow-sm"
|
|
||||||
>
|
|
||||||
{t('monitoring.tabs.messages')}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="errors"
|
|
||||||
className="px-4 py-1.5 text-sm font-medium cursor-pointer data-[state=active]:bg-white dark:data-[state=active]:bg-[#2a2a2e] data-[state=active]:shadow-sm"
|
|
||||||
>
|
|
||||||
{t('monitoring.tabs.errors')}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="llmCalls"
|
|
||||||
className="px-4 py-1.5 text-sm font-medium cursor-pointer data-[state=active]:bg-white dark:data-[state=active]:bg-[#2a2a2e] data-[state=active]:shadow-sm"
|
|
||||||
>
|
|
||||||
{t('monitoring.tabs.modelCalls')}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
|
||||||
{/* Messages Tab */}
|
|
||||||
<TabsContent value="messages" className="m-0 h-full">
|
|
||||||
{loading && (
|
|
||||||
<div className="py-12 flex justify-center">
|
|
||||||
<LoadingSpinner text={t('monitoring.messageList.loading')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && data && data.messages && data.messages.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{data.messages
|
|
||||||
.filter((msg) => {
|
|
||||||
const content = msg.messageContent?.trim();
|
|
||||||
return content && content !== '[]' && content !== '""';
|
|
||||||
})
|
|
||||||
.map((msg) => (
|
|
||||||
<div
|
|
||||||
key={msg.id}
|
|
||||||
className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-md transition-all duration-200"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="p-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
|
||||||
onClick={() => toggleMessageExpand(msg.id)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex items-start flex-1">
|
|
||||||
<div className="mr-2 mt-0.5">
|
|
||||||
{expandedMessageId === msg.id ? (
|
|
||||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span
|
|
||||||
className={`text-xs px-2 py-0.5 rounded ${
|
|
||||||
msg.status === 'success'
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
|
||||||
: msg.status === 'error'
|
|
||||||
? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
|
||||||
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{msg.status}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{msg.botName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300 line-clamp-2">
|
|
||||||
<MessageContentRenderer
|
|
||||||
content={msg.messageContent}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ml-4">
|
|
||||||
{msg.timestamp.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{expandedMessageId === msg.id && (
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900">
|
|
||||||
{loadingDetails[msg.id] && (
|
|
||||||
<div className="flex justify-center py-8">
|
|
||||||
<LoadingSpinner
|
|
||||||
text={t('monitoring.messageList.loading')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loadingDetails[msg.id] &&
|
|
||||||
messageDetails[msg.id] && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{messageDetails[msg.id].errors.length > 0 && (
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
|
|
||||||
<h4 className="text-sm font-semibold text-red-700 dark:text-red-400 mb-2">
|
|
||||||
{t('monitoring.errors.errorMessage')}
|
|
||||||
</h4>
|
|
||||||
{messageDetails[msg.id].errors.map(
|
|
||||||
(error) => (
|
|
||||||
<div
|
|
||||||
key={error.id}
|
|
||||||
className="text-sm space-y-2"
|
|
||||||
>
|
|
||||||
<div className="text-red-600 dark:text-red-400">
|
|
||||||
{error.errorType}:{' '}
|
|
||||||
{error.errorMessage}
|
|
||||||
</div>
|
|
||||||
{error.stackTrace && (
|
|
||||||
<pre className="text-xs text-gray-600 dark:text-gray-400 overflow-auto max-h-40 bg-white dark:bg-gray-900 p-2 rounded whitespace-pre-wrap break-words">
|
|
||||||
{error.stackTrace}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{messageDetails[msg.id].llmCalls.length > 0 && (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
|
||||||
<h4 className="text-sm font-semibold text-blue-700 dark:text-blue-400 mb-2">
|
|
||||||
{t('monitoring.tabs.modelCalls')} (
|
|
||||||
{messageDetails[msg.id].llmCalls.length})
|
|
||||||
</h4>
|
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
|
|
||||||
<div>
|
|
||||||
{t('monitoring.llmCalls.totalTokens')}:{' '}
|
|
||||||
{
|
|
||||||
messageDetails[msg.id].llmStats
|
|
||||||
.totalTokens
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t('monitoring.llmCalls.duration')}:{' '}
|
|
||||||
{messageDetails[
|
|
||||||
msg.id
|
|
||||||
].llmStats.totalDurationMs.toFixed(0)}
|
|
||||||
ms
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading &&
|
|
||||||
(!data || !data.messages || data.messages.length === 0) && (
|
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-16">
|
|
||||||
<svg
|
|
||||||
className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p className="text-base font-medium">
|
|
||||||
{t('monitoring.messageList.noMessages')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Errors Tab */}
|
|
||||||
<TabsContent value="errors" className="m-0 h-full">
|
|
||||||
{loading && (
|
|
||||||
<div className="py-12 flex justify-center">
|
|
||||||
<LoadingSpinner text={t('common.loading')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && data && data.errors && data.errors.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{data.errors.map((error) => (
|
|
||||||
<div
|
|
||||||
key={error.id}
|
|
||||||
className="border border-red-200 dark:border-red-900 rounded-lg overflow-hidden hover:shadow-md transition-all duration-200"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="p-4 cursor-pointer hover:bg-red-50 dark:hover:bg-red-950/50 transition-colors bg-red-50/50 dark:bg-red-950/30"
|
|
||||||
onClick={() => toggleErrorExpand(error.id)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex items-start flex-1">
|
|
||||||
<div className="mr-2 mt-0.5">
|
|
||||||
{expandedErrorId === error.id ? (
|
|
||||||
<ChevronDown className="w-4 h-4 text-red-500" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-4 h-4 text-red-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
{error.messageId && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 px-1.5 text-xs"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
jumpToMessage(error.messageId!);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExternalLink className="w-3 h-3 mr-1" />
|
|
||||||
{t('monitoring.messageList.viewConversation')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-sm text-red-700 dark:text-red-300 mb-1">
|
|
||||||
{error.errorType}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-red-600 dark:text-red-400 line-clamp-2">
|
|
||||||
{error.errorMessage}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ml-4">
|
|
||||||
{error.timestamp.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{expandedErrorId === error.id && (
|
|
||||||
<div className="border-t border-red-200 dark:border-red-900 p-4 bg-white dark:bg-gray-900">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-3">
|
|
||||||
<h4 className="text-sm font-semibold text-red-700 dark:text-red-400 mb-2">
|
|
||||||
{t('monitoring.errors.errorMessage')}
|
|
||||||
</h4>
|
|
||||||
<div className="text-sm text-red-600 dark:text-red-400 whitespace-pre-wrap break-words">
|
|
||||||
{error.errorMessage}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error.stackTrace && (
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
|
||||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{t('monitoring.errors.stackTrace')}
|
|
||||||
</h4>
|
|
||||||
<pre className="text-xs text-gray-600 dark:text-gray-400 overflow-auto max-h-60 bg-white dark:bg-gray-900 p-2 rounded whitespace-pre-wrap break-words">
|
|
||||||
{error.stackTrace}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading &&
|
|
||||||
(!data || !data.errors || data.errors.length === 0) && (
|
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-16">
|
|
||||||
<svg
|
|
||||||
className="w-16 h-16 mx-auto mb-4 text-green-300 dark:text-green-600"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p className="text-base font-medium text-green-600 dark:text-green-400">
|
|
||||||
{t('monitoring.errors.noErrors')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* LLM Calls Tab */}
|
|
||||||
<TabsContent value="llmCalls" className="m-0 h-full">
|
|
||||||
{loading && (
|
|
||||||
<div className="py-12 flex justify-center">
|
|
||||||
<LoadingSpinner text={t('common.loading')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && data && data.llmCalls && data.llmCalls.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{data.llmCalls.map((call) => (
|
|
||||||
<div
|
|
||||||
key={call.id}
|
|
||||||
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-all duration-200"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span
|
|
||||||
className={`text-xs px-2 py-0.5 rounded ${
|
|
||||||
call.status === 'success'
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{call.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-sm text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
{call.modelName}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
|
|
||||||
<div className="flex flex-wrap gap-4">
|
|
||||||
<span>
|
|
||||||
{t('monitoring.llmCalls.inputTokens')}:{' '}
|
|
||||||
{call.tokens.input}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{t('monitoring.llmCalls.outputTokens')}:{' '}
|
|
||||||
{call.tokens.output}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{t('monitoring.llmCalls.totalTokens')}:{' '}
|
|
||||||
{call.tokens.total}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{t('monitoring.llmCalls.duration')}:{' '}
|
|
||||||
{call.duration}ms
|
|
||||||
</span>
|
|
||||||
{call.cost && (
|
|
||||||
<span>
|
|
||||||
{t('monitoring.llmCalls.cost')}: $
|
|
||||||
{call.cost.toFixed(4)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{call.errorMessage && (
|
|
||||||
<div className="mt-2 text-xs text-red-600 dark:text-red-400">
|
|
||||||
Error: {call.errorMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ml-4">
|
|
||||||
{call.timestamp.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading &&
|
|
||||||
(!data || !data.llmCalls || data.llmCalls.length === 0) && (
|
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-16">
|
|
||||||
<svg
|
|
||||||
className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<p className="text-base font-medium">
|
|
||||||
{t('monitoring.llmCalls.noData')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -644,12 +644,6 @@ const enUS = {
|
|||||||
showMarkdown: 'Show Markdown',
|
showMarkdown: 'Show Markdown',
|
||||||
showRaw: 'Show Raw',
|
showRaw: 'Show Raw',
|
||||||
},
|
},
|
||||||
monitoring: {
|
|
||||||
title: 'Monitoring',
|
|
||||||
description:
|
|
||||||
'View execution logs and errors for this pipeline (last 24 hours)',
|
|
||||||
detailedLogs: 'Detailed Logs',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
title: 'Knowledge',
|
title: 'Knowledge',
|
||||||
|
|||||||
@@ -647,11 +647,6 @@ const jaJP = {
|
|||||||
showMarkdown: 'Markdownで表示',
|
showMarkdown: 'Markdownで表示',
|
||||||
showRaw: '原文で表示',
|
showRaw: '原文で表示',
|
||||||
},
|
},
|
||||||
monitoring: {
|
|
||||||
title: 'モニタリング',
|
|
||||||
description: 'このパイプラインの実行ログとエラー情報を表示(過去24時間)',
|
|
||||||
detailedLogs: '詳細ログ',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
title: '知識ベース',
|
title: '知識ベース',
|
||||||
|
|||||||
@@ -620,11 +620,6 @@ const zhHans = {
|
|||||||
showMarkdown: '渲染',
|
showMarkdown: '渲染',
|
||||||
showRaw: '原文',
|
showRaw: '原文',
|
||||||
},
|
},
|
||||||
monitoring: {
|
|
||||||
title: '监控日志',
|
|
||||||
description: '查看此流水线的运行记录和错误信息(最近24小时)',
|
|
||||||
detailedLogs: '详细日志',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
title: '知识库',
|
title: '知识库',
|
||||||
|
|||||||
@@ -613,11 +613,6 @@ const zhHant = {
|
|||||||
showMarkdown: '渲染',
|
showMarkdown: '渲染',
|
||||||
showRaw: '原文',
|
showRaw: '原文',
|
||||||
},
|
},
|
||||||
monitoring: {
|
|
||||||
title: '監控日誌',
|
|
||||||
description: '檢視此流程線的執行記錄和錯誤資訊(最近24小時)',
|
|
||||||
detailedLogs: '詳細日誌',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
title: '知識庫',
|
title: '知識庫',
|
||||||
|
|||||||
Reference in New Issue
Block a user