mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-13 09:16:04 +00:00
352 lines
14 KiB
Python
352 lines
14 KiB
Python
from __future__ import annotations
|
||
|
||
import typing
|
||
import json
|
||
|
||
|
||
from langbot.pkg.provider import runner
|
||
from langbot.pkg.core import app
|
||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||
from langbot.libs.weknora_api import client, errors
|
||
|
||
|
||
@runner.runner_class('weknora-api')
|
||
class WeKnoraAPIRunner(runner.RequestRunner):
|
||
"""WeKnora API 对话请求器"""
|
||
|
||
weknora_client: client.AsyncWeKnoraClient
|
||
|
||
def __init__(self, ap: app.Application, pipeline_config: dict):
|
||
super().__init__(ap, pipeline_config)
|
||
|
||
valid_app_types = ['chat', 'agent']
|
||
if self.pipeline_config['ai']['weknora-api']['app-type'] not in valid_app_types:
|
||
raise errors.WeKnoraAPIError(
|
||
f'不支持的 WeKnora 应用类型: {self.pipeline_config["ai"]["weknora-api"]["app-type"]}'
|
||
)
|
||
|
||
api_key = self.pipeline_config['ai']['weknora-api'].get('api-key', '').strip()
|
||
if not api_key:
|
||
raise errors.WeKnoraAPIError(
|
||
'WeKnora API Key 未配置,请在流水线的 WeKnora API 配置中填入 API Key '
|
||
'(从 WeKnora 前端 设置 → API Keys 生成)'
|
||
)
|
||
|
||
base_url = self.pipeline_config['ai']['weknora-api'].get('base-url', '').strip()
|
||
if not base_url:
|
||
raise errors.WeKnoraAPIError('WeKnora Base URL 未配置,请填入服务器地址,例如 http://localhost:8080/api/v1')
|
||
|
||
self.weknora_client = client.AsyncWeKnoraClient(
|
||
api_key=api_key,
|
||
base_url=base_url,
|
||
)
|
||
|
||
async def _extract_plain_text(self, query: pipeline_query.Query) -> str:
|
||
"""从用户消息中提取纯文本内容"""
|
||
plain_text = ''
|
||
if isinstance(query.user_message.content, str):
|
||
plain_text = query.user_message.content
|
||
elif isinstance(query.user_message.content, list):
|
||
for ce in query.user_message.content:
|
||
if ce.type == 'text':
|
||
plain_text += ce.text
|
||
|
||
if not plain_text:
|
||
plain_text = self.pipeline_config['ai']['weknora-api'].get('base-prompt', '')
|
||
|
||
return plain_text
|
||
|
||
async def _ensure_session(self, query: pipeline_query.Query) -> str:
|
||
"""确保会话存在,如果不存在则创建"""
|
||
session_id = query.session.using_conversation.uuid or ''
|
||
|
||
if not session_id:
|
||
user_tag = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||
session_id = await self.weknora_client.create_session(title=f'IM Chat - {user_tag}')
|
||
query.session.using_conversation.uuid = session_id
|
||
|
||
return session_id
|
||
|
||
async def _agent_chat_messages(
|
||
self, query: pipeline_query.Query
|
||
) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||
"""调用 Agent 智能对话(非流式聚合输出)"""
|
||
session_id = await self._ensure_session(query)
|
||
plain_text = await self._extract_plain_text(query)
|
||
user_tag = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||
|
||
config = self.pipeline_config['ai']['weknora-api']
|
||
agent_id = config.get('agent-id', 'builtin-smart-reasoning')
|
||
knowledge_base_ids = config.get('knowledge-base-ids', [])
|
||
web_search_enabled = config.get('web-search-enabled', False)
|
||
timeout = config.get('timeout', 120)
|
||
|
||
full_answer = ''
|
||
chunk = None
|
||
|
||
async for chunk in self.weknora_client.agent_chat(
|
||
session_id=session_id,
|
||
query=plain_text,
|
||
user=user_tag,
|
||
agent_id=agent_id,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
web_search_enabled=web_search_enabled,
|
||
timeout=timeout,
|
||
):
|
||
self.ap.logger.debug('weknora-agent-chunk: ' + str(chunk))
|
||
|
||
response_type = chunk.get('response_type', '')
|
||
content = chunk.get('content', '')
|
||
|
||
if response_type == 'tool_call':
|
||
# 工具调用
|
||
tool_data = chunk.get('data', {})
|
||
tool_name = tool_data.get('tool_name', '')
|
||
if tool_name:
|
||
yield provider_message.Message(
|
||
role='assistant',
|
||
tool_calls=[
|
||
provider_message.ToolCall(
|
||
id=chunk.get('id', ''),
|
||
type='function',
|
||
function=provider_message.FunctionCall(
|
||
name=tool_name,
|
||
arguments=json.dumps(tool_data.get('arguments', {})),
|
||
),
|
||
)
|
||
],
|
||
)
|
||
|
||
elif response_type == 'answer':
|
||
if content:
|
||
full_answer += content
|
||
|
||
elif response_type == 'error':
|
||
raise errors.WeKnoraAPIError(f'WeKnora 服务错误: {content}')
|
||
|
||
if chunk is None:
|
||
raise errors.WeKnoraAPIError('WeKnora API 没有返回任何响应,请检查网络连接和API配置')
|
||
|
||
if full_answer:
|
||
yield provider_message.Message(
|
||
role='assistant',
|
||
content=full_answer,
|
||
)
|
||
|
||
async def _chat_messages(
|
||
self, query: pipeline_query.Query
|
||
) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||
"""调用知识库 RAG 问答(非流式聚合输出)"""
|
||
session_id = await self._ensure_session(query)
|
||
plain_text = await self._extract_plain_text(query)
|
||
user_tag = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||
|
||
config = self.pipeline_config['ai']['weknora-api']
|
||
agent_id = config.get('agent-id', 'builtin-quick-answer')
|
||
knowledge_base_ids = config.get('knowledge-base-ids', [])
|
||
timeout = config.get('timeout', 120)
|
||
|
||
full_answer = ''
|
||
chunk = None
|
||
|
||
async for chunk in self.weknora_client.knowledge_chat(
|
||
session_id=session_id,
|
||
query=plain_text,
|
||
user=user_tag,
|
||
agent_id=agent_id,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
timeout=timeout,
|
||
):
|
||
self.ap.logger.debug('weknora-chat-chunk: ' + str(chunk))
|
||
|
||
response_type = chunk.get('response_type', '')
|
||
content = chunk.get('content', '')
|
||
|
||
if response_type == 'answer':
|
||
if content:
|
||
full_answer += content
|
||
|
||
elif response_type == 'error':
|
||
raise errors.WeKnoraAPIError(f'WeKnora 服务错误: {content}')
|
||
|
||
if chunk is None:
|
||
raise errors.WeKnoraAPIError('WeKnora API 没有返回任何响应,请检查网络连接和API配置')
|
||
|
||
if full_answer:
|
||
yield provider_message.Message(
|
||
role='assistant',
|
||
content=full_answer,
|
||
)
|
||
|
||
async def _agent_chat_messages_chunk(
|
||
self, query: pipeline_query.Query
|
||
) -> typing.AsyncGenerator[provider_message.MessageChunk, None]:
|
||
"""调用 Agent 智能对话(流式输出)"""
|
||
session_id = await self._ensure_session(query)
|
||
plain_text = await self._extract_plain_text(query)
|
||
user_tag = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||
|
||
config = self.pipeline_config['ai']['weknora-api']
|
||
agent_id = config.get('agent-id', 'builtin-smart-reasoning')
|
||
knowledge_base_ids = config.get('knowledge-base-ids', [])
|
||
web_search_enabled = config.get('web-search-enabled', False)
|
||
timeout = config.get('timeout', 120)
|
||
|
||
pending_answer = ''
|
||
message_idx = 0
|
||
is_final = False
|
||
chunk = None
|
||
|
||
async for chunk in self.weknora_client.agent_chat(
|
||
session_id=session_id,
|
||
query=plain_text,
|
||
user=user_tag,
|
||
agent_id=agent_id,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
web_search_enabled=web_search_enabled,
|
||
timeout=timeout,
|
||
):
|
||
self.ap.logger.debug('weknora-agent-chunk: ' + str(chunk))
|
||
|
||
response_type = chunk.get('response_type', '')
|
||
content = chunk.get('content', '')
|
||
done = chunk.get('done', False)
|
||
|
||
if response_type == 'tool_call':
|
||
tool_data = chunk.get('data', {})
|
||
tool_name = tool_data.get('tool_name', '')
|
||
if tool_name:
|
||
message_idx += 1
|
||
yield provider_message.MessageChunk(
|
||
role='assistant',
|
||
tool_calls=[
|
||
provider_message.ToolCall(
|
||
id=chunk.get('id', ''),
|
||
type='function',
|
||
function=provider_message.FunctionCall(
|
||
name=tool_name,
|
||
arguments=json.dumps(tool_data.get('arguments', {})),
|
||
),
|
||
)
|
||
],
|
||
)
|
||
|
||
elif response_type == 'answer':
|
||
message_idx += 1
|
||
if content:
|
||
pending_answer += content
|
||
|
||
if done:
|
||
is_final = True
|
||
|
||
# 每 8 个 chunk 输出一次,或最终输出
|
||
if message_idx % 8 == 0 or is_final:
|
||
yield provider_message.MessageChunk(
|
||
role='assistant',
|
||
content=pending_answer,
|
||
is_final=is_final,
|
||
)
|
||
|
||
elif response_type == 'error':
|
||
raise errors.WeKnoraAPIError(f'WeKnora 服务错误: {content}')
|
||
|
||
if chunk is None:
|
||
raise errors.WeKnoraAPIError('WeKnora API 没有返回任何响应,请检查网络连接和API配置')
|
||
|
||
# 确保最终消息已发出
|
||
if not is_final and pending_answer:
|
||
yield provider_message.MessageChunk(
|
||
role='assistant',
|
||
content=pending_answer,
|
||
is_final=True,
|
||
)
|
||
|
||
async def _chat_messages_chunk(
|
||
self, query: pipeline_query.Query
|
||
) -> typing.AsyncGenerator[provider_message.MessageChunk, None]:
|
||
"""调用知识库 RAG 问答(流式输出)"""
|
||
session_id = await self._ensure_session(query)
|
||
plain_text = await self._extract_plain_text(query)
|
||
user_tag = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||
|
||
config = self.pipeline_config['ai']['weknora-api']
|
||
agent_id = config.get('agent-id', 'builtin-quick-answer')
|
||
knowledge_base_ids = config.get('knowledge-base-ids', [])
|
||
timeout = config.get('timeout', 120)
|
||
|
||
pending_answer = ''
|
||
message_idx = 0
|
||
is_final = False
|
||
chunk = None
|
||
|
||
async for chunk in self.weknora_client.knowledge_chat(
|
||
session_id=session_id,
|
||
query=plain_text,
|
||
user=user_tag,
|
||
agent_id=agent_id,
|
||
knowledge_base_ids=knowledge_base_ids,
|
||
timeout=timeout,
|
||
):
|
||
self.ap.logger.debug('weknora-chat-chunk: ' + str(chunk))
|
||
|
||
response_type = chunk.get('response_type', '')
|
||
content = chunk.get('content', '')
|
||
done = chunk.get('done', False)
|
||
|
||
if response_type == 'answer':
|
||
message_idx += 1
|
||
if content:
|
||
pending_answer += content
|
||
|
||
if done:
|
||
is_final = True
|
||
|
||
if message_idx % 8 == 0 or is_final:
|
||
yield provider_message.MessageChunk(
|
||
role='assistant',
|
||
content=pending_answer,
|
||
is_final=is_final,
|
||
)
|
||
|
||
elif response_type == 'error':
|
||
raise errors.WeKnoraAPIError(f'WeKnora 服务错误: {content}')
|
||
|
||
if chunk is None:
|
||
raise errors.WeKnoraAPIError('WeKnora API 没有返回任何响应,请检查网络连接和API配置')
|
||
|
||
if not is_final and pending_answer:
|
||
yield provider_message.MessageChunk(
|
||
role='assistant',
|
||
content=pending_answer,
|
||
is_final=True,
|
||
)
|
||
|
||
async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||
"""运行请求"""
|
||
app_type = self.pipeline_config['ai']['weknora-api']['app-type']
|
||
|
||
if await query.adapter.is_stream_output_supported():
|
||
msg_idx = 0
|
||
if app_type == 'agent':
|
||
async for msg in self._agent_chat_messages_chunk(query):
|
||
msg_idx += 1
|
||
msg.msg_sequence = msg_idx
|
||
yield msg
|
||
elif app_type == 'chat':
|
||
async for msg in self._chat_messages_chunk(query):
|
||
msg_idx += 1
|
||
msg.msg_sequence = msg_idx
|
||
yield msg
|
||
else:
|
||
raise errors.WeKnoraAPIError(f'不支持的 WeKnora 应用类型: {app_type}')
|
||
else:
|
||
if app_type == 'agent':
|
||
async for msg in self._agent_chat_messages(query):
|
||
yield msg
|
||
elif app_type == 'chat':
|
||
async for msg in self._chat_messages(query):
|
||
yield msg
|
||
else:
|
||
raise errors.WeKnoraAPIError(f'不支持的 WeKnora 应用类型: {app_type}')
|