diff --git a/README.md b/README.md index 76d96b1cd..3da2a6230 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,10 @@ or [one-api](https://github.com/songquanpeng/one-api) independently. ### 🍔 Login Modes -- `Login via url`: Use `/login token$https://provider.com` to Login. The program posts the token to the interface to +- `Login via url`: Use `/login $` to Login. The program posts the token to the interface to retrieve configuration information, [how to develop this](https://github.com/LlmKira/Openaibot/blob/81eddbff0f136697d5ad6e13ee1a7477b26624ed/app/components/credential.py#L20). -- `Login`: Use `/login https://api.com/v1$key$model` to login +- `Login`: Use `/login https:///v1$$` to login ### 🧀 Plugin Can Do More @@ -145,6 +145,9 @@ npm install pm2 -g pm2 start pm2.json ``` +> **Be sure to change the default password for the command, or disable open ports to prevent the database from being +scanned and attacked.** + ### 🥣 Docker Build Hub: [sudoskys/llmbot](https://hub.docker.com/repository/docker/sudoskys/llmbot/general) diff --git a/app/components/__init__.py b/app/components/__init__.py index e69de29bb..51fadcfc9 100644 --- a/app/components/__init__.py +++ b/app/components/__init__.py @@ -0,0 +1,9 @@ +from typing import Optional + +from app.components.credential import Credential +from app.components.user_manager import USER_MANAGER + + +async def read_user_credential(user_id: str) -> Optional[Credential]: + user = await USER_MANAGER.read(user_id=user_id) + return user.credential diff --git a/app/components/credential.py b/app/components/credential.py index 55fa623b9..cd80e6d44 100644 --- a/app/components/credential.py +++ b/app/components/credential.py @@ -1,5 +1,4 @@ import os -from urllib.parse import urlparse import requests from dotenv import load_dotenv @@ -15,6 +14,7 @@ class Credential(BaseModel): api_key: str api_endpoint: str api_model: str + api_tool_model: str = "gpt-3.5-turbo" @classmethod def from_provider(cls, token, provider_url): @@ -36,37 +36,10 @@ def from_provider(cls, token, provider_url): api_key=user_data["api_key"], api_endpoint=user_data["api_endpoint"], api_model=user_data["api_model"], + api_tool_model=user_data.get("api_tool_model", "gpt-3.5-turbo"), ) -def split_setting_string(input_string): - if not isinstance(input_string, str): - return None - segments = input_string.split("$") - - # 检查链接的有效性 - def is_valid_url(url): - try: - result = urlparse(url) - return all([result.scheme, result.netloc]) - except ValueError: - return False - - # 开头为链接的情况 - if is_valid_url(segments[0]) and len(segments) >= 3: - return segments[:3] - # 第二个元素为链接,第一个元素为字符串的情况 - elif ( - len(segments) == 2 - and not is_valid_url(segments[0]) - and is_valid_url(segments[1]) - ): - return segments - # 其他情况 - else: - return None - - load_dotenv() if os.getenv("GLOBAL_OAI_KEY") and os.getenv("GLOBAL_OAI_ENDPOINT"): diff --git a/app/receiver/function.py b/app/receiver/function.py index d5538f9ba..b068989dc 100644 --- a/app/receiver/function.py +++ b/app/receiver/function.py @@ -14,8 +14,12 @@ from aio_pika.abc import AbstractIncomingMessage from loguru import logger +from app.components import read_user_credential +from app.components.credential import global_credential from llmkira.kv_manager.env import EnvManager from llmkira.kv_manager.tool_call import GLOBAL_TOOLCALL_CACHE_HANDLER +from llmkira.logic import LLMLogic +from llmkira.memory import global_message_runtime from llmkira.openai.cell import ToolCall from llmkira.sdk.tools.register import ToolRegister from llmkira.task import Task, TaskHeader @@ -235,20 +239,7 @@ async def run_pending_task(task: TaskHeader, pending_task: ToolCall): return logger.info( f"[Snapshot Auth] \n--auth-require {pending_task.name} require." ) - - # Resign Chain - # 时序实现,防止过度注册 - if len(task.task_sign.tool_calls_pending) == 1: - if not has_been_called_recently(userid=task.receiver.uid, n_seconds=5): - logger.debug( - "ToolCall run out, resign a new request to request stop sign." - ) - await create_child_snapshot( - task=task, - memory_able=True, - channel=task.receiver.platform, - ) - # 运行函数, 传递模型的信息,以及上一条的结果的openai raw信息 + # Run Function run_result = await _tool_obj.load( task=task, receiver=task.receiver, @@ -257,11 +248,72 @@ async def run_pending_task(task: TaskHeader, pending_task: ToolCall): pending_task=pending_task, refer_llm_result=task.task_sign.llm_response, ) + run_status = True # 更新任务状态 + if run_result.get("exception"): + run_status = False await task.task_sign.complete_task( tool_calls=pending_task, success_or_not=True, run_result=run_result ) - return run_result + # Resign Chain + # 时序实现,防止过度注册 + if len(task.task_sign.tool_calls_pending) == 0: + if not has_been_called_recently(userid=task.receiver.uid, n_seconds=3): + credentials = await read_user_credential(user_id=task.receiver.uid) + if global_credential: + credentials = global_credential + logic = LLMLogic( + api_key=credentials.api_key, + api_endpoint=credentials.api_endpoint, + api_model=credentials.api_tool_model, + ) + history = await global_message_runtime.update_session( + session_id=task.receiver.uid, + ).read(lines=3) + logger.debug(f"Read History:{history}") + continue_ = await logic.llm_continue( + context=f"History:{history},ToolCallResult:{run_status}", + condition="Would you like to continue a chat?", + default=False, + ) + if continue_.continue_it: + logger.debug( + "ToolCall run out, resign a new request to request stop sign." + ) + await create_child_snapshot( + task=task, + memory_able=True, + channel=task.receiver.platform, + ) + # 运行函数, 传递模型的信息,以及上一条的结果的openai raw信息 + await Task.create_and_send( + queue_name=task.receiver.platform, + task=TaskHeader( + sender=task.sender, + receiver=task.receiver, + task_sign=task.task_sign.notify( + plugin_name=__receiver__, + response_snapshot=True, + memory_able=False, + ), + message=[ + EventMessage( + user_id=task.receiver.user_id, + chat_id=task.receiver.chat_id, + text=continue_.comment_to_user, + ) + ], + ), + ) + else: + if continue_.comment_to_user: + await reply_user( + platform=task.receiver.platform, + receiver=task.receiver, + task=task, + text=continue_.comment_to_user, + ) + return run_status async def process_function_call(self, message: AbstractIncomingMessage): """ @@ -307,9 +359,6 @@ async def run_task(self, task, pending_task): try: await self.run_pending_task(task=task, pending_task=pending_task) except Exception as e: - await task.task_sign.complete_task( - tool_calls=pending_task, success_or_not=False, run_result=str(e) - ) logger.error(f"Function Call Error {e}") raise e finally: diff --git a/app/receiver/receiver_client.py b/app/receiver/receiver_client.py index 8efbdcaea..869b54bd2 100644 --- a/app/receiver/receiver_client.py +++ b/app/receiver/receiver_client.py @@ -17,8 +17,8 @@ from loguru import logger from telebot import formatting -from app.components.credential import Credential, global_credential -from app.components.user_manager import USER_MANAGER +from app.components import read_user_credential +from app.components.credential import global_credential from app.middleware.llm_task import OpenaiMiddleware from llmkira.kv_manager.env import EnvManager from llmkira.openai import OpenaiError @@ -48,11 +48,6 @@ async def get(self, user_id): user_locks = UserLocks() -async def read_user_credential(user_id: str) -> Optional[Credential]: - user = await USER_MANAGER.read(user_id=user_id) - return user.credential - - async def generate_authorization( secrets: Dict, tool_invocation: ToolCall ) -> Tuple[dict, list, bool]: diff --git a/app/sender/discord/__init__.py b/app/sender/discord/__init__.py index c61fd651d..acdf3d81d 100644 --- a/app/sender/discord/__init__.py +++ b/app/sender/discord/__init__.py @@ -30,7 +30,13 @@ __sender__ = "discord_hikari" __default_disable_tool_action__ = False -from ..util_func import auth_reloader, is_command, is_empty_command, uid_make, login +from ..util_func import ( + auth_reloader, + is_command, + is_empty_command, + uid_make, + save_credential, +) from llmkira.openapi.trigger import get_trigger_loop from ...components.credential import Credential, ProviderError @@ -238,7 +244,7 @@ async def listen_login_url_command( credential = Credential.from_provider( token=token, provider_url=provider_url ) - await login( + await save_credential( uid=uid_make(__sender__, ctx.user.id), credential=credential, ) @@ -264,17 +270,19 @@ async def listen_login_url_command( ) async def listen_endpoint_command( ctx: crescent.Context, - openai_endpoint: str, - openai_key: str, - openai_model: str, + api_endpoint: str, + api_key: str, + api_model: str, + api_tool_model: str = "gpt-3.5-turbo", ): try: credential = Credential( - api_endpoint=openai_endpoint, - api_key=openai_key, - api_model=openai_model, + api_endpoint=api_endpoint, + api_key=api_key, + api_model=api_model, + api_tool_model=api_tool_model, ) - await login( + await save_credential( uid=uid_make(__sender__, ctx.user.id), credential=credential, ) diff --git a/app/sender/kook/__init__.py b/app/sender/kook/__init__.py index 8bdcad1cc..0cd1403e8 100644 --- a/app/sender/kook/__init__.py +++ b/app/sender/kook/__init__.py @@ -27,7 +27,13 @@ __sender__ = "kook" __default_disable_tool_action__ = False -from ..util_func import auth_reloader, is_command, is_empty_command, uid_make, login +from ..util_func import ( + auth_reloader, + is_command, + is_empty_command, + uid_make, + save_credential, +) from llmkira.openapi.trigger import get_trigger_loop from ...components.credential import ProviderError, Credential @@ -244,7 +250,7 @@ async def listen_login_url_command( credential = Credential.from_provider( token=token, provider_url=provider_url ) - await login( + await save_credential( uid=uid_make(__sender__, msg.author_id), credential=credential, ) @@ -272,17 +278,19 @@ async def listen_login_url_command( @bot.command(name="login") async def listen_login_command( msg: Message, - openai_endpoint: str, - openai_key: str, - openai_model: str, + api_endpoint: str, + api_key: str, + api_model: str = "gpt-3.5-turbo", + api_tool_model: str = "gpt-3.5-turbo", ): try: credential = Credential( - api_endpoint=openai_endpoint, - api_key=openai_key, - api_model=openai_model, + api_endpoint=api_endpoint, + api_key=api_key, + api_model=api_model, + api_tool_model=api_tool_model, ) - await login( + await save_credential( uid=uid_make(__sender__, msg.author_id), credential=credential, ) diff --git a/app/sender/slack/__init__.py b/app/sender/slack/__init__.py index 44439b1dc..6492b9129 100644 --- a/app/sender/slack/__init__.py +++ b/app/sender/slack/__init__.py @@ -39,8 +39,6 @@ __sender__ = "slack" -from ...components.credential import split_setting_string, Credential, ProviderError - SlackTask = Task(queue=__sender__) __default_disable_tool_action__ = False __join_cache__ = {} @@ -232,46 +230,10 @@ async def listen_login_command(ack: AsyncAck, respond: AsyncRespond, command): if not command.text: return _arg = command.text - settings = split_setting_string(_arg) - if not settings: - return await respond( - text=convert( - "🔑 **Incorrect format.**\n" - "You can set it via `https://api.com/v1$key$model` format, " - "or you can log in via URL using `token$https://provider.com`." - ), - ) - if len(settings) == 2: - try: - credential = Credential.from_provider( - token=settings[0], provider_url=settings[1] - ) - except ProviderError as e: - return await respond(text=f"Login failed, website return {e}") - except Exception as e: - logger.error(f"Login failed {e}") - return await respond(text=f"Login failed, because {type(e)}") - else: - await login( - uid=uid_make(__sender__, command.user_id), - credential=credential, - ) - return await respond( - text="Login success as provider! Welcome master!" - ) - elif len(settings) == 3: - credential = Credential( - api_endpoint=settings[0], api_key=settings[1], api_model=settings[2] - ) - await login( - uid=uid_make(__sender__, command.user_id), - credential=credential, - ) - return await respond( - text=f"Login success as {settings[2]}! Welcome master! " - ) - else: - return logger.trace(f"Login failed {settings}") + reply = await login( + uid=uid_make(__sender__, command.user_id), arg_string=_arg + ) + return await respond(text=reply) @bot.command(command="/env") async def listen_env_command(ack: AsyncAck, respond: AsyncRespond, command): diff --git a/app/sender/telegram/__init__.py b/app/sender/telegram/__init__.py index a60dad0b1..144de0d5d 100644 --- a/app/sender/telegram/__init__.py +++ b/app/sender/telegram/__init__.py @@ -36,8 +36,6 @@ __sender__ = "telegram" __default_disable_tool_action__ = False -from app.components.credential import split_setting_string, Credential, ProviderError - StepCache = StateMemoryStorage() FileWindow = TimerObjectContainer() TelegramTask = Task(queue=__sender__) @@ -233,52 +231,10 @@ async def create_task(message: types.Message, disable_tool_action: bool = True): async def listen_login_command(message: types.Message): logger.debug("Debug:login command") _cmd, _arg = parse_command(command=message.text) - settings = split_setting_string(_arg) - if not settings: - return await bot.reply_to( - message, - text=convert( - "🔑 **Incorrect format.**\n" - "You can set it via `https://api.com/v1$key$model` format, " - "or you can log in via URL using `token$https://provider.com`." - ), - parse_mode="MarkdownV2", - ) - if len(settings) == 2: - try: - credential = Credential.from_provider( - token=settings[0], provider_url=settings[1] - ) - except ProviderError as e: - return await bot.reply_to( - message, text=f"Login failed, website return {e}" - ) - except Exception as e: - logger.error(f"Login failed {e}") - return await bot.reply_to( - message, text=f"Login failed, because {type(e)}" - ) - else: - await login( - uid=uid_make(__sender__, message.from_user.id), - credential=credential, - ) - return await bot.reply_to( - message, text="Login success as provider! Welcome master!" - ) - elif len(settings) == 3: - credential = Credential( - api_endpoint=settings[0], api_key=settings[1], api_model=settings[2] - ) - await login( - uid=uid_make(__sender__, message.from_user.id), - credential=credential, - ) - return await bot.reply_to( - message, text=f"Login success as {settings[2]}! Welcome master! " - ) - else: - return logger.trace(f"Login failed {settings}") + reply = await login( + uid=uid_make(__sender__, message.from_user.id), arg_string=_arg + ) + await bot.reply_to(message, text=reply) @bot.message_handler(commands="env", chat_types=["private"]) async def listen_env_command(message: types.Message): diff --git a/app/sender/util_func.py b/app/sender/util_func.py index 3bc25705f..67efa10fe 100644 --- a/app/sender/util_func.py +++ b/app/sender/util_func.py @@ -7,9 +7,10 @@ from typing import Tuple, Optional, Union from urllib.parse import urlparse +import telegramify_markdown from loguru import logger -from app.components.credential import Credential +from app.components.credential import Credential, ProviderError from app.components.user_manager import USER_MANAGER from llmkira.task import Task from llmkira.task.snapshot import SnapData, global_snapshot_storage @@ -19,12 +20,6 @@ def uid_make(platform: str, user_id: Union[int, str]): return f"{platform}:{user_id}" -async def login(uid, credential: Credential): - user = await USER_MANAGER.read(user_id=uid) - user.credential = credential - await USER_MANAGER.save(user_model=user) - - def parse_command(command: str) -> Tuple[Optional[str], Optional[str]]: """ :param command like `/chat something` @@ -114,6 +109,85 @@ async def auth_reloader(snapshot_credential: str, platform: str, user_id: str) - ) +def split_setting_string(input_string): + if not isinstance(input_string, str): + return None + segments = input_string.split("$") + + # 开头为链接的情况 + if is_valid_url(segments[0]) and len(segments) >= 3: + return segments[:3] + # 第二个元素为链接,第一个元素为字符串的情况 + elif ( + len(segments) == 2 + and not is_valid_url(segments[0]) + and is_valid_url(segments[1]) + ): + return segments + # 其他情况 + else: + return None + + +async def save_credential(uid, credential: Credential): + user = await USER_MANAGER.read(user_id=uid) + user.credential = credential + await USER_MANAGER.save(user_model=user) + + +async def login(uid: str, arg_string) -> str: + error = telegramify_markdown.convert( + "🔑 **Incorrect format.**\n" + "You can set it via `https:///v1$" + "$$` format, " + "or you can log in via URL using `token$https://provider.com`." + ) + settings = split_setting_string(arg_string) + if not settings: + return error + if len(settings) == 2: + try: + credential = Credential.from_provider( + token=settings[0], provider_url=settings[1] + ) + except ProviderError as e: + return telegramify_markdown.convert(f"Login failed, website return {e}") + except Exception as e: + logger.error(f"Login failed {e}") + return telegramify_markdown.convert(f"Login failed, because {type(e)}") + else: + await save_credential( + uid=uid, + credential=credential, + ) + return telegramify_markdown.convert( + "Login success as provider! Welcome master!" + ) + elif len(settings) == 3 or len(settings) == 4: + api_endpoint = settings[0] + api_key = settings[1] + api_model = settings[2] + if len(settings) == 4: + api_tool_model = settings[3] + else: + api_tool_model = "gpt-3.5-turbo" + credential = Credential( + api_endpoint=api_endpoint, + api_key=api_key, + api_model=api_model, + api_tool_model=api_tool_model, + ) + await save_credential( + uid=uid, + credential=credential, + ) + return telegramify_markdown.convert( + f"Login success as {settings[2]}! Welcome master!" + ) + else: + return error + + class TimerObjectContainer: def __init__(self): self.users = {} diff --git a/app/setting/database.py b/app/setting/database.py index 565cd7c4e..960bbf785 100644 --- a/app/setting/database.py +++ b/app/setting/database.py @@ -25,10 +25,10 @@ class RabbitMQ(BaseSettings): @model_validator(mode="after") def is_connect(self): - import aio_pika + from aio_pika import connect_robust try: - sync(aio_pika.connect_robust(self.amqp_dsn)) + sync(connect_robust(url=self.amqp_dsn)) except Exception as e: logger.exception( f"\n⚠️ RabbitMQ DISCONNECT, pls set AMQP_DSN in .env\n--error {e} \n--dsn {self.amqp_dsn}" diff --git a/llmkira/extra/plugins/search/__init__.py b/llmkira/extra/plugins/search/__init__.py index d863a6734..ac2d319a4 100644 --- a/llmkira/extra/plugins/search/__init__.py +++ b/llmkira/extra/plugins/search/__init__.py @@ -105,7 +105,7 @@ async def failed( refer_llm_result: dict = None, **kwargs, ): - meta = task.task_sign.notify( + meta = task.task_sign.reply( plugin_name=__plugin_name__, tool_response=[ ToolResponse( @@ -115,8 +115,6 @@ async def failed( tool_call=pending_task, ) ], - memory_able=True, - response_snapshot=True, ) await Task.create_and_send( queue_name=receiver.platform, diff --git a/llmkira/logic/__init__.py b/llmkira/logic/__init__.py new file mode 100644 index 000000000..f9f963f80 --- /dev/null +++ b/llmkira/logic/__init__.py @@ -0,0 +1,79 @@ +from typing import Optional + +from loguru import logger +from pydantic import BaseModel, Field, SecretStr + +from llmkira.openai.cell import UserMessage +from llmkira.openai.request import OpenAI, OpenAICredential + + +class whether(BaseModel): + """ + Decide whether to agree to the decision based on the content + """ + + yes_no: bool = Field(description="Whether the condition is true or false") + comment_to_user: Optional[str] = Field( + default="", description="Comment on the decision" + ) + + +class continue_act(BaseModel): + """ + Decide whether to continue execution based on circumstances + """ + + continue_it: bool = Field(description="Whether to continue execution") + comment_to_user: Optional[str] = Field( + default="", description="Comment on the decision" + ) + + +class LLMLogic(object): + """ + LLMLogic is a class that provides some basic logic operations. + + """ + + def __init__(self, api_endpoint, api_key, api_model): + self.api_endpoint = api_endpoint + self.api_key = api_key + self.api_model = api_model + + async def llm_if(self, context: str, condition: str, default: bool): + message = f"Context:{context}\nCondition{condition}\nPlease make a decision." + try: + logic_if = await OpenAI( + model=self.api_model, messages=[UserMessage(content=message)] + ).extract( + response_model=whether, + session=OpenAICredential( + api_key=SecretStr(self.api_key), + base_url=self.api_endpoint, + model=self.api_model, + ), + ) + logic_if: whether + return logic_if + except Exception as e: + logger.error(f"llm_if error: {e}") + return whether(yes_no=default) + + async def llm_continue(self, context: str, condition: str, default: bool): + message = f"Context:{context}\nCondition{condition}\nPlease make a decision whether to continue." + try: + logic_continue = await OpenAI( + model=self.api_model, messages=[UserMessage(content=message)] + ).extract( + response_model=continue_act, + session=OpenAICredential( + api_key=SecretStr(self.api_key), + base_url=self.api_endpoint, + model=self.api_model, + ), + ) + logic_continue: continue_act + return logic_continue + except Exception as e: + logger.error(f"llm_continue error: {e}") + return continue_act(continue_it=default) diff --git a/llmkira/openai/request.py b/llmkira/openai/request.py index 30e8c59c4..b4022c878 100644 --- a/llmkira/openai/request.py +++ b/llmkira/openai/request.py @@ -225,17 +225,25 @@ async def request(self, session: OpenAICredential) -> OpenAIResult: @retry(stop=stop_after_attempt(3), reraise=True) async def extract( - self, response_model: Union[Type[BaseModel], Tool], session: OpenAICredential + self, response_model: Union[Type[BaseModel]], session: OpenAICredential ): + """ + Extract the result from the response + :param response_model: BaseModel + :param session: OpenAICredential + :return: BaseModel + :raises NetworkError, UnexpectedFormatError, RuntimeError: The response model is not matched with the result + """ self.n = 1 self.response_format = None - if not isinstance(response_model, Tool): - response_model = Tool(function=response_model) - self.tools = [response_model] - self.tool_choice = ToolChoice(function=response_model.function) + tool = Tool(function=response_model) + self.tools = [tool] + self.tool_choice = ToolChoice(function=tool.function) result = await self.request(session) try: tool_call = ToolCall.model_validate(result.choices[0].message.tool_calls[0]) - return response_model.model_validate(tool_call.function.arguments) - except Exception: + logger.debug(f"Extracted: {tool_call}") + return response_model.model_validate(tool_call.function.json_arguments) + except Exception as exc: + logger.error(f"extract:{exc}") raise RuntimeError("The response model is not matched with the result")