diff --git a/README.md b/README.md index 4dab60d90..5fc6bff3a 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ The model adheres to the Openai Schema, other models are not supported. Please a Refer to the [🧀 Deployment Document](https://llmkira.github.io/Docs/) for more information. ```shell +# Install Telegram Voice dependencies +apt install ffmpeg # Install RabbitMQ docker pull rabbitmq:3.10-management docker run -d -p 5672:5672 -p 15672:15672 \ @@ -149,6 +151,9 @@ cp .env.exp .env&&nano .env docker-compose -f docker-compose.yml up -d ``` +The Docker configuration file `docker-compose.yml` contains all databases. In fact, Redis and MongoDB are not required. +You can remove these databases yourself and use the local file system. + Update image using `docker-compose pull`. Use `docker exec -it llmbot /bin/bash` to view Shell in Docker, enter `exit` to exit. @@ -183,6 +188,8 @@ you can enable it by setting `VOICE_REPLY_ME=true` in `.env`. /env REECHO_VOICE_KEY= ``` +use `/env VOICE_REPLY_ME=NONE` to disable this env. + check the source code in `llmkira/extra/voice_hook.py`, learn to write your own hooks. ## 🧀 Sponsor diff --git a/app/receiver/function.py b/app/receiver/function.py index 6856cf03d..54daa76dd 100644 --- a/app/receiver/function.py +++ b/app/receiver/function.py @@ -88,7 +88,8 @@ async def create_snapshot( creator=task_snapshot.receiver.uid, expire_at=int(time.time()) + 60 * 2, ) - logger.debug(f"Create a snapshot {task_id}") + if snapshot_credential: + logger.debug(f"Create a snapshot {task_id}") return snapshot_credential @@ -154,7 +155,14 @@ async def run_pending_task(task: TaskHeader, pending_task: ToolCall): logger.debug(f"Save ToolCall {pending_task.id} to Cache Map") # Run Function _tool_obj = tool_cls() - if _tool_obj.require_auth: + + # Get Env + secrets = await EnvManager(user_id=task.receiver.uid).read_env() + if not secrets: + secrets = {} + env_map = {name: secrets.get(name, None) for name in _tool_obj.env_list} + # Auth + if _tool_obj.require_auth(env_map): if task.task_sign.snapshot_credential: # 是携带密钥的函数,是预先构建的可信任务头 task.task_sign.snapshot_credential = None @@ -179,24 +187,21 @@ async def run_pending_task(task: TaskHeader, pending_task: ToolCall): return logger.info( f"[Snapshot Auth] \n--auth-require {pending_task.name} require." ) - # Get Env - env_all = await EnvManager(user_id=task.receiver.uid).read_env() - if not env_all: - env_all = {} - env_map = {} - for require in _tool_obj.env_list: - env_map[require] = env_all.get(require, None) # Resign Chain if len(task.task_sign.tool_calls_pending) == 1: logger.debug("ToolCall run out, resign a new request to request stop sign.") + # NOTE:因为 ToolCall 破坏了递归的链式调用,所以这里不再继续调用 + """ await create_snapshot( task=task, tool_calls_pending_now=pending_task, memory_able=True, channel=task.receiver.platform, ) - # 运行函数, 传递模型的信息,以及上一条的结果的openai raw信息 + """ + pass + # 运行函数, 传递模型的信息,以及上一条的结果的openai raw信息 run_result = await _tool_obj.load( task=task, receiver=task.receiver, diff --git a/app/receiver/receiver_client.py b/app/receiver/receiver_client.py index 905f2772a..b7c51f84b 100644 --- a/app/receiver/receiver_client.py +++ b/app/receiver/receiver_client.py @@ -6,7 +6,6 @@ ##### # This file is not a top-level schematic file! ##### - import os import time from abc import ABCMeta, abstractmethod @@ -59,8 +58,8 @@ async def generate_authorization( tool = tool_object() # 实例化 icon = "🌟" - - if tool.require_auth: + env_map = {name: secrets.get(name, None) for name in tool.env_list} + if tool.require_auth(env_map): icon = "🔐" auth_key = str(shortuuid.uuid()[0:5].upper()) authorization_map[auth_key] = tool_invocation @@ -78,7 +77,8 @@ async def generate_authorization( escaped_env_vars = [ f"`{formatting.escape_markdown(name)}`" for name in missing_env_vars ] - function_tips.append(f"🦴 Env required: {','.join(escaped_env_vars)} ") + if escaped_env_vars: + function_tips.append(f"🦴 Env required: {','.join(escaped_env_vars)} ") help_docs = tool.env_help_docs(missing_env_vars) if help_docs: function_tips.append(help_docs) @@ -294,10 +294,11 @@ async def deal_message(self, message) -> Tuple: :param message: 消息 :return: 任务,中间件,路由类型,是否释放函数快照 """ - logger.debug("Received MQ Message") + logger.debug(f"Received MQ Message 📩{message.message_id}") task_head: TaskHeader = TaskHeader.model_validate_json( json_data=message.body.decode("utf-8") ) + logger.debug(f"Received Task:{task_head.model_dump_json(indent=2)}") router = task_head.task_sign.router # Deliver 直接转发 if router == Router.DELIVER: @@ -328,7 +329,7 @@ async def deal_message(self, message) -> Tuple: task=task_head, intercept_function=True, disable_tool=True, - remember=False, + remember=True, ) return ( task_head, @@ -347,8 +348,8 @@ async def deal_message(self, message) -> Tuple: await self._flash( task=task_head, llm=llm_middleware, - remember=True, intercept_function=True, + remember=True, ) return ( task_head, @@ -383,27 +384,38 @@ async def on_message(self, message: AbstractIncomingMessage): data = snap_data.data renew_snap_data = [] for task in data: - if not task.snapshot_credential and not task.processed: - if task.expire_at < int(time.time()): - logger.info( - f"🧀 Expire snapshot {task.snap_uuid} at {router}" - ) - continue - try: - await Task.create_and_send( - queue_name=task.channel, task=task.snapshot_data - ) - except Exception as e: - logger.exception(f"Response to snapshot error {e}") + if task.expire_at < int(time.time()): + logger.info( + f"🧀 Expire snapshot {task.snap_uuid} at {router}" + ) + # 跳过过期的任何任务 + continue + # 不是认证任务 + if not task.snapshot_credential: + # 没有被处理 + if not task.processed: + try: + # await asyncio.sleep(10) + logger.debug( + f"🧀 Send snapshot {task.snap_uuid} at {router}" + ) + await Task.create_and_send( + queue_name=task.channel, task=task.snapshot_data + ) + except Exception as e: + logger.exception(f"Response to snapshot error {e}") + else: + logger.info( + f"🧀 Response to snapshot {task.snap_uuid} at {router}" + ) + finally: + task.processed_at = int(time.time()) + # renew_snap_data.append(task) else: - logger.info( - f"🧀 Response to snapshot {task.snap_uuid} at {router}" - ) - finally: - task.processed_at = int(time.time()) - renew_snap_data.append(task) + # 被处理过的任务。不再处理 + pass else: - task.processed_at = None + # 认证任务 renew_snap_data.append(task) snap_data.data = renew_snap_data await global_snapshot_storage.write( diff --git a/app/sender/discord/__init__.py b/app/sender/discord/__init__.py index 23ce8c3cc..c61fd651d 100644 --- a/app/sender/discord/__init__.py +++ b/app/sender/discord/__init__.py @@ -336,7 +336,7 @@ async def listen_help_command(ctx: crescent.Context): @crescent.command(dm_enabled=True, name="auth", description="auth [credential]") async def listen_auth_command(ctx: crescent.Context, credential: str): try: - await auth_reloader( + result = await auth_reloader( snapshot_credential=credential, user_id=f"{ctx.user.id}", platform=__sender__, @@ -347,7 +347,10 @@ async def listen_auth_command(ctx: crescent.Context, credential: str): ) logger.error(f"[270563]auth_reloader failed {exc}") else: - message = "🪄 Auth Pass" + if result: + message = "🪄 Auth Pass" + else: + message = "You dont have this snapshot" return await ctx.respond(content=message, ephemeral=True) @client.include diff --git a/app/sender/discord/event.py b/app/sender/discord/event.py index 2dcc5141f..800efaa61 100644 --- a/app/sender/discord/event.py +++ b/app/sender/discord/event.py @@ -18,7 +18,7 @@ def help_message(): `/auth` - activate a task (my power) `/login` - login `/login_via_url` - login via url - `/env` - set environment variable + `/env` - set environment variable, split by ; , use `/env ENV=NONE` to disable a env. **Please confirm that that bot instance is secure, some plugins may be dangerous on unsafe instance.** """.format(prefix=BotSetting.prefix) diff --git a/app/sender/kook/__init__.py b/app/sender/kook/__init__.py index 69685df6c..8bdcad1cc 100644 --- a/app/sender/kook/__init__.py +++ b/app/sender/kook/__init__.py @@ -356,19 +356,24 @@ async def listen_tool_command(msg: Message): @bot.command(name="auth") async def listen_auth_command(msg: Message, credential: str): try: - await auth_reloader( + result = await auth_reloader( snapshot_credential=credential, user_id=f"{msg.author_id}", platform=__sender__, ) except Exception as e: - message = ( + auth_result = ( "❌ Auth failed,You dont have permission or the task do not exist" ) - logger.error(f"[2753383]auth_reloader failed:{e}") + logger.info(f"Auth failed {e}") else: - message = "🪄 Auth Pass" - return await msg.reply(content=message, is_temp=True, type=MessageTypes.KMD) + if result: + auth_result = "🪄 Snapshot released" + else: + auth_result = "You dont have this snapshot" + return await msg.reply( + content=auth_result, is_temp=True, type=MessageTypes.KMD + ) @bot.command(name="env") async def listen_env_command(msg: Message, env_string: str): diff --git a/app/sender/kook/event.py b/app/sender/kook/event.py index 14a2c830a..6ab8738a3 100644 --- a/app/sender/kook/event.py +++ b/app/sender/kook/event.py @@ -37,7 +37,7 @@ def help_message(): `/auth` - activate a task (my power) `/login` - login openai `/login_via_url` - login via provider url - `/env` - set environment variable + `/env` - set environment variable, split by ; , use `/env ENV=NONE` to disable a env. **Please confirm that that bot instance is secure, some plugins may be dangerous on unsafe instance.** """.format(prefix=BotSetting.prefix) diff --git a/app/sender/slack/__init__.py b/app/sender/slack/__init__.py index 07b4d781e..44439b1dc 100644 --- a/app/sender/slack/__init__.py +++ b/app/sender/slack/__init__.py @@ -331,7 +331,7 @@ async def listen_tool_command(ack: AsyncAck, respond: AsyncRespond, command): async def auth_chain(uuid, user_id): try: - await auth_reloader( + result = await auth_reloader( snapshot_credential=uuid, user_id=f"{user_id}", platform=__sender__, @@ -340,9 +340,12 @@ async def auth_chain(uuid, user_id): auth_result = ( "❌ Auth failed,You dont have permission or the task do not exist" ) - logger.info(f"[3031]auth_reloader failed {e}") + logger.info(f"Auth failed {e}") else: - auth_result = "🪄 Auth Pass" + if result: + auth_result = "🪄 Snapshot released" + else: + auth_result = "You dont have this snapshot" return auth_result @bot.command(command="/auth") diff --git a/app/sender/slack/event.py b/app/sender/slack/event.py index 1ee11e200..e11ced619 100644 --- a/app/sender/slack/event.py +++ b/app/sender/slack/event.py @@ -24,7 +24,7 @@ def help_message(): `/clear` - forget...you `/auth` - activate a task (my power),but outside the thread `/login` - login via url or raw -`/env` - set environment variable +`/env` - set environment variable, split by ; , use `/env ENV=NONE` to disable a env. Make sure you invite me before you call me in channel, wink~ diff --git a/app/sender/telegram/__init__.py b/app/sender/telegram/__init__.py index 8253abb75..24e6dcd13 100644 --- a/app/sender/telegram/__init__.py +++ b/app/sender/telegram/__init__.py @@ -341,7 +341,7 @@ async def listen_auth_command(message: types.Message): if not _arg: return None try: - await auth_reloader( + result = await auth_reloader( snapshot_credential=_arg, user_id=f"{message.from_user.id}", platform=__sender__, @@ -352,7 +352,10 @@ async def listen_auth_command(message: types.Message): ) logger.info(f"Auth failed {e}") else: - auth_result = "🪄 Snapshot released" + if result: + auth_result = "🪄 Snapshot released" + else: + auth_result = "You dont have this snapshot" return await bot.reply_to(message, text=convert(auth_result)) @bot.message_handler( diff --git a/app/sender/telegram/event.py b/app/sender/telegram/event.py index f394a7bdf..7958ebfa5 100644 --- a/app/sender/telegram/event.py +++ b/app/sender/telegram/event.py @@ -17,7 +17,7 @@ def help_message(): Private Chat Only: /login - login via url or something - /env - 配置变量,use as shell + /env - 配置变量 split by ; , use `/env ENV=NONE` to disable a env. !Please confirm that that bot instance is secure, some plugins may be dangerous on unsafe instance. """ diff --git a/llmkira/extra/plugins/alarm/__init__.py b/llmkira/extra/plugins/alarm/__init__.py index 3b8510dc6..c5a9bd399 100644 --- a/llmkira/extra/plugins/alarm/__init__.py +++ b/llmkira/extra/plugins/alarm/__init__.py @@ -66,10 +66,12 @@ class AlarmTool(BaseTool): pattern: Optional[re.Pattern] = re.compile( r"(\d+)(分钟|小时|天|周|月|年)后提醒我(.*)" ) - require_auth: bool = True # env_required: list = ["SCHEDULER", "TIMEZONE"] + def require_auth(self, env_map: dict) -> bool: + return True + def func_message(self, message_text, **kwargs): """ 如果合格则返回message,否则返回None,表示不处理 diff --git a/llmkira/extra/plugins/search/__init__.py b/llmkira/extra/plugins/search/__init__.py index 42012ddee..05f4bf689 100644 --- a/llmkira/extra/plugins/search/__init__.py +++ b/llmkira/extra/plugins/search/__init__.py @@ -43,59 +43,30 @@ class SearchTool(BaseTool): silent: bool = False function: Union[Tool, Type[BaseModel]] = Search - require_auth: bool = True keywords: list = [ "怎么", - "How", - "件事", - "牢大", - "作用", - "知道", - "什么", - "认识", - "What", - "http", - "what", - "who", - "how", - "Who", - "Why", - "作品", - "why", "Where", - "了解", - "简述", + "Search", + "search", "How to", - "是谁", + "为什么", "how to", + "news", + "新闻", "解释", "怎样的", - "新闻", - "ニュース", - "电影", - "番剧", - "アニメ", - "2022", - "2023", "请教", "介绍", - "怎样", - "吗", - "么", - "?", - "?", - "呢", - "评价", "搜索", - "百度", - "谷歌", - "bing", - "谁是", - "上网", ] env_required: List[str] = ["API_KEY"] env_prefix: str = "SERPER_" + def require_auth(self, env_map: dict) -> bool: + if "SERPER_API_KEY" in env_map: + return False + return True + @classmethod def env_help_docs(cls, empty_env: List[str]) -> str: """ @@ -110,13 +81,17 @@ def env_help_docs(cls, empty_env: List[str]) -> str: ) return message - def func_message(self, message_text, **kwargs): + def func_message(self, message_text, message_raw, address, **kwargs): """ 如果合格则返回message,否则返回None,表示不处理 """ for i in self.keywords: if i in message_text: return self.function + if message_text.endswith("?"): + return self.function + if message_text.endswith("?"): + return self.function # 正则匹配 if self.pattern: match = self.pattern.match(message_text) @@ -192,7 +167,7 @@ async def run( _set = Search.model_validate(arg) _search_result = await search_on_serper( search_sentence=_set.keywords, - api_key=env.get("serper_api_key"), + api_key=env.get("SERPER_API_KEY"), ) # META _meta = task.task_sign.reprocess( @@ -212,13 +187,7 @@ async def run( sender=task.sender, # 继承发送者 receiver=receiver, # 因为可能有转发,所以可以单配 task_sign=_meta, - message=[ - EventMessage( - user_id=receiver.user_id, - chat_id=receiver.chat_id, - text="🔍 Searching Done", - ) - ], + message=[], ), ) diff --git a/llmkira/extra/voice/__init__.py b/llmkira/extra/voice/__init__.py index d2ca8d086..e12121175 100644 --- a/llmkira/extra/voice/__init__.py +++ b/llmkira/extra/voice/__init__.py @@ -48,7 +48,7 @@ def get_audio_bytes_from_data_url(data_url): async def request_reecho_speech( - text: str, reecho_api_key: str, voiceId="eb5d7f8c-eea1-483f-b718-1f28d6649339" + text: str, reecho_api_key: str, voiceId="8c581931-94a8-4d0b-a76f-a35ddd7b5ec3" ): """ Call the Reecho endpoint to generate synthesized voice. @@ -65,7 +65,15 @@ async def request_reecho_speech( "Content-Type": "application/json", "Authorization": f"Bearer {reecho_api_key}", # Replace {token} with your Reecho API token } - data = {"voiceId": voiceId, "text": text, "origin_audio": True} + data = { + "voiceId": voiceId, + "text": text, + "origin_audio": True, + "randomness": 97, + "stability_boost": 50, + "probability_optimization": 99, + } + audio_bytes = None async with aiohttp.ClientSession() as session: async with session.post( url, headers=headers, data=json.dumps(data) @@ -74,8 +82,8 @@ async def request_reecho_speech( response_json = await response.json() audio_url = response_json["data"].get("audio", None) audio_bytes = get_audio_bytes_from_data_url(audio_url) - if not audio_bytes: - return await request_cn_speech(text) + if not audio_bytes: + return await request_cn_speech(text) return audio_bytes diff --git a/llmkira/kv_manager/env.py b/llmkira/kv_manager/env.py index 1fa6084b8..2197814f0 100644 --- a/llmkira/kv_manager/env.py +++ b/llmkira/kv_manager/env.py @@ -18,7 +18,7 @@ def parse_env_string(env_string) -> Dict[str, str]: env_value = f"{match[1]}" env_value = env_value.strip().strip('"') env_key = env_key.upper() - if env_value.lower() == "none": + if env_value.upper() == "NONE": env_value = None env_table[env_key] = env_value return env_table @@ -66,6 +66,8 @@ async def set_env( else: raise ValueError("Env String Should be dict or str") current_env.update(env_map) + # 去除 None + current_env = {k: v for k, v in current_env.items() if v is not None} await self.save_data(self.user_id, json.dumps(current_env)) if return_all: return current_env diff --git a/llmkira/kv_manager/instruction.py b/llmkira/kv_manager/instruction.py index 0a68f5661..8b8fdb28a 100644 --- a/llmkira/kv_manager/instruction.py +++ b/llmkira/kv_manager/instruction.py @@ -3,7 +3,10 @@ from llmkira.kv_manager._base import KvManager DEFAULT_INSTRUCTION = ( - "ACT STEP BY STEP, SPEAK IN MORE CUTE STYLE, DONT REPEAT YOURSELF,CALL USER MASTER" + "[ASSISTANT RULE]" + "SPEAK IN MORE CUTE STYLE, DONT REPEAT, ACT STEP BY STEP, CALL USER MASTER, REPLY IN USER " + "LANGUAGE" + "[RULE END]" ) @@ -23,7 +26,7 @@ async def read_instruction(self) -> str: result = await self.read_data(self.user_id) if not result: return f"Now={time_now()}\n{DEFAULT_INSTRUCTION}" - return result + return f"Now={time_now()}\n{result}" async def set_instruction(self, instruction: str) -> str: if not isinstance(instruction, str): diff --git a/llmkira/openai/cell.py b/llmkira/openai/cell.py index d8469dc2c..e325824f1 100644 --- a/llmkira/openai/cell.py +++ b/llmkira/openai/cell.py @@ -163,7 +163,7 @@ class Message(BaseModel, ABC): class SystemMessage(Message): role: Literal["system"] = "system" content: str - name: str = None + name: Optional[str] = None class UserMessage(Message): diff --git a/llmkira/sdk/tools/schema.py b/llmkira/sdk/tools/schema.py index cb1e7fb66..f99abf16f 100644 --- a/llmkira/sdk/tools/schema.py +++ b/llmkira/sdk/tools/schema.py @@ -55,9 +55,6 @@ def _check_keywords(cls, v): pattern: Optional[re.Pattern] = None """The pattern to be matched to load tools in session""" - require_auth: bool = False - """Is user authentication required""" - env_required: List[str] = Field([], description="环境变量要求,ALSO NEED env_prefix") """Pre-required environment variables, you should provide env_prefix""" @@ -69,6 +66,12 @@ def _check_keywords(cls, v): ) """File name regular expression to use the tool, exp: re.compile(r"file_id=([a-z0-9]{8})")""" + def require_auth(self, env_map: dict) -> bool: + """ + Check if authentication is required + """ + return False + @final def get_os_env(self, env_name): """ @@ -120,10 +123,12 @@ def env_help_docs(cls, empty_env: List[str]) -> Optional[str]: return "You need to configure ENV to start use this tool" @abstractmethod - def func_message(self, message_text, **kwargs): + def func_message(self, message_text, message_raw, address, **kwargs): """ If the message_text contains the keyword, return the function to be executed, otherwise return None :param message_text: 消息文本 + :param message_raw: 消息原始数据 `EventMessage` + :param address: 消息地址 `tuple(sender,receiver)` :param kwargs : message_raw: 消息原始数据 `EventMessage` address: 消息地址 `tuple(sender,receiver)`