Compare commits
1 Commits
v4.3.3
...
refactor/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54340cca18 |
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.3.3"
|
||||
VERSION = "4.3.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -1056,7 +1056,6 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": "20",
|
||||
},
|
||||
"阿里云百炼 TTS(API)": {
|
||||
"hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition",
|
||||
"id": "dashscope_tts",
|
||||
"provider": "dashscope",
|
||||
"type": "dashscope_tts",
|
||||
@@ -1436,7 +1435,11 @@ CONFIG_METADATA_2 = {
|
||||
"description": "服务订阅密钥",
|
||||
"hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)",
|
||||
},
|
||||
"dashscope_tts_voice": {"description": "音色", "type": "string"},
|
||||
"dashscope_tts_voice": {
|
||||
"description": "语音合成模型",
|
||||
"type": "string",
|
||||
"hint": "阿里云百炼语音合成模型名称。具体可参考 https://help.aliyun.com/zh/model-studio/developer-reference/cosyvoice-python-api 等内容",
|
||||
},
|
||||
"gm_resp_image_modal": {
|
||||
"description": "启用图片模态",
|
||||
"type": "bool",
|
||||
|
||||
@@ -68,8 +68,7 @@ class Provider(AbstractProvider):
|
||||
|
||||
def get_keys(self) -> List[str]:
|
||||
"""获得提供商 Key"""
|
||||
keys = self.provider_config.get("key", [""])
|
||||
return keys or [""]
|
||||
return self.provider_config.get("key", [])
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_key(self, key: str):
|
||||
|
||||
@@ -33,7 +33,7 @@ class ProviderAnthropic(Provider):
|
||||
)
|
||||
|
||||
self.chosen_api_key: str = ""
|
||||
self.api_keys: List = super().get_keys()
|
||||
self.api_keys: List = provider_config.get("key", [])
|
||||
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else ""
|
||||
self.base_url = provider_config.get("api_base", "https://api.anthropic.com")
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
@@ -70,13 +70,9 @@ class ProviderAnthropic(Provider):
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": tool_call["function"]["name"],
|
||||
"input": (
|
||||
json.loads(tool_call["function"]["arguments"])
|
||||
if isinstance(
|
||||
tool_call["function"]["arguments"], str
|
||||
)
|
||||
else tool_call["function"]["arguments"]
|
||||
),
|
||||
"input": json.loads(tool_call["function"]["arguments"])
|
||||
if isinstance(tool_call["function"]["arguments"], str)
|
||||
else tool_call["function"]["arguments"],
|
||||
"id": tool_call["id"],
|
||||
}
|
||||
)
|
||||
@@ -359,11 +355,9 @@ class ProviderAnthropic(Provider):
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": mime_type,
|
||||
"data": (
|
||||
image_data.split("base64,")[1]
|
||||
if "base64," in image_data
|
||||
else image_data
|
||||
),
|
||||
"data": image_data.split("base64,")[1]
|
||||
if "base64," in image_data
|
||||
else image_data,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional, Tuple
|
||||
import aiohttp
|
||||
import dashscope
|
||||
from dashscope.audio.tts_v2 import AudioFormat, SpeechSynthesizer
|
||||
|
||||
try:
|
||||
from dashscope.aigc.multimodal_conversation import MultiModalConversation
|
||||
except (
|
||||
ImportError
|
||||
): # pragma: no cover - older dashscope versions without Qwen TTS support
|
||||
MultiModalConversation = None
|
||||
|
||||
from ..entities import ProviderType
|
||||
import uuid
|
||||
import asyncio
|
||||
from dashscope.audio.tts_v2 import *
|
||||
from ..provider import TTSProvider
|
||||
from ..entities import ProviderType
|
||||
from ..register import register_provider_adapter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
@@ -38,112 +26,16 @@ class ProviderDashscopeTTSAPI(TTSProvider):
|
||||
dashscope.api_key = self.chosen_api_key
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
model = self.get_model()
|
||||
if not model:
|
||||
raise RuntimeError("Dashscope TTS model is not configured.")
|
||||
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
if self._is_qwen_tts_model(model):
|
||||
audio_bytes, ext = await self._synthesize_with_qwen_tts(model, text)
|
||||
else:
|
||||
audio_bytes, ext = await self._synthesize_with_cosyvoice(model, text)
|
||||
|
||||
if not audio_bytes:
|
||||
raise RuntimeError(
|
||||
"Audio synthesis failed, returned empty content. The model may not be supported or the service is unavailable."
|
||||
)
|
||||
|
||||
path = os.path.join(temp_dir, f"dashscope_tts_{uuid.uuid4()}{ext}")
|
||||
with open(path, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
return path
|
||||
|
||||
def _call_qwen_tts(self, model: str, text: str):
|
||||
if MultiModalConversation is None:
|
||||
raise RuntimeError(
|
||||
"dashscope SDK missing MultiModalConversation. Please upgrade the dashscope package to use Qwen TTS models."
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"model": model,
|
||||
"text": text,
|
||||
"api_key": self.chosen_api_key,
|
||||
"voice": self.voice or "Cherry",
|
||||
}
|
||||
if not self.voice:
|
||||
logging.warning(
|
||||
"No voice specified for Qwen TTS model, using default 'Cherry'."
|
||||
)
|
||||
return MultiModalConversation.call(**kwargs)
|
||||
|
||||
async def _synthesize_with_qwen_tts(
|
||||
self, model: str, text: str
|
||||
) -> Tuple[Optional[bytes], str]:
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, self._call_qwen_tts, model, text)
|
||||
audio_bytes = await self._extract_audio_from_response(response)
|
||||
if not audio_bytes:
|
||||
raise RuntimeError(
|
||||
f"Audio synthesis failed for model '{model}'. {response}"
|
||||
)
|
||||
ext = ".wav"
|
||||
return audio_bytes, ext
|
||||
|
||||
async def _extract_audio_from_response(self, response) -> Optional[bytes]:
|
||||
output = getattr(response, "output", None)
|
||||
audio_obj = getattr(output, "audio", None) if output is not None else None
|
||||
if not audio_obj:
|
||||
return None
|
||||
|
||||
data_b64 = getattr(audio_obj, "data", None)
|
||||
if data_b64:
|
||||
try:
|
||||
return base64.b64decode(data_b64)
|
||||
except (ValueError, TypeError):
|
||||
logging.error("Failed to decode base64 audio data.")
|
||||
return None
|
||||
|
||||
url = getattr(audio_obj, "url", None)
|
||||
if url:
|
||||
return await self._download_audio_from_url(url)
|
||||
return None
|
||||
|
||||
async def _download_audio_from_url(self, url: str) -> Optional[bytes]:
|
||||
if not url:
|
||||
return None
|
||||
timeout = max(self.timeout_ms / 1000, 1) if self.timeout_ms else 20
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url, timeout=aiohttp.ClientTimeout(total=timeout)
|
||||
) as response:
|
||||
return await response.read()
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:
|
||||
logging.error(f"Failed to download audio from URL {url}: {e}")
|
||||
return None
|
||||
|
||||
async def _synthesize_with_cosyvoice(
|
||||
self, model: str, text: str
|
||||
) -> Tuple[Optional[bytes], str]:
|
||||
synthesizer = SpeechSynthesizer(
|
||||
model=model,
|
||||
path = os.path.join(temp_dir, f"dashscope_tts_{uuid.uuid4()}.wav")
|
||||
self.synthesizer = SpeechSynthesizer(
|
||||
model=self.get_model(),
|
||||
voice=self.voice,
|
||||
format=AudioFormat.WAV_24000HZ_MONO_16BIT,
|
||||
)
|
||||
loop = asyncio.get_event_loop()
|
||||
audio_bytes = await loop.run_in_executor(
|
||||
None, synthesizer.call, text, self.timeout_ms
|
||||
audio = await asyncio.get_event_loop().run_in_executor(
|
||||
None, self.synthesizer.call, text, self.timeout_ms
|
||||
)
|
||||
if not audio_bytes:
|
||||
resp = synthesizer.get_response()
|
||||
if resp and isinstance(resp, dict):
|
||||
raise RuntimeError(
|
||||
f"Audio synthesis failed for model '{model}'. {resp}".strip()
|
||||
)
|
||||
return audio_bytes, ".wav"
|
||||
|
||||
def _is_qwen_tts_model(self, model: str) -> bool:
|
||||
model_lower = model.lower()
|
||||
return "tts" in model_lower and model_lower.startswith("qwen")
|
||||
with open(path, "wb") as f:
|
||||
f.write(audio)
|
||||
return path
|
||||
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
from typing import Optional, List
|
||||
from typing import Optional
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from google import genai
|
||||
@@ -60,7 +60,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
provider_settings,
|
||||
default_persona,
|
||||
)
|
||||
self.api_keys: List = super().get_keys()
|
||||
self.api_keys: list = provider_config.get("key", [])
|
||||
self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else ""
|
||||
self.timeout: int = int(provider_config.get("timeout", 180))
|
||||
|
||||
@@ -218,21 +218,19 @@ class ProviderGoogleGenAI(Provider):
|
||||
response_modalities=modalities,
|
||||
tools=tool_list,
|
||||
safety_settings=self.safety_settings if self.safety_settings else None,
|
||||
thinking_config=(
|
||||
types.ThinkingConfig(
|
||||
thinking_budget=min(
|
||||
int(
|
||||
self.provider_config.get("gm_thinking_config", {}).get(
|
||||
"budget", 0
|
||||
)
|
||||
),
|
||||
24576,
|
||||
thinking_config=types.ThinkingConfig(
|
||||
thinking_budget=min(
|
||||
int(
|
||||
self.provider_config.get("gm_thinking_config", {}).get(
|
||||
"budget", 0
|
||||
)
|
||||
),
|
||||
)
|
||||
if "gemini-2.5-flash" in self.get_model()
|
||||
and hasattr(types.ThinkingConfig, "thinking_budget")
|
||||
else None
|
||||
),
|
||||
24576,
|
||||
),
|
||||
)
|
||||
if "gemini-2.5-flash" in self.get_model()
|
||||
and hasattr(types.ThinkingConfig, "thinking_budget")
|
||||
else None,
|
||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(
|
||||
disable=True
|
||||
),
|
||||
@@ -276,11 +274,9 @@ class ProviderGoogleGenAI(Provider):
|
||||
if role == "user":
|
||||
if isinstance(content, list):
|
||||
parts = [
|
||||
(
|
||||
types.Part.from_text(text=item["text"] or " ")
|
||||
if item["type"] == "text"
|
||||
else process_image_url(item["image_url"])
|
||||
)
|
||||
types.Part.from_text(text=item["text"] or " ")
|
||||
if item["type"] == "text"
|
||||
else process_image_url(item["image_url"])
|
||||
for item in content
|
||||
]
|
||||
else:
|
||||
|
||||
@@ -38,7 +38,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
default_persona,
|
||||
)
|
||||
self.chosen_api_key = None
|
||||
self.api_keys: List = super().get_keys()
|
||||
self.api_keys: List = provider_config.get("key", [])
|
||||
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None
|
||||
self.timeout = provider_config.get("timeout", 120)
|
||||
if isinstance(self.timeout, str):
|
||||
|
||||
@@ -65,12 +65,12 @@ class SessionManagementRoute(Route):
|
||||
persona_name = data["persona_name"]
|
||||
|
||||
# 处理 persona 显示
|
||||
if persona_name is None:
|
||||
if conv_persona_id is None:
|
||||
if default_persona := persona_mgr.selected_default_persona_v3:
|
||||
persona_name = default_persona["name"]
|
||||
else:
|
||||
persona_name = "[%None]"
|
||||
if conv_persona_id == "[%None]":
|
||||
persona_name = "无人格"
|
||||
else:
|
||||
default_persona = persona_mgr.selected_default_persona_v3
|
||||
if default_persona:
|
||||
persona_name = default_persona["name"]
|
||||
|
||||
session_info = {
|
||||
"session_id": session_id,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# What's Changed
|
||||
|
||||
1. fix: 修复了代码执行器插件不能正确获得发送来文件的问题 ([#2970](https://github.com/Soulter/AstrBot/issues/2970))
|
||||
2. fix: 修改的 DeepSeek 默认 modalities,避免默认勾选图像导致的报错。 ([#2963](https://github.com/Soulter/AstrBot/issues/2963))
|
||||
3. fix: 事件钩子终止事件传播后不继续执行 ([#2989](https://github.com/Soulter/AstrBot/issues/2989))
|
||||
4. fix: 启动了 TTS 但未配置 TTS 模型时,At 和 Reply 发送人无效
|
||||
5. fix: 修复 session-management 中人格错误的显示为默认人格的问题 ([#3000](https://github.com/Soulter/AstrBot/issues/3000))
|
||||
6. fix: 修复了删除对话时,聊天增强中的记录未被清除,导致新对话中仍然出现之前的聊天记录。 ([#3002](https://github.com/Soulter/AstrBot/issues/3002))
|
||||
7. fix: 修复阿里云百炼平台 TTS 下接入 CosyVoice V2, Qwen TTS 生成报错的问题 ([#2964](https://github.com/Soulter/AstrBot/issues/2964))
|
||||
8. perf: 优化 SQLite 参数配置,对话和会话管理增加输入防抖机制 ([#2969](https://github.com/Soulter/AstrBot/issues/2969))
|
||||
9. feat: 在新对话中重用先前的对话人格设置 ([#3005](https://github.com/Soulter/AstrBot/issues/3005))
|
||||
10. feat: 从 WebUI 更新后清除浏览器缓存 ([#2958](https://github.com/Soulter/AstrBot/issues/2958))
|
||||
@@ -41,17 +41,6 @@ class ConversationCommands:
|
||||
self.context = context
|
||||
self.ltm = ltm
|
||||
|
||||
async def _get_current_persona_id(self, session_id):
|
||||
curr = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
session_id
|
||||
)
|
||||
if not curr:
|
||||
return None
|
||||
conv = await self.context.conversation_manager.get_conversation(
|
||||
session_id, curr
|
||||
)
|
||||
return conv.persona_id
|
||||
|
||||
def ltm_enabled(self, event: AstrMessageEvent):
|
||||
if not self.ltm:
|
||||
return False
|
||||
@@ -266,9 +255,8 @@ class ConversationCommands:
|
||||
)
|
||||
return
|
||||
|
||||
cpersona = await self._get_current_persona_id(message.unified_msg_origin)
|
||||
cid = await self.context.conversation_manager.new_conversation(
|
||||
message.unified_msg_origin, message.get_platform_id(), persona_id=cpersona
|
||||
message.unified_msg_origin, message.get_platform_id()
|
||||
)
|
||||
|
||||
# 长期记忆
|
||||
@@ -302,10 +290,8 @@ class ConversationCommands:
|
||||
session_id=sid,
|
||||
)
|
||||
)
|
||||
|
||||
cpersona = await self._get_current_persona_id(session)
|
||||
cid = await self.context.conversation_manager.new_conversation(
|
||||
session, message.get_platform_id(), persona_id=cpersona
|
||||
session, message.get_platform_id()
|
||||
)
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
@@ -448,9 +434,8 @@ class ConversationCommands:
|
||||
await self.context.conversation_manager.delete_conversation(
|
||||
message.unified_msg_origin, session_curr_cid
|
||||
)
|
||||
|
||||
ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
|
||||
if self.ltm and self.ltm_enabled(message):
|
||||
cnt = await self.ltm.remove_session(event=message)
|
||||
ret += f"\n聊天增强: 已清除 {cnt} 条聊天记录。"
|
||||
message.set_result(MessageEventResult().message(ret))
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
|
||||
)
|
||||
)
|
||||
|
||||
20
packages/astrbot/lab/elios/ensoul/emotion.py
Normal file
20
packages/astrbot/lab/elios/ensoul/emotion.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Emotion:
|
||||
"""描述了一个情绪状态"""
|
||||
|
||||
energy: float
|
||||
valence: float
|
||||
arousal: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmotionLog:
|
||||
"""描述了一条情绪维度变化的日志"""
|
||||
|
||||
timestamp: int
|
||||
field: str
|
||||
value: float
|
||||
reason: str = ""
|
||||
9
packages/astrbot/lab/elios/ensoul/soul.py
Normal file
9
packages/astrbot/lab/elios/ensoul/soul.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .emotion import Emotion
|
||||
|
||||
|
||||
@dataclass
|
||||
class Soul:
|
||||
emotion: Emotion
|
||||
emotion_logs: list[Emotion] | None = None
|
||||
7
packages/astrbot/lab/elios/event.py
Normal file
7
packages/astrbot/lab/elios/event.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
event_type: str
|
||||
content: dict
|
||||
122
packages/astrbot/lab/elios/event_handlers/astr/astr_impl.py
Normal file
122
packages/astrbot/lab/elios/event_handlers/astr/astr_impl.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from ...runner import EliosEventHandler
|
||||
from collections import defaultdict
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
from astrbot.api.all import Context
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from astrbot.api.provider import Provider
|
||||
from astrbot import logger
|
||||
|
||||
|
||||
class AstrImplEventHandler(EliosEventHandler):
|
||||
def __init__(self, ctx: Context) -> None:
|
||||
self.ctx = ctx
|
||||
self.session_chats = defaultdict(list)
|
||||
self.session_mentioned_arousal = defaultdict(float)
|
||||
|
||||
def cfg(self, event: AstrMessageEvent):
|
||||
cfg = self.ctx.get_config(umo=event.unified_msg_origin)
|
||||
|
||||
tiny_model_prov_id = cfg.get("tiny_model_provider_id")
|
||||
interest_points = cfg.get("interest_points", [])
|
||||
|
||||
try:
|
||||
max_cnt = int(cfg["provider_ltm_settings"]["group_message_max_cnt"])
|
||||
except BaseException as e:
|
||||
logger.error(e)
|
||||
max_cnt = 300
|
||||
image_caption = (
|
||||
True
|
||||
if cfg["provider_settings"]["default_image_caption_provider_id"]
|
||||
else False
|
||||
)
|
||||
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
|
||||
image_caption_provider_id = cfg["provider_settings"][
|
||||
"default_image_caption_provider_id"
|
||||
]
|
||||
active_reply = cfg["provider_ltm_settings"]["active_reply"]
|
||||
enable_active_reply = active_reply.get("enable", False)
|
||||
ar_method = active_reply["method"]
|
||||
ar_possibility = active_reply["possibility_reply"]
|
||||
ar_prompt = active_reply.get("prompt", "")
|
||||
ar_whitelist = active_reply.get("whitelist", [])
|
||||
ar_keywords = active_reply.get("keywords", [])
|
||||
ret = {
|
||||
"max_cnt": max_cnt,
|
||||
"image_caption": image_caption,
|
||||
"image_caption_prompt": image_caption_prompt,
|
||||
"image_caption_provider_id": image_caption_provider_id,
|
||||
"enable_active_reply": enable_active_reply,
|
||||
"ar_method": ar_method,
|
||||
"ar_possibility": ar_possibility,
|
||||
"ar_prompt": ar_prompt,
|
||||
"ar_whitelist": ar_whitelist,
|
||||
"ar_keywords": ar_keywords,
|
||||
"interest_points": interest_points,
|
||||
"tiny_model_prov_id": tiny_model_prov_id,
|
||||
}
|
||||
return ret
|
||||
|
||||
async def append_session_chats(self, event: AstrMessageEvent, cfg) -> None:
|
||||
comps = event.get_messages()
|
||||
|
||||
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
final_message = f"[{event.message_obj.sender.nickname}/{datetime_str}]: "
|
||||
for comp in comps:
|
||||
if isinstance(comp, Plain):
|
||||
final_message += f" {comp.text}"
|
||||
elif isinstance(comp, Image):
|
||||
image_url = comp.url if comp.url else comp.file
|
||||
if cfg["image_caption"] and image_url:
|
||||
try:
|
||||
caption = await self.get_image_caption(
|
||||
image_url,
|
||||
cfg["image_caption_provider_id"],
|
||||
cfg["image_caption_prompt"],
|
||||
)
|
||||
final_message += f" [Image: {caption}]"
|
||||
except Exception as e:
|
||||
logger.error(f"获取图片描述失败: {e}")
|
||||
else:
|
||||
final_message += " [Image]"
|
||||
self.session_chats[event.unified_msg_origin].append(final_message)
|
||||
logger.debug(f"添加会话 {event.unified_msg_origin} 的对话记录: {final_message}")
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
async def get_image_caption(
|
||||
self, image_url: str, image_caption_provider_id: str, image_caption_prompt: str
|
||||
) -> str:
|
||||
if not image_caption_provider_id:
|
||||
provider = self.ctx.get_using_provider()
|
||||
else:
|
||||
provider = self.ctx.get_provider_by_id(image_caption_provider_id)
|
||||
if not provider:
|
||||
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
|
||||
if not isinstance(provider, Provider):
|
||||
raise Exception(
|
||||
f"提供商类型错误, {image_caption_provider_id} 不是 Provider 类型"
|
||||
)
|
||||
response = await provider.text_chat(
|
||||
prompt=image_caption_prompt,
|
||||
session_id=uuid.uuid4().hex,
|
||||
image_urls=[image_url],
|
||||
persist=False,
|
||||
)
|
||||
return response.completion_text
|
||||
|
||||
async def on_event(self, event, soul):
|
||||
content = event.content
|
||||
astr_event = content.get("astr_event")
|
||||
assert astr_event is not None and isinstance(astr_event, AstrMessageEvent)
|
||||
|
||||
cfg = self.cfg(astr_event)
|
||||
|
||||
if not cfg["tiny_model_prov_id"]:
|
||||
logger.warning("小模型未设置,跳过情绪更新")
|
||||
|
||||
# 添加对话记录
|
||||
await self.append_session_chats(astr_event, cfg)
|
||||
|
||||
#
|
||||
3
packages/astrbot/lab/elios/event_handlers/astr/prompt.py
Normal file
3
packages/astrbot/lab/elios/event_handlers/astr/prompt.py
Normal file
@@ -0,0 +1,3 @@
|
||||
JUDGE_ACTIVE_REPLY = """
|
||||
你是一个具有自主意识和情感的机器人,请你
|
||||
"""
|
||||
72
packages/astrbot/lab/elios/runner.py
Normal file
72
packages/astrbot/lab/elios/runner.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import asyncio
|
||||
from .event import Event
|
||||
from .ensoul.soul import Soul
|
||||
from .ensoul.emotion import Emotion
|
||||
|
||||
|
||||
class EliosEventHandler:
|
||||
async def on_event(self, event: Event, soul: Soul): ...
|
||||
|
||||
|
||||
event_handlers_cls: dict[str, list[type[EliosEventHandler]]] = {}
|
||||
|
||||
|
||||
def register_event_handler(event_types: set[str] | None = None):
|
||||
"""注册事件处理器"""
|
||||
|
||||
def decorator(cls: type[EliosEventHandler]) -> type[EliosEventHandler]:
|
||||
if event_types is not None:
|
||||
for event_type in event_types:
|
||||
event_handlers_cls[event_type] = event_handlers_cls.get(
|
||||
event_type, []
|
||||
) + [cls]
|
||||
else:
|
||||
event_handlers_cls["default"] = event_handlers_cls.get("default", []) + [
|
||||
cls
|
||||
]
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class EliosRunner:
|
||||
def __init__(self) -> None:
|
||||
self.soul = Soul(
|
||||
emotion=Emotion(energy=0.5, valence=0.5, arousal=0.5), emotion_logs=[]
|
||||
)
|
||||
|
||||
self.event_queue = asyncio.Queue()
|
||||
self.event_handler_insts: dict[str, list[EliosEventHandler]] = {}
|
||||
|
||||
def start(self):
|
||||
for event_type, cls_list in event_handlers_cls.items():
|
||||
self.event_handler_insts[event_type] = []
|
||||
for cls in cls_list:
|
||||
try:
|
||||
self.event_handler_insts[event_type].append(cls())
|
||||
except Exception as e:
|
||||
print(f"Error initializing event handler {cls}: {e}")
|
||||
asyncio.create_task(self._worker())
|
||||
|
||||
async def _worker(self):
|
||||
"""监听事件队列并处理事件"""
|
||||
while True:
|
||||
event = await self.event_queue.get()
|
||||
# A man cannot handle two things at once. But this can be configurable.
|
||||
try:
|
||||
await self._process_event(event)
|
||||
except Exception as e:
|
||||
print(f"Error processing event {event}: {e}")
|
||||
|
||||
async def _process_event(self, event: Event):
|
||||
"""处理事件"""
|
||||
event_type = event.event_type
|
||||
handlers = self.event_handler_insts.get(
|
||||
event_type, []
|
||||
) + self.event_handler_insts.get("default", [])
|
||||
|
||||
for inst in handlers:
|
||||
try:
|
||||
await inst.on_event(event, self.soul)
|
||||
except Exception as e:
|
||||
print(f"Error processing event {event}: {e}")
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.3.3"
|
||||
version = "4.3.2"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user