Compare commits
3 Commits
feat/file-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9be8cf69f | ||
|
|
31d53edb9d | ||
|
|
2ba0460f19 |
@@ -1 +1 @@
|
||||
__version__ = "4.7.3"
|
||||
__version__ = "4.7.4"
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.7.3"
|
||||
VERSION = "4.7.4"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -76,6 +76,11 @@ DEFAULT_CONFIG = {
|
||||
"reachability_check": False,
|
||||
"max_agent_step": 30,
|
||||
"tool_call_timeout": 60,
|
||||
"file_extract": {
|
||||
"enable": False,
|
||||
"provider": "moonshotai",
|
||||
"moonshotai_api_key": "",
|
||||
},
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": False,
|
||||
@@ -2069,6 +2074,20 @@ CONFIG_METADATA_2 = {
|
||||
"tool_call_timeout": {
|
||||
"type": "int",
|
||||
},
|
||||
"file_extract": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"enable": {
|
||||
"type": "bool",
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
},
|
||||
"moonshotai_api_key": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
@@ -2403,6 +2422,36 @@ CONFIG_METADATA_3 = {
|
||||
"provider_settings.enable": True,
|
||||
},
|
||||
},
|
||||
# "file_extract": {
|
||||
# "description": "文档解析能力 [beta]",
|
||||
# "type": "object",
|
||||
# "items": {
|
||||
# "provider_settings.file_extract.enable": {
|
||||
# "description": "启用文档解析能力",
|
||||
# "type": "bool",
|
||||
# },
|
||||
# "provider_settings.file_extract.provider": {
|
||||
# "description": "文档解析提供商",
|
||||
# "type": "string",
|
||||
# "options": ["moonshotai"],
|
||||
# "condition": {
|
||||
# "provider_settings.file_extract.enable": True,
|
||||
# },
|
||||
# },
|
||||
# "provider_settings.file_extract.moonshotai_api_key": {
|
||||
# "description": "Moonshot AI API Key",
|
||||
# "type": "string",
|
||||
# "condition": {
|
||||
# "provider_settings.file_extract.provider": "moonshotai",
|
||||
# "provider_settings.file_extract.enable": True,
|
||||
# },
|
||||
# },
|
||||
# },
|
||||
# "condition": {
|
||||
# "provider_settings.agent_runner_type": "local",
|
||||
# "provider_settings.enable": True,
|
||||
# },
|
||||
# },
|
||||
"others": {
|
||||
"description": "其他配置",
|
||||
"type": "object",
|
||||
|
||||
@@ -722,7 +722,12 @@ class File(BaseMessageComponent):
|
||||
"""下载文件"""
|
||||
download_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
file_path = os.path.join(download_dir, f"{uuid.uuid4().hex}")
|
||||
if self.name:
|
||||
name, ext = os.path.splitext(self.name)
|
||||
filename = f"{name}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
else:
|
||||
filename = f"{uuid.uuid4().hex}"
|
||||
file_path = os.path.join(download_dir, filename)
|
||||
await download_file(self.url, file_path)
|
||||
self.file_ = os.path.abspath(file_path)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from astrbot.core import logger
|
||||
from astrbot.core.agent.tool import ToolSet
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
from astrbot.core.conversation_mgr import Conversation
|
||||
from astrbot.core.message.components import Image
|
||||
from astrbot.core.message.components import File, Image, Reply
|
||||
from astrbot.core.message.message_event_result import (
|
||||
MessageChain,
|
||||
MessageEventResult,
|
||||
@@ -22,6 +22,7 @@ from astrbot.core.provider.entities import (
|
||||
ProviderRequest,
|
||||
)
|
||||
from astrbot.core.star.star_handler import EventType, star_map
|
||||
from astrbot.core.utils.file_extract import extract_file_moonshotai
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
from astrbot.core.utils.session_lock import session_lock_manager
|
||||
|
||||
@@ -56,6 +57,13 @@ class InternalAgentSubStage(Stage):
|
||||
self.show_reasoning = settings.get("display_reasoning_text", False)
|
||||
self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
|
||||
|
||||
file_extract_conf: dict = settings.get("file_extract", {})
|
||||
self.file_extract_enabled: bool = file_extract_conf.get("enable", False)
|
||||
self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai")
|
||||
self.file_extract_msh_api_key: str = file_extract_conf.get(
|
||||
"moonshotai_api_key", ""
|
||||
)
|
||||
|
||||
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
def _select_provider(self, event: AstrMessageEvent):
|
||||
@@ -114,6 +122,50 @@ class InternalAgentSubStage(Stage):
|
||||
req.func_tool = ToolSet()
|
||||
req.func_tool.add_tool(KNOWLEDGE_BASE_QUERY_TOOL)
|
||||
|
||||
async def _apply_file_extract(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
req: ProviderRequest,
|
||||
):
|
||||
"""Apply file extract to the provider request"""
|
||||
file_paths = []
|
||||
file_names = []
|
||||
for comp in event.message_obj.message:
|
||||
if isinstance(comp, File):
|
||||
file_paths.append(await comp.get_file())
|
||||
file_names.append(comp.name)
|
||||
elif isinstance(comp, Reply) and comp.chain:
|
||||
for reply_comp in comp.chain:
|
||||
if isinstance(reply_comp, File):
|
||||
file_paths.append(await reply_comp.get_file())
|
||||
file_names.append(reply_comp.name)
|
||||
if not file_paths:
|
||||
return
|
||||
if not req.prompt:
|
||||
req.prompt = "总结一下文件里面讲了什么?"
|
||||
if self.file_extract_prov == "moonshotai":
|
||||
if not self.file_extract_msh_api_key:
|
||||
logger.error("Moonshot AI API key for file extract is not set")
|
||||
return
|
||||
file_contents = await asyncio.gather(
|
||||
*[
|
||||
extract_file_moonshotai(file_path, self.file_extract_msh_api_key)
|
||||
for file_path in file_paths
|
||||
]
|
||||
)
|
||||
else:
|
||||
logger.error(f"Unsupported file extract provider: {self.file_extract_prov}")
|
||||
return
|
||||
|
||||
# add file extract results to contexts
|
||||
for file_content, file_name in zip(file_contents, file_names):
|
||||
req.contexts.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"File Extract Results of user uploaded files:\n{file_content}\nFile Name: {file_name or 'Unknown'}",
|
||||
},
|
||||
)
|
||||
|
||||
def _truncate_contexts(
|
||||
self,
|
||||
contexts: list[dict],
|
||||
@@ -346,6 +398,17 @@ class InternalAgentSubStage(Stage):
|
||||
|
||||
event.set_extra("provider_request", req)
|
||||
|
||||
# fix contexts json str
|
||||
if isinstance(req.contexts, str):
|
||||
req.contexts = json.loads(req.contexts)
|
||||
|
||||
# apply file extract
|
||||
if self.file_extract_enabled:
|
||||
try:
|
||||
await self._apply_file_extract(event, req)
|
||||
except Exception as e:
|
||||
logger.error(f"Error occurred while applying file extract: {e}")
|
||||
|
||||
if not req.prompt and not req.image_urls:
|
||||
return
|
||||
|
||||
@@ -356,10 +419,6 @@ class InternalAgentSubStage(Stage):
|
||||
# apply knowledge base feature
|
||||
await self._apply_kb(event, req)
|
||||
|
||||
# fix contexts json str
|
||||
if isinstance(req.contexts, str):
|
||||
req.contexts = json.loads(req.contexts)
|
||||
|
||||
# truncate contexts to fit max length
|
||||
if req.contexts:
|
||||
req.contexts = self._truncate_contexts(req.contexts)
|
||||
|
||||
@@ -381,7 +381,9 @@ class TelegramPlatformAdapter(Platform):
|
||||
f"Telegram document file_path is None, cannot save the file {file_name}.",
|
||||
)
|
||||
else:
|
||||
message.message.append(Comp.File(file=file_path, name=file_name))
|
||||
message.message.append(
|
||||
Comp.File(file=file_path, name=file_name, url=file_path)
|
||||
)
|
||||
|
||||
elif update.message.video:
|
||||
file = await update.message.video.get_file()
|
||||
|
||||
@@ -45,13 +45,13 @@ class AbstractProvider(abc.ABC):
|
||||
)
|
||||
return meta
|
||||
|
||||
async def test(self) -> bool:
|
||||
async def test(self):
|
||||
"""test the provider is a
|
||||
|
||||
Returns:
|
||||
bool: the provider is available
|
||||
raises:
|
||||
Exception: if the provider is not available
|
||||
"""
|
||||
return True
|
||||
...
|
||||
|
||||
|
||||
class Provider(AbstractProvider):
|
||||
@@ -175,15 +175,11 @@ class Provider(AbstractProvider):
|
||||
|
||||
return dicts
|
||||
|
||||
async def test(self, timeout: float = 45.0) -> bool:
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
self.text_chat(prompt="REPLY `PONG` ONLY"),
|
||||
timeout=timeout,
|
||||
)
|
||||
return response is not None
|
||||
except Exception:
|
||||
return False
|
||||
async def test(self, timeout: float = 45.0):
|
||||
await asyncio.wait_for(
|
||||
self.text_chat(prompt="REPLY `PONG` ONLY"),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
class STTProvider(AbstractProvider):
|
||||
@@ -197,19 +193,13 @@ class STTProvider(AbstractProvider):
|
||||
"""获取音频的文本"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def test(self) -> bool:
|
||||
try:
|
||||
sample_audio_path = os.path.join(
|
||||
get_astrbot_path(),
|
||||
"samples",
|
||||
"stt_health_check.wav",
|
||||
)
|
||||
if not os.path.exists(sample_audio_path):
|
||||
return False
|
||||
text_result = await self.get_text(sample_audio_path)
|
||||
return isinstance(text_result, str) and bool(text_result)
|
||||
except Exception:
|
||||
return False
|
||||
async def test(self):
|
||||
sample_audio_path = os.path.join(
|
||||
get_astrbot_path(),
|
||||
"samples",
|
||||
"stt_health_check.wav",
|
||||
)
|
||||
await self.get_text(sample_audio_path)
|
||||
|
||||
|
||||
class TTSProvider(AbstractProvider):
|
||||
@@ -223,12 +213,8 @@ class TTSProvider(AbstractProvider):
|
||||
"""获取文本的音频,返回音频文件路径"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def test(self) -> bool:
|
||||
try:
|
||||
audio_result = await self.get_audio("hi")
|
||||
return isinstance(audio_result, str) and bool(audio_result)
|
||||
except Exception:
|
||||
return False
|
||||
async def test(self):
|
||||
await self.get_audio("hi")
|
||||
|
||||
|
||||
class EmbeddingProvider(AbstractProvider):
|
||||
@@ -252,14 +238,8 @@ class EmbeddingProvider(AbstractProvider):
|
||||
"""获取向量的维度"""
|
||||
...
|
||||
|
||||
async def test(self) -> bool:
|
||||
try:
|
||||
embedding_result = await self.get_embedding("health_check")
|
||||
return isinstance(embedding_result, list) and (
|
||||
not embedding_result or isinstance(embedding_result[0], float)
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
async def test(self):
|
||||
await self.get_embedding("astrbot")
|
||||
|
||||
async def get_embeddings_batch(
|
||||
self,
|
||||
@@ -345,9 +325,7 @@ class RerankProvider(AbstractProvider):
|
||||
"""获取查询和文档的重排序分数"""
|
||||
...
|
||||
|
||||
async def test(self) -> bool:
|
||||
try:
|
||||
await self.rerank("Apple", documents=["apple", "banana"])
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
async def test(self):
|
||||
result = await self.rerank("Apple", documents=["apple", "banana"])
|
||||
if not result:
|
||||
raise Exception("Rerank provider test failed, no results returned")
|
||||
|
||||
23
astrbot/core/utils/file_extract.py
Normal file
23
astrbot/core/utils/file_extract.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
|
||||
async def extract_file_moonshotai(file_path: str, api_key: str) -> str:
|
||||
"""Extract text from a file using Moonshot AI API"""
|
||||
"""
|
||||
Args:
|
||||
file_path: The path to the file to extract text from
|
||||
api_key: The API key to use to extract text from the file
|
||||
Returns:
|
||||
The text extracted from the file
|
||||
"""
|
||||
client = AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url="https://api.moonshot.cn/v1",
|
||||
)
|
||||
file_object = await client.files.create(
|
||||
file=Path(file_path),
|
||||
purpose="file-extract", # type: ignore
|
||||
)
|
||||
return (await client.files.content(file_id=file_object.id)).text
|
||||
@@ -354,17 +354,11 @@ class ConfigRoute(Route):
|
||||
)
|
||||
|
||||
try:
|
||||
result = await provider.test()
|
||||
if result:
|
||||
status_info["status"] = "available"
|
||||
logger.info(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) is available.",
|
||||
)
|
||||
else:
|
||||
status_info["error"] = "Provider test returned False."
|
||||
logger.warning(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) test returned False.",
|
||||
)
|
||||
await provider.test()
|
||||
status_info["status"] = "available"
|
||||
logger.info(
|
||||
f"Provider {status_info['name']} (ID: {status_info['id']}) is available.",
|
||||
)
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
status_info["error"] = error_message
|
||||
|
||||
7
changelogs/v4.7.4.md
Normal file
7
changelogs/v4.7.4.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:assistant message 中 tool_call 存在但 content 不存在时,导致验证错误的问题 ([#3862](https://github.com/AstrBotDevs/AstrBot/issues/3862))
|
||||
2. 修复:fix: aiocqhttp 适配器 NapCat 文件名获取为空 ([#3853](https://github.com/AstrBotDevs/AstrBot/issues/3853))
|
||||
3. 新增:升级所有插件按钮
|
||||
4. 新增:/provider 指令支持同时测试提供商可用性
|
||||
5. 优化:主动回复的 prompt
|
||||
@@ -109,6 +109,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_extract": {
|
||||
"description": "File Extract",
|
||||
"provider_settings": {
|
||||
"file_extract": {
|
||||
"enable": {
|
||||
"description": "Enable File Extract"
|
||||
},
|
||||
"provider": {
|
||||
"description": "File Extract Provider"
|
||||
},
|
||||
"moonshotai_api_key": {
|
||||
"description": "Moonshot AI API Key"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "Other Settings",
|
||||
"provider_settings": {
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
},
|
||||
"agent_runner_type": {
|
||||
"description": "执行器",
|
||||
"labels": ["内置 Agent", "Dify", "Coze", "阿里云百炼应用"]
|
||||
"labels": [
|
||||
"内置 Agent",
|
||||
"Dify",
|
||||
"Coze",
|
||||
"阿里云百炼应用"
|
||||
]
|
||||
},
|
||||
"coze_agent_runner_provider_id": {
|
||||
"description": "Coze Agent 执行器提供商 ID"
|
||||
@@ -109,6 +114,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_extract": {
|
||||
"description": "文档解析能力",
|
||||
"provider_settings": {
|
||||
"file_extract": {
|
||||
"enable": {
|
||||
"description": "启用文档解析能力"
|
||||
},
|
||||
"provider": {
|
||||
"description": "文档解析提供商"
|
||||
},
|
||||
"moonshotai_api_key": {
|
||||
"description": "Moonshot AI API Key"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"others": {
|
||||
"description": "其他配置",
|
||||
"provider_settings": {
|
||||
@@ -142,7 +163,10 @@
|
||||
"unsupported_streaming_strategy": {
|
||||
"description": "不支持流式回复的平台",
|
||||
"hint": "选择在不支持流式回复的平台上的处理方式。实时分段回复会在系统接收流式响应检测到诸如标点符号等分段点时,立即发送当前已接收的内容",
|
||||
"labels": ["实时分段回复", "关闭流式回复"]
|
||||
"labels": [
|
||||
"实时分段回复",
|
||||
"关闭流式回复"
|
||||
]
|
||||
},
|
||||
"max_context_length": {
|
||||
"description": "最多携带对话轮数",
|
||||
@@ -457,4 +481,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,25 +34,11 @@ class ProviderCommands:
|
||||
provider_capability_type = meta.provider_type
|
||||
|
||||
try:
|
||||
result = await provider.test()
|
||||
if result:
|
||||
return True, None, None
|
||||
await provider.test()
|
||||
return True, None, None
|
||||
except Exception as e:
|
||||
err_code = "TEST_FAILED"
|
||||
err_reason = "Provider test returned False"
|
||||
self._log_reachability_failure(
|
||||
provider, provider_capability_type, err_code, err_reason
|
||||
)
|
||||
return False, err_code, err_reason
|
||||
except Exception as exc:
|
||||
err_code = (
|
||||
getattr(exc, "status_code", None)
|
||||
or getattr(exc, "code", None)
|
||||
or getattr(exc, "error_code", None)
|
||||
)
|
||||
err_reason = str(exc)
|
||||
if not err_code:
|
||||
err_code = exc.__class__.__name__
|
||||
|
||||
err_reason = str(e)
|
||||
self._log_reachability_failure(
|
||||
provider, provider_capability_type, err_code, err_reason
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.7.3"
|
||||
version = "4.7.4"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user