diff --git a/lionagi/__init__.py b/lionagi/__init__.py index fd7f9c95e..1d3daf9fa 100644 --- a/lionagi/__init__.py +++ b/lionagi/__init__.py @@ -6,11 +6,8 @@ from .version import __version__ from dotenv import load_dotenv - -from .libs import * -from .core import * -from .integrations import * - +from .core import direct, Branch, Session, Structure, Tool, BaseAgent +from .integrations.provider.services import Services logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) diff --git a/lionagi/core/__init__.py b/lionagi/core/__init__.py index b293b75dd..ec913f3c2 100644 --- a/lionagi/core/__init__.py +++ b/lionagi/core/__init__.py @@ -1,6 +1,8 @@ -from .tool import func_to_tool -from .session import Branch, Session - -from .flow import direct +from . import * -__all__ = ["Session", "Branch", "func_to_tool", "direct"] +from .branch import Branch, ExecutableBranch +from .session import Session +from .schema import Tool, Structure, ActionNode, Relationship +from .agent import BaseAgent +from .messages import Instruction, System, Response +from .tool import func_to_tool diff --git a/lionagi/core/agent/__init__.py b/lionagi/core/agent/__init__.py index e69de29bb..fd72758a8 100644 --- a/lionagi/core/agent/__init__.py +++ b/lionagi/core/agent/__init__.py @@ -0,0 +1,3 @@ +from .base_agent import BaseAgent + +__all__ = ["BaseAgent"] diff --git a/lionagi/core/agent/base_agent.py b/lionagi/core/agent/base_agent.py index 1e82c6d76..2f4185b86 100644 --- a/lionagi/core/agent/base_agent.py +++ b/lionagi/core/agent/base_agent.py @@ -1,27 +1,20 @@ -from collections import deque - from lionagi.core.mail.schema import StartMail from lionagi.core.schema.base_node import BaseRelatableNode from lionagi.core.mail.mail_manager import MailManager -import lionagi.libs.ln_func_call as func_call -from lionagi.libs.ln_async import AsyncUtil +from lionagi.libs import func_call, AsyncUtil class BaseAgent(BaseRelatableNode): - def __init__( - self, - structure, - executable_class, - output_parser=None, - executable_class_kwargs={}, - ) -> None: + def __init__(self, structure, executable_obj, output_parser=None) -> None: + super().__init__() self.structure = structure - self.executable = executable_class(**executable_class_kwargs) + self.executable = executable_obj self.start = StartMail() self.mailManager = MailManager([self.structure, self.executable, self.start]) self.output_parser = output_parser + self.start_context = None async def mail_manager_control(self, refresh_time=1): while not self.structure.execute_stop or not self.executable.execute_stop: @@ -29,6 +22,7 @@ async def mail_manager_control(self, refresh_time=1): self.mailManager.execute_stop = True async def execute(self, context=None): + self.start_context = context self.start.trigger( context=context, structure_id=self.structure.id_, @@ -44,5 +38,9 @@ async def execute(self, context=None): ], ) + self.structure.execute_stop = False + self.executable.execute_stop = False + self.mailManager.execute_stop = False + if self.output_parser: return self.output_parser(self) diff --git a/lionagi/core/branch/__init__.py b/lionagi/core/branch/__init__.py index e69de29bb..72b31e55b 100644 --- a/lionagi/core/branch/__init__.py +++ b/lionagi/core/branch/__init__.py @@ -0,0 +1,4 @@ +from .branch import Branch +from .executable_branch import ExecutableBranch + +__all__ = ["Branch", "ExecutableBranch"] diff --git a/lionagi/core/branch/base_branch.py b/lionagi/core/branch/base_branch.py index 9ea601c75..b5906cd98 100644 --- a/lionagi/core/branch/base_branch.py +++ b/lionagi/core/branch/base_branch.py @@ -1,22 +1,19 @@ from abc import ABC from typing import Any -from lionagi.libs.sys_util import SysUtil, PATH_TYPE +from lionagi.libs.sys_util import PATH_TYPE +from lionagi.libs import convert, dataframe, SysUtil -import lionagi.libs.ln_convert as convert -import lionagi.libs.ln_dataframe as dataframe - -from lionagi.core.schema.base_node import BaseRelatableNode -from lionagi.core.schema.data_logger import DataLogger, DLog -from lionagi.core.messages.schema import ( +from ..schema.base_node import BaseRelatableNode +from ..schema.data_logger import DataLogger, DLog +from ..messages.schema import ( BranchColumns, System, Response, Instruction, BaseMessage, ) -from lionagi.core.branch.util import MessageUtil -from lionagi.libs.ln_parse import ParseUtil +from .util import MessageUtil class BaseBranch(BaseRelatableNode, ABC): @@ -25,9 +22,9 @@ class BaseBranch(BaseRelatableNode, ABC): and logging functionality. Attributes: - messages (dataframe.ln_DataFrame): Holds the messages in the branch. - datalogger (DataLogger): Logs data related to the branch's operation. - persist_path (PATH_TYPE): Filesystem path for data persistence. + messages (dataframe.ln_DataFrame): Holds the messages in the branch. + datalogger (DataLogger): Logs data related to the branch's operation. + persist_path (PATH_TYPE): Filesystem path for data persistence. """ _columns: list[str] = BranchColumns.COLUMNS.value @@ -49,9 +46,7 @@ def __init__( else: self.messages = dataframe.ln_DataFrame(columns=self._columns) - self.datalogger = ( - datalogger if datalogger else DataLogger(persist_path=persist_path) - ) + self.datalogger = datalogger or DataLogger(persist_path=persist_path) self.name = name def add_message( @@ -68,11 +63,11 @@ def add_message( Adds a message to the branch. Args: - system: Information for creating a System message. - instruction: Information for creating an Instruction message. - context: Context information for the message. - response: Response data for creating a message. - **kwargs: Additional keyword arguments for message creation. + system: Information for creating a System message. + instruction: Information for creating an Instruction message. + context: Context information for the message. + response: Response data for creating a message. + **kwargs: Additional keyword arguments for message creation. """ _msg = MessageUtil.create_message( system=system, @@ -87,15 +82,20 @@ def add_message( if isinstance(_msg, System): self.system_node = _msg + # sourcery skip: merge-nested-ifs if isinstance(_msg, Instruction): if recipient is None and self.name is not None: _msg.recipient = self.name if isinstance(_msg, Response): if "action_response" in _msg.content.keys(): - _msg.recipient = recipient or self.name - if 'response' in _msg.content.keys(): - _msg.sender = self.name + if recipient is None and self.name is not None: + _msg.recipient = self.name + if recipient is not None and self.name is None: + _msg.recipient = recipient + if "response" in _msg.content.keys(): + if self.name is not None: + _msg.sender = self.name _msg.content = _msg.msg_content self.messages.loc[len(self.messages)] = _msg.to_pd_series() @@ -108,11 +108,11 @@ def _to_chatcompletion_message( optionally including sender information. Args: - with_sender: Flag to include sender information in the output. + with_sender: Flag to include sender information in the output. Returns: - A list of message dictionaries, each with 'role' and 'content' keys, - and optionally prefixed by 'Sender' if with_sender is True. + A list of message dictionaries, each with 'role' and 'content' keys, + and optionally prefixed by 'Sender' if with_sender is True. """ message = [] @@ -143,7 +143,7 @@ def chat_messages(self) -> list[dict[str, Any]]: Retrieves all chat messages without sender information. Returns: - A list of dictionaries representing chat messages. + A list of dictionaries representing chat messages. """ return self._to_chatcompletion_message() @@ -154,7 +154,7 @@ def chat_messages_with_sender(self) -> list[dict[str, Any]]: Retrieves all chat messages, including sender information. Returns: - A list of dictionaries representing chat messages, each prefixed with its sender. + A list of dictionaries representing chat messages, each prefixed with its sender. """ return self._to_chatcompletion_message(with_sender=True) @@ -165,7 +165,7 @@ def last_message(self) -> dataframe.ln_DataFrame: Retrieves the last message from the branch as a pandas Series. Returns: - A pandas Series representing the last message in the branch. + A pandas Series representing the last message in the branch. """ return MessageUtil.get_message_rows(self.messages, n=1, from_="last") @@ -176,7 +176,7 @@ def last_message_content(self) -> dict[str, Any]: Extracts the content of the last message in the branch. Returns: - A dictionary representing the content of the last message. + A dictionary representing the content of the last message. """ return convert.to_dict(self.messages.content.iloc[-1]) @@ -187,7 +187,7 @@ def first_system(self) -> dataframe.ln_DataFrame: Retrieves the first message marked with the 'system' role. Returns: - A pandas Series representing the first 'system' message in the branch. + A pandas Series representing the first 'system' message in the branch. """ return MessageUtil.get_message_rows( @@ -200,7 +200,7 @@ def last_response(self) -> dataframe.ln_DataFrame: Retrieves the last message marked with the 'assistant' role. Returns: - A pandas Series representing the last 'assistant' (response) message in the branch. + A pandas Series representing the last 'assistant' (response) message in the branch. """ return MessageUtil.get_message_rows( @@ -213,7 +213,7 @@ def last_response_content(self) -> dict[str, Any]: Extracts the content of the last 'assistant' (response) message. Returns: - A dictionary representing the content of the last 'assistant' message. + A dictionary representing the content of the last 'assistant' message. """ return convert.to_dict(self.last_response.content.iloc[-1]) @@ -224,7 +224,7 @@ def action_request(self) -> dataframe.ln_DataFrame: Filters and retrieves all messages sent by 'action_request'. Returns: - A pandas DataFrame containing all 'action_request' messages. + A pandas DataFrame containing all 'action_request' messages. """ return convert.to_df(self.messages[self.messages.sender == "action_request"]) @@ -235,7 +235,7 @@ def action_response(self) -> dataframe.ln_DataFrame: Filters and retrieves all messages sent by 'action_response'. Returns: - A pandas DataFrame containing all 'action_response' messages. + A pandas DataFrame containing all 'action_response' messages. """ return convert.to_df(self.messages[self.messages.sender == "action_response"]) @@ -246,7 +246,7 @@ def responses(self) -> dataframe.ln_DataFrame: Retrieves all messages marked with the 'assistant' role. Returns: - A pandas DataFrame containing all messages with an 'assistant' role. + A pandas DataFrame containing all messages with an 'assistant' role. """ return convert.to_df(self.messages[self.messages.role == "assistant"]) @@ -257,7 +257,7 @@ def assistant_responses(self) -> dataframe.ln_DataFrame: Filters 'assistant' role messages excluding 'action_request' and 'action_response'. Returns: - A pandas DataFrame of 'assistant' messages excluding action requests/responses. + A pandas DataFrame of 'assistant' messages excluding action requests/responses. """ a_responses = self.responses[self.responses.sender != "action_response"] @@ -274,7 +274,7 @@ def info(self) -> dict[str, Any]: Summarizes branch information, including message counts by role. Returns: - A dictionary containing counts of messages categorized by their role. + A dictionary containing counts of messages categorized by their role. """ return self._info() @@ -285,7 +285,7 @@ def sender_info(self) -> dict[str, int]: Provides a summary of message counts categorized by sender. Returns: - A dictionary with senders as keys and counts of their messages as values. + A dictionary with senders as keys and counts of their messages as values. """ return self._info(use_sender=True) @@ -296,8 +296,8 @@ def describe(self) -> dict[str, Any]: Provides a detailed description of the branch, including a summary of messages. Returns: - A dictionary with a summary of total messages, a breakdown by role, and - a preview of the first five messages. + A dictionary with a summary of total messages, a breakdown by role, and + a preview of the first five messages. """ return { @@ -344,13 +344,13 @@ def to_csv_file( Exports the branch messages to a CSV file. Args: - filepath: Destination path for the CSV file. Defaults to 'messages.csv'. - dir_exist_ok: If False, an error is raised if the directory exists. Defaults to True. - timestamp: If True, appends a timestamp to the filename. Defaults to True. - time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. - verbose: If True, prints a message upon successful export. Defaults to True. - clear: If True, clears the messages after exporting. Defaults to True. - **kwargs: Additional keyword arguments for pandas.DataFrame.to_csv(). + filepath: Destination path for the CSV file. Defaults to 'messages.csv'. + dir_exist_ok: If False, an error is raised if the directory exists. Defaults to True. + timestamp: If True, appends a timestamp to the filename. Defaults to True. + time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. + verbose: If True, prints a message upon successful export. Defaults to True. + clear: If True, clears the messages after exporting. Defaults to True. + **kwargs: Additional keyword arguments for pandas.DataFrame.to_csv(). """ if not filename.endswith(".csv"): @@ -371,7 +371,7 @@ def to_csv_file( if clear: self.clear_messages() except Exception as e: - raise ValueError(f"Error in saving to csv: {e}") + raise ValueError(f"Error in saving to csv: {e}") from e def to_json_file( self, @@ -387,13 +387,13 @@ def to_json_file( Exports the branch messages to a JSON file. Args: - filename: Destination path for the JSON file. Defaults to 'messages.json'. - dir_exist_ok: If False, an error is raised if the dirctory exists. Defaults to True. - timestamp: If True, appends a timestamp to the filename. Defaults to True. - time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. - verbose: If True, prints a message upon successful export. Defaults to True. - clear: If True, clears the messages after exporting. Defaults to True. - **kwargs: Additional keyword arguments for pandas.DataFrame.to_json(). + filename: Destination path for the JSON file. Defaults to 'messages.json'. + dir_exist_ok: If False, an error is raised if the dirctory exists. Defaults to True. + timestamp: If True, appends a timestamp to the filename. Defaults to True. + time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. + verbose: If True, prints a message upon successful export. Defaults to True. + clear: If True, clears the messages after exporting. Defaults to True. + **kwargs: Additional keyword arguments for pandas.DataFrame.to_json(). """ if not filename.endswith(".json"): @@ -416,7 +416,7 @@ def to_json_file( if clear: self.clear_messages() except Exception as e: - raise ValueError(f"Error in saving to json: {e}") + raise ValueError(f"Error in saving to json: {e}") from e def log_to_csv( self, @@ -434,13 +434,13 @@ def log_to_csv( Exports the data logger contents to a CSV file. Args: - filename: Destination path for the CSV file. Defaults to 'log.csv'. - dir_exist_ok: If False, an error is raised if the directory exists. Defaults to True. - timestamp: If True, appends a timestamp to the filename. Defaults to True. - time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. - verbose: If True, prints a message upon successful export. Defaults to True. - clear: If True, clears the logger after exporting. Defaults to True. - **kwargs: Additional keyword arguments for pandas.DataFrame.to_csv(). + filename: Destination path for the CSV file. Defaults to 'log.csv'. + dir_exist_ok: If False, an error is raised if the directory exists. Defaults to True. + timestamp: If True, appends a timestamp to the filename. Defaults to True. + time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. + verbose: If True, prints a message upon successful export. Defaults to True. + clear: If True, clears the logger after exporting. Defaults to True. + **kwargs: Additional keyword arguments for pandas.DataFrame.to_csv(). """ self.datalogger.to_csv_file( filename=filename, @@ -470,13 +470,13 @@ def log_to_json( Exports the data logger contents to a JSON file. Args: - filename: Destination path for the JSON file. Defaults to 'log.json'. - dir_exist_ok: If False, an error is raised if the directory exists. Defaults to True. - timestamp: If True, appends a timestamp to the filename. Defaults to True. - time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. - verbose: If True, prints a message upon successful export. Defaults to True. - clear: If True, clears the logger after exporting. Defaults to True. - **kwargs: Additional keyword arguments for pandas.DataFrame.to_json(). + filename: Destination path for the JSON file. Defaults to 'log.json'. + dir_exist_ok: If False, an error is raised if the directory exists. Defaults to True. + timestamp: If True, appends a timestamp to the filename. Defaults to True. + time_prefix: If True, prefixes the filename with a timestamp. Defaults to False. + verbose: If True, prints a message upon successful export. Defaults to True. + clear: If True, clears the logger after exporting. Defaults to True. + **kwargs: Additional keyword arguments for pandas.DataFrame.to_json(). """ self.datalogger.to_json_file( @@ -513,14 +513,14 @@ def load_log(self, filename, flattened=True, sep="[^_^]", verbose=True, **kwargs if verbose: print(f"Loaded {len(df)} logs from {filename}") except Exception as e: - raise ValueError(f"Error in loading log: {e}") + raise ValueError(f"Error in loading log: {e}") from e def remove_message(self, node_id: str) -> None: """ Removes a message from the branch based on its node ID. Args: - node_id: The unique identifier of the message to be removed. + node_id: The unique identifier of the message to be removed. """ MessageUtil.remove_message(self.messages, node_id) @@ -529,9 +529,9 @@ def update_message(self, node_id: str, column: str, value: Any) -> bool: Updates a specific column of a message identified by node_id with a new value. Args: - value: The new value to update the message with. - node_id: The unique identifier of the message to update. - column: The column of the message to update. + value: The new value to update the message with. + node_id: The unique identifier of the message to update. + column: The column of the message to update. """ index = self.messages[self.messages["node_id"] == node_id].index[0] @@ -547,8 +547,8 @@ def change_first_system_message( Updates the first system message with new content and/or sender. Args: - system: The new system message content or a System object. - sender: The identifier of the sender for the system message. + system: The new system message content or a System object. + sender: The identifier of the sender for the system message. """ if len(self.messages[self.messages["role"] == "system"]) == 0: @@ -570,7 +570,7 @@ def rollback(self, steps: int) -> None: Removes the last 'n' messages from the branch. Args: - steps: The number of messages to remove from the end. + steps: The number of messages to remove from the end. """ self.messages = dataframe.remove_last_n_rows(self.messages, steps) @@ -642,10 +642,10 @@ def _info(self, use_sender: bool = False) -> dict[str, int]: Helper method to generate summaries of messages either by role or sender. Args: - use_sender: If True, summary is categorized by sender. Otherwise, by role. + use_sender: If True, summary is categorized by sender. Otherwise, by role. Returns: - A dictionary summarizing the count of messages either by role or sender. + A dictionary summarizing the count of messages either by role or sender. """ messages = self.messages["sender"] if use_sender else self.messages["role"] diff --git a/lionagi/core/branch/branch.py b/lionagi/core/branch/branch.py index b8eb214ed..b12a92b28 100644 --- a/lionagi/core/branch/branch.py +++ b/lionagi/core/branch/branch.py @@ -2,27 +2,22 @@ from typing import Any, Union, TypeVar, Callable from lionagi.libs.sys_util import PATH_TYPE -from lionagi.libs.ln_api import StatusTracker, BaseService -from lionagi.libs import ln_convert as convert -from lionagi.libs import ln_dataframe as dataframe +from lionagi.libs import StatusTracker, BaseService, convert, dataframe -from lionagi.core.schema.base_node import TOOL_TYPE, Tool -from lionagi.core.schema.data_logger import DataLogger -from lionagi.core.tool.tool_manager import ToolManager, func_to_tool +from ..schema import TOOL_TYPE, Tool, DataLogger +from ..tool import ToolManager, func_to_tool -from lionagi.core.branch.base_branch import BaseBranch -from lionagi.core.messages.schema import System -from lionagi.core.mail.schema import BaseMail - -from lionagi.core.branch.util import MessageUtil +from ..messages import System +from ..mail import BaseMail +from .util import MessageUtil +from .base_branch import BaseBranch from .branch_flow_mixin import BranchFlowMixin from dotenv import load_dotenv load_dotenv() - T = TypeVar("T", bound=Tool) @@ -38,8 +33,7 @@ def __init__( llmconfig: dict[str, str | int | dict] | None = None, tools: list[Callable | Tool] | None = None, datalogger: None | DataLogger = None, - persist_path: PATH_TYPE | None = None, - # instruction_sets=None, + persist_path: PATH_TYPE | None = None, # instruction_sets=None, tool_manager: ToolManager | None = None, **kwargs, ): @@ -57,7 +51,7 @@ def __init__( self.sender = sender or "system" # add tool manager and register tools - self.tool_manager = tool_manager if tool_manager else ToolManager() + self.tool_manager = tool_manager or ToolManager() if tools: try: tools_ = [] @@ -70,7 +64,7 @@ def __init__( self.register_tools(tools_) except Exception as e: - raise TypeError(f"Error in registering tools: {e}") + raise TypeError(f"Error in registering tools: {e}") from e # add service and llmconfig self.service, self.llmconfig = self._add_service(service, llmconfig) @@ -96,14 +90,13 @@ def from_csv( llmconfig: dict[str, str | int | dict] | None = None, tools: TOOL_TYPE | None = None, datalogger: None | DataLogger = None, - persist_path: PATH_TYPE | None = None, - # instruction_sets=None, + persist_path: PATH_TYPE | None = None, # instruction_sets=None, tool_manager: ToolManager | None = None, read_kwargs=None, **kwargs, ): - self = cls._from_csv( + return cls._from_csv( filepath=filepath, read_kwargs=read_kwargs, name=name, @@ -117,8 +110,6 @@ def from_csv( **kwargs, ) - return self - @classmethod def from_json_string( cls, @@ -128,14 +119,13 @@ def from_json_string( llmconfig: dict[str, str | int | dict] | None = None, tools: TOOL_TYPE | None = None, datalogger: None | DataLogger = None, - persist_path: PATH_TYPE | None = None, - # instruction_sets=None, + persist_path: PATH_TYPE | None = None, # instruction_sets=None, tool_manager: ToolManager | None = None, read_kwargs=None, **kwargs, ): - self = cls._from_json( + return cls._from_json( filepath=filepath, read_kwargs=read_kwargs, name=name, @@ -149,8 +139,6 @@ def from_json_string( **kwargs, ) - return self - def messages_describe(self) -> dict[str, Any]: return dict( @@ -301,7 +289,7 @@ def _is_invoked(self) -> bool: Check if the conversation has been invoked with an action response. Returns: - bool: True if the conversation has been invoked, False otherwise. + bool: True if the conversation has been invoked, False otherwise. """ content = self.messages.iloc[-1]["content"] @@ -312,5 +300,5 @@ def _is_invoked(self) -> bool: "output", }: return True - except: + except Exception: return False diff --git a/lionagi/core/branch/branch_flow_mixin.py b/lionagi/core/branch/branch_flow_mixin.py index c9273679a..4490ec3c4 100644 --- a/lionagi/core/branch/branch_flow_mixin.py +++ b/lionagi/core/branch/branch_flow_mixin.py @@ -1,12 +1,9 @@ from abc import ABC from typing import Any, Optional, Union, TypeVar -from lionagi.core.schema.base_node import TOOL_TYPE, Tool -from lionagi.core.flow.monoflow.chat import MonoChat -from lionagi.core.flow.monoflow.followup import MonoFollowup -from lionagi.core.flow.monoflow.ReAct import MonoReAct - -from lionagi.core.messages.schema import Instruction, System +from ..schema import TOOL_TYPE, Tool +from ..messages import Instruction, System +from ..flow.monoflow import MonoChat, MonoFollowup, MonoReAct T = TypeVar("T", bound=Tool) @@ -25,7 +22,6 @@ async def chat( output_fields=None, **kwargs, ) -> Any: - flow = MonoChat(self) return await flow.chat( instruction=instruction, diff --git a/lionagi/core/branch/executable_branch.py b/lionagi/core/branch/executable_branch.py index 25853af8f..e34a872dd 100644 --- a/lionagi/core/branch/executable_branch.py +++ b/lionagi/core/branch/executable_branch.py @@ -1,27 +1,20 @@ +import contextlib from collections import deque from typing import Any -from IPython.display import Markdown, display +from lionagi.libs import convert, AsyncUtil, ParseUtil -import lionagi.libs.ln_convert as convert -from lionagi.libs.ln_async import AsyncUtil -from lionagi.libs.ln_parse import ParseUtil +from ..schema import BaseRelatableNode, ActionNode +from ..mail import BaseMail +from ..messages import System, Instruction +from ..agent import BaseAgent -from lionagi.core.schema.base_node import BaseRelatableNode -from lionagi.core.schema.action_node import ActionNode - -from lionagi.core.mail.schema import BaseMail - -from lionagi.core.messages.schema import System, Instruction - - -from lionagi import Branch -from lionagi.core.agent.base_agent import BaseAgent +from .branch import Branch class ExecutableBranch(BaseRelatableNode): - def __init__(self, **kwargs): + def __init__(self, verbose=True, **kwargs): super().__init__() self.branch: Branch = Branch(**kwargs) self.pending_ins = {} # needed @@ -29,6 +22,8 @@ def __init__(self, **kwargs): self.responses = [] self.execute_stop = False # needed self.context = None # needed + self.context_log = [] + self.verbose = verbose def send(self, recipient_id: str, category: str, package: Any) -> None: mail = BaseMail( @@ -45,8 +40,12 @@ async def forward(self): mail = self.pending_ins[key].popleft() if mail.category == "start": # needed self._process_start(mail) - if mail.category == "node": + elif mail.category == "node": await self._process_node(mail) + elif mail.category == "node_list": + self._process_node_list(mail) + elif mail.category == "condition": + self._process_condition(mail) elif mail.category == "end": # needed self._process_end(mail) @@ -58,28 +57,40 @@ async def execute(self, refresh_time=1): # needed async def _process_node(self, mail: BaseMail): if isinstance(mail.package, System): - self._system_process(mail.package) + self._system_process(mail.package, verbose=self.verbose) self.send(mail.sender_id, "node_id", mail.package.id_) - return elif isinstance(mail.package, Instruction): - await self._instruction_process(mail.package) + await self._instruction_process(mail.package, verbose=self.verbose) self.send(mail.sender_id, "node_id", mail.package.id_) - return elif isinstance(mail.package, ActionNode): - await self._action_process(mail.package) + await self._action_process(mail.package, verbose=self.verbose) self.send(mail.sender_id, "node_id", mail.package.instruction.id_) - return + else: + try: + await self._agent_process(mail.package, verbose=self.verbose) + self.send(mail.sender_id, "node_id", mail.package.id_) + except: + raise ValueError(f"Invalid mail to process. Mail:{mail}") - elif isinstance(mail.package, BaseAgent): - await self._agent_process(mail.package) - self.send(mail.sender_id, "node_id", mail.package.id_) - return + def _process_node_list(self, mail: BaseMail): + self.send(mail.sender_id, "end", "end") + self.execute_stop = True + raise ValueError("Multiple path selection is currently not supported") + + def _process_condition(self, mail: BaseMail): + relationship = mail.package + check_result = relationship.condition(self) + back_mail = {"relationship_id": mail.package.id_, "check_result": check_result} + self.send(mail.sender_id, "condition", back_mail) def _system_process(self, system: System, verbose=True, context_verbose=False): + from lionagi.libs import SysUtil + SysUtil.check_import('IPython') + from IPython.display import Markdown, display if verbose: - print(f"---------------Welcome: {system.recipient}------------------") + print(f"------------------Welcome: {system.sender}--------------------") display(Markdown(f"system: {convert.to_str(system.system_info)}")) if self.context and context_verbose: display(Markdown(f"context: {convert.to_str(self.context)}")) @@ -89,6 +100,9 @@ def _system_process(self, system: System, verbose=True, context_verbose=False): async def _instruction_process( self, instruction: Instruction, verbose=True, **kwargs ): + from lionagi.libs import SysUtil + SysUtil.check_import('IPython') + from IPython.display import Markdown, display if verbose: display( Markdown( @@ -101,54 +115,41 @@ async def _instruction_process( self.context = None result = await self.branch.chat(instruction, **kwargs) - try: + with contextlib.suppress(Exception): result = ParseUtil.fuzzy_parse_json(result) if "response" in result.keys(): result = result["response"] - except: - pass - - if verbose: + if verbose and len(self.branch.assistant_responses) != 0: display( Markdown( f"{self.branch.last_assistant_response.sender}: {convert.to_str(result)}" ) ) + print("-----------------------------------------------------") self.responses.append(result) - async def _agent_process(self, agent): - context = self.responses - result = await agent.execute(context) - - self.context = result - self.responses.append(result) - - def _process_start(self, mail): - start_mail_content = mail.package - self.context = start_mail_content["context"] - self.send(start_mail_content["structure_id"], "start", "start") - - def _process_end(self, mail): - self.execute_stop = True - self.send(mail.sender_id, "end", "end") - - async def _action_process(self, action: ActionNode): - # instruction = action.instruction - # if self.context: - # instruction.content.update({"context": self.context}) - # self.context=None + async def _action_process(self, action: ActionNode, verbose=True): + from lionagi.libs import SysUtil + SysUtil.check_import('IPython') + from IPython.display import Markdown, display try: func = getattr(self.branch, action.action) except: raise ValueError(f"{action.action} is not a valid action") + if verbose: + display( + Markdown( + f"{action.instruction.sender}: {convert.to_str(action.instruction.instruct)}" + ) + ) + if action.tools: self.branch.register_tools(action.tools) - # result = await func(instruction, tools=action.tools, **action.action_kwargs) if self.context: result = await func( - action.instruction.content, + action.instruction.content["instruction"], context=self.context, tools=action.tools, **action.action_kwargs, @@ -158,5 +159,34 @@ async def _action_process(self, action: ActionNode): result = await func( action.instruction.content, tools=action.tools, **action.action_kwargs ) - print("action calls:", result) + + if verbose and len(self.branch.assistant_responses) != 0: + display( + Markdown( + f"{self.branch.last_assistant_response.sender}: {convert.to_str(result)}" + ) + ) + print("-----------------------------------------------------") + self.responses.append(result) + + async def _agent_process(self, agent, verbose=True): + context = self.responses + if verbose: + print("*****************************************************") + result = await agent.execute(context) + + if verbose: + print("*****************************************************") + + self.context = result + self.responses.append(result) + + def _process_start(self, mail): + start_mail_content = mail.package + self.context = start_mail_content["context"] + self.send(start_mail_content["structure_id"], "start", "start") + + def _process_end(self, mail): + self.execute_stop = True + self.send(mail.sender_id, "end", "end") diff --git a/lionagi/core/branch/util.py b/lionagi/core/branch/util.py index d8722d622..f4716b0ce 100644 --- a/lionagi/core/branch/util.py +++ b/lionagi/core/branch/util.py @@ -1,10 +1,8 @@ +import contextlib from datetime import datetime from typing import Any -from lionagi.libs import ln_convert as convert -from lionagi.libs import ln_nested as nested -from lionagi.libs import ln_func_call as func_call -from lionagi.libs import ln_dataframe as dataframe +from lionagi.libs import convert, nested, func_call, dataframe from lionagi.core.messages.schema import ( System, @@ -14,7 +12,6 @@ BranchColumns, ) - CUSTOM_TYPE = dict[str, Any] | str | list[Any] | None @@ -33,42 +30,41 @@ def create_message( Creates a message object based on the input parameters, ensuring only one message role is present. Args: - system: Information for creating a System message. - instruction: Information for creating an Instruction message. - context: Context information for the message. - response: Response data for creating a message. - **kwargs: Additional keyword arguments for message creation. + system: Information for creating a System message. + instruction: Information for creating an Instruction message. + context: Context information for the message. + response: Response data for creating a message. + **kwargs: Additional keyword arguments for message creation. Returns: - A message object of the appropriate type based on provided inputs. + A message object of the appropriate type based on provided inputs. Raises: - ValueError: If more than one of the role-specific parameters are provided. + ValueError: If more than one of the role-specific parameters are provided. """ if sum(func_call.lcall([system, instruction, response], bool)) != 1: raise ValueError("Error: Message must have one and only one role.") - else: - if isinstance(system, System): - return system - elif isinstance(instruction, Instruction): - return instruction - elif isinstance(response, Response): - return response - - msg = 0 - if response: - msg = Response(response=response, **kwargs) - elif instruction: - msg = Instruction( - instruction=instruction, - context=context, - output_fields=output_fields, - **kwargs, - ) - elif system: - msg = System(system=system, **kwargs) - return msg + if isinstance(system, System): + return system + elif isinstance(instruction, Instruction): + return instruction + elif isinstance(response, Response): + return response + + msg = 0 + if response: + msg = Response(response=response, **kwargs) + elif instruction: + msg = Instruction( + instruction=instruction, + context=context, + output_fields=output_fields, + **kwargs, + ) + elif system: + msg = System(system=system, **kwargs) + return msg @staticmethod def validate_messages(messages: dataframe.ln_DataFrame) -> bool: @@ -76,21 +72,21 @@ def validate_messages(messages: dataframe.ln_DataFrame) -> bool: Validates the format and content of a DataFrame containing messages. Args: - messages: A DataFrame with message information. + messages: A DataFrame with message information. Returns: - True if the messages DataFrame is correctly formatted, False otherwise. + True if the messages DataFrame is correctly formatted, False otherwise. Raises: - ValueError: If the DataFrame does not match expected schema or content requirements. + ValueError: If the DataFrame does not match expected schema or content requirements. """ if list(messages.columns) != BranchColumns.COLUMNS.value: raise ValueError("Invalid messages dataframe. Unmatched columns.") if messages.isnull().values.any(): raise ValueError("Invalid messages dataframe. Cannot have null.") - if not all( - role in ["system", "user", "assistant"] + if any( + role not in ["system", "user", "assistant"] for role in messages["role"].unique() ): raise ValueError( @@ -115,14 +111,14 @@ def sign_message( Appends a sender prefix to the 'content' field of each message in a DataFrame. Args: - messages: A DataFrame containing message data. - sender: The identifier of the sender to prefix to message contents. + messages: A DataFrame containing message data. + sender: The identifier of the sender to prefix to message contents. Returns: - A DataFrame with sender-prefixed message contents. + A DataFrame with sender-prefixed message contents. Raises: - ValueError: If the sender is None or the value is 'none'. + ValueError: If the sender is None or the value is 'none'. """ if sender is None or convert.strip_lower(sender) == "none": @@ -152,16 +148,16 @@ def filter_messages_by( Filters messages in a DataFrame based on specified criteria. Args: - messages: The DataFrame to filter. - role: The role to filter by. - sender: The sender to filter by. - start_time: The minimum timestamp for messages. - end_time: The maximum timestamp for messages. - content_keywords: Keywords to look for in message content. - case_sensitive: Whether the keyword search should be case-sensitive. + messages: The DataFrame to filter. + role: The role to filter by. + sender: The sender to filter by. + start_time: The minimum timestamp for messages. + end_time: The maximum timestamp for messages. + content_keywords: Keywords to look for in message content. + case_sensitive: Whether the keyword search should be case-sensitive. Returns: - A filtered DataFrame based on the specified criteria. + A filtered DataFrame based on the specified criteria. """ try: @@ -180,7 +176,7 @@ def filter_messages_by( return convert.to_df(outs) except Exception as e: - raise ValueError(f"Error in filtering messages: {e}") + raise ValueError(f"Error in filtering messages: {e}") from e @staticmethod def remove_message(messages: dataframe.ln_DataFrame, node_id: str) -> bool: @@ -188,15 +184,15 @@ def remove_message(messages: dataframe.ln_DataFrame, node_id: str) -> bool: Removes a message from the DataFrame based on its node ID. Args: - messages: The DataFrame containing messages. - node_id: The unique identifier of the message to be removed. + messages: The DataFrame containing messages. + node_id: The unique identifier of the message to be removed. Returns: - If any messages are removed. + If any messages are removed. Examples: - >>> messages = dataframe.ln_DataFrame([...]) - >>> updated_messages = MessageUtil.remove_message(messages, "node_id_123") + >>> messages = dataframe.ln_DataFrame([...]) + >>> updated_messages = MessageUtil.remove_message(messages, "node_id_123") """ initial_length = len(messages) @@ -218,15 +214,15 @@ def get_message_rows( Retrieves a specified number of message rows based on sender and role. Args: - messages: The DataFrame containing messages. - sender: Filter messages by the sender. - role: Filter messages by the role. - n: The number of messages to retrieve. - sign_: If True, sign the message with the sender. - from_: Specify retrieval from the 'front' or 'last' of the DataFrame. + messages: The DataFrame containing messages. + sender: Filter messages by the sender. + role: Filter messages by the role. + n: The number of messages to retrieve. + sign_: If True, sign the message with the sender. + from_: Specify retrieval from the 'front' or 'last' of the DataFrame. Returns: - A DataFrame containing the filtered messages. + A DataFrame containing the filtered messages. """ outs = "" @@ -266,17 +262,17 @@ def extend( Extends a DataFrame with another DataFrame's rows, ensuring no duplicate 'node_id'. Args: - df1: The primary DataFrame. - df2: The DataFrame to merge with the primary DataFrame. - **kwargs: Additional keyword arguments for `drop_duplicates`. + df1: The primary DataFrame. + df2: The DataFrame to merge with the primary DataFrame. + **kwargs: Additional keyword arguments for `drop_duplicates`. Returns: - A DataFrame combined from df1 and df2 with duplicates removed based on 'node_id'. + A DataFrame combined from df1 and df2 with duplicates removed based on 'node_id'. Examples: - >>> df_main = dataframe.ln_DataFrame([...]) - >>> df_additional = dataframe.ln_DataFrame([...]) - >>> combined_df = MessageUtil.extend(df_main, df_additional, keep='first') + >>> df_main = dataframe.ln_DataFrame([...]) + >>> df_additional = dataframe.ln_DataFrame([...]) + >>> combined_df = MessageUtil.extend(df_main, df_additional, keep='first') """ MessageUtil.validate_messages(df2) @@ -288,7 +284,7 @@ def extend( ) return convert.to_df(df) except Exception as e: - raise ValueError(f"Error in extending messages: {e}") + raise ValueError(f"Error in extending messages: {e}") from e @staticmethod def to_markdown_string(messages: dataframe.ln_DataFrame) -> str: @@ -296,11 +292,11 @@ def to_markdown_string(messages: dataframe.ln_DataFrame) -> str: Converts messages in a DataFrame to a Markdown-formatted string for easy reading. Args: - messages: A DataFrame containing messages with columns for 'role' and 'content'. + messages: A DataFrame containing messages with columns for 'role' and 'content'. Returns: - A string formatted in Markdown, where each message's content is presented - according to its role in a readable format. + A string formatted in Markdown, where each message's content is presented + according to its role in a readable format. """ answers = [] @@ -308,101 +304,20 @@ def to_markdown_string(messages: dataframe.ln_DataFrame) -> str: content = convert.to_dict(i.content) if i.role == "assistant": - try: + with contextlib.suppress(Exception): a = nested.nget(content, ["action_response", "func"]) b = nested.nget(content, ["action_response", "arguments"]) c = nested.nget(content, ["action_response", "output"]) if a is not None: - answers.append(f"Function: {a}") - answers.append(f"Arguments: {b}") - answers.append(f"Output: {c}") + answers.extend( + (f"Function: {a}", f"Arguments: {b}", f"Output: {c}") + ) else: answers.append(nested.nget(content, ["assistant_response"])) - except: - pass elif i.role == "user": - try: + with contextlib.suppress(Exception): answers.append(nested.nget(content, ["instruction"])) - except: - pass else: - try: + with contextlib.suppress(Exception): answers.append(nested.nget(content, ["system_info"])) - except: - pass - - out_ = "\n".join(answers) - return out_ - - # @staticmethod - # def to_json_content(value): - # if isinstance(value, dict): - # for key, val in value.items(): - # value[key] = MessageUtil.to_json_content(val) - # value = json.dumps(value) - # if isinstance(value, list): - # for i in range(len(value)): - # value[i] = MessageUtil.to_json_content(value[i]) - # return value - - # @staticmethod - # def to_dict_content(value): - # try: - # value = json.loads(value) - # if isinstance(value, dict): - # for key, val in value.items(): - # value[key] = MessageUtil.to_dict_content(val) - # if isinstance(value, list): - # for i in range(len(value)): - # value[i] = MessageUtil.to_dict_content(value[i]) - # return value - # except: - # return value - - # @staticmethod - # def response_to_message(response: dict[str, Any], **kwargs) -> Any: - # """ - # Processes a message response dictionary to generate an appropriate message object. - - # Args: - # response: A dictionary potentially containing message information. - # **kwargs: Additional keyword arguments to pass to the message constructors. - - # Returns: - # An instance of a message class, such as ActionRequest or AssistantResponse, - # depending on the content of the response. - # """ - # try: - # response = response["message"] - # if .strip_lower(response['content']) == "none": - - # content = ActionRequest._handle_action_request(response) - # return ActionRequest(action_request=content, **kwargs) - - # else: - - # try: - # if 'tool_uses' in to_dict(response[MessageField.CONTENT.value]): - # content_ = to_dict(response[MessageField.CONTENT.value])[ - # 'tool_uses'] - # return ActionRequest(action_request=content_, **kwargs) - - # elif MessageContentKey.RESPONSE.value in to_dict( - # response[MessageField.CONTENT.value]): - # content_ = to_dict(response[MessageField.CONTENT.value])[ - # MessageContentKey.RESPONSE.value] - # return AssistantResponse(assistant_response=content_, **kwargs) - - # elif MessageContentKey.ACTION_REQUEST.value in to_dict( - # response[MessageField.CONTENT.value]): - # content_ = to_dict(response[MessageField.CONTENT.value])[ - # MessageContentKey.ACTION_REQUEST.value] - # return ActionRequest(action_request=content_, **kwargs) - - # else: - # return AssistantResponse(assistant_response=response, **kwargs) - - # except: - # return AssistantResponse(assistant_response=response, **kwargs) - # except: - # return ActionResponse(action_response=response, **kwargs) + return "\n".join(answers) diff --git a/lionagi/core/flow/direct/__init__.py b/lionagi/core/direct/__init__.py similarity index 99% rename from lionagi/core/flow/direct/__init__.py rename to lionagi/core/direct/__init__.py index b0f58413a..3e6db4eb6 100644 --- a/lionagi/core/flow/direct/__init__.py +++ b/lionagi/core/direct/__init__.py @@ -10,4 +10,4 @@ "score", "sentiment", "react", -] \ No newline at end of file +] diff --git a/lionagi/core/flow/direct/predict.py b/lionagi/core/direct/parallel_predict.py similarity index 58% rename from lionagi/core/flow/direct/predict.py rename to lionagi/core/direct/parallel_predict.py index 914f9a5d6..ff141aed1 100644 --- a/lionagi/core/flow/direct/predict.py +++ b/lionagi/core/direct/parallel_predict.py @@ -1,19 +1,22 @@ -from lionagi.libs import ln_func_call as func_call -from lionagi.core.branch.branch import Branch -from .utils import _handle_single_out +from lionagi.libs import func_call +from ..branch import Branch +from ..session import Session +from .utils import _handle_single_out, _handle_multi_out -async def predict( +async def parallel_predict( sentence, *, num_sentences=1, default_key="answer", confidence_score=False, reason=False, - retry_kwargs={}, + retry_kwargs=None, **kwargs, ): - return await _force_predict( + if retry_kwargs is None: + retry_kwargs = {} + return await _force_parallel_predict( sentence, num_sentences, default_key, @@ -24,19 +27,26 @@ async def predict( ) -async def _force_predict( +async def _force_parallel_predict( sentence, num_sentences, default_key="answer", confidence_score=False, reason=False, retry_kwargs={}, + include_mapping=False, **kwargs, ): async def _inner(): - out_ = await _predict( - sentence, num_sentences, default_key, confidence_score, reason, **kwargs + out_ = await _parallel_predict( + sentence=sentence, + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + include_mapping=include_mapping, + **kwargs, ) if out_ is None: raise ValueError("No output from the model") @@ -81,25 +91,37 @@ def _create_predict_config( return instruct, output_fields, kwargs -async def _predict( +async def _parallel_predict( sentence, num_sentences, default_key="answer", confidence_score=False, reason=False, + include_mapping=False, **kwargs, ): _instruct, _output_fields, _kwargs = _create_predict_config( - num_sentences=num_sentences, default_key=default_key, - confidence_score=confidence_score, reason=reason, **kwargs, + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + **kwargs, ) - branch = Branch() + session = Session() - out_ = await branch.chat( - _instruct, context=sentence, output_fields=_output_fields, **_kwargs + out_ = await session.parallel_chat( + _instruct, + context=sentence, + output_fields=_output_fields, + include_mapping=include_mapping, + **_kwargs, ) - return _handle_single_out( - out_, default_key=default_key, to_type="str", to_default=True + return _handle_multi_out( + out_, + default_key=default_key, + to_type="str", + to_default=True, + include_mapping=include_mapping, ) diff --git a/lionagi/core/direct/parallel_react.py b/lionagi/core/direct/parallel_react.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/direct/parallel_score.py b/lionagi/core/direct/parallel_score.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/direct/parallel_select.py b/lionagi/core/direct/parallel_select.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/direct/parallel_sentiment.py b/lionagi/core/direct/parallel_sentiment.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/direct/predict.py b/lionagi/core/direct/predict.py new file mode 100644 index 000000000..2e7e48cdd --- /dev/null +++ b/lionagi/core/direct/predict.py @@ -0,0 +1,174 @@ +from lionagi.libs import func_call +from ..branch import Branch +from ..session import Session +from .utils import _handle_single_out, _handle_multi_out + + +async def predict( + sentence, + *, + num_sentences=1, + default_key="answer", + confidence_score=False, + reason=False, + retry_kwargs=None, + include_mapping=False, + **kwargs, +): + if retry_kwargs is None: + retry_kwargs = {} + return await _force_predict( + sentence=sentence, + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + retry_kwargs=retry_kwargs, + include_mapping=include_mapping, + **kwargs, + ) + + +async def _force_predict( + sentence, + num_sentences, + default_key="answer", + confidence_score=False, + reason=False, + retry_kwargs={}, + include_mapping=False, + **kwargs, +): + + async def _inner1(): + out_ = await _predict( + sentence=sentence, + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + **kwargs, + ) + if out_ is None: + raise ValueError("No output from the model") + + return out_ + + async def _inner2(): + out_ = await _parallel_predict( + sentence=sentence, + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + include_mapping=include_mapping, + **kwargs, + ) + + if out_ is None: + raise ValueError("No output from the model") + + return out_ + + if "retries" not in retry_kwargs: + retry_kwargs["retries"] = 2 + + if "delay" not in retry_kwargs: + retry_kwargs["delay"] = 0.5 + + if (isinstance(sentence, (list, tuple)) and len(sentence) > 1) or include_mapping: + return await func_call.rcall(_inner2, **retry_kwargs) + + return await func_call.rcall(_inner1, **retry_kwargs) + + +def _create_predict_config( + num_sentences, + default_key="answer", + confidence_score=False, + reason=False, + **kwargs, +): + instruct = { + "task": f"predict the next {num_sentences} sentence(s)", + } + extra_fields = kwargs.pop("output_fields", {}) + + output_fields = {default_key: "the predicted sentence(s)"} + output_fields = {**output_fields, **extra_fields} + + if reason: + output_fields["reason"] = "brief reason for the prediction" + + if confidence_score: + output_fields["confidence_score"] = ( + "a numeric score between 0 to 1 formatted in num:0.2f" + ) + + if "temperature" not in kwargs: + kwargs["temperature"] = 0.1 + + return instruct, output_fields, kwargs + + +async def _predict( + sentence, + num_sentences, + default_key="answer", + confidence_score=False, + reason=False, + **kwargs, +): + _instruct, _output_fields, _kwargs = _create_predict_config( + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + **kwargs, + ) + + branch = Branch() + + out_ = await branch.chat( + _instruct, context=sentence, output_fields=_output_fields, **_kwargs + ) + + return _handle_single_out( + out_, default_key=default_key, to_type="str", to_default=True + ) + + +async def _parallel_predict( + sentence, + num_sentences, + default_key="answer", + confidence_score=False, + reason=False, + include_mapping=False, + **kwargs, +): + _instruct, _output_fields, _kwargs = _create_predict_config( + num_sentences=num_sentences, + default_key=default_key, + confidence_score=confidence_score, + reason=reason, + **kwargs, + ) + + session = Session() + + out_ = await session.parallel_chat( + _instruct, + context=sentence, + output_fields=_output_fields, + include_mapping=include_mapping, + **_kwargs, + ) + + return _handle_multi_out( + out_, + default_key=default_key, + to_type="str", + to_default=True, + include_mapping=include_mapping, + ) diff --git a/lionagi/core/flow/direct/react.py b/lionagi/core/direct/react.py similarity index 88% rename from lionagi/core/flow/direct/react.py rename to lionagi/core/direct/react.py index ba3f8bba6..71e595486 100644 --- a/lionagi/core/flow/direct/react.py +++ b/lionagi/core/direct/react.py @@ -1,5 +1,5 @@ -from lionagi.core.branch.branch import Branch -from lionagi.core.flow.monoflow.ReAct import MonoReAct +from ..branch import Branch +from ..flow.monoflow import MonoReAct async def react( diff --git a/lionagi/core/flow/direct/score.py b/lionagi/core/direct/score.py similarity index 77% rename from lionagi/core/flow/direct/score.py rename to lionagi/core/direct/score.py index 6e87fa684..a8cd9548f 100644 --- a/lionagi/core/flow/direct/score.py +++ b/lionagi/core/direct/score.py @@ -1,7 +1,5 @@ -from lionagi.core.branch.branch import Branch -from lionagi.libs import ln_func_call as func_call -import lionagi.libs.ln_convert as convert - +from lionagi.libs import func_call, convert +from ..branch import Branch from .utils import _handle_single_out @@ -16,20 +14,22 @@ async def score( method="llm", reason=False, confidence_score=False, - retry_kwargs={}, + retry_kwargs=None, **kwargs, ): + if retry_kwargs is None: + retry_kwargs = {} return await _force_score( - context = context, - instruction = instruction, - score_range = score_range, - inclusive = inclusive, - num_digit = num_digit, - default_key = default_key, - method = method, - reason = reason, - confidence_score = confidence_score, - retry_kwargs = retry_kwargs, + context=context, + instruction=instruction, + score_range=score_range, + inclusive=inclusive, + num_digit=num_digit, + default_key=default_key, + method=method, + reason=reason, + confidence_score=confidence_score, + retry_kwargs=retry_kwargs, **kwargs, ) @@ -65,7 +65,7 @@ async def _inner(): raise ValueError("No output from the model") return out_ - + if "retries" not in retry_kwargs: retry_kwargs["retries"] = 2 @@ -86,28 +86,28 @@ def _create_score_config( **kwargs, ): instruct = { - "task": f"score context according to the following constraints", + "task": "score context according to the following constraints", "instruction": convert.to_str(instruction), "score_range": convert.to_str(score_range), "include_endpoints": "yes" if inclusive else "no", } - return_precision = '' + return_precision = "" if num_digit == 0: return_precision = "integer" else: return_precision = f"num:{convert.to_str(num_digit)}f" - + extra_fields = kwargs.pop("output_fields", {}) output_fields = {default_key: f"""a numeric score as {return_precision}"""} output_fields = {**output_fields, **extra_fields} if reason: - output_fields.update({"reason": "brief reason for the score"}) + output_fields["reason"] = "brief reason for the score" if confidence_score: - output_fields.update( - {"confidence_score": "a numeric score between 0 to 1 formatted in num:0.2f"} + output_fields["confidence_score"] = ( + "a numeric score between 0 to 1 formatted in num:0.2f" ) if "temperature" not in kwargs: @@ -143,7 +143,12 @@ async def _score( out_ = "" if method == "llm": - out_ = await branch.chat(_instruct, tools=None, context=context, output_fields=_output_fields, **_kwargs, + out_ = await branch.chat( + _instruct, + tools=None, + context=context, + output_fields=_output_fields, + **_kwargs, ) to_num_kwargs = { diff --git a/lionagi/core/flow/direct/select.py b/lionagi/core/direct/select.py similarity index 64% rename from lionagi/core/flow/direct/select.py rename to lionagi/core/direct/select.py index 8112c85f3..ad491fb28 100644 --- a/lionagi/core/flow/direct/select.py +++ b/lionagi/core/direct/select.py @@ -1,22 +1,23 @@ -from lionagi.libs import ln_func_call as func_call -from lionagi.core.branch.branch import Branch -from lionagi.libs.ln_parse import StringMatch - +from lionagi.libs import StringMatch, func_call +from ..branch.branch import Branch from .utils import _handle_single_out + async def select( - context, - choices, + context, + choices, *, num_choices=1, - method='llm', + method="llm", objective=None, - default_key='answer', - reason=False, - confidence_score=False, - retry_kwargs={}, + default_key="answer", + reason=False, + confidence_score=False, + retry_kwargs=None, **kwargs, ): + if retry_kwargs is None: + retry_kwargs = {} return await _force_select( context=context, choices=choices, @@ -32,19 +33,18 @@ async def select( async def _force_select( - context, - choices, + context, + choices, num_choices=1, - method='llm', + method="llm", objective=None, - default_key='answer', - reason=False, - confidence_score=False, + default_key="answer", + reason=False, + confidence_score=False, retry_kwargs={}, **kwargs, ): - - + async def _inner(): out_ = await _select( context=context, @@ -61,10 +61,9 @@ async def _inner(): if out_ is None: raise ValueError("No output from the model") - if isinstance(out_, dict): - if out_[default_key] not in choices: - v = StringMatch.choose_most_similar(out_.pop(default_key, ""), choices) - out_[default_key] = v + if isinstance(out_, dict) and out_[default_key] not in choices: + v = StringMatch.choose_most_similar(out_.pop(default_key, ""), choices) + out_[default_key] = v return out_ @@ -78,15 +77,15 @@ async def _inner(): def _create_select_config( - choices, - num_choices=1, - objective=None, - default_key='answer', - reason=False, - confidence_score=False, - **kwargs, + choices, + num_choices=1, + objective=None, + default_key="answer", + reason=False, + confidence_score=False, + **kwargs, ): - + instruct = {"task": f"select {num_choices} from provided", "choices": choices} if objective is not None: instruct["objective"] = objective @@ -96,31 +95,31 @@ def _create_select_config( output_fields = {**output_fields, **extra_fields} if reason: - output_fields.update({"reason": "brief reason for the selection"}) + output_fields["reason"] = "brief reason for the selection" if confidence_score: - output_fields.update( - {"confidence_score": "a numeric score between 0 to 1 formatted in num:0.2f"} + output_fields["confidence_score"] = ( + "a numeric score between 0 to 1 formatted in num:0.2f" ) if "temperature" not in kwargs: kwargs["temperature"] = 0.1 - + return instruct, output_fields, kwargs async def _select( - context, - choices, + context, + choices, num_choices=1, - method='llm', + method="llm", objective=None, - default_key='answer', - reason=False, - confidence_score=False, + default_key="answer", + reason=False, + confidence_score=False, **kwargs, ): - + _instruct, _output_fields, _kwargs = _create_select_config( choices=choices, num_choices=num_choices, @@ -130,12 +129,16 @@ async def _select( confidence_score=confidence_score, **kwargs, ) - + branch = Branch() out_ = "" if method == "llm": out_ = await branch.chat( - _instruct, tools=None, context=context, output_fields=_output_fields, **_kwargs + _instruct, + tools=None, + context=context, + output_fields=_output_fields, + **_kwargs, ) - + return _handle_single_out(out_, default_key) diff --git a/lionagi/core/flow/direct/sentiment.py b/lionagi/core/direct/sentiment.py similarity index 100% rename from lionagi/core/flow/direct/sentiment.py rename to lionagi/core/direct/sentiment.py diff --git a/lionagi/core/direct/utils.py b/lionagi/core/direct/utils.py new file mode 100644 index 000000000..0030dbbc2 --- /dev/null +++ b/lionagi/core/direct/utils.py @@ -0,0 +1,83 @@ +import contextlib +from lionagi.libs import ParseUtil, StringMatch, convert, func_call + + +def _parse_out(out_): + if isinstance(out_, str): + try: + out_ = ParseUtil.md_to_json(out_) + except Exception: + with contextlib.suppress(Exception): + out_ = ParseUtil.fuzzy_parse_json(out_.strip("```json").strip("```")) + return out_ + + +def _handle_single_out( + out_, + default_key, + choices=None, + to_type="dict", + to_type_kwargs=None, + to_default=True, +): + + if to_type_kwargs is None: + to_type_kwargs = {} + out_ = _parse_out(out_) + + if default_key not in out_: + raise ValueError(f"Key {default_key} not found in output") + + answer = out_[default_key] + + if ( + choices is not None + and answer not in choices + and convert.strip_lower(out_) in ["", "none", "null", "na", "n/a"] + ): + raise ValueError(f"Answer {answer} not in choices {choices}") + + if to_type == "str": + out_[default_key] = convert.to_str(answer, **to_type_kwargs) + + elif to_type == "num": + out_[default_key] = convert.to_num(answer, **to_type_kwargs) + + return out_[default_key] if to_default and len(out_.keys()) == 1 else out_ + + +def _handle_multi_out( + out_, + default_key, + choices=None, + to_type="dict", + to_type_kwargs=None, + to_default=True, + include_mapping=False, +): + if to_type_kwargs is None: + to_type_kwargs = {} + if include_mapping: + for i in out_: + i[default_key] = _handle_single_out( + i[default_key], + choices=choices, + default_key=default_key, + to_type=to_type, + to_type_kwargs=to_type_kwargs, + to_default=to_default, + ) + else: + _out = [] + for i in out_: + i = _handle_single_out( + i, + choices=choices, + default_key=default_key, + to_type=to_type, + to_type_kwargs=to_type_kwargs, + to_default=to_default, + ) + _out.append(i) + + return out_ if len(out_) > 1 else out_[0] diff --git a/lionagi/core/flow/direct/utils.py b/lionagi/core/flow/direct/utils.py deleted file mode 100644 index 36f9e686d..000000000 --- a/lionagi/core/flow/direct/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -from lionagi.core.branch.branch import Branch -from lionagi.libs.ln_parse import ParseUtil, StringMatch -import lionagi.libs.ln_convert as convert - - -def _parse_out(out_): - if isinstance(out_, str): - try: - out_ = ParseUtil.md_to_json(out_) - except: - try: - out_ = ParseUtil.fuzzy_parse_json(out_.strip("```json").strip("```")) - except: - pass - return out_ - - -def _handle_single_out( - out_, default_key, choices=None, to_type="dict", to_type_kwargs={}, to_default=True -): - - out_ = _parse_out(out_) - - if default_key not in out_: - raise ValueError(f"Key {default_key} not found in output") - - answer = out_[default_key] - - if choices is not None: - if answer not in choices: - if convert.strip_lower(out_) in ["", "none", "null", "na", "n/a"]: - raise ValueError(f"Answer {answer} not in choices {choices}") - - if to_type == 'str': - out_[default_key] = convert.to_str(answer, **to_type_kwargs) - - elif to_type == 'num': - out_[default_key] = convert.to_num(answer, **to_type_kwargs) - - if to_default and len(out_.keys()) == 1: - return out_[default_key] - - return out_ diff --git a/lionagi/core/flow/monoflow/ReAct.py b/lionagi/core/flow/monoflow/ReAct.py index 461aa3741..7c7170ec8 100644 --- a/lionagi/core/flow/monoflow/ReAct.py +++ b/lionagi/core/flow/monoflow/ReAct.py @@ -42,16 +42,17 @@ def _get_prompt(prompt=None, default=None, num_steps=None, instruction=None): try: try: return default.format(num_steps=num_steps) - except: + except Exception: return default.format(instruction=instruction) - except: + except Exception: return default def _create_followup_config(self, tools, auto=True, **kwargs): - if tools is not None: - if isinstance(tools, list) and isinstance(tools[0], Tool): - self.branch.tool_manager.register_tools(tools) + if tools is not None and ( + isinstance(tools, list) and isinstance(tools[0], Tool) + ): + self.branch.tool_manager.register_tools(tools) if not self.branch.tool_manager.has_tools: raise ValueError("No tools found, You need to register tools") diff --git a/lionagi/core/flow/monoflow/__init__.py b/lionagi/core/flow/monoflow/__init__.py index e69de29bb..e31f79c5d 100644 --- a/lionagi/core/flow/monoflow/__init__.py +++ b/lionagi/core/flow/monoflow/__init__.py @@ -0,0 +1,9 @@ +from .chat import MonoChat +from .followup import MonoFollowup +from .ReAct import MonoReAct + +__all__ = [ + "MonoChat", + "MonoFollowup", + "MonoReAct", +] diff --git a/lionagi/core/flow/monoflow/chat.py b/lionagi/core/flow/monoflow/chat.py index be711d026..e9a178d0d 100644 --- a/lionagi/core/flow/monoflow/chat.py +++ b/lionagi/core/flow/monoflow/chat.py @@ -25,18 +25,18 @@ async def chat( a chat conversation with LLM, processing instructions and system messages, optionally invoking tools. Args: - branch: The Branch instance to perform chat operations. - instruction (Union[Instruction, str]): The instruction for the chat. - context (Optional[Any]): Additional context for the chat. - sender (Optional[str]): The sender of the chat message. - system (Optional[Union[System, str, Dict[str, Any]]]): System message to be processed. - tools (Union[bool, Tool, List[Tool], str, List[str]]): Specifies tools to be invoked. - out (bool): If True, outputs the chat response. - invoke (bool): If True, invokes tools as part of the chat. - **kwargs: Arbitrary keyword arguments for chat completion. + branch: The Branch instance to perform chat operations. + instruction (Union[Instruction, str]): The instruction for the chat. + context (Optional[Any]): Additional context for the chat. + sender (Optional[str]): The sender of the chat message. + system (Optional[Union[System, str, Dict[str, Any]]]): System message to be processed. + tools (Union[bool, Tool, List[Tool], str, List[str]]): Specifies tools to be invoked. + out (bool): If True, outputs the chat response. + invoke (bool): If True, invokes tools as part of the chat. + **kwargs: Arbitrary keyword arguments for chat completion. Examples: - >>> await ChatFlow.chat(branch, "Ask about user preferences") + >>> await ChatFlow.chat(branch, "Ask about user preferences") """ config = self._create_chat_config( diff --git a/lionagi/core/flow/monoflow/chat_mixin.py b/lionagi/core/flow/monoflow/chat_mixin.py index 7b97aef17..90a9a271a 100644 --- a/lionagi/core/flow/monoflow/chat_mixin.py +++ b/lionagi/core/flow/monoflow/chat_mixin.py @@ -36,32 +36,33 @@ def _create_chat_config( if "tool_parsed" in kwargs: kwargs.pop("tool_parsed") tool_kwarg = {"tools": tools} - kwargs = {**tool_kwarg, **kwargs} - else: - if tools and self.branch.has_tools: - kwargs = self.branch.tool_manager.parse_tool(tools=tools, **kwargs) + kwargs = tool_kwarg | kwargs + elif tools and self.branch.has_tools: + kwargs = self.branch.tool_manager.parse_tool(tools=tools, **kwargs) config = {**self.branch.llmconfig, **kwargs} if sender is not None: - config.update({"sender": sender}) + config["sender"] = sender return config class MonoChatInvokeMixin(ABC): async def _output(self, invoke, out, output_fields, func_calls_=None): + # sourcery skip: use-contextlib-suppress content_ = self.branch.last_message_content if invoke: try: await self._invoke_tools(content_, func_calls_=func_calls_) - except: + except Exception: pass if out: return self._return_response(content_, output_fields) @staticmethod def _return_response(content_, output_fields): + # sourcery skip: assign-if-exp, use-contextlib-suppress out_ = "" if len(content_.items()) == 1 and len(nested.get_flattened_keys(content_)) == 1: @@ -75,7 +76,7 @@ def _return_response(content_, output_fields): else: out_ = ParseUtil.md_to_json(out_) out_ = StringMatch.correct_keys(output_fields=output_fields, out_=out_) - except: + except Exception: pass return out_ @@ -118,9 +119,9 @@ def _process_chatcompletion(self, payload, completion, sender): async def _call_chatcompletion(self, sender=None, with_sender=False, **kwargs): messages = ( - self.branch.chat_messages - if not with_sender - else self.branch.chat_messages_with_sender + self.branch.chat_messages_with_sender + if with_sender + else self.branch.chat_messages ) payload, completion = await self.branch.service.serve_chat( messages=messages, **kwargs diff --git a/lionagi/core/flow/monoflow/followup.py b/lionagi/core/flow/monoflow/followup.py index fd1a8a4b9..3a93e1d22 100644 --- a/lionagi/core/flow/monoflow/followup.py +++ b/lionagi/core/flow/monoflow/followup.py @@ -41,16 +41,17 @@ def _get_prompt(prompt=None, default=None, num_followup=None, instruction=None): try: try: return default.format(num_followup=num_followup) - except: + except Exception: return default.format(instruction=instruction) - except: + except Exception: return default def _create_followup_config(self, tools, **kwargs): - if tools is not None: - if isinstance(tools, list) and isinstance(tools[0], Tool): - self.branch.tool_manager.register_tools(tools) + if tools is not None and ( + isinstance(tools, list) and isinstance(tools[0], Tool) + ): + self.branch.tool_manager.register_tools(tools) if not self.branch.tool_manager.has_tools: raise ValueError("No tools found, You need to register tools") diff --git a/lionagi/core/flow/polyflow/__init__.py b/lionagi/core/flow/polyflow/__init__.py index e69de29bb..7b2a514c4 100644 --- a/lionagi/core/flow/polyflow/__init__.py +++ b/lionagi/core/flow/polyflow/__init__.py @@ -0,0 +1 @@ +from .chat import PolyChat diff --git a/lionagi/core/flow/polyflow/chat.py b/lionagi/core/flow/polyflow/chat.py index 0f4f77590..a66299e0d 100644 --- a/lionagi/core/flow/polyflow/chat.py +++ b/lionagi/core/flow/polyflow/chat.py @@ -6,7 +6,6 @@ from lionagi.core.messages.schema import Instruction from lionagi.core.branch.branch import Branch - from lionagi.core.flow.base.baseflow import BasePolyFlow @@ -28,7 +27,7 @@ async def parallel_chat( invoke: bool = True, output_fields=None, persist_path=None, - branch_config={}, + branch_config=None, explode=False, **kwargs, ) -> Any: @@ -36,6 +35,8 @@ async def parallel_chat( parallel chat """ + if branch_config is None: + branch_config = {} return await self._parallel_chat( instruction, num_instances=num_instances, @@ -67,6 +68,8 @@ async def _parallel_chat( persist_path=None, branch_config={}, explode=False, + include_mapping=True, + default_key="response", **kwargs, ) -> Any: """ @@ -102,7 +105,16 @@ async def _inner(i, ins_, cxt_): ) branches[branch_.id_] = branch_ - return res_ + if include_mapping: + return { + "instruction": ins_ or instruction, + "context": cxt_ or context, + "branch_id": branch_.id_, + default_key: res_, + } + + else: + return res_ async def _inner_2(i, ins_=None, cxt_=None): """returns num_instances of branches performing for same task/context""" diff --git a/lionagi/core/mail/mail_manager.py b/lionagi/core/mail/mail_manager.py index 5427563f1..c1f9e4b8b 100644 --- a/lionagi/core/mail/mail_manager.py +++ b/lionagi/core/mail/mail_manager.py @@ -1,7 +1,7 @@ from collections import deque -from lionagi.core.schema.base_node import BaseNode -from lionagi.core.mail.schema import BaseMail -from lionagi.libs.ln_async import AsyncUtil +from lionagi.libs import AsyncUtil +from ..schema import BaseNode +from .schema import BaseMail class MailManager: @@ -12,12 +12,12 @@ class MailManager: and deletion of sources, and it handles the collection and dispatch of mails to and from these sources. Attributes: - sources (Dict[str, Any]): A dictionary mapping source identifiers to their attributes. - mails (Dict[str, Dict[str, deque]]): A nested dictionary storing queued mail items, organized by recipient - and sender. + sources (Dict[str, Any]): A dictionary mapping source identifiers to their attributes. + mails (Dict[str, Dict[str, deque]]): A nested dictionary storing queued mail items, organized by recipient + and sender. """ - def __init__(self, sources: list[BaseNode]): + def __init__(self, sources): self.sources = {} self.mails = {} self.add_sources(sources) @@ -26,12 +26,12 @@ def __init__(self, sources: list[BaseNode]): def add_sources(self, sources): if isinstance(sources, dict): for _, v in sources.items(): - if not v.id_ in self.sources: + if v.id_ not in self.sources: self.sources[v.id_] = v self.mails[v.id_] = {} elif isinstance(sources, list): for v in sources: - if not v.id_ in self.sources: + if v.id_ not in self.sources: self.sources[v.id_] = v self.mails[v.id_] = {} @@ -73,15 +73,14 @@ def send(self, recipient_id): raise ValueError(f"Recipient source {recipient_id} does not exist.") if not self.mails[recipient_id]: return - else: - for key in list(self.mails[recipient_id].keys()): - mails_deque = self.mails[recipient_id].pop(key) - if key not in self.sources[recipient_id].pending_ins: - self.sources[recipient_id].pending_ins[key] = mails_deque - else: - while mails_deque: - mail_ = mails_deque.popleft() - self.sources[recipient_id].pending_ins[key].append(mail_) + for key in list(self.mails[recipient_id].keys()): + mails_deque = self.mails[recipient_id].pop(key) + if key not in self.sources[recipient_id].pending_ins: + self.sources[recipient_id].pending_ins[key] = mails_deque + else: + while mails_deque: + mail_ = mails_deque.popleft() + self.sources[recipient_id].pending_ins[key].append(mail_) def collect_all(self): for ids in self.sources: @@ -95,4 +94,4 @@ async def execute(self, refresh_time=1): while not self.execute_stop: self.collect_all() self.send_all() - await AsyncUtil.sleep(refresh_time) \ No newline at end of file + await AsyncUtil.sleep(refresh_time) diff --git a/lionagi/core/mail/schema.py b/lionagi/core/mail/schema.py index b083c814d..adedff37b 100644 --- a/lionagi/core/mail/schema.py +++ b/lionagi/core/mail/schema.py @@ -10,10 +10,11 @@ class MailCategory(str, Enum): SERVICE = "service" MODEL = "model" NODE = "node" - CONTEXT = "context" + NODE_LIST = "node_list" NODE_ID = "node_id" START = "start" END = "end" + CONDITION = "condition" class BaseMail: @@ -34,14 +35,14 @@ def __init__(self, sender_id, recipient_id, category, package): raise ValueError( f"Invalid request title. Valid titles are " f"{list(MailCategory)}, Error: {e}" - ) + ) from e self.package = package class StartMail(BaseRelatableNode): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) self.pending_outs = deque() def trigger(self, context, structure_id, executable_id): diff --git a/lionagi/core/messages/schema.py b/lionagi/core/messages/schema.py index 27c756fe8..0024e4cdc 100644 --- a/lionagi/core/messages/schema.py +++ b/lionagi/core/messages/schema.py @@ -1,12 +1,11 @@ from enum import Enum -from lionagi.libs import ln_nested as nested -from lionagi.libs import ln_convert as convert - -from lionagi.core.schema.data_node import DataNode +from lionagi.libs import nested, convert +from ..schema import DataNode _message_fields = ["node_id", "timestamp", "role", "sender", "recipient", "content"] + # ToDo: actually implement the new message classes @@ -99,9 +98,9 @@ class BaseMessage(DataNode): Represents a message in a chatbot-like system, inheriting from BaseNode. Attributes: - role (str | None): The role of the entity sending the message, e.g., 'user', 'system'. - sender (str | None): The identifier of the sender of the message. - content (Any): The actual content of the message. + role (str | None): The role of the entity sending the message, e.g., 'user', 'system'. + sender (str | None): The identifier of the sender of the message. + content (Any): The actual content of the message. """ role: str | None = None @@ -114,7 +113,7 @@ def msg(self) -> dict: Constructs and returns a dictionary representation of the message. Returns: - A dictionary representation of the message with 'role' and 'content' keys. + A dictionary representation of the message with 'role' and 'content' keys. """ return self._to_message() @@ -124,7 +123,7 @@ def msg_content(self) -> str | dict: Gets the 'content' field of the message. Returns: - The 'content' part of the message. + The 'content' part of the message. """ return self.msg["content"] @@ -133,14 +132,13 @@ def _to_message(self): Constructs and returns a dictionary representation of the message. Returns: - dict: A dictionary representation of the message with 'role' and 'content' keys. + dict: A dictionary representation of the message with 'role' and 'content' keys. """ - out = {"role": self.role, "content": convert.to_str(self.content)} - return out + return {"role": self.role, "content": convert.to_str(self.content)} def __str__(self): content_preview = ( - (str(self.content)[:75] + "...") + f"{str(self.content)[:75]}..." if self.content and len(self.content) > 75 else str(self.content) ) @@ -162,7 +160,7 @@ def __init__( sender: str | None = None, output_fields=None, recipient=None, - ): + ): # sourcery skip: avoid-builtin-shadow super().__init__( role="user", sender=sender or "user", @@ -256,13 +254,13 @@ def __init__( content_key = content_key or "response" sender = sender or "assistant" recipient = recipient or "user" - except: + except Exception: content_ = response["content"] content_key = content_key or "response" sender = sender or "assistant" recipient = recipient or "user" - except: + except Exception: sender = sender or "action_response" content_ = response content_key = content_key or "action_response" @@ -281,13 +279,13 @@ def _handle_action_request(response): Processes an action request response and extracts relevant information. Args: - response (dict): The response dictionary containing tool calls and other information. + response (dict): The response dictionary containing tool calls and other information. Returns: - list: A list of dictionaries, each representing a function call with action and arguments. + list: A list of dictionaries, each representing a function call with action and arguments. Raises: - ValueError: If the response does not conform to the expected format for action requests. + ValueError: If the response does not conform to the expected format for action requests. """ try: tool_count = 0 @@ -300,7 +298,7 @@ def _handle_action_request(response): _path2 = ["tool_calls", tool_count, "function", "arguments"] func_content = { - "action": ("action_" + nested.nget(response, _path1)), + "action": f"action_{nested.nget(response, _path1)}", "arguments": nested.nget(response, _path2), } func_list.append(func_content) diff --git a/lionagi/core/prompt/__init__.py b/lionagi/core/prompt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/prompt/prompt_template.py b/lionagi/core/prompt/prompt_template.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/schema/__init__.py b/lionagi/core/schema/__init__.py index 19be23ac4..5dbdd5e79 100644 --- a/lionagi/core/schema/__init__.py +++ b/lionagi/core/schema/__init__.py @@ -1,10 +1,9 @@ -from .base_node import BaseNode, BaseRelatableNode, Tool +from .base_node import BaseNode, BaseRelatableNode, Tool, TOOL_TYPE from .data_node import DataNode from .data_logger import DLog, DataLogger from .structure import Relationship, Graph, Structure from .action_node import ActionNode - __all__ = [ "BaseNode", "BaseRelatableNode", @@ -16,4 +15,5 @@ "Graph", "Structure", "ActionNode", + "TOOL_TYPE", ] diff --git a/lionagi/core/schema/action_node.py b/lionagi/core/schema/action_node.py index b519c31f0..36cc0d309 100644 --- a/lionagi/core/schema/action_node.py +++ b/lionagi/core/schema/action_node.py @@ -1,11 +1,13 @@ from enum import Enum -from lionagi.core.schema.base_node import BaseNode +from .base_node import BaseNode class ActionSelection(BaseNode): - def __init__(self, action: str = "chat", action_kwargs={}): + def __init__(self, action: str = "chat", action_kwargs=None): + if action_kwargs is None: + action_kwargs = {} super().__init__() self.action = action self.action_kwargs = action_kwargs @@ -13,7 +15,13 @@ def __init__(self, action: str = "chat", action_kwargs={}): class ActionNode(BaseNode): - def __init__(self, instruction, action: str = "chat", tools=[], action_kwargs={}): + def __init__( + self, instruction, action: str = "chat", tools=None, action_kwargs=None + ): + if tools is None: + tools = [] + if action_kwargs is None: + action_kwargs = {} super().__init__() self.instruction = instruction self.action = action diff --git a/lionagi/core/schema/base_mixin.py b/lionagi/core/schema/base_mixin.py index 551ebdf8d..fb9f039b2 100644 --- a/lionagi/core/schema/base_mixin.py +++ b/lionagi/core/schema/base_mixin.py @@ -5,10 +5,7 @@ import pandas as pd from pydantic import BaseModel, ValidationError -import lionagi.libs.ln_nested as nested -import lionagi.libs.ln_convert as convert -from lionagi.libs.ln_parse import ParseUtil -from lionagi.libs.sys_util import SysUtil +from lionagi.libs import nested, convert, ParseUtil, SysUtil T = TypeVar("T") # Generic type for return type of from_obj method @@ -24,12 +21,12 @@ def to_json_str(self, *args, **kwargs) -> str: string. It supports passing arbitrary arguments to the underlying `model_dump_json` method. Args: - *args: Variable-length argument list to be passed to `model_dump_json`. - **kwargs: Arbitrary keyword arguments, with `by_alias=True` set by default to use - model field aliases in the output JSON, if any. + *args: Variable-length argument list to be passed to `model_dump_json`. + **kwargs: Arbitrary keyword arguments, with `by_alias=True` set by default to use + model field aliases in the output JSON, if any. Returns: - str: A JSON string representation of the model instance. + str: A JSON string representation of the model instance. """ return self.model_dump_json(*args, by_alias=True, **kwargs) @@ -42,12 +39,12 @@ def to_dict(self, *args, **kwargs) -> dict[str, Any]: aliases instead of the original field names. Args: - *args: Variable-length argument list for the `model_dump` method. - **kwargs: Arbitrary keyword arguments. By default, `by_alias=True` is applied, indicating - that field aliases should be used as keys in the resulting dictionary. + *args: Variable-length argument list for the `model_dump` method. + **kwargs: Arbitrary keyword arguments. By default, `by_alias=True` is applied, indicating + that field aliases should be used as keys in the resulting dictionary. Returns: - dict[str, Any]: The dictionary representation of the model instance. + dict[str, Any]: The dictionary representation of the model instance. """ return self.model_dump(*args, by_alias=True, **kwargs) @@ -60,7 +57,7 @@ def to_xml(self) -> str: The root element of the XML tree is named after the class of the model instance. Returns: - str: An XML string representation of the model instance. + str: An XML string representation of the model instance. """ import xml.etree.ElementTree as ET @@ -88,15 +85,15 @@ def to_pd_series(self, *args, pd_kwargs: dict | None = None, **kwargs) -> pd.Ser customize the Series creation through `pd_kwargs`. Args: - *args: Variable-length argument list for the `to_dict` method. - pd_kwargs (dict | None): Optional dictionary of keyword arguments to pass to the pandas - Series constructor. Defaults to None, in which case an empty - dictionary is used. - **kwargs: Arbitrary keyword arguments for the `to_dict` method, influencing the dictionary - representation used for Series creation. + *args: Variable-length argument list for the `to_dict` method. + pd_kwargs (dict | None): Optional dictionary of keyword arguments to pass to the pandas + Series constructor. Defaults to None, in which case an empty + dictionary is used. + **kwargs: Arbitrary keyword arguments for the `to_dict` method, influencing the dictionary + representation used for Series creation. Returns: - pd.Series: A pandas Series representation of the model instance. + pd.Series: A pandas Series representation of the model instance. """ pd_kwargs = {} if pd_kwargs is None else pd_kwargs dict_ = self.to_dict(*args, **kwargs) @@ -118,15 +115,11 @@ def _from_dict(cls, obj: dict, *args, **kwargs) -> T: @from_obj.register(str) @classmethod def _from_str(cls, obj: str, *args, fuzzy_parse=False, **kwargs) -> T: - if fuzzy_parse: - obj = ParseUtil.fuzzy_parse_json(obj) - else: - obj = convert.to_dict(obj) - + obj = ParseUtil.fuzzy_parse_json(obj) if fuzzy_parse else convert.to_dict(obj) try: return cls.from_obj(obj, *args, **kwargs) except ValidationError as e: - raise ValueError(f"Invalid JSON for deserialization: {e}") + raise ValueError(f"Invalid JSON for deserialization: {e}") from e @from_obj.register(list) @classmethod @@ -135,23 +128,27 @@ def _from_list(cls, obj: list[Any], *args, **kwargs) -> list[T]: @from_obj.register(pd.Series) @classmethod - def _from_pd_series(cls, obj: pd.Series, *args, pd_kwargs={}, **kwargs) -> T: + def _from_pd_series(cls, obj: pd.Series, *args, pd_kwargs=None, **kwargs) -> T: + if pd_kwargs is None: + pd_kwargs = {} return cls.from_obj(obj.to_dict(**pd_kwargs), *args, **kwargs) @from_obj.register(pd.DataFrame) @classmethod def _from_pd_dataframe( - cls, obj: pd.DataFrame, *args, pd_kwargs={}, **kwargs + cls, obj: pd.DataFrame, *args, pd_kwargs=None, **kwargs ) -> list[T]: + if pd_kwargs is None: + pd_kwargs = {} return [ cls.from_obj(row, *args, **pd_kwargs, **kwargs) for _, row in obj.iterrows() ] @from_obj.register(BaseModel) @classmethod - def _from_base_model( - cls, obj: BaseModel, pydantic_kwargs={"by_alias": True}, **kwargs - ) -> T: + def _from_base_model(cls, obj: BaseModel, pydantic_kwargs=None, **kwargs) -> T: + if pydantic_kwargs is None: + pydantic_kwargs = {"by_alias": True} config_ = {**obj.model_dump(**pydantic_kwargs), **kwargs} return cls.from_obj(**config_) @@ -163,11 +160,11 @@ def meta_keys(self, flattened: bool = False, **kwargs) -> list[str]: Retrieves a list of metadata keys. Args: - flattened (bool): If True, returns keys from a flattened metadata structure. - **kwargs: Additional keyword arguments passed to the flattening function. + flattened (bool): If True, returns keys from a flattened metadata structure. + **kwargs: Additional keyword arguments passed to the flattening function. Returns: - list[str]: List of metadata keys. + list[str]: List of metadata keys. """ if flattened: return nested.get_flattened_keys(self.metadata, **kwargs) @@ -178,12 +175,12 @@ def meta_has_key(self, key: str, flattened: bool = False, **kwargs) -> bool: Checks if a specified key exists in the metadata. Args: - key (str): The key to check. - flattened (bool): If True, checks within a flattened metadata structure. - **kwargs: Additional keyword arguments for flattening. + key (str): The key to check. + flattened (bool): If True, checks within a flattened metadata structure. + **kwargs: Additional keyword arguments for flattening. Returns: - bool: True if key exists, False otherwise. + bool: True if key exists, False otherwise. """ if flattened: return key in nested.get_flattened_keys(self.metadata, **kwargs) @@ -196,12 +193,12 @@ def meta_get( Retrieves the value associated with a given key from the metadata. Args: - key (str): The key for the desired value. - indices: Optional indices for nested retrieval. - default (Any): The default value to return if the key is not found. + key (str): The key for the desired value. + indices: Optional indices for nested retrieval. + default (Any): The default value to return if the key is not found. Returns: - Any: The value associated with the key or the default value. + Any: The value associated with the key or the default value. """ if indices: return nested.nget(self.metadata, key, indices, default) @@ -212,11 +209,11 @@ def meta_change_key(self, old_key: str, new_key: str) -> bool: Renames a key in the metadata. Args: - old_key (str): The current key name. - new_key (str): The new key name. + old_key (str): The current key name. + new_key (str): The new key name. Returns: - bool: True if the key was changed, False otherwise. + bool: True if the key was changed, False otherwise. """ if old_key in self.metadata: SysUtil.change_dict_key(self.metadata, old_key, new_key) @@ -228,12 +225,12 @@ def meta_insert(self, indices: str | list, value: Any, **kwargs) -> bool: Inserts a value into the metadata at specified indices. Args: - indices (str | list): The indices where the value should be inserted. - value (Any): The value to insert. - **kwargs: Additional keyword arguments. + indices (str | list): The indices where the value should be inserted. + value (Any): The value to insert. + **kwargs: Additional keyword arguments. Returns: - bool: True if the insertion was successful, False otherwise. + bool: True if the insertion was successful, False otherwise. """ return nested.ninsert(self.metadata, indices, value, **kwargs) @@ -243,11 +240,11 @@ def meta_pop(self, key: str, default: Any = None) -> Any: Removes a key from the metadata and returns its value. Args: - key (str): The key to remove. - default (Any): The default value to return if the key is not found. + key (str): The key to remove. + default (Any): The default value to return if the key is not found. Returns: - Any: The value of the removed key or the default value. + Any: The value of the removed key or the default value. """ return self.metadata.pop(key, default) @@ -258,12 +255,12 @@ def meta_merge( Merges additional metadata into the existing metadata. Args: - additional_metadata (dict[str, Any]): The metadata to merge in. - overwrite (bool): If True, existing keys will be overwritten by those in additional_metadata. - **kwargs: Additional keyword arguments for the merge. + additional_metadata (dict[str, Any]): The metadata to merge in. + overwrite (bool): If True, existing keys will be overwritten by those in additional_metadata. + **kwargs: Additional keyword arguments for the merge. Returns: - None + None """ nested.nmerge( [self.metadata, additional_metadata], overwrite=overwrite, **kwargs @@ -278,7 +275,7 @@ def meta_clear(self) -> None: Clears all metadata. Returns: - None + None """ self.metadata.clear() @@ -287,10 +284,10 @@ def meta_filter(self, condition: Callable[[Any, Any], bool]) -> dict[str, Any]: Filters the metadata based on a condition. Args: - condition (Callable[[Any, Any], bool]): The condition function to apply. + condition (Callable[[Any, Any], bool]): The condition function to apply. Returns: - dict[str, Any]: The filtered metadata. + dict[str, Any]: The filtered metadata. """ return nested.nfilter(self.metadata, condition) diff --git a/lionagi/core/schema/base_node.py b/lionagi/core/schema/base_node.py index 7572febe6..9d1fd133f 100644 --- a/lionagi/core/schema/base_node.py +++ b/lionagi/core/schema/base_node.py @@ -5,14 +5,11 @@ from abc import ABC from typing import Any, TypeVar -import lionagi.integrations.bridge.pydantic_.base_model as pyd -from lionagi.libs.sys_util import SysUtil - -from lionagi.libs import ln_convert as convert +from pydantic import Field, field_serializer, AliasChoices +from lionagi.libs import SysUtil, convert from .base_mixin import BaseComponentMixin - T = TypeVar("T", bound="BaseComponent") @@ -23,16 +20,16 @@ class BaseComponent(BaseComponentMixin, ABC): and validating metadata keys and values. Attributes: - id_ (str): Unique identifier, defaulted using SysUtil.create_id. - timestamp (str | None): Timestamp of creation or modification. - metadata (dict[str, Any]): Metadata associated with the component. + id_ (str): Unique identifier, defaulted using SysUtil.create_id. + timestamp (str | None): Timestamp of creation or modification. + metadata (dict[str, Any]): Metadata associated with the component. """ - id_: str = pyd.ln_Field(default_factory=SysUtil.create_id, alias="node_id") - timestamp: str | None = pyd.ln_Field(default_factory=SysUtil.get_timestamp) - metadata: dict[str, Any] = pyd.ln_Field(default_factory=dict, alias="meta") + id_: str = Field(default_factory=SysUtil.create_id, alias="node_id") + timestamp: str | None = Field(default_factory=SysUtil.get_timestamp) + metadata: dict[str, Any] = Field(default_factory=dict, alias="meta") - class Config: + class ConfigDict: """Model configuration settings.""" extra = "allow" @@ -46,11 +43,11 @@ def copy(self, *args, **kwargs) -> T: Creates a deep copy of the instance, with an option to update specific fields. Args: - *args: Variable length argument list for additional options. - **kwargs: Arbitrary keyword arguments specifying updates to the instance. + *args: Variable length argument list for additional options. + **kwargs: Arbitrary keyword arguments specifying updates to the instance. Returns: - BaseComponent: A new instance of BaseComponent as a deep copy of the original, with updates applied. + BaseComponent: A new instance of BaseComponent as a deep copy of the original, with updates applied. """ return self.model_copy(*args, **kwargs) @@ -64,15 +61,15 @@ class BaseNode(BaseComponent): extending BaseComponent with content handling capabilities. Attributes: - content: The content of the node, which can be a string, a dictionary with any structure, - None, or any other type. It is flexible to accommodate various types of content. - This attribute also supports aliasing through validation_alias for compatibility with - different naming conventions like "text", "page_content", or "chunk_content". + content: The content of the node, which can be a string, a dictionary with any structure, + None, or any other type. It is flexible to accommodate various types of content. + This attribute also supports aliasing through validation_alias for compatibility with + different naming conventions like "text", "page_content", or "chunk_content". """ - content: str | dict[str, Any] | None | Any = pyd.ln_Field( + content: str | dict[str, Any] | None | Any = Field( default=None, - validation_alias=pyd.ln_AliasChoices("text", "page_content", "chunk_content"), + validation_alias=AliasChoices("text", "page_content", "chunk_content"), ) @property @@ -81,8 +78,8 @@ def content_str(self): Attempts to serialize the node's content to a string. Returns: - str: The serialized content string. If serialization fails, returns "null" and - logs an error message indicating the content is not serializable. + str: The serialized content string. If serialization fails, returns "null" and + logs an error message indicating the content is not serializable. """ try: return convert.to_str(self.content) @@ -98,17 +95,17 @@ def __str__(self): metadata preview, and optionally the timestamp if present. Returns: - str: A string representation of the instance. + str: A string representation of the instance. """ timestamp = f" ({self.timestamp})" if self.timestamp else "" if self.content: content_preview = ( - self.content[:50] + "..." if len(self.content) > 50 else self.content + f"{self.content[:50]}..." if len(self.content) > 50 else self.content ) else: content_preview = "" meta_preview = ( - str(self.metadata)[:50] + "..." + f"{str(self.metadata)[:50]}..." if len(str(self.metadata)) > 50 else str(self.metadata) ) @@ -123,11 +120,11 @@ class BaseRelatableNode(BaseNode): Extends BaseNode with functionality to manage relationships with other nodes. Attributes: - related_nodes: A list of identifiers (str) for nodes that are related to this node. - label: An optional label for the node, providing additional context or classification. + related_nodes: A list of identifiers (str) for nodes that are related to this node. + label: An optional label for the node, providing additional context or classification. """ - related_nodes: list[str] = pyd.ln_Field(default_factory=list) + related_nodes: list[str] = Field(default_factory=list) label: str | None = None def add_related_node(self, node_id: str) -> bool: @@ -135,10 +132,10 @@ def add_related_node(self, node_id: str) -> bool: Adds a node to the list of related nodes if it's not already present. Args: - node_id: The identifier of the node to add. + node_id: The identifier of the node to add. Returns: - bool: True if the node was added, False if it was already in the list. + bool: True if the node was added, False if it was already in the list. """ if node_id not in self.related_nodes: self.related_nodes.append(node_id) @@ -150,10 +147,10 @@ def remove_related_node(self, node_id: str) -> bool: Removes a node from the list of related nodes if it's present. Args: - node_id: The identifier of the node to remove. + node_id: The identifier of the node to remove. Returns: - bool: True if the node was removed, False if it was not found in the list. + bool: True if the node was removed, False if it was not found in the list. """ if node_id in self.related_nodes: @@ -167,10 +164,10 @@ class Tool(BaseRelatableNode): Represents a tool, extending BaseRelatableNode with specific functionalities and configurations. Attributes: - func: The main function or capability of the tool. - schema_: An optional schema defining the structure and constraints of data the tool works with. - manual: Optional documentation or manual for using the tool. - parser: An optional parser associated with the tool for data processing or interpretation. + func: The main function or capability of the tool. + schema_: An optional schema defining the structure and constraints of data the tool works with. + manual: Optional documentation or manual for using the tool. + parser: An optional parser associated with the tool for data processing or interpretation. """ func: Any @@ -178,7 +175,7 @@ class Tool(BaseRelatableNode): manual: Any | None = None parser: Any | None = None - @pyd.ln_field_serializer("func") + @field_serializer("func") def serialize_func(self, func): return func.__name__ diff --git a/lionagi/core/schema/condition.py b/lionagi/core/schema/condition.py new file mode 100644 index 000000000..f385edc9b --- /dev/null +++ b/lionagi/core/schema/condition.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from enum import Enum + + +class SourceType(str, Enum): + STRUCTURE = "structure" + EXECUTABLE = "executable" + + +class Condition(ABC): + def __init__(self, source_type): + try: + if isinstance(source_type, str): + source_type = SourceType(source_type) + if isinstance(source_type, SourceType): + self.source_type = source_type + except: + raise ValueError( + f"Invalid source_type. Valid source types are {list(SourceType)}" + ) + + @abstractmethod + def __call__(self, source_obj): + pass diff --git a/lionagi/core/schema/data_logger.py b/lionagi/core/schema/data_logger.py index 186d751db..f4e5717b1 100644 --- a/lionagi/core/schema/data_logger.py +++ b/lionagi/core/schema/data_logger.py @@ -4,10 +4,7 @@ from pathlib import Path from typing import Any, Dict, List -from lionagi.libs.sys_util import SysUtil - -from lionagi.libs import ln_convert as convert -from lionagi.libs import ln_nested as nested +from lionagi.libs import SysUtil, convert, nested # TODO: there should be a global data logger, under setting @@ -26,12 +23,12 @@ class DLog: operations. Attributes: - input_data (Any): The data received by the operation. This attribute can be of - any type, reflecting the flexible nature of input data to - various processes. - output_data (Any): The data produced by the operation. Similar to `input_data`, - this attribute supports any type, accommodating the diverse - outputs that different operations may generate. + input_data (Any): The data received by the operation. This attribute can be of + any type, reflecting the flexible nature of input data to + various processes. + output_data (Any): The data produced by the operation. Similar to `input_data`, + this attribute supports any type, accommodating the diverse + outputs that different operations may generate. Methods: serialize: Converts the instance into a dictionary, suitable for serialization, and appends a timestamp to this dictionary, reflecting the current @@ -49,8 +46,8 @@ def serialize(self, *, flatten_=True, sep="[^_^]") -> Dict[str, Any]: dictionary, capturing the exact time the log entry was serialized. Returns: - Dict[str, Any]: A dictionary representation of the DLog instance, including - 'input_data', 'output_data', and 'timestamp' keys. + Dict[str, Any]: A dictionary representation of the DLog instance, including + 'input_data', 'output_data', and 'timestamp' keys. """ log_dict = {} @@ -86,8 +83,8 @@ def deserialize( dictionary, capturing the exact time the log entry was serialized. Returns: - Dict[str, Any]: A dictionary representation of the DLog instance, including - 'input_data', 'output_data', and 'timestamp' keys. + Dict[str, Any]: A dictionary representation of the DLog instance, including + 'input_data', 'output_data', and 'timestamp' keys. """ input_data = "" output_data = "" @@ -116,27 +113,27 @@ class DataLogger: at program exit, among other features. Attributes: - persist_path (Path): The filesystem path to the directory where log files will - be saved. Defaults to a subdirectory 'data/logs/' within - the current working directory. - log (Deque[Dict]): A deque object that acts as the container for log entries. - Each log entry is stored as a dictionary, facilitating easy - conversion to various data formats. - filename (str): The base name used for log files when saved. The actual filepath - may include a timestamp or other modifiers based on the class's - configuration. + persist_path (Path): The filesystem path to the directory where log files will + be saved. Defaults to a subdirectory 'data/logs/' within + the current working directory. + log (Deque[Dict]): A deque object that acts as the container for log entries. + Each log entry is stored as a dictionary, facilitating easy + conversion to various data formats. + filename (str): The base name used for log files when saved. The actual filepath + may include a timestamp or other modifiers based on the class's + configuration. Methods: - append: Adds a new log entry to the datalogger. - to_csv_file: Exports accumulated log entries to a CSV file. - to_json_file: Exports accumulated log entries to a JSON file. - save_at_exit: Ensures that unsaved log entries are persisted to a CSV file when - the program terminates. + append: Adds a new log entry to the datalogger. + to_csv_file: Exports accumulated log entries to a CSV file. + to_json_file: Exports accumulated log entries to a JSON file. + save_at_exit: Ensures that unsaved log entries are persisted to a CSV file when + the program terminates. Usage Example: - >>> datalogger = DataLogger(persist_path='my/logs/directory', filepath='process_logs') - >>> datalogger.append(input_data="Example input", output_data="Example output") - >>> datalogger.to_csv_file('finalized_logs.csv', clear=True) + >>> datalogger = DataLogger(persist_path='my/logs/directory', filepath='process_logs') + >>> datalogger.append(input_data="Example input", output_data="Example output") + >>> datalogger.to_csv_file('finalized_logs.csv', clear=True) This example demonstrates initializing a `DataLogger` with a custom directory and filepath, appending a log entry, and then exporting the log to a CSV file. @@ -154,18 +151,18 @@ def __init__( logs, and base filepath for exports. Args: - persist_path (str | Path | None, optional): - The file system path to the directory where log files will be persisted. - if not provided, defaults to 'data/logs/' within the current working - directory. this path is used for all subsequent log export operations. - log (list[Dict[str, Any]] | None, optional): - An initial collection of log entries to populate the datalogger. each entry - should be a dictionary reflecting the structure used by the datalogger - (input, output, timestamp). if omitted, the datalogger starts empty. - filename (str | None, optional): - The base name for exported log files. this name may be augmented with - timestamps and format-specific extensions during export operations. - defaults to 'log'. + persist_path (str | Path | None, optional): + The file system path to the directory where log files will be persisted. + if not provided, defaults to 'data/logs/' within the current working + directory. this path is used for all subsequent log export operations. + log (list[Dict[str, Any]] | None, optional): + An initial collection of log entries to populate the datalogger. each entry + should be a dictionary reflecting the structure used by the datalogger + (input, output, timestamp). if omitted, the datalogger starts empty. + filename (str | None, optional): + The base name for exported log files. this name may be augmented with + timestamps and format-specific extensions during export operations. + defaults to 'log'. register an at-exit handler to ensure unsaved logs are automatically persisted to a CSV file upon program termination. @@ -187,10 +184,10 @@ def append(self, *, input_data: Any, output_data: Any) -> None: record deque. Args: - input_data (Any): - Data provided as input to a tracked operation or process. - output_data (Any): - Data resulting from the operation, recorded as the output. + input_data (Any): + Data provided as input to a tracked operation or process. + output_data (Any): + Data resulting from the operation, recorded as the output. constructs a log entry from the provided data and automatically includes a timestamp upon serialization. @@ -217,25 +214,25 @@ def to_csv_file( and timestamping options. Args: - filename (str, optional): - Filename for the CSV output, appended with '.csv' if not included, saved - within the specified persisting directory. - dir_exist_ok (bool, optional): - If False, raises an error if the directory already exists; otherwise, - writes without an error. - timestamp (bool, optional): - If True, appends a current timestamp to the filepath for uniqueness. - time_prefix (bool, optional): - If True, place the timestamp prefix before the filepath; otherwise, - it's suffixed. - verbose (bool, optional): - If True, print a message upon successful file save, detailing the file - path and number of logs saved. - clear (bool, optional): - If True, empties the internal log record after saving. - **kwargs: - Additional keyword arguments for pandas.DataFrame.to_csv(), allowing - customization of the CSV output, such as excluding the index. + filename (str, optional): + Filename for the CSV output, appended with '.csv' if not included, saved + within the specified persisting directory. + dir_exist_ok (bool, optional): + If False, raises an error if the directory already exists; otherwise, + writes without an error. + timestamp (bool, optional): + If True, appends a current timestamp to the filepath for uniqueness. + time_prefix (bool, optional): + If True, place the timestamp prefix before the filepath; otherwise, + it's suffixed. + verbose (bool, optional): + If True, print a message upon successful file save, detailing the file + path and number of logs saved. + clear (bool, optional): + If True, empties the internal log record after saving. + **kwargs: + Additional keyword arguments for pandas.DataFrame.to_csv(), allowing + customization of the CSV output, such as excluding the index. raises a ValueError with an explanatory message if an error occurs during the file writing or DataFrame conversion process. @@ -260,7 +257,7 @@ def to_csv_file( if clear: self.log.clear() except Exception as e: - raise ValueError(f"Error in saving to csv: {e}") + raise ValueError(f"Error in saving to csv: {e}") from e def to_json_file( self, @@ -281,41 +278,41 @@ def to_json_file( offering customization for file naming and timestamping. Args: - filename (str, optional): - The filepath for the JSON output.'.json' is appended if not specified. - The file is saved within the designated persisting directory. - timestamp (bool, optional): - If True, adds a timestamp to the filepath to ensure uniqueness. - time_prefix (bool, optional): - Determines the placement of the timestamp in the filepath. A prefix if - True; otherwise, a suffix. - dir_exist_ok (bool, optional): - Allows writing to an existing directory without raising an error. - If False, an error is raised when attempting to write to an existing - directory. - verbose (bool, optional): - Print a message upon successful save, indicating the file path and - number of logs saved. - clear (bool, optional): - Clears the log deque after saving, aiding in memory management. - **kwargs: - Additional arguments passed to pandas.DataFrame.to_json(), - enabling customization of the JSON output. + filename (str, optional): + The filepath for the JSON output.'.json' is appended if not specified. + The file is saved within the designated persisting directory. + timestamp (bool, optional): + If True, adds a timestamp to the filepath to ensure uniqueness. + time_prefix (bool, optional): + Determines the placement of the timestamp in the filepath. A prefix if + True; otherwise, a suffix. + dir_exist_ok (bool, optional): + Allows writing to an existing directory without raising an error. + If False, an error is raised when attempting to write to an existing + directory. + verbose (bool, optional): + Print a message upon successful save, indicating the file path and + number of logs saved. + clear (bool, optional): + Clears the log deque after saving, aiding in memory management. + **kwargs: + Additional arguments passed to pandas.DataFrame.to_json(), + enabling customization of the JSON output. Raises: - ValueError: When an error occurs during file writing or DataFrame conversion, - encapsulating - the exception with a descriptive message. + ValueError: When an error occurs during file writing or DataFrame conversion, + encapsulating + the exception with a descriptive message. Examples: - Default usage saving logs to 'log.json' within the specified persisting - directory: - >>> datalogger.to_json_file() - # Save path: 'data/logs/log.json' - - Custom filepath without a timestamp, using additional pandas options: - >>> datalogger.to_json_file(filepath='detailed_log.json', orient='records') - # Save a path: 'data/logs/detailed_log.json' + Default usage saving logs to 'log.json' within the specified persisting + directory: + >>> datalogger.to_json_file() + # Save path: 'data/logs/log.json' + + Custom filepath without a timestamp, using additional pandas options: + >>> datalogger.to_json_file(filepath='detailed_log.json', orient='records') + # Save a path: 'data/logs/detailed_log.json' """ if not filename.endswith(".json"): filename += ".json" @@ -337,7 +334,7 @@ def to_json_file( if clear: self.log.clear() except Exception as e: - raise ValueError(f"Error in saving to csv: {e}") + raise ValueError(f"Error in saving to csv: {e}") from e def save_at_exit(self): """ diff --git a/lionagi/core/schema/data_node.py b/lionagi/core/schema/data_node.py index ef1fa8b05..7b96b899a 100644 --- a/lionagi/core/schema/data_node.py +++ b/lionagi/core/schema/data_node.py @@ -1,5 +1,5 @@ from typing import Any -from lionagi.core.schema.base_node import BaseNode +from .base_node import BaseNode from lionagi.integrations.bridge import LlamaIndexBridge, LangchainBridge @@ -22,15 +22,15 @@ def to_llama_index(self, node_type=None, **kwargs) -> Any: integration and usage within that ecosystem. Args: - node_type: - **kwargs: Additional keyword arguments for customization. + node_type: + **kwargs: Additional keyword arguments for customization. Returns: - Any: The llama index format representation of the node. + Any: The llama index format representation of the node. Examples: - >>> node = DataNode(content="Example content") - >>> llama_index = node.to_llama_index() + >>> node = DataNode(content="Example content") + >>> llama_index = node.to_llama_index() """ return LlamaIndexBridge.to_llama_index_node(self, node_type=node_type, **kwargs) @@ -42,14 +42,14 @@ def to_langchain(self, **kwargs) -> Any: use within langchain_ applications and workflows. Args: - **kwargs: Additional keyword arguments for customization. + **kwargs: Additional keyword arguments for customization. Returns: - Any: The langchain_ document representation of the node. + Any: The langchain_ document representation of the node. Examples: - >>> node = DataNode(content="Example content") - >>> langchain_doc = node.to_langchain() + >>> node = DataNode(content="Example content") + >>> langchain_doc = node.to_langchain() """ return LangchainBridge.to_langchain_document(self, **kwargs) @@ -59,15 +59,15 @@ def from_llama_index(cls, llama_node: Any, **kwargs) -> "DataNode": Creates a DataNode instance from a llama index node. Args: - llama_node: The llama index node object. - **kwargs: Variable length argument list. + llama_node: The llama index node object. + **kwargs: Variable length argument list. Returns: - An instance of DataNode. + An instance of DataNode. Examples: - llama_node = SomeLlamaIndexNode() - data_node = DataNode.from_llama_index(llama_node) + llama_node = SomeLlamaIndexNode() + data_node = DataNode.from_llama_index(llama_node) """ llama_dict = llama_node.to_dict(**kwargs) return cls.from_obj(llama_dict) @@ -78,14 +78,14 @@ def from_langchain(cls, lc_doc: Any) -> "DataNode": Creates a DataNode instance from a langchain_ document. Args: - lc_doc: The langchain_ document object. + lc_doc: The langchain_ document object. Returns: - An instance of DataNode. + An instance of DataNode. Examples: - lc_doc = SomeLangChainDocument() - data_node = DataNode.from_langchain(lc_doc) + lc_doc = SomeLangChainDocument() + data_node = DataNode.from_langchain(lc_doc) """ info_json = lc_doc.to_json() info_node = {"lc_id": info_json["id"]} diff --git a/lionagi/core/schema/prompt_template.py b/lionagi/core/schema/prompt_template.py new file mode 100644 index 000000000..e69de29bb diff --git a/lionagi/core/schema/structure.py b/lionagi/core/schema/structure.py index ace846856..f832ebded 100644 --- a/lionagi/core/schema/structure.py +++ b/lionagi/core/schema/structure.py @@ -1,14 +1,14 @@ -from typing import List, Any, Dict +import time +from typing import List, Any, Dict, Callable from collections import deque from pydantic import Field -from lionagi.libs.sys_util import SysUtil -from lionagi.libs import ln_func_call as func_call -from lionagi.libs.ln_async import AsyncUtil +from lionagi.libs import SysUtil, func_call, AsyncUtil -from lionagi.core.schema.base_node import BaseRelatableNode, BaseNode +from .base_node import BaseRelatableNode, BaseNode, Tool from lionagi.core.mail.schema import BaseMail +from lionagi.core.schema.condition import Condition from lionagi.core.schema.action_node import ActionNode, ActionSelection from lionagi.core.schema.base_node import Tool @@ -19,113 +19,44 @@ class Relationship(BaseRelatableNode): Represents a relationship between two nodes in a graph. Attributes: - source_node_id (str): The identifier of the source node. - target_node_id (str): The identifier of the target node. - condition (Dict[str, Any]): A dictionary representing conditions for the relationship. + source_node_id (str): The identifier of the source node. + target_node_id (str): The identifier of the target node. + condition (Dict[str, Any]): A dictionary representing conditions for the relationship. Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") - >>> relationship.add_condition({"key": "value"}) - >>> condition_value = relationship.get_condition("key") - >>> relationship.remove_condition("key") + >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") + >>> relationship.add_condition({"key": "value"}) + >>> condition_value = relationship.get_condition("key") + >>> relationship.remove_condition("key") """ source_node_id: str target_node_id: str bundle: bool = False - condition: dict = Field(default={}) - - def add_condition(self, condition: Dict[str, Any]) -> None: - """ - Adds a condition to the relationship. - - Args: - condition: The condition to be added. - - Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") - >>> relationship.add_condition({"key": "value"}) - """ - self.condition.update(condition) - - def remove_condition(self, condition_key: str) -> Any: - """ - Removes a condition from the relationship. - - Args: - condition_key: The key of the condition to be removed. - - Returns: - The value of the removed condition. - - Raises: - KeyError: If the condition key is not found. - - Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2", condition={"key": "value"}) - >>> relationship.remove_condition("key") - 'value' - """ - if condition_key not in self.condition.keys(): - raise KeyError(f"condition {condition_key} is not found") - return self.condition.pop(condition_key) - - def condition_exists(self, condition_key: str) -> bool: - """ - Checks if a condition exists in the relationship. - - Args: - condition_key: The key of the condition to check. - - Returns: - True if the condition exists, False otherwise. - - Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2", condition={"key": "value"}) - >>> relationship.condition_exists("key") - True - """ - if condition_key in self.condition.keys(): - return True - else: - return False - - def get_condition(self, condition_key: str | None = None) -> Any: - """ - Retrieves a specific condition or all conditions of the relationship. + condition: Callable = None - Args: - condition_key: The key of the specific condition. If None, all conditions are returned. - - Returns: - The requested condition or all conditions if no key is provided. - - Raises: - ValueError: If the specified condition key does not exist. + def add_condition(self, condition: Condition): + if not isinstance(condition, Condition): + raise ValueError( + "Invalid condition type, please use Condition class to build a valid condition" + ) + self.condition = condition - Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2", condition={"key": "value"}) - >>> relationship.get_condition("key") - 'value' - >>> relationship.get_condition() - {'key': 'value'} - """ - if condition_key is None: - return self.condition - if self.condition_exists(condition_key=condition_key): - return self.condition[condition_key] - else: - raise ValueError(f"Condition {condition_key} does not exist") + def check_condition(self, source_obj): + try: + return bool(self.condition(source_obj)) + except: + raise ValueError("Invalid relationship condition function") def _source_existed(self, obj: Dict[str, Any]) -> bool: """ Checks if the source node exists in a given object. Args: - obj (Dict[str, Any]): The object to check. + obj (Dict[str, Any]): The object to check. Returns: - bool: True if the source node exists, False otherwise. + bool: True if the source node exists, False otherwise. """ return self.source_node_id in obj.keys() @@ -134,10 +65,10 @@ def _target_existed(self, obj: Dict[str, Any]) -> bool: Checks if the target node exists in a given object. Args: - obj (Dict[str, Any]): The object to check. + obj (Dict[str, Any]): The object to check. Returns: - bool: True if the target node exists, False otherwise. + bool: True if the target node exists, False otherwise. """ return self.target_node_id in obj.keys() @@ -146,13 +77,13 @@ def _is_in(self, obj: Dict[str, Any]) -> bool: Validates the existence of both source and target nodes in a given object. Args: - obj (Dict[str, Any]): The object to check. + obj (Dict[str, Any]): The object to check. Returns: - bool: True if both nodes exist. + bool: True if both nodes exist. Raises: - ValueError: If either the source or target node does not exist. + ValueError: If either the source or target node does not exist. """ if self._source_existed(obj) and self._target_existed(obj): return True @@ -167,9 +98,9 @@ def __str__(self) -> str: Returns a simple string representation of the Relationship. Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") - >>> str(relationship) - 'Relationship (id_=None, from=node1, to=node2, label=None)' + >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") + >>> str(relationship) + 'Relationship (id_=None, from=node1, to=node2, label=None)' """ return ( f"Relationship (id_={self.id_}, from={self.source_node_id}, to={self.target_node_id}, " @@ -181,9 +112,9 @@ def __repr__(self) -> str: Returns a detailed string representation of the Relationship. Examples: - >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") - >>> repr(relationship) - 'Relationship(id_=None, from=node1, to=node2, content=None, metadata=None, label=None)' + >>> relationship = Relationship(source_node_id="node1", target_node_id="node2") + >>> repr(relationship) + 'Relationship(id_=None, from=node1, to=node2, content=None, metadata=None, label=None)' """ return ( f"Relationship(id_={self.id_}, from={self.source_node_id}, to={self.target_node_id}, " @@ -196,20 +127,20 @@ class Graph(BaseRelatableNode): Represents a graph structure, consisting of nodes and their relationship. Attributes: - nodes (Dict[str, BaseNode]): A dictionary of nodes in the graph. - relationships (Dict[str, Relationship]): A dictionary of relationship between nodes in the graph. - node_relationships (Dict[str, Dict[str, Dict[str, str]]]): A dictionary tracking the relationship of each node. + nodes (Dict[str, BaseNode]): A dictionary of nodes in the graph. + relationships (Dict[str, Relationship]): A dictionary of relationship between nodes in the graph. + node_relationships (Dict[str, Dict[str, Dict[str, str]]]): A dictionary tracking the relationship of each node. Examples: - >>> graph = Graph() - >>> node = BaseNode(id_='node1') - >>> graph.add_node(node) - >>> graph.node_exists(node) - True - >>> relationship = Relationship(id_='rel1', source_node_id='node1', target_node_id='node2') - >>> graph.add_relationship(relationship) - >>> graph.relationship_exists(relationship) - True + >>> graph = Graph() + >>> node = BaseNode(id_='node1') + >>> graph.add_node(node) + >>> graph.node_exists(node) + True + >>> relationship = Relationship(id_='rel1', source_node_id='node1', target_node_id='node2') + >>> graph.add_relationship(relationship) + >>> graph.relationship_exists(relationship) + True """ nodes: dict = Field(default={}) @@ -221,7 +152,7 @@ def add_node(self, node: BaseNode) -> None: Adds a node to the graph. Args: - node (BaseNode): The node to add to the graph. + node (BaseNode): The node to add to the graph. """ self.nodes[node.id_] = node @@ -232,10 +163,10 @@ def add_relationship(self, relationship: Relationship) -> None: Adds a relationship between nodes in the graph. Args: - relationship (Relationship): The relationship to add. + relationship (Relationship): The relationship to add. Raises: - KeyError: If either the source or target node of the relationship is not found in the graph. + KeyError: If either the source or target node of the relationship is not found in the graph. """ if relationship.source_node_id not in self.node_relationships.keys(): raise KeyError(f"node {relationship.source_node_id} is not found.") @@ -257,14 +188,14 @@ def get_node_relationships( Retrieves relationship of a specific node or all relationship in the graph. Args: - node (Optional[BaseNode]): The node whose relationship to retrieve. If None, retrieves all relationship. - out_edge (bool): Whether to retrieve outgoing relationship. If False, retrieves incoming relationship. + node (Optional[BaseNode]): The node whose relationship to retrieve. If None, retrieves all relationship. + out_edge (bool): Whether to retrieve outgoing relationship. If False, retrieves incoming relationship. Returns: - List[Relationship]: A list of relationship. + List[Relationship]: A list of relationship. Raises: - KeyError: If the specified node is not found in the graph. + KeyError: If the specified node is not found in the graph. """ if node is None: return list(self.relationships.values()) @@ -285,18 +216,28 @@ def get_node_relationships( ) return relationships + def get_predecessors(self, node: BaseNode): + node_ids = list(self.node_relationships[node.id_]["in"].values()) + nodes = func_call.lcall(node_ids, lambda i: self.nodes[i]) + return nodes + + def get_successors(self, node: BaseNode): + node_ids = list(self.node_relationships[node.id_]["out"].values()) + nodes = func_call.lcall(node_ids, lambda i: self.nodes[i]) + return nodes + def remove_node(self, node: BaseNode) -> BaseNode: """ Removes a node and its associated relationship from the graph. Args: - node (BaseNode): The node to remove. + node (BaseNode): The node to remove. Returns: - BaseNode: The removed node. + BaseNode: The removed node. Raises: - KeyError: If the node is not found in the graph. + KeyError: If the node is not found in the graph. """ if node.id_ not in self.nodes.keys(): raise KeyError(f"node {node.id_} is not found") @@ -319,13 +260,13 @@ def remove_relationship(self, relationship: Relationship) -> Relationship: Removes a relationship from the graph. Args: - relationship (Relationship): The relationship to remove. + relationship (Relationship): The relationship to remove. Returns: - Relationship: The removed relationship. + Relationship: The removed relationship. Raises: - KeyError: If the relationship is not found in the graph. + KeyError: If the relationship is not found in the graph. """ if relationship.id_ not in self.relationships.keys(): raise KeyError(f"relationship {relationship.id_} is not found") @@ -342,10 +283,10 @@ def node_exist(self, node: BaseNode) -> bool: Checks if a node exists in the graph. Args: - node (BaseNode): The node to check. + node (BaseNode): The node to check. Returns: - bool: True if the node exists, False otherwise. + bool: True if the node exists, False otherwise. """ if node.id_ in self.nodes.keys(): return True @@ -357,10 +298,10 @@ def relationship_exist(self, relationship: Relationship) -> bool: Checks if a relationship exists in the graph. Args: - relationship (Relationship): The relationship to check. + relationship (Relationship): The relationship to check. Returns: - bool: True if the relationship exists, False otherwise. + bool: True if the relationship exists, False otherwise. """ if relationship.id_ in self.relationships.keys(): return True @@ -372,7 +313,7 @@ def is_empty(self) -> bool: Determines if the graph is empty. Returns: - bool: True if the graph has no nodes, False otherwise. + bool: True if the graph has no nodes, False otherwise. """ if self.nodes: return False @@ -390,14 +331,14 @@ def to_networkx(self, **kwargs) -> Any: Converts the graph to a NetworkX graph object. Args: - **kwargs: Additional keyword arguments to pass to the NetworkX DiGraph constructor. + **kwargs: Additional keyword arguments to pass to the NetworkX DiGraph constructor. Returns: - Any: A NetworkX directed graph representing the graph. + Any: A NetworkX directed graph representing the graph. Examples: - >>> graph = Graph() - >>> nx_graph = graph.to_networkx() + >>> graph = Graph() + >>> nx_graph = graph.to_networkx() """ SysUtil.check_import("networkx") @@ -408,11 +349,13 @@ def to_networkx(self, **kwargs) -> Any: for node_id, node in self.nodes.items(): node_info = node.to_dict() node_info.pop("node_id") + node_info.update({"class_name": node.__class__.__name__}) g.add_node(node_id, **node_info) for _, relationship in self.relationships.items(): relationship_info = relationship.to_dict() relationship_info.pop("node_id") + relationship_info.update({"class_name": relationship.__class__.__name__}) source_node_id = relationship_info.pop("source_node_id") target_node_id = relationship_info.pop("target_node_id") g.add_edge(source_node_id, target_node_id, **relationship_info) @@ -422,20 +365,37 @@ def to_networkx(self, **kwargs) -> Any: class Structure(BaseRelatableNode): graph: Graph = Graph() - processing_mails: deque = deque() pending_ins: dict = {} pending_outs: deque = deque() execute_stop: bool = False + condition_check_result: bool | None = None def add_node(self, node: BaseNode): self.graph.add_node(node) - # def add_relationship(self, relationship: Relationship): - # self.graph.add_relationship(relationship) - def add_relationship(self, from_node: BaseNode, to_node: BaseNode, **kwargs): + def add_relationship( + self, + from_node: BaseNode, + to_node: BaseNode, + bundle=False, + condition=None, + **kwargs, + ): + if isinstance(from_node, Tool) or isinstance(from_node, ActionSelection): + raise ValueError( + f"type {type(from_node)} should not be the head of the relationship, " + f"please switch position and attach it to the tail of the relationship" + ) + if isinstance(to_node, Tool) or isinstance(to_node, ActionSelection): + bundle = True relationship = Relationship( - source_node_id=from_node.id_, target_node_id=to_node.id_, **kwargs + source_node_id=from_node.id_, + target_node_id=to_node.id_, + bundle=bundle, + **kwargs, ) + if condition: + relationship.add_condition(condition) self.graph.add_relationship(relationship) def get_relationships(self) -> list[Relationship]: @@ -453,6 +413,12 @@ def get_node_relationships(self, node: BaseNode, out_edge=True, labels=None): relationships = result return relationships + def get_predecessors(self, node: BaseNode): + return self.graph.get_predecessors(node) + + def get_successors(self, node: BaseNode): + return self.graph.get_successors(node) + def node_exist(self, node: BaseNode) -> bool: return self.graph.node_exist(node) @@ -469,24 +435,12 @@ def is_empty(self) -> bool: return self.graph.is_empty() def get_heads(self): - heads = deque() + heads = [] for key in self.graph.node_relationships: if not self.graph.node_relationships[key]["in"]: heads.append(self.graph.nodes[key]) return heads - # def get_next_step(self, current_node: BaseNode): - # next_nodes = deque() - # next_relationships = self.get_node_relationships(current_node) - # for relationship in next_relationships: - # node = self.graph.nodes[relationship.target_node_id] - # next_nodes.append(node) - # further_relationships = self.get_node_relationships(node) - # for f_relationship in further_relationships: - # if f_relationship.bundle: - # next_nodes.append(self.graph.nodes[f_relationship.target_node_id]) - # return next_nodes - @staticmethod def parse_to_action(instruction: BaseNode, bundled_nodes: deque): action_node = ActionNode(instruction) @@ -501,12 +455,36 @@ def parse_to_action(instruction: BaseNode, bundled_nodes: deque): raise ValueError("Invalid bundles nodes") return action_node - def get_next_step(self, current_node: BaseNode): - next_nodes = deque() + async def check_condition(self, relationship, executable_id): + if relationship.condition.source_type == "structure": + return self.check_condition_structure(relationship) + elif relationship.condition.source_type == "executable": + self.send( + recipient_id=executable_id, category="condition", package=relationship + ) + while self.condition_check_result is None: + await AsyncUtil.sleep(0.1) + self.process_relationship_condition(relationship.id_) + continue + check_result = self.condition_check_result + self.condition_check_result = None + return check_result + else: + raise ValueError(f"Invalid source_type.") + + def check_condition_structure(self, relationship): + return relationship.condition(self) + + async def get_next_step(self, current_node: BaseNode, executable_id): + next_nodes = [] next_relationships = self.get_node_relationships(current_node) for relationship in next_relationships: if relationship.bundle: continue + if relationship.condition: + check = await self.check_condition(relationship, executable_id) + if not check: + continue node = self.graph.nodes[relationship.target_node_id] further_relationships = self.get_node_relationships(node) bundled_nodes = deque() @@ -559,7 +537,21 @@ def send(self, recipient_id: str, category: str, package: Any) -> None: ) self.pending_outs.append(mail) - def process(self) -> None: + def process_relationship_condition(self, relationship_id): + for key in list(self.pending_ins.keys()): + skipped_requests = deque() + while self.pending_ins[key]: + mail = self.pending_ins[key].popleft() + if ( + mail.category == "condition" + and mail.package["relationship_id"] == relationship_id + ): + self.condition_check_result = mail.package["check_result"] + else: + skipped_requests.append(mail) + self.pending_ins[key] = skipped_requests + + async def process(self) -> None: for key in list(self.pending_ins.keys()): while self.pending_ins[key]: mail = self.pending_ins[key].popleft() @@ -573,13 +565,15 @@ def process(self) -> None: raise ValueError( f"Node {mail.package} does not exist in the structure {self.id_}" ) - next_nodes = self.get_next_step(self.graph.nodes[mail.package]) + next_nodes = await self.get_next_step( + self.graph.nodes[mail.package], mail.sender_id + ) elif mail.category == "node" and isinstance(mail.package, BaseNode): if not self.node_exist(mail.package): raise ValueError( f"Node {mail.package} does not exist in the structure {self.id_}" ) - next_nodes = self.get_next_step(mail.package) + next_nodes = await self.get_next_step(mail.package, mail.sender_id) else: raise ValueError(f"Invalid mail type for structure") @@ -587,16 +581,24 @@ def process(self) -> None: self.send( recipient_id=mail.sender_id, category="end", package="end" ) - while next_nodes: - package = next_nodes.popleft() - self.send( - recipient_id=mail.sender_id, category="node", package=package - ) + else: + if len(next_nodes) == 1: + self.send( + recipient_id=mail.sender_id, + category="node", + package=next_nodes[0], + ) + else: + self.send( + recipient_id=mail.sender_id, + category="node_list", + package=next_nodes, + ) async def execute(self, refresh_time=1): if not self.acyclic(): raise ValueError("Structure is not acyclic") while not self.execute_stop: - self.process() + await self.process() await AsyncUtil.sleep(refresh_time) diff --git a/lionagi/core/session/__init__.py b/lionagi/core/session/__init__.py index 6c97e0f95..414557f5a 100644 --- a/lionagi/core/session/__init__.py +++ b/lionagi/core/session/__init__.py @@ -1,5 +1,3 @@ from .session import Session -from lionagi.core.branch.branch import Branch - -__all__ = ["Session", "Branch"] +__all__ = ["Session"] diff --git a/lionagi/core/session/session.py b/lionagi/core/session/session.py index 6a3562f69..c95204d12 100644 --- a/lionagi/core/session/session.py +++ b/lionagi/core/session/session.py @@ -1,19 +1,15 @@ from collections import deque from typing import Tuple -from lionagi.libs.ln_api import BaseService from lionagi.libs.sys_util import PATH_TYPE +from lionagi.libs import BaseService, convert, dataframe -from lionagi.libs import ln_convert as convert -from lionagi.libs import ln_dataframe as dataframe - -from lionagi.core.schema.base_node import TOOL_TYPE, Tool -from lionagi.core.schema.data_logger import DataLogger -from lionagi.core.tool.tool_manager import ToolManager -from lionagi.core.mail.mail_manager import MailManager -from lionagi.core.messages.schema import System, Instruction -from lionagi.core.branch.branch import Branch -from lionagi.core.flow.polyflow.chat import PolyChat +from lionagi.core.schema import TOOL_TYPE, Tool, DataLogger +from lionagi.core.tool import ToolManager +from lionagi.core.mail import MailManager +from lionagi.core.messages import System, Instruction +from lionagi.core.branch import Branch +from lionagi.core.flow.polyflow import PolyChat class Session: @@ -25,10 +21,10 @@ class Session: branches, configuring language learning models, managing tools, and handling session data logging. Attributes: - branches (dict[str, Branch]): A dictionary of branch instances associated with the session. - service (BaseService]): The external service instance associated with the | Nonesession. - mail_manager (BranchManager): The manager for handling branches within the session. - datalogger (Optional[Any]): The datalogger instance for session data logging. + branches (dict[str, Branch]): A dictionary of branch instances associated with the session. + service (BaseService]): The external service instance associated with the | Nonesession. + mail_manager (BranchManager): The manager for handling branches within the session. + datalogger (Optional[Any]): The datalogger instance for session data logging. """ def __init__( @@ -50,22 +46,22 @@ def __init__( """Initialize a new session with optional configuration for managing conversations. Args: - system (Optional[Union[str, System]]): The system message. - sender (str | None): the default sender name for default branch - llmconfig (dict[str, Any] | None): Configuration for language learning models. - service (BaseService]): External service | Nonenstance. - branches (dict[str, Branch] | None): dictionary of branch instances. - default_branch (Branch | None): The default branch for the session. - default_branch_name (str | None): The name of the default branch. - tools (TOOL_TYPE | None): List of tools available for the session. - instruction_sets (Optional[List[Instruction]]): List of instruction sets. - tool_manager (Optional[Any]): Manager for handling tools. - messages (Optional[List[dict[str, Any]]]): Initial list of messages. - datalogger (Optional[Any]): Logger instance for the session. - persist_path (str | None): Directory path for saving session data. + system (Optional[Union[str, System]]): The system message. + sender (str | None): the default sender name for default branch + llmconfig (dict[str, Any] | None): Configuration for language learning models. + service (BaseService]): External service | Nonenstance. + branches (dict[str, Branch] | None): dictionary of branch instances. + default_branch (Branch | None): The default branch for the session. + default_branch_name (str | None): The name of the default branch. + tools (TOOL_TYPE | None): List of tools available for the session. + instruction_sets (Optional[List[Instruction]]): List of instruction sets. + tool_manager (Optional[Any]): Manager for handling tools. + messages (Optional[List[dict[str, Any]]]): Initial list of messages. + datalogger (Optional[Any]): Logger instance for the session. + persist_path (str | None): Directory path for saving session data. Examples: - >>> session = Session(system="you are a helpful assistant", sender="researcher") + >>> session = Session(system="you are a helpful assistant", sender="researcher") """ self.branches = branches if isinstance(branches, dict) else {} self.service = service @@ -100,12 +96,12 @@ def messages_describe(self): Provides a descriptive summary of all messages in the branch. Returns: - dict[str, Any]: A dictionary containing summaries of messages by role and sender, total message count, - instruction sets, registered tools, and message details. + dict[str, Any]: A dictionary containing summaries of messages by role and sender, total message count, + instruction sets, registered tools, and message details. Examples: - >>> session.messages_describe - {'total_messages': 100, 'by_sender': {'User123': 60, 'Bot': 40}} + >>> session.messages_describe + {'total_messages': 100, 'by_sender': {'User123': 60, 'Bot': 40}} """ return self.default_branch.messages_describe @@ -115,11 +111,11 @@ def has_tools(self) -> bool: Checks if there are any tools registered in the tool manager. Returns: - bool: True if there are tools registered, False otherwise. + bool: True if there are tools registered, False otherwise. Examples: - >>> session.has_tools - True + >>> session.has_tools + True """ return self.default_branch.has_tools @@ -129,7 +125,7 @@ def last_message(self) -> dataframe.ln_DataFrame: Retrieves the last message from the conversation. Returns: - pd.Series: The last message as a pandas Series. + pd.Series: The last message as a pandas Series. """ return self.default_branch.last_message @@ -139,7 +135,7 @@ def first_system(self) -> dataframe.ln_DataFrame: Retrieves the first system message from the conversation. Returns: - pd.Series: The first system message as a pandas Series. + pd.Series: The first system message as a pandas Series. """ return self.default_branch.first_system @@ -149,7 +145,7 @@ def last_response(self) -> dataframe.ln_DataFrame: Retrieves the last response message from the conversation. Returns: - pd.Series: The last response message as a pandas Series. + pd.Series: The last response message as a pandas Series. """ return self.default_branch.last_response @@ -159,7 +155,7 @@ def last_response_content(self) -> dict: Retrieves the content of the last response message from the conversation. Returns: - dict: The content of the last response message as a dictionary + dict: The content of the last response message as a dictionary """ return self.default_branch.last_response_content @@ -169,7 +165,7 @@ def tool_request(self) -> dataframe.ln_DataFrame: Retrieves all tool request messages from the conversation. Returns: - dataframe.ln_DataFrame: A DataFrame containing all tool request messages. + dataframe.ln_DataFrame: A DataFrame containing all tool request messages. """ return self.default_branch.tool_request @@ -179,7 +175,7 @@ def tool_response(self) -> dataframe.ln_DataFrame: Retrieves all tool response messages from the conversation. Returns: - dataframe.ln_DataFrame: A DataFrame containing all tool response messages. + dataframe.ln_DataFrame: A DataFrame containing all tool response messages. """ return self.default_branch.tool_response @@ -189,7 +185,7 @@ def responses(self) -> dataframe.ln_DataFrame: Retrieves all response messages from the conversation. Returns: - dataframe.ln_DataFrame: A DataFrame containing all response messages. + dataframe.ln_DataFrame: A DataFrame containing all response messages. """ return self.default_branch.responses @@ -199,7 +195,7 @@ def assistant_responses(self) -> dataframe.ln_DataFrame: Retrieves all assistant responses from the conversation, excluding tool requests and responses. Returns: - dataframe.ln_DataFrame: A DataFrame containing assistant responses excluding tool requests and responses. + dataframe.ln_DataFrame: A DataFrame containing assistant responses excluding tool requests and responses. """ return self.default_branch.assistant_responses @@ -209,7 +205,7 @@ def info(self) -> dict[str, int]: Get a summary of the conversation messages categorized by role. Returns: - dict[str, int]: A dictionary with keys as message roles and values as counts. + dict[str, int]: A dictionary with keys as message roles and values as counts. """ return self.default_branch.info @@ -220,7 +216,7 @@ def sender_info(self) -> dict[str, int]: Provides a descriptive summary of the conversation, including total message count and summary by sender. Returns: - dict[str, Any]: A dictionary containing the total number of messages and a summary categorized by sender. + dict[str, Any]: A dictionary containing the total number of messages and a summary categorized by sender. """ return self.default_branch.sender_info @@ -236,8 +232,7 @@ def from_csv( llmconfig: dict[str, str | int | dict] | None = None, service: BaseService = None, default_branch_name: str = "main", - tools: TOOL_TYPE = False, - # instruction_sets=None, + tools: TOOL_TYPE = False, # instruction_sets=None, tool_manager=None, **kwargs, ) -> "Session": @@ -245,24 +240,24 @@ def from_csv( Creates a Session instance from a CSV file containing messages. Args: - filepath (str): Path to the CSV file. - name (str | None): Name of the branch, default is None. - instruction_sets (Optional[dict[str, InstructionSet]]): Instruction sets, default is None. - tool_manager (Optional[ToolManager]): Tool manager for the branch, default is None. - service (BaseService]): External service for the branch, default | Noneis None. - llmconfig (Optional[dict]): Configuration for language learning models, default is None. - tools (TOOL_TYPE | None): Initial list of tools to register, default is None. - **kwargs: Additional keyword arguments for pd.read_csv(). + filepath (str): Path to the CSV file. + name (str | None): Name of the branch, default is None. + instruction_sets (Optional[dict[str, InstructionSet]]): Instruction sets, default is None. + tool_manager (Optional[ToolManager]): Tool manager for the branch, default is None. + service (BaseService]): External service for the branch, default | Noneis None. + llmconfig (Optional[dict]): Configuration for language learning models, default is None. + tools (TOOL_TYPE | None): Initial list of tools to register, default is None. + **kwargs: Additional keyword arguments for pd.read_csv(). Returns: - Branch: A new Branch instance created from the CSV data. + Branch: A new Branch instance created from the CSV data. Examples: - >>> branch = Branch.from_csv("path/to/messages.csv", name="ImportedBranch") + >>> branch = Branch.from_csv("path/to/messages.csv", name="ImportedBranch") """ df = dataframe.read_csv(filepath, **kwargs) - self = cls( + return cls( system=system, sender=sender, llmconfig=llmconfig, @@ -274,8 +269,6 @@ def from_csv( **kwargs, ) - return self - @classmethod def from_json( cls, @@ -285,8 +278,7 @@ def from_json( llmconfig: dict[str, str | int | dict] | None = None, service: BaseService = None, default_branch_name: str = "main", - tools: TOOL_TYPE = False, - # instruction_sets=None, + tools: TOOL_TYPE = False, # instruction_sets=None, tool_manager=None, **kwargs, ) -> "Session": @@ -294,36 +286,33 @@ def from_json( Creates a Branch instance from a JSON file containing messages. Args: - filepath (str): Path to the JSON file. - name (str | None): Name of the branch, default is None. - instruction_sets (Optional[dict[str, InstructionSet]]): Instruction sets, default is None. - tool_manager (Optional[ToolManager]): Tool manager for the branch, default is None. - service (BaseService]): External service for the branch, default | Noneis None. - llmconfig (Optional[dict]): Configuration for language learning models, default is None. - **kwargs: Additional keyword arguments for pd.read_json(). + filepath (str): Path to the JSON file. + name (str | None): Name of the branch, default is None. + instruction_sets (Optional[dict[str, InstructionSet]]): Instruction sets, default is None. + tool_manager (Optional[ToolManager]): Tool manager for the branch, default is None. + service (BaseService]): External service for the branch, default | Noneis None. + llmconfig (Optional[dict]): Configuration for language learning models, default is None. + **kwargs: Additional keyword arguments for pd.read_json(). Returns: - Branch: A new Branch instance created from the JSON data. + Branch: A new Branch instance created from the JSON data. Examples: - >>> branch = Branch.from_json_string("path/to/messages.json", name="JSONBranch") + >>> branch = Branch.from_json_string("path/to/messages.json", name="JSONBranch") """ df = dataframe.read_json(filepath, **kwargs) - self = cls( + return cls( system=system, sender=sender, llmconfig=llmconfig, service=service, default_branch_name=default_branch_name, - tools=tools, - # instruction_sets=instruction_sets, + tools=tools, # instruction_sets=instruction_sets, tool_manager=tool_manager, messages=df, **kwargs, ) - return self - def to_csv_file( self, filename: str = "messages.csv", @@ -338,17 +327,17 @@ def to_csv_file( Saves the branch's messages to a CSV file. Args: - filename (str): The name of the output CSV file, default is 'messages.csv'. - dir_exist_ok (bool): If True, does not raise an error if the directory already exists, default is True. - timestamp (bool): If True, appends a timestamp to the filename, default is True. - time_prefix (bool): If True, adds a timestamp prefix to the filename, default is False. - verbose (bool): If True, prints a message upon successful save, default is True. - clear (bool): If True, clears the messages after saving, default is True. - **kwargs: Additional keyword arguments for DataFrame.to_csv(). + filename (str): The name of the output CSV file, default is 'messages.csv'. + dir_exist_ok (bool): If True, does not raise an error if the directory already exists, default is True. + timestamp (bool): If True, appends a timestamp to the filename, default is True. + time_prefix (bool): If True, adds a timestamp prefix to the filename, default is False. + verbose (bool): If True, prints a message upon successful save, default is True. + clear (bool): If True, clears the messages after saving, default is True. + **kwargs: Additional keyword arguments for DataFrame.to_csv(). Examples: - >>> branch.to_csv_file("exported_messages.csv") - >>> branch.to_csv_file("timed_export.csv", timestamp=True, time_prefix=True) + >>> branch.to_csv_file("exported_messages.csv") + >>> branch.to_csv_file("timed_export.csv", timestamp=True, time_prefix=True) """ for name, branch in self.branches.items(): f_name = f"{name}_{filename}" @@ -376,17 +365,17 @@ def to_json_file( Saves the branch's messages to a JSON file. Args: - filename (str): The name of the output JSON file, default is 'messages.json'. - dir_exist_ok (bool): If True, does not raise an error if the directory already exists, default is True. - timestamp (bool): If True, appends a timestamp to the filename, default is True. - time_prefix (bool): If True, adds a timestamp prefix to the filename, default is False. - verbose (bool): If True, prints a message upon successful save, default is True. - clear (bool): If True, clears the messages after saving, default is True. - **kwargs: Additional keyword arguments for DataFrame.to_json(). + filename (str): The name of the output JSON file, default is 'messages.json'. + dir_exist_ok (bool): If True, does not raise an error if the directory already exists, default is True. + timestamp (bool): If True, appends a timestamp to the filename, default is True. + time_prefix (bool): If True, adds a timestamp prefix to the filename, default is False. + verbose (bool): If True, prints a message upon successful save, default is True. + clear (bool): If True, clears the messages after saving, default is True. + **kwargs: Additional keyword arguments for DataFrame.to_json(). Examples: - >>> branch.to_json_file("exported_messages.json") - >>> branch.to_json_file("timed_export.json", timestamp=True, time_prefix=True) + >>> branch.to_json_file("exported_messages.json") + >>> branch.to_json_file("timed_export.json", timestamp=True, time_prefix=True) """ for name, branch in self.branches.items(): @@ -418,17 +407,17 @@ def log_to_csv( to a CSV file for analysis or record-keeping. Args: - filename (str): The name of the output CSV file. Defaults to 'log.csv'. - dir_exist_ok (bool): If True, will not raise an error if the directory already exists. Defaults to True. - timestamp (bool): If True, appends a timestamp to the filename for uniqueness. Defaults to True. - time_prefix (bool): If True, adds a timestamp prefix to the filename. Defaults to False. - verbose (bool): If True, prints a success message upon completion. Defaults to True. - clear (bool): If True, clears the log after saving. Defaults to True. - **kwargs: Additional keyword arguments for `DataFrame.to_csv()`. + filename (str): The name of the output CSV file. Defaults to 'log.csv'. + dir_exist_ok (bool): If True, will not raise an error if the directory already exists. Defaults to True. + timestamp (bool): If True, appends a timestamp to the filename for uniqueness. Defaults to True. + time_prefix (bool): If True, adds a timestamp prefix to the filename. Defaults to False. + verbose (bool): If True, prints a success message upon completion. Defaults to True. + clear (bool): If True, clears the log after saving. Defaults to True. + **kwargs: Additional keyword arguments for `DataFrame.to_csv()`. Examples: - >>> branch.log_to_csv("branch_log.csv") - >>> branch.log_to_csv("detailed_branch_log.csv", timestamp=True, verbose=True) + >>> branch.log_to_csv("branch_log.csv") + >>> branch.log_to_csv("detailed_branch_log.csv", timestamp=True, verbose=True) """ for name, branch in self.branches.items(): f_name = f"{name}_{filename}" @@ -459,17 +448,17 @@ def log_to_json( and services that consume JSON. Args: - filename (str): The name of the output JSON file. Defaults to 'log.json'. - dir_exist_ok (bool): If directory existence should not raise an error. Defaults to True. - timestamp (bool): If True, appends a timestamp to the filename. Defaults to True. - time_prefix (bool): If True, adds a timestamp prefix to the filename. Defaults to False. - verbose (bool): If True, prints a success message upon completion. Defaults to True. - clear (bool): If True, clears the log after saving. Defaults to True. - **kwargs: Additional keyword arguments for `DataFrame.to_json()`. + filename (str): The name of the output JSON file. Defaults to 'log.json'. + dir_exist_ok (bool): If directory existence should not raise an error. Defaults to True. + timestamp (bool): If True, appends a timestamp to the filename. Defaults to True. + time_prefix (bool): If True, adds a timestamp prefix to the filename. Defaults to False. + verbose (bool): If True, prints a success message upon completion. Defaults to True. + clear (bool): If True, clears the log after saving. Defaults to True. + **kwargs: Additional keyword arguments for `DataFrame.to_json()`. Examples: - >>> branch.log_to_json("branch_log.json") - >>> branch.log_to_json("detailed_branch_log.json", verbose=True, timestamp=True) + >>> branch.log_to_json("branch_log.json") + >>> branch.log_to_json("detailed_branch_log.json", verbose=True, timestamp=True) """ for name, branch in self.branches.items(): f_name = f"{name}_{filename}" @@ -507,12 +496,12 @@ async def call_chatcompletion( This method prepares the messages for chat completion, sends the request to the configured service, and handles the response. The method supports additional keyword arguments that are passed directly to the service. Args: - sender (str | None): The name of the sender to be included in the chat completion request. Defaults to None. - with_sender (bool): If True, includes the sender's name in the messages. Defaults to False. - **kwargs: Arbitrary keyword arguments passed directly to the chat completion service. + sender (str | None): The name of the sender to be included in the chat completion request. Defaults to None. + with_sender (bool): If True, includes the sender's name in the messages. Defaults to False. + **kwargs: Arbitrary keyword arguments passed directly to the chat completion service. Examples: - >>> await branch.call_chatcompletion() + >>> await branch.call_chatcompletion() """ branch = self.get_branch(branch) await branch.call_chatcompletion( @@ -538,18 +527,18 @@ async def chat( a chat conversation with LLM, processing instructions and system messages, optionally invoking tools. Args: - branch: The Branch instance to perform chat operations. - instruction (dict | list | Instruction | str): The instruction for the chat. - context (Optional[Any]): Additional context for the chat. - sender (str | None): The sender of the chat message. - system (Optional[Union[System, str, dict[str, Any]]]): System message to be processed. - tools (Union[bool, Tool, List[Tool], str, List[str]]): Specifies tools to be invoked. - out (bool): If True, outputs the chat response. - invoke (bool): If True, invokes tools as part of the chat. - **kwargs: Arbitrary keyword arguments for chat completion. + branch: The Branch instance to perform chat operations. + instruction (dict | list | Instruction | str): The instruction for the chat. + context (Optional[Any]): Additional context for the chat. + sender (str | None): The sender of the chat message. + system (Optional[Union[System, str, dict[str, Any]]]): System message to be processed. + tools (Union[bool, Tool, List[Tool], str, List[str]]): Specifies tools to be invoked. + out (bool): If True, outputs the chat response. + invoke (bool): If True, invokes tools as part of the chat. + **kwargs: Arbitrary keyword arguments for chat completion. Examples: - >>> await ChatFlow.chat(branch, "Ask about user preferences") + >>> await ChatFlow.chat(branch, "Ask about user preferences") """ branch = self.get_branch(branch) @@ -584,17 +573,17 @@ async def ReAct( Performs a reason-tool cycle with optional tool invocation over multiple rounds. Args: - branch: The Branch instance to perform ReAct operations. - instruction (dict | list | Instruction | str): Initial instruction for the cycle. - context: Context relevant to the instruction. - sender (str | None): Identifier for the message sender. - system: Initial system message or configuration. - tools: Tools to be registered or used during the cycle. - num_rounds (int): Number of reason-tool cycles to perform. - **kwargs: Additional keyword arguments for customization. + branch: The Branch instance to perform ReAct operations. + instruction (dict | list | Instruction | str): Initial instruction for the cycle. + context: Context relevant to the instruction. + sender (str | None): Identifier for the message sender. + system: Initial system message or configuration. + tools: Tools to be registered or used during the cycle. + num_rounds (int): Number of reason-tool cycles to perform. + **kwargs: Additional keyword arguments for customization. Examples: - >>> await ChatFlow.ReAct(branch, "Analyze user feedback", num_rounds=2) + >>> await ChatFlow.ReAct(branch, "Analyze user feedback", num_rounds=2) """ branch = self.get_branch(branch) @@ -631,18 +620,18 @@ async def followup( Automatically performs follow-up tools based on chat intertools and tool invocations. Args: - branch: The Branch instance to perform follow-up operations. - instruction (dict | list | Instruction | str): The initial instruction for follow-up. - context: Context relevant to the instruction. - sender (str | None): Identifier for the message sender. - system: Initial system message or configuration. - tools: Specifies tools to be considered for follow-up tools. - max_followup (int): Maximum number of follow-up chats allowed. - out (bool): If True, outputs the result of the follow-up tool. - **kwargs: Additional keyword arguments for follow-up customization. + branch: The Branch instance to perform follow-up operations. + instruction (dict | list | Instruction | str): The initial instruction for follow-up. + context: Context relevant to the instruction. + sender (str | None): Identifier for the message sender. + system: Initial system message or configuration. + tools: Specifies tools to be considered for follow-up tools. + max_followup (int): Maximum number of follow-up chats allowed. + out (bool): If True, outputs the result of the follow-up tool. + **kwargs: Additional keyword arguments for follow-up customization. Examples: - >>> await ChatFlow.auto_followup(branch, "Finalize report", max_followup=2) + >>> await ChatFlow.auto_followup(branch, "Finalize report", max_followup=2) """ branch = self.get_branch(branch) return await branch.followup( @@ -672,14 +661,17 @@ async def parallel_chat( invoke: bool = True, output_fields=None, persist_path=None, - branch_config={}, + branch_config=None, explode=False, + include_mapping=False, **kwargs, ): """ parallel chat """ + if branch_config is None: + branch_config = {} flow = PolyChat(self) return await flow.parallel_chat( @@ -696,6 +688,7 @@ async def parallel_chat( persist_path=persist_path, branch_config=branch_config, explode=explode, + include_mapping=include_mapping, **kwargs, ) @@ -714,21 +707,21 @@ def new_branch( """Create a new branch with the specified configurations. Args: - branch_name (str | None): Name of the new branch. - system (Optional[Union[System, str]]): System or context identifier for the new branch. - sender (str | None): Default sender identifier for the new branch. - messages (Optional[dataframe.ln_DataFrame]): Initial set of messages for the new branch. - instruction_sets (Optional[Any]): Instruction sets for the new branch. - tool_manager (Optional[Any]): Tool manager for handling tools in the new branch. - service (BaseService]): External service instance for the ne | None branch. - llmconfig (dict[str, Any] | None): Configuration for language learning models in the new branch. - tools (TOOL_TYPE | None): List of tools available for the new branch. + branch_name (str | None): Name of the new branch. + system (Optional[Union[System, str]]): System or context identifier for the new branch. + sender (str | None): Default sender identifier for the new branch. + messages (Optional[dataframe.ln_DataFrame]): Initial set of messages for the new branch. + instruction_sets (Optional[Any]): Instruction sets for the new branch. + tool_manager (Optional[Any]): Tool manager for handling tools in the new branch. + service (BaseService]): External service instance for the ne | None branch. + llmconfig (dict[str, Any] | None): Configuration for language learning models in the new branch. + tools (TOOL_TYPE | None): List of tools available for the new branch. Raises: - ValueError: If the branch name already exists. + ValueError: If the branch name already exists. Examples: - >>> session.new_branch("new_branch_name") + >>> session.new_branch("new_branch_name") """ if branch_name in self.branches.keys(): raise ValueError( @@ -758,36 +751,27 @@ def get_branch( Retrieve a branch by name or instance. Args: - branch (Optional[Branch | str]): The branch name or instance to retrieve. - get_name (bool): If True, returns a tuple of the branch instance and its name. + branch (Optional[Branch | str]): The branch name or instance to retrieve. + get_name (bool): If True, returns a tuple of the branch instance and its name. Returns: - Union[Branch, Tuple[Branch, str]]: The branch instance or a tuple of the branch instance and its name. + Union[Branch, Tuple[Branch, str]]: The branch instance or a tuple of the branch instance and its name. Raises: - ValueError: If the branch name does not exist or the branch input is invalid. + ValueError: If the branch name does not exist or the branch input is invalid. Examples: - >>> branch_instance = session.get_branch("existing_branch_name") - >>> branch_instance, branch_name = session.get_branch("existing_branch_name", get_name=True) + >>> branch_instance = session.get_branch("existing_branch_name") + >>> branch_instance, branch_name = session.get_branch("existing_branch_name", get_name=True) """ if isinstance(branch, str): if branch not in self.branches.keys(): raise ValueError(f"Invalid branch name {branch}. Not exist.") - else: - if get_name: - return self.branches[branch], branch - return self.branches[branch] - + return ( + (self.branches[branch], branch) if get_name else self.branches[branch] + ) elif isinstance(branch, Branch) and branch in self.branches.values(): - if get_name: - return ( - branch, - # [key for key, value in self.branches.items() if value == branch][0], - branch.name, - ) - return branch - + return (branch, branch.name) if get_name else branch elif branch is None: if get_name: return self.default_branch, self.default_branch_name @@ -800,10 +784,10 @@ def change_default_branch(self, branch: str | Branch) -> None: """Change the default branch of the session. Args: - branch (str | Branch): The branch name or instance to set as the new default. + branch (str | Branch): The branch name or instance to set as the new default. Examples: - >>> session.change_default_branch("new_default_branch") + >>> session.change_default_branch("new_default_branch") """ branch_, name_ = self.get_branch(branch, get_name=True) self.default_branch = branch_ @@ -813,17 +797,17 @@ def delete_branch(self, branch: Branch | str, verbose: bool = True) -> bool: """Delete a branch from the session. Args: - branch (Branch | str): The branch name or instance to delete. - verbose (bool): If True, prints a message upon deletion. + branch (Branch | str): The branch name or instance to delete. + verbose (bool): If True, prints a message upon deletion. Returns: - bool: True if the branch was successfully deleted. + bool: True if the branch was successfully deleted. Raises: - ValueError: If attempting to delete the current default branch. + ValueError: If attempting to delete the current default branch. Examples: - >>> session.delete_branch("branch_to_delete") + >>> session.delete_branch("branch_to_delete") """ _, branch_name = self.get_branch(branch, get_name=True) @@ -831,13 +815,12 @@ def delete_branch(self, branch: Branch | str, verbose: bool = True) -> bool: raise ValueError( f"{branch_name} is the current default branch, please switch to another branch before delete it." ) - else: - self.branches.pop(branch_name) - # self.mail_manager.sources.pop(branch_name) - self.mail_manager.mails.pop(branch_name) - if verbose: - print(f"Branch {branch_name} is deleted.") - return True + self.branches.pop(branch_name) + # self.mail_manager.sources.pop(branch_name) + self.mail_manager.mails.pop(branch_name) + if verbose: + print(f"Branch {branch_name} is deleted.") + return True def merge_branch( self, @@ -849,13 +832,13 @@ def merge_branch( """Merge messages and settings from one branch to another. Args: - from_ (str | Branch): The source branch name or instance. - to_branch (str | Branch): The target branch name or instance where the merge will happen. - update (bool): If True, updates the target branch with the source branch's settings. - del_ (bool): If True, deletes the source branch after merging. + from_ (str | Branch): The source branch name or instance. + to_branch (str | Branch): The target branch name or instance where the merge will happen. + update (bool): If True, updates the target branch with the source branch's settings. + del_ (bool): If True, deletes the source branch after merging. Examples: - >>> session.merge_branch("source_branch", "target_branch", del_=True) + >>> session.merge_branch("source_branch", "target_branch", del_=True) """ from_ = self.get_branch(branch=from_) to_branch, to_name = self.get_branch(branch=to_branch, get_name=True) @@ -879,14 +862,14 @@ def collect(self, from_: str | Branch | list[str | Branch] | None = None): This method is intended to aggregate data or requests from one or more branches for processing or analysis. Args: - from_ (Optional[Union[str, Branch, List[str | Branch]]]): The branch(es) from which to collect requests. - Can be a single branch name, a single branch instance, a list of branch names, a list of branch instances, or None. - If None, requests are collected from all branches. + from_ (Optional[Union[str, Branch, List[str | Branch]]]): The branch(es) from which to collect requests. + Can be a single branch name, a single branch instance, a list of branch names, a list of branch instances, or None. + If None, requests are collected from all branches. Examples: - >>> session.collect("branch_name") - >>> session.collect([branch_instance_1, "branch_name_2"]) - >>> session.collect() # Collects from all branches + >>> session.collect("branch_name") + >>> session.collect([branch_instance_1, "branch_name_2"]) + >>> session.collect() # Collects from all branches """ if from_ is None: for branch in self.branches.keys(): @@ -907,14 +890,14 @@ def send(self, to_: str | Branch | list[str | Branch] | None = None): This method facilitates the distribution of data or requests to one or more branches, potentially for further tool or processing. Args: - to_ (Optional[Union[str, Branch, List[str | Branch]]]): The target branch(es) to which to send requests. - Can be a single branch name, a single branch instance, a list of branch names, a list of branch instances, or None. - If None, requests are sent to all branches. + to_ (Optional[Union[str, Branch, List[str | Branch]]]): The target branch(es) to which to send requests. + Can be a single branch name, a single branch instance, a list of branch names, a list of branch instances, or None. + If None, requests are sent to all branches. Examples: - >>> session.send("target_branch") - >>> session.send([branch_instance_1, "target_branch_2"]) - >>> session.send() # Sends to all branches + >>> session.send("target_branch") + >>> session.send([branch_instance_1, "target_branch_2"]) + >>> session.send() # Sends to all branches """ if to_ is None: for branch in self.branches.keys(): @@ -936,12 +919,12 @@ def collect_send_all(self, receive_all=False): useful in scenarios where data or requests need to be aggregated and then distributed uniformly. Args: - receive_all (bool): If True, triggers a `receive_all` method on each branch after sending requests, - which can be used to process or acknowledge the received data. + receive_all (bool): If True, triggers a `receive_all` method on each branch after sending requests, + which can be used to process or acknowledge the received data. Examples: - >>> session.collect_send_all() - >>> session.collect_send_all(receive_all=True) + >>> session.collect_send_all() + >>> session.collect_send_all(receive_all=True) """ self.collect() self.send() @@ -971,8 +954,7 @@ def _setup_default_branch( sender, default_branch, default_branch_name, - messages, - # instruction_sets, + messages, # instruction_sets, tool_manager, service, llmconfig, diff --git a/lionagi/core/tool/tool_manager.py b/lionagi/core/tool/tool_manager.py index 72dc6bb76..c38507135 100644 --- a/lionagi/core/tool/tool_manager.py +++ b/lionagi/core/tool/tool_manager.py @@ -1,13 +1,9 @@ from typing import Tuple, Any, TypeVar, Callable import asyncio -from lionagi.core.schema.base_node import Tool, TOOL_TYPE - -# from lionagi.libs.ln_async import AsyncUtil -from lionagi.libs.ln_parse import ParseUtil -from lionagi.libs import ln_convert as convert -from lionagi.libs import ln_func_call as func_call +from lionagi.libs import func_call, convert, ParseUtil +from lionagi.core.schema.base_node import Tool, TOOL_TYPE T = TypeVar("T", bound=Tool) @@ -21,7 +17,7 @@ class ToolManager: calls. Attributes: - registry (dict[str, Tool]): A dictionary to hold registered tools, keyed by their names. + registry (dict[str, Tool]): A dictionary to hold registered tools, keyed by their names. """ registry: dict = {} @@ -31,12 +27,12 @@ def name_existed(self, name: str) -> bool: Checks if a tool name already exists in the registry. Args: - name (str): The name of the tool to check. + name (str): The name of the tool to check. Returns: - bool: True if the name exists, False otherwise. + bool: True if the name exists, False otherwise. """ - return True if name in self.registry.keys() else False + return name in self.registry @property def has_tools(self): @@ -47,10 +43,10 @@ def _register_tool(self, tool: Tool) -> None: Registers a tool in the registry. Raises a TypeError if the object is not an instance of Tool. Args: - tool (Tool): The tool instance to register. + tool (Tool): The tool instance to register. Raises: - TypeError: If the provided object is not an instance of Tool. + TypeError: If the provided object is not an instance of Tool. """ if not isinstance(tool, Tool): raise TypeError("Please register a Tool object.") @@ -62,33 +58,32 @@ async def invoke(self, func_calls: Tuple[str, dict[str, Any]]) -> Any: Invokes a registered tool's function with the given arguments. Supports both coroutine and regular functions. Args: - func_call (Tuple[str, Dict[str, Any]]): A tuple containing the function name and a dictionary of keyword arguments. + func_call (Tuple[str, Dict[str, Any]]): A tuple containing the function name and a dictionary of keyword arguments. Returns: - Any: The result of the function call. + Any: The result of the function call. Raises: - ValueError: If the function name is not registered or if there's an error during function invocation. + ValueError: If the function name is not registered or if there's an error during function invocation. """ name, kwargs = func_calls - if self.name_existed(name): - tool = self.registry[name] - func = tool.func - parser = tool.parser - try: - if func_call.is_coroutine_func(func): - tasks = [func_call.call_handler(func, **kwargs)] - out = await asyncio.gather(*tasks) - return parser(out[0]) if parser else out[0] - else: - out = func(**kwargs) - return parser(out) if parser else out - except Exception as e: - raise ValueError( - f"Error when invoking function {name} with arguments {kwargs} with error message {e}" - ) - else: + if not self.name_existed(name): raise ValueError(f"Function {name} is not registered.") + tool = self.registry[name] + func = tool.func + parser = tool.parser + try: + if func_call.is_coroutine_func(func): + tasks = [func_call.call_handler(func, **kwargs)] + out = await asyncio.gather(*tasks) + return parser(out[0]) if parser else out[0] + else: + out = func(**kwargs) + return parser(out) if parser else out + except Exception as e: + raise ValueError( + f"Error when invoking function {name} with arguments {kwargs} with error message {e}" + ) from e @staticmethod def get_function_call(response: dict) -> Tuple[str, dict]: @@ -96,19 +91,19 @@ def get_function_call(response: dict) -> Tuple[str, dict]: Extracts a function call and arguments from a response dictionary. Args: - response (dict): The response dictionary containing the function call information. + response (dict): The response dictionary containing the function call information. Returns: - Tuple[str, dict]: A tuple containing the function name and a dictionary of arguments. + Tuple[str, dict]: A tuple containing the function name and a dictionary of arguments. Raises: - ValueError: If the response does not contain valid function call information. + ValueError: If the response does not contain valid function call information. """ try: func = response["action"][7:] args = convert.to_dict(response["arguments"]) return func, args - except: + except Exception: try: func = response["recipient_name"].split(".")[-1] args = response["parameters"] @@ -121,7 +116,7 @@ def register_tools(self, tools: list[Tool]) -> None: Registers multiple tools in the registry. Args: - tools (list[Tool]): A list of tool instances to register. + tools (list[Tool]): A list of tool instances to register. """ func_call.lcall(tools, self._register_tool) @@ -130,27 +125,24 @@ def to_tool_schema_list(self) -> list[dict[str, Any]]: Generates a list of schemas for all registered tools. Returns: - list[dict[str, Any]]: A list of tool schemas. + list[dict[str, Any]]: A list of tool schemas. """ - schema_list = [] - for tool in self.registry.values(): - schema_list.append(tool.schema_) - return schema_list + return [tool.schema_ for tool in self.registry.values()] def parse_tool(self, tools: TOOL_TYPE, **kwargs) -> dict: """ Parses tool information and generates a dictionary for tool invocation. Args: - tools: Tool information which can be a single Tool instance, a list of Tool instances, a tool name, or a list of tool names. - **kwargs: Additional keyword arguments. + tools: Tool information which can be a single Tool instance, a list of Tool instances, a tool name, or a list of tool names. + **kwargs: Additional keyword arguments. Returns: - dict: A dictionary containing tool schema information and any additional keyword arguments. + dict: A dictionary containing tool schema information and any additional keyword arguments. Raises: - ValueError: If a tool name is provided that is not registered. + ValueError: If a tool name is provided that is not registered. """ def tool_check(tool): @@ -167,13 +159,13 @@ def tool_check(tool): if isinstance(tools, bool): tool_kwarg = {"tools": self.to_tool_schema_list()} - kwargs = {**tool_kwarg, **kwargs} + kwargs = tool_kwarg | kwargs else: if not isinstance(tools, list): tools = [tools] tool_kwarg = {"tools": func_call.lcall(tools, tool_check)} - kwargs = {**tool_kwarg, **kwargs} + kwargs = tool_kwarg | kwargs return kwargs @@ -196,66 +188,66 @@ def func_to_tool( objects with structured metadata. Args: - func_ (Callable): The function to be transformed into a Tool object. This - function should have a docstring that follows the - specified docstring style for accurate schema generation. - parser (Optional[Any]): An optional parser object associated with the Tool. - This parameter is currently not utilized in the - transformation process but is included for future - compatibility and extension purposes. - docstring_style (str): The format of the docstring to be parsed, indicating - the convention used in the function's docstring. - Supports 'google' for Google-style docstrings and - 'reST' for reStructuredText-style docstrings. The - chosen style affects how the docstring is parsed and - how the schema is generated. + func_ (Callable): The function to be transformed into a Tool object. This + function should have a docstring that follows the + specified docstring style for accurate schema generation. + parser (Optional[Any]): An optional parser object associated with the Tool. + This parameter is currently not utilized in the + transformation process but is included for future + compatibility and extension purposes. + docstring_style (str): The format of the docstring to be parsed, indicating + the convention used in the function's docstring. + Supports 'google' for Google-style docstrings and + 'reST' for reStructuredText-style docstrings. The + chosen style affects how the docstring is parsed and + how the schema is generated. Returns: - Tool: An object representing the original function wrapped as a Tool, along - with its generated schema. This Tool object can be used in systems that - require detailed metadata about functions, facilitating tasks such as - automatic documentation generation, user interface creation, or - integration with other software tools. + Tool: An object representing the original function wrapped as a Tool, along + with its generated schema. This Tool object can be used in systems that + require detailed metadata about functions, facilitating tasks such as + automatic documentation generation, user interface creation, or + integration with other software tools. Examples: - >>> def example_function_google(param1: int, param2: str) -> bool: - ... ''' - ... An example function using Google style docstrings. - ... - ... Args: - ... param1 (int): The first parameter, demonstrating an integer input_. - ... param2 (str): The second parameter, demonstrating a string input_. - ... - ... Returns: - ... bool: A boolean value, illustrating the return type. - ... ''' - ... return True - ... - >>> tool_google = func_to_tool(example_function_google, docstring_style='google') - >>> print(isinstance(tool_google, Tool)) - True - - >>> def example_function_reST(param1: int, param2: str) -> bool: - ... ''' - ... An example function using reStructuredText (reST) style docstrings. - ... - ... :param param1: The first parameter, demonstrating an integer input_. - ... :type param1: int - ... :param param2: The second parameter, demonstrating a string input_. - ... :type param2: str - ... :returns: A boolean value, illustrating the return type. - ... :rtype: bool - ... ''' - ... return True - ... - >>> tool_reST = func_to_tool(example_function_reST, docstring_style='reST') - >>> print(isinstance(tool_reST, Tool)) - True + >>> def example_function_google(param1: int, param2: str) -> bool: + ... ''' + ... An example function using Google style docstrings. + ... + ... Args: + ... param1 (int): The first parameter, demonstrating an integer input_. + ... param2 (str): The second parameter, demonstrating a string input_. + ... + ... Returns: + ... bool: A boolean value, illustrating the return type. + ... ''' + ... return True + ... + >>> tool_google = func_to_tool(example_function_google, docstring_style='google') + >>> print(isinstance(tool_google, Tool)) + True + + >>> def example_function_reST(param1: int, param2: str) -> bool: + ... ''' + ... An example function using reStructuredText (reST) style docstrings. + ... + ... :param param1: The first parameter, demonstrating an integer input_. + ... :type param1: int + ... :param param2: The second parameter, demonstrating a string input_. + ... :type param2: str + ... :returns: A boolean value, illustrating the return type. + ... :rtype: bool + ... ''' + ... return True + ... + >>> tool_reST = func_to_tool(example_function_reST, docstring_style='reST') + >>> print(isinstance(tool_reST, Tool)) + True Note: - The transformation process relies heavily on the accuracy and completeness of - the function's docstring. Functions with incomplete or incorrectly formatted - docstrings may result in incomplete or inaccurate Tool schemas. + The transformation process relies heavily on the accuracy and completeness of + the function's docstring. Functions with incomplete or incorrectly formatted + docstrings may result in incomplete or inaccurate Tool schemas. """ fs = [] @@ -263,7 +255,7 @@ def func_to_tool( parsers = convert.to_list(parser, flatten=True, dropna=True) if parser: - if len(funcs) != len(parsers) and len(parsers) != 1: + if len(funcs) != len(parsers) != 1: raise ValueError( "Length of parser must match length of func. Except if you only pass one" ) diff --git a/lionagi/integrations/__init__.py b/lionagi/integrations/__init__.py index 85a69333f..b6e690fd5 100644 --- a/lionagi/integrations/__init__.py +++ b/lionagi/integrations/__init__.py @@ -1,3 +1 @@ -from .provider.services import Services - -__all__ = ["Services"] +from . import * diff --git a/lionagi/integrations/bridge/langchain_/documents.py b/lionagi/integrations/bridge/langchain_/documents.py index 5b0890e3f..4cb4f8612 100644 --- a/lionagi/integrations/bridge/langchain_/documents.py +++ b/lionagi/integrations/bridge/langchain_/documents.py @@ -1,6 +1,5 @@ from typing import Union, Callable, List, Dict, Any, TypeVar - from lionagi.libs.sys_util import SysUtil T = TypeVar("T") @@ -15,11 +14,11 @@ def to_langchain_document(datanode: T, **kwargs: Any) -> Any: to match the Langchain Document schema before creating a Langchain Document object. Args: - datanode (T): The data node to convert. Must have a `to_dict` method. - **kwargs: Additional keyword arguments to be passed to the Langchain Document constructor. + datanode (T): The data node to convert. Must have a `to_dict` method. + **kwargs: Additional keyword arguments to be passed to the Langchain Document constructor. Returns: - Any: An instance of `LangchainDocument` populated with data from the input node. + Any: An instance of `LangchainDocument` populated with data from the input node. """ SysUtil.check_import("langchain") @@ -44,20 +43,20 @@ def langchain_loader( It passes specified arguments and keyword arguments to the loader for data retrieval or processing. Args: - loader (Union[str, Callable]): A string representing the loader's name or a callable loader function. - loader_args (List[Any], optional): A list of positional arguments for the loader. - loader_kwargs (Dict[str, Any], optional): A dictionary of keyword arguments for the loader. + loader (Union[str, Callable]): A string representing the loader's name or a callable loader function. + loader_args (List[Any], optional): A list of positional arguments for the loader. + loader_kwargs (Dict[str, Any], optional): A dictionary of keyword arguments for the loader. Returns: - Any: The result returned by the loader function, typically data loaded into a specified format. + Any: The result returned by the loader function, typically data loaded into a specified format. Raises: - ValueError: If the loader cannot be initialized or fails to load data. + ValueError: If the loader cannot be initialized or fails to load data. Examples: - >>> data = langchain_loader("json_loader", loader_args=["data.json"]) - >>> isinstance(data, dict) - True + >>> data = langchain_loader("json_loader", loader_args=["data.json"]) + >>> isinstance(data, dict) + True """ SysUtil.check_import("langchain") @@ -92,16 +91,16 @@ def langchain_text_splitter( or documents into chunks. The splitter can be configured with additional arguments and keyword arguments. Args: - data (Union[str, List]): The text or list of texts to be split. - splitter (Union[str, Callable]): The name of the splitter function or the splitter function itself. - splitter_args (List[Any], optional): Positional arguments to pass to the splitter function. - splitter_kwargs (Dict[str, Any], optional): Keyword arguments to pass to the splitter function. + data (Union[str, List]): The text or list of texts to be split. + splitter (Union[str, Callable]): The name of the splitter function or the splitter function itself. + splitter_args (List[Any], optional): Positional arguments to pass to the splitter function. + splitter_kwargs (Dict[str, Any], optional): Keyword arguments to pass to the splitter function. Returns: - List[str]: A list of text chunks produced by the text splitter. + List[str]: A list of text chunks produced by the text splitter. Raises: - ValueError: If the splitter is invalid or fails during the split operation. + ValueError: If the splitter is invalid or fails during the split operation. """ splitter_args = splitter_args or [] splitter_kwargs = splitter_kwargs or {} diff --git a/lionagi/integrations/bridge/langchain_/langchain_bridge.py b/lionagi/integrations/bridge/langchain_/langchain_bridge.py index 64b8cb399..c0b537bae 100644 --- a/lionagi/integrations/bridge/langchain_/langchain_bridge.py +++ b/lionagi/integrations/bridge/langchain_/langchain_bridge.py @@ -10,11 +10,11 @@ def to_langchain_document(*args, **kwargs): to match the Langchain Document schema before creating a Langchain Document object. Args: - datanode (T): The data node to convert. Must have a `to_dict` method. - **kwargs: Additional keyword arguments to be passed to the Langchain Document constructor. + datanode (T): The data node to convert. Must have a `to_dict` method. + **kwargs: Additional keyword arguments to be passed to the Langchain Document constructor. Returns: - Any: An instance of `LangchainDocument` populated with data from the input node. + Any: An instance of `LangchainDocument` populated with data from the input node. """ from .documents import to_langchain_document @@ -29,15 +29,15 @@ def langchain_loader(*args, **kwargs): It passes specified arguments and keyword arguments to the loader for data retrieval or processing. Args: - loader (Union[str, Callable]): A string representing the loader's name or a callable loader function. - loader_args (List[Any], optional): A list of positional arguments for the loader. - loader_kwargs (Dict[str, Any], optional): A dictionary of keyword arguments for the loader. + loader (Union[str, Callable]): A string representing the loader's name or a callable loader function. + loader_args (List[Any], optional): A list of positional arguments for the loader. + loader_kwargs (Dict[str, Any], optional): A dictionary of keyword arguments for the loader. Returns: - Any: The result returned by the loader function, typically data loaded into a specified format. + Any: The result returned by the loader function, typically data loaded into a specified format. Raises: - ValueError: If the loader cannot be initialized or fails to load data. + ValueError: If the loader cannot be initialized or fails to load data. """ from .documents import langchain_loader @@ -52,16 +52,16 @@ def langchain_text_splitter(*args, **kwargs): or documents into chunks. The splitter can be configured with additional arguments and keyword arguments. Args: - data (Union[str, List]): The text or list of texts to be split. - splitter (Union[str, Callable]): The name of the splitter function or the splitter function itself. - splitter_args (List[Any], optional): Positional arguments to pass to the splitter function. - splitter_kwargs (Dict[str, Any], optional): Keyword arguments to pass to the splitter function. + data (Union[str, List]): The text or list of texts to be split. + splitter (Union[str, Callable]): The name of the splitter function or the splitter function itself. + splitter_args (List[Any], optional): Positional arguments to pass to the splitter function. + splitter_kwargs (Dict[str, Any], optional): Keyword arguments to pass to the splitter function. Returns: - List[str]: A list of text chunks produced by the text splitter. + List[str]: A list of text chunks produced by the text splitter. Raises: - ValueError: If the splitter is invalid or fails during the split operation. + ValueError: If the splitter is invalid or fails during the split operation. """ from .documents import langchain_text_splitter diff --git a/lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py b/lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py index 294e258e8..1902355f7 100644 --- a/lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py +++ b/lionagi/integrations/bridge/llamaindex_/llama_index_bridge.py @@ -9,17 +9,17 @@ def to_llama_index_node(*args, **kwargs): the expected Llama Index node schema, and then creates a Llama Index node object of the specified type. Args: - lion_node: The Lion node to convert. Must have a `to_dict` method. - node_type (Any, optional): The type of Llama Index node to create. Can be a string name of a node class - within the Llama Index schema or a class that inherits from `BaseNode`. Defaults to 'TextNode'. - **kwargs: Additional keyword arguments to be included in the Llama Index node's initialization. + lion_node: The Lion node to convert. Must have a `to_dict` method. + node_type (Any, optional): The type of Llama Index node to create. Can be a string name of a node class + within the Llama Index schema or a class that inherits from `BaseNode`. Defaults to 'TextNode'. + **kwargs: Additional keyword arguments to be included in the Llama Index node's initialization. Returns: - Any: A new instance of the specified Llama Index node type populated with data from the Lion node. + Any: A new instance of the specified Llama Index node type populated with data from the Lion node. Raises: - TypeError: If `node_type` is neither a string nor a subclass of `BaseNode`. - AttributeError: If an error occurs due to an invalid node type or during the creation of the node object. + TypeError: If `node_type` is neither a string nor a subclass of `BaseNode`. + AttributeError: If an error occurs due to an invalid node type or during the creation of the node object. """ from .textnode import to_llama_index_node @@ -34,18 +34,18 @@ def llama_index_read_data(*args, **kwargs): then loads data using the reader's `load_data` method with the provided loader arguments and keyword arguments. Args: - reader (Union[None, str, Any], optional): The reader to use. This can be a class, a string identifier, - or None. If None, a default reader is used. - reader_args (List[Any], optional): Positional arguments to initialize the reader. - reader_kwargs (Dict[str, Any], optional): Keyword arguments to initialize the reader. - loader_args (List[Any], optional): Positional arguments for the reader's `load_data` method. - loader_kwargs (Dict[str, Any], optional): Keyword arguments for the reader's `load_data` method. + reader (Union[None, str, Any], optional): The reader to use. This can be a class, a string identifier, + or None. If None, a default reader is used. + reader_args (List[Any], optional): Positional arguments to initialize the reader. + reader_kwargs (Dict[str, Any], optional): Keyword arguments to initialize the reader. + loader_args (List[Any], optional): Positional arguments for the reader's `load_data` method. + loader_kwargs (Dict[str, Any], optional): Keyword arguments for the reader's `load_data` method. Returns: - Any: The documents or data loaded by the reader. + Any: The documents or data loaded by the reader. Raises: - ValueError: If there is an error initializing the reader or loading the data. + ValueError: If there is an error initializing the reader or loading the data. """ from .reader import llama_index_read_data @@ -60,16 +60,16 @@ def llama_index_parse_node(*args, **kwargs): then parses documents using the node parser's `get_nodes_from_documents` method. Args: - documents (Any): The documents to be parsed by the node parser. - node_parser (Any): The node parser to use. This can be a class, a string identifier, or None. - parser_args (Optional[List[Any]], optional): Positional arguments to initialize the node parser. - parser_kwargs (Optional[Dict[str, Any]], optional): Keyword arguments to initialize the node parser. + documents (Any): The documents to be parsed by the node parser. + node_parser (Any): The node parser to use. This can be a class, a string identifier, or None. + parser_args (Optional[List[Any]], optional): Positional arguments to initialize the node parser. + parser_kwargs (Optional[Dict[str, Any]], optional): Keyword arguments to initialize the node parser. Returns: - Any: The nodes extracted from the documents by the node parser. + Any: The nodes extracted from the documents by the node parser. Raises: - ValueError: If there is an error initializing the node parser or parsing the documents. + ValueError: If there is an error initializing the node parser or parsing the documents. """ from .node_parser import llama_index_parse_node @@ -87,7 +87,7 @@ def get_llama_index_reader(*args, **kwargs): Args: reader (Union[Any, str], optional): The reader identifier, which can be a reader class, a string alias - for a reader class, or None. If None, returns the SimpleDirectoryReader class. + for a reader class, or None. If None, returns the SimpleDirectoryReader class. Returns: Any: The llama index reader class corresponding to the specified reader. diff --git a/lionagi/integrations/bridge/llamaindex_/node_parser.py b/lionagi/integrations/bridge/llamaindex_/node_parser.py index c96eb32f2..f07c64310 100644 --- a/lionagi/integrations/bridge/llamaindex_/node_parser.py +++ b/lionagi/integrations/bridge/llamaindex_/node_parser.py @@ -12,16 +12,16 @@ def get_llama_index_node_parser(node_parser: Any): that the class is a subclass of NodeParser. Args: - node_parser (Any): The node parser identifier, which can be a node parser class, a string alias - for a node parser class, or None. + node_parser (Any): The node parser identifier, which can be a node parser class, a string alias + for a node parser class, or None. Returns: - Any: The llama index node parser object corresponding to the specified node parser. + Any: The llama index node parser object corresponding to the specified node parser. Raises: - TypeError: If the node_parser is neither a string nor a subclass of NodeParser. - AttributeError: If there is an issue importing the specified node parser due to it not being - found within the llama_index.core.node_parser module. + TypeError: If the node_parser is neither a string nor a subclass of NodeParser. + AttributeError: If there is an issue importing the specified node parser due to it not being + found within the llama_index.core.node_parser module. """ SysUtil.check_import("llama_index", pip_name="llama-index") @@ -57,16 +57,16 @@ def llama_index_parse_node( then parses documents using the node parser's `get_nodes_from_documents` method. Args: - documents (Any): The documents to be parsed by the node parser. - node_parser (Any): The node parser to use. This can be a class, a string identifier, or None. - parser_args (Optional[List[Any]], optional): Positional arguments to initialize the node parser. - parser_kwargs (Optional[Dict[str, Any]], optional): Keyword arguments to initialize the node parser. + documents (Any): The documents to be parsed by the node parser. + node_parser (Any): The node parser to use. This can be a class, a string identifier, or None. + parser_args (Optional[List[Any]], optional): Positional arguments to initialize the node parser. + parser_kwargs (Optional[Dict[str, Any]], optional): Keyword arguments to initialize the node parser. Returns: - Any: The nodes extracted from the documents by the node parser. + Any: The nodes extracted from the documents by the node parser. Raises: - ValueError: If there is an error initializing the node parser or parsing the documents. + ValueError: If there is an error initializing the node parser or parsing the documents. """ try: diff --git a/lionagi/integrations/bridge/llamaindex_/reader.py b/lionagi/integrations/bridge/llamaindex_/reader.py index b58d12fa4..a70a30a80 100644 --- a/lionagi/integrations/bridge/llamaindex_/reader.py +++ b/lionagi/integrations/bridge/llamaindex_/reader.py @@ -15,7 +15,7 @@ def get_llama_index_reader(reader: Any | str = None) -> Any: Args: reader (Union[Any, str], optional): The reader identifier, which can be a reader class, a string alias - for a reader class, or None. If None, returns the SimpleDirectoryReader class. + for a reader class, or None. If None, returns the SimpleDirectoryReader class. Returns: Any: The llama index reader class corresponding to the specified reader. @@ -76,10 +76,10 @@ def parse_reader_name(reader_str): to facilitate dynamic import and installation if necessary. Args: - reader_str (str): The name of the reader as a string. + reader_str (str): The name of the reader as a string. Returns: - Tuple[str, str]: A tuple containing the package name and the pip name corresponding to the reader. + Tuple[str, str]: A tuple containing the package name and the pip name corresponding to the reader. """ package_name = "" @@ -172,18 +172,18 @@ def llama_index_read_data( then loads data using the reader's `load_data` method with the provided loader arguments and keyword arguments. Args: - reader (Union[None, str, Any], optional): The reader to use. This can be a class, a string identifier, - or None. If None, a default reader is used. - reader_args (List[Any], optional): Positional arguments to initialize the reader. - reader_kwargs (Dict[str, Any], optional): Keyword arguments to initialize the reader. - loader_args (List[Any], optional): Positional arguments for the reader's `load_data` method. - loader_kwargs (Dict[str, Any], optional): Keyword arguments for the reader's `load_data` method. + reader (Union[None, str, Any], optional): The reader to use. This can be a class, a string identifier, + or None. If None, a default reader is used. + reader_args (List[Any], optional): Positional arguments to initialize the reader. + reader_kwargs (Dict[str, Any], optional): Keyword arguments to initialize the reader. + loader_args (List[Any], optional): Positional arguments for the reader's `load_data` method. + loader_kwargs (Dict[str, Any], optional): Keyword arguments for the reader's `load_data` method. Returns: - Any: The documents or data loaded by the reader. + Any: The documents or data loaded by the reader. Raises: - ValueError: If there is an error initializing the reader or loading the data. + ValueError: If there is an error initializing the reader or loading the data. """ try: reader_args = reader_args or [] diff --git a/lionagi/integrations/bridge/llamaindex_/textnode.py b/lionagi/integrations/bridge/llamaindex_/textnode.py index a904f7fe2..34fbedca1 100644 --- a/lionagi/integrations/bridge/llamaindex_/textnode.py +++ b/lionagi/integrations/bridge/llamaindex_/textnode.py @@ -10,17 +10,17 @@ def to_llama_index_node(lion_node, node_type: Any = None, **kwargs: Any) -> Any: the expected Llama Index node schema, and then creates a Llama Index node object of the specified type. Args: - lion_node: The Lion node to convert. Must have a `to_dict` method. - node_type (Any, optional): The type of Llama Index node to create. Can be a string name of a node class - within the Llama Index schema or a class that inherits from `BaseNode`. Defaults to 'TextNode'. - **kwargs: Additional keyword arguments to be included in the Llama Index node's initialization. + lion_node: The Lion node to convert. Must have a `to_dict` method. + node_type (Any, optional): The type of Llama Index node to create. Can be a string name of a node class + within the Llama Index schema or a class that inherits from `BaseNode`. Defaults to 'TextNode'. + **kwargs: Additional keyword arguments to be included in the Llama Index node's initialization. Returns: - Any: A new instance of the specified Llama Index node type populated with data from the Lion node. + Any: A new instance of the specified Llama Index node type populated with data from the Lion node. Raises: - TypeError: If `node_type` is neither a string nor a subclass of `BaseNode`. - AttributeError: If an error occurs due to an invalid node type or during the creation of the node object. + TypeError: If `node_type` is neither a string nor a subclass of `BaseNode`. + AttributeError: If an error occurs due to an invalid node type or during the creation of the node object. """ SysUtil.check_import("llama_index", pip_name="llama-index") diff --git a/lionagi/integrations/config/openrouter_configs.py b/lionagi/integrations/config/openrouter_configs.py index a9b41b079..26a8d6562 100644 --- a/lionagi/integrations/config/openrouter_configs.py +++ b/lionagi/integrations/config/openrouter_configs.py @@ -56,7 +56,6 @@ "config": openrouter_finetune_llmconfig, } - openrouter_schema = { "chat/completions": openrouter_chat_schema, "finetune": openrouter_finetune_schema, diff --git a/lionagi/integrations/provider/oai.py b/lionagi/integrations/provider/oai.py index 24f6c19f4..43f320db5 100644 --- a/lionagi/integrations/provider/oai.py +++ b/lionagi/integrations/provider/oai.py @@ -8,19 +8,19 @@ class OpenAIService(BaseService): A service to interact with OpenAI's API endpoints. Attributes: - base_url (str): The base URL for the OpenAI API. - available_endpoints (list): A list of available API endpoints. - schema (dict): The schema configuration for the API. - key_scheme (str): The environment variable name for OpenAI API key. - token_encoding_name (str): The default token encoding scheme. + base_url (str): The base URL for the OpenAI API. + available_endpoints (list): A list of available API endpoints. + schema (dict): The schema configuration for the API. + key_scheme (str): The environment variable name for OpenAI API key. + token_encoding_name (str): The default token encoding scheme. Examples: - >>> service = OpenAIService(api_key="your_api_key") - >>> asyncio.run(service.serve("Hello, world!","chat/completions")) - (payload, completion) + >>> service = OpenAIService(api_key="your_api_key") + >>> asyncio.run(service.serve("Hello, world!","chat/completions")) + (payload, completion) - >>> service = OpenAIService() - >>> asyncio.run(service.serve("Convert this text to speech.","audio_speech")) + >>> service = OpenAIService() + >>> asyncio.run(service.serve("Convert this text to speech.","audio_speech")) """ base_url = "https://api.openai.com/v1/" @@ -57,25 +57,25 @@ async def serve(self, input_, endpoint="chat/completions", method="post", **kwar Serves the input using the specified endpoint and method. Args: - input_: The input text to be processed. - endpoint: The API endpoint to use for processing. - method: The HTTP method to use for the request. - **kwargs: Additional keyword arguments to pass to the payload creation. + input_: The input text to be processed. + endpoint: The API endpoint to use for processing. + method: The HTTP method to use for the request. + **kwargs: Additional keyword arguments to pass to the payload creation. Returns: - A tuple containing the payload and the completion assistant_response from the API. + A tuple containing the payload and the completion assistant_response from the API. Raises: - ValueError: If the specified endpoint is not supported. + ValueError: If the specified endpoint is not supported. Examples: - >>> service = OpenAIService(api_key="your_api_key") - >>> asyncio.run(service.serve("Hello, world!","chat/completions")) - (payload, completion) + >>> service = OpenAIService(api_key="your_api_key") + >>> asyncio.run(service.serve("Hello, world!","chat/completions")) + (payload, completion) - >>> service = OpenAIService() - >>> asyncio.run(service.serve("Convert this text to speech.","audio_speech")) - ValueError: 'audio_speech' is currently not supported + >>> service = OpenAIService() + >>> asyncio.run(service.serve("Convert this text to speech.","audio_speech")) + ValueError: 'audio_speech' is currently not supported """ if endpoint not in self.active_endpoint: await self.init_endpoint(endpoint) @@ -89,14 +89,14 @@ async def serve_chat(self, messages, **kwargs): Serves the chat completion request with the given messages. Args: - messages: The messages to be included in the chat completion. - **kwargs: Additional keyword arguments for payload creation. + messages: The messages to be included in the chat completion. + **kwargs: Additional keyword arguments for payload creation. Returns: - A tuple containing the payload and the completion assistant_response from the API. + A tuple containing the payload and the completion assistant_response from the API. Raises: - Exception: If the API call fails. + Exception: If the API call fails. """ if "chat/completions" not in self.active_endpoint: await self.init_endpoint("chat/completions") diff --git a/lionagi/integrations/provider/services.py b/lionagi/integrations/provider/services.py index fd4faefcb..3069c1ec1 100644 --- a/lionagi/integrations/provider/services.py +++ b/lionagi/integrations/provider/services.py @@ -6,15 +6,15 @@ def OpenAI(**kwargs): A provider to interact with OpenAI's API endpoints. Attributes: - api_key (Optional[str]): The API key used for authentication. - schema (Dict[str, Any]): The schema defining the provider's endpoints. - status_tracker (StatusTracker): The object tracking the status of API calls. - endpoints (Dict[str, EndPoint]): A dictionary of endpoint objects. - base_url (str): The base URL for the OpenAI API. - available_endpoints (list): A list of available API endpoints, including - 'chat/completions' - key_scheme (str): The environment variable name for API key. - token_encoding_name (str): The default token encoding scheme. + api_key (Optional[str]): The API key used for authentication. + schema (Dict[str, Any]): The schema defining the provider's endpoints. + status_tracker (StatusTracker): The object tracking the status of API calls. + endpoints (Dict[str, EndPoint]): A dictionary of endpoint objects. + base_url (str): The base URL for the OpenAI API. + available_endpoints (list): A list of available API endpoints, including + 'chat/completions' + key_scheme (str): The environment variable name for API key. + token_encoding_name (str): The default token encoding scheme. """ from lionagi.integrations.provider.oai import OpenAIService @@ -27,15 +27,15 @@ def OpenRouter(**kwargs): A provider to interact with OpenRouter's API endpoints. Attributes: - api_key (Optional[str]): The API key used for authentication. - schema (Dict[str, Any]): The schema defining the provider's endpoints. - status_tracker (StatusTracker): The object tracking the status of API calls. - endpoints (Dict[str, EndPoint]): A dictionary of endpoint objects. - base_url (str): The base URL for the OpenAI API. - available_endpoints (list): A list of available API endpoints, including - 'chat/completions' - key_scheme (str): The environment variable name for API key. - token_encoding_name (str): The default token encoding scheme. + api_key (Optional[str]): The API key used for authentication. + schema (Dict[str, Any]): The schema defining the provider's endpoints. + status_tracker (StatusTracker): The object tracking the status of API calls. + endpoints (Dict[str, EndPoint]): A dictionary of endpoint objects. + base_url (str): The base URL for the OpenAI API. + available_endpoints (list): A list of available API endpoints, including + 'chat/completions' + key_scheme (str): The environment variable name for API key. + token_encoding_name (str): The default token encoding scheme. """ from lionagi.integrations.provider.openrouter import OpenRouterService @@ -48,24 +48,24 @@ def Transformers(**kwargs): A provider to interact with Transformers' pipeline Attributes: - task (str): The specific task to be performed by the transformer model. - Currently, only 'conversational' tasks are supported. - model (Union[str, Any]): Identifier for the transformer model to be used. This - can be a model name or a path to a model. - config (Union[str, Dict, Any]): Configuration for the transformer model. Can - include tokenizer information among others. - pipe (pipeline): The loaded transformer pipeline for the specified task, model, - and configuration. + task (str): The specific task to be performed by the transformer model. + Currently, only 'conversational' tasks are supported. + model (Union[str, Any]): Identifier for the transformer model to be used. This + can be a model name or a path to a model. + config (Union[str, Dict, Any]): Configuration for the transformer model. Can + include tokenizer information among others. + pipe (pipeline): The loaded transformer pipeline for the specified task, model, + and configuration. Warnings: - - Ensure the selected model is suitable for conversational tasks to avoid - unexpected behavior. - - As this provider heavily relies on external libraries (Hugging Face's - Transformers), ensure they are installed and updated to compatible versions. + - Ensure the selected model is suitable for conversational tasks to avoid + unexpected behavior. + - As this provider heavily relies on external libraries (Hugging Face's + Transformers), ensure they are installed and updated to compatible versions. Dependencies: - - Requires the `transformers` library by Hugging Face and `asyncio` for - asynchronous operations. + - Requires the `transformers` library by Hugging Face and `asyncio` for + asynchronous operations. """ from lionagi.integrations.provider.transformers import TransformersService @@ -99,8 +99,8 @@ def Ollama(**kwargs): A provider to interact with Ollama Attributes: - model (str): name of the model to use - kwargs (Optional[Any]): additional kwargs for calling the model + model (str): name of the model to use + kwargs (Optional[Any]): additional kwargs for calling the model """ from lionagi.integrations.provider.ollama import OllamaService @@ -113,8 +113,8 @@ def LiteLLM(**kwargs): A provider to interact with Litellm Attributes: - model (str): name of the model to use - kwargs (Optional[Any]): additional kwargs for calling the model + model (str): name of the model to use + kwargs (Optional[Any]): additional kwargs for calling the model """ from .litellm import LiteLLMService @@ -127,8 +127,8 @@ def MLX(**kwargs): A provider to interact with MlX Attributes: - model (str): name of the model to use - kwargs (Optional[Any]): additional kwargs for calling the model + model (str): name of the model to use + kwargs (Optional[Any]): additional kwargs for calling the model """ from lionagi.integrations.provider.mlx_service import MlXService diff --git a/lionagi/libs/__init__.py b/lionagi/libs/__init__.py index b6e690fd5..9c2f2ce42 100644 --- a/lionagi/libs/__init__.py +++ b/lionagi/libs/__init__.py @@ -1 +1,34 @@ -from . import * +from lionagi.libs.sys_util import SysUtil +from lionagi.libs.ln_async import AsyncUtil + +import lionagi.libs.ln_convert as convert +import lionagi.libs.ln_dataframe as dataframe +import lionagi.libs.ln_func_call as func_call +from lionagi.libs.ln_func_call import CallDecorator +import lionagi.libs.ln_nested as nested +from lionagi.libs.ln_parse import ParseUtil, StringMatch + +from lionagi.libs.ln_api import ( + APIUtil, + SimpleRateLimiter, + StatusTracker, + BaseService, + PayloadPackage, +) + +__all__ = [ + "SysUtil", + "convert", + "func_call", + "dataframe", + "nested", + "AsyncUtil", + "ParseUtil", + "StringMatch", + "APIUtil", + "BaseService", + "PayloadPackage", + "StatusTracker", + "SimpleRateLimiter", + "CallDecorator", +] diff --git a/lionagi/libs/ln_api.py b/lionagi/libs/ln_api.py index e25ab5ac4..55dc614c6 100644 --- a/lionagi/libs/ln_api.py +++ b/lionagi/libs/ln_api.py @@ -29,26 +29,27 @@ def api_method( Returns the corresponding HTTP method function from the http_session object. Args: - http_session: The session object from the aiohttp library. - method: The HTTP method as a string. + http_session: The session object from the aiohttp library. + method: The HTTP method as a string. Returns: - The Callable for the specified HTTP method. + The Callable for the specified HTTP method. Raises: - ValueError: If the method is not one of the allowed ones. + ValueError: If the method is not one of the allowed ones. Examples: - >>> session = aiohttp.ClientSession() - >>> post_method = APIUtil.api_method(session, "post") - >>> print(post_method) - > + >>> session = aiohttp.ClientSession() + >>> post_method = APIUtil.api_method(session, "post") + >>> print(post_method) + > """ - if method not in ["post", "delete", "head", "options", "patch"]: + if method in {"post", "delete", "head", "options", "patch"}: + return getattr(http_session, method) + else: raise ValueError( "Invalid request, method must be in ['post', 'delete', 'head', 'options', 'patch']" ) - return getattr(http_session, method) @staticmethod def api_error(response_json: Mapping[str, Any]) -> bool: @@ -56,18 +57,18 @@ def api_error(response_json: Mapping[str, Any]) -> bool: Checks if the given response_json dictionary contains an "error" key. Args: - response_json: The JSON assistant_response as a dictionary. + response_json: The JSON assistant_response as a dictionary. Returns: - True if there is an error, False otherwise. + True if there is an error, False otherwise. Examples: - >>> response_json_with_error = {"error": "Something went wrong"} - >>> APIUtil.api_error(response_json_with_error) - True - >>> response_json_without_error = {"result": "Success"} - >>> APIUtil.api_error(response_json_without_error) - False + >>> response_json_with_error = {"error": "Something went wrong"} + >>> APIUtil.api_error(response_json_with_error) + True + >>> response_json_without_error = {"result": "Success"} + >>> APIUtil.api_error(response_json_without_error) + False """ if "error" in response_json: logging.warning(f"API call failed with error: {response_json['error']}") @@ -80,18 +81,18 @@ def api_rate_limit_error(response_json: Mapping[str, Any]) -> bool: Checks if the error message in the response_json dictionary contains the phrase "Rate limit". Args: - response_json: The JSON assistant_response as a dictionary. + response_json: The JSON assistant_response as a dictionary. Returns: - True if the phrase "Rate limit" is found, False otherwise. + True if the phrase "Rate limit" is found, False otherwise. Examples: - >>> response_json_with_rate_limit = {"error": {"message": "Rate limit exceeded"}} - >>> api_rate_limit_error(response_json_with_rate_limit) - True - >>> response_json_without_rate_limit = {"error": {"message": "Another error"}} - >>> api_rate_limit_error(response_json_without_rate_limit) - False + >>> response_json_with_rate_limit = {"error": {"message": "Rate limit exceeded"}} + >>> api_rate_limit_error(response_json_with_rate_limit) + True + >>> response_json_without_rate_limit = {"error": {"message": "Another error"}} + >>> api_rate_limit_error(response_json_without_rate_limit) + False """ return "Rate limit" in response_json.get("error", {}).get("message", "") @@ -102,21 +103,21 @@ def api_endpoint_from_url(request_url: str) -> str: Extracts the API endpoint from a given URL using a regular expression. Args: - request_url: The full URL to the API endpoint. + request_url: The full URL to the API endpoint. Returns: - The extracted endpoint or an empty string if the pattern does not match. + The extracted endpoint or an empty string if the pattern does not match. Examples: - >>> valid_url = "https://api.example.com/v1/users" - >>> api_endpoint_from_url(valid_url) - 'users' - >>> invalid_url = "https://api.example.com/users" - >>> api_endpoint_from_url(invalid_url) - '' + >>> valid_url = "https://api.example.com/v1/users" + >>> api_endpoint_from_url(valid_url) + 'users' + >>> invalid_url = "https://api.example.com/users" + >>> api_endpoint_from_url(invalid_url) + '' """ match = re.search(r"^https://[^/]+(/.+)?/v\d+/(.+)$", request_url) - return match.group(2) if match else "" + return match[2] if match else "" @staticmethod async def unified_api_call( @@ -126,22 +127,22 @@ async def unified_api_call( Makes an API call and automatically retries on rate limit error. Args: - http_session: The session object from the aiohttp library. - method: The HTTP method as a string. - url: The URL to which the request is made. - **kwargs: Additional keyword arguments to pass to the API call. + http_session: The session object from the aiohttp library. + method: The HTTP method as a string. + url: The URL to which the request is made. + **kwargs: Additional keyword arguments to pass to the API call. Returns: - The JSON assistant_response as a dictionary. + The JSON assistant_response as a dictionary. Examples: - >>> session = aiohttp.ClientSession() - >>> success_url = "https://api.example.com/v1/success" - >>> print(await unified_api_call(session, 'get', success_url)) - {'result': 'Success'} - >>> rate_limit_url = "https://api.example.com/v1/rate_limit" - >>> print(await unified_api_call(session, 'get', rate_limit_url)) - {'error': {'message': 'Rate limit exceeded'}} + >>> session = aiohttp.ClientSession() + >>> success_url = "https://api.example.com/v1/success" + >>> print(await unified_api_call(session, 'get', success_url)) + {'result': 'Success'} + >>> rate_limit_url = "https://api.example.com/v1/rate_limit" + >>> print(await unified_api_call(session, 'get', rate_limit_url)) + {'error': {'message': 'Rate limit exceeded'}} """ api_call = APIUtil.api_method(http_session, method) retry_count = 3 @@ -189,14 +190,14 @@ async def retry_api_call( Retries an API call on failure, with exponential backoff. Args: - http_session: The aiohttp client session. - url: The URL to make the API call. - retries: The number of times to retry. - backoff_factor: The backoff factor for retries. - **kwargs: Additional arguments for the API call. + http_session: The aiohttp client session. + url: The URL to make the API call. + retries: The number of times to retry. + backoff_factor: The backoff factor for retries. + **kwargs: Additional arguments for the API call. Returns: - The assistant_response from the API call, if successful; otherwise, None. + The assistant_response from the API call, if successful; otherwise, None. """ for attempt in range(retries): try: @@ -227,27 +228,27 @@ async def upload_file_with_retry( Uploads a file to a specified URL with a retry mechanism for handling failures. Args: - http_session: The HTTP session object to use for making the request. - url: The URL to which the file will be uploaded. - file_path: The path to the file that will be uploaded. - param_name: The name of the parameter expected by the server for the file upload. - additional_data: Additional data to be sent with the upload. - retries: The number of times to retry the upload in case of failure. + http_session: The HTTP session object to use for making the request. + url: The URL to which the file will be uploaded. + file_path: The path to the file that will be uploaded. + param_name: The name of the parameter expected by the server for the file upload. + additional_data: Additional data to be sent with the upload. + retries: The number of times to retry the upload in case of failure. Returns: - The HTTP assistant_response object. + The HTTP assistant_response object. Examples: - >>> session = aiohttp.ClientSession() - >>> assistant_response = await APIUtil.upload_file_with_retry(session, 'http://example.com/upload', 'path/to/file.txt') - >>> assistant_response.status - 200 + >>> session = aiohttp.ClientSession() + >>> assistant_response = await APIUtil.upload_file_with_retry(session, 'http://example.com/upload', 'path/to/file.txt') + >>> assistant_response.status + 200 """ for attempt in range(retries): try: with open(file_path, "rb") as file: files = {param_name: file} - additional_data = additional_data if additional_data else {} + additional_data = additional_data or {} async with http_session.post( url, data={**files, **additional_data} ) as response: @@ -273,20 +274,20 @@ async def get_oauth_token_with_cache( Retrieves an OAuth token from the authentication server and caches it to avoid unnecessary requests. Args: - http_session: The HTTP session object to use for making the request. - auth_url: The URL of the authentication server. - client_id: The client ID for OAuth authentication. - client_secret: The client secret for OAuth authentication. - scope: The scope for which the OAuth token is requested. + http_session: The HTTP session object to use for making the request. + auth_url: The URL of the authentication server. + client_id: The client ID for OAuth authentication. + client_secret: The client secret for OAuth authentication. + scope: The scope for which the OAuth token is requested. Returns: - The OAuth token as a string. + The OAuth token as a string. Examples: - >>> session = aiohttp.ClientSession() - >>> token = await APIUtil.get_oauth_token_with_cache(session, 'http://auth.example.com', 'client_id', 'client_secret', 'read') - >>> token - 'mock_access_token' + >>> session = aiohttp.ClientSession() + >>> token = await APIUtil.get_oauth_token_with_cache(session, 'http://auth.example.com', 'client_id', 'client_secret', 'read') + >>> token + 'mock_access_token' """ async with http_session.post( auth_url, @@ -309,12 +310,12 @@ async def cached_api_call( Makes an API call. Args: - http_session: The aiohttp client session. - url: The URL for the API call. - **kwargs: Additional arguments for the API call. + http_session: The aiohttp client session. + url: The URL for the API call. + **kwargs: Additional arguments for the API call. Returns: - The assistant_response from the API call, if successful; otherwise, None. + The assistant_response from the API call, if successful; otherwise, None. """ try: async with http_session.get(url, **kwargs) as response: @@ -325,12 +326,11 @@ async def cached_api_call( return None @staticmethod - # @lru_cache(maxsize=1024) def calculate_num_token( payload: Mapping[str, Any] = None, api_endpoint: str = None, token_encoding_name: str = None, - ) -> int: + ) -> int: # sourcery skip: avoid-builtin-shadow """ Calculates the number of tokens required for a request based on the payload and API endpoint. @@ -339,20 +339,20 @@ def calculate_num_token( for the OpenAI API. Parameters: - payload (Mapping[str, Any]): The payload of the request. + payload (Mapping[str, Any]): The payload of the request. - api_endpoint (str): The specific API endpoint for the request. + api_endpoint (str): The specific API endpoint for the request. - token_encoding_name (str): The name of the token encoding method. + token_encoding_name (str): The name of the token encoding method. Returns: - int: The estimated number of tokens required for the request. + int: The estimated number of tokens required for the request. Example: - >>> rate_limiter = OpenAIRateLimiter(100, 200) - >>> payload = {'prompt': 'Translate the following text:', 'max_tokens': 50} - >>> rate_limiter.calculate_num_token(payload, 'completions') - # Expected token calculation for the given payload and endpoint. + >>> rate_limiter = OpenAIRateLimiter(100, 200) + >>> payload = {'prompt': 'Translate the following text:', 'max_tokens': 50} + >>> rate_limiter.calculate_num_token(payload, 'completions') + # Expected token calculation for the given payload and endpoint. """ import tiktoken @@ -371,21 +371,19 @@ def calculate_num_token( num_tokens += len(encoding.encode(value)) if key == "name": # if there's a name, the role is omitted num_tokens -= ( - 1 # role is always required and always 1 token + 1 + # role is always required and always 1 token ) num_tokens += 2 # every reply is primed with assistant return num_tokens + completion_tokens - # normal completions else: prompt = payload["prompt"] if isinstance(prompt, str): # single prompt prompt_tokens = len(encoding.encode(prompt)) - num_tokens = prompt_tokens + completion_tokens - return num_tokens + return prompt_tokens + completion_tokens elif isinstance(prompt, list): # multiple prompts - prompt_tokens = sum([len(encoding.encode(p)) for p in prompt]) - num_tokens = prompt_tokens + completion_tokens * len(prompt) - return num_tokens + prompt_tokens = sum(len(encoding.encode(p)) for p in prompt) + return prompt_tokens + completion_tokens * len(prompt) else: raise TypeError( 'Expecting either string or list of strings for "prompt" field in completion request' @@ -393,11 +391,9 @@ def calculate_num_token( elif api_endpoint == "embeddings": input = payload["input"] if isinstance(input, str): # single input - num_tokens = len(encoding.encode(input)) - return num_tokens + return len(encoding.encode(input)) elif isinstance(input, list): # multiple inputs - num_tokens = sum([len(encoding.encode(i)) for i in input]) - return num_tokens + return sum(len(encoding.encode(i)) for i in input) else: raise TypeError( 'Expecting either string or list of strings for "inputs" field in embedding request' @@ -413,11 +409,11 @@ def create_payload(input_, config, required_, optional_, input_key, **kwargs): payload = {input_key: input_} for key in required_: - payload.update({key: config[key]}) + payload[key] = config[key] for key in optional_: - if bool(config[key]) is True and convert.strip_lower(config[key]) != "none": - payload.update({key: config[key]}) + if bool(config[key]) and convert.strip_lower(config[key]) != "none": + payload[key] = config[key] return payload @@ -428,18 +424,18 @@ class StatusTracker: Keeps track of various task statuses within a system. Attributes: - num_tasks_started (int): The number of tasks that have been initiated. - num_tasks_in_progress (int): The number of tasks currently being processed. - num_tasks_succeeded (int): The number of tasks that have completed successfully. - num_tasks_failed (int): The number of tasks that have failed. - num_rate_limit_errors (int): The number of tasks that failed due to rate limiting. - num_api_errors (int): The number of tasks that failed due to API errors. - num_other_errors (int): The number of tasks that failed due to other errors. + num_tasks_started (int): The number of tasks that have been initiated. + num_tasks_in_progress (int): The number of tasks currently being processed. + num_tasks_succeeded (int): The number of tasks that have completed successfully. + num_tasks_failed (int): The number of tasks that have failed. + num_rate_limit_errors (int): The number of tasks that failed due to rate limiting. + num_api_errors (int): The number of tasks that failed due to API errors. + num_other_errors (int): The number of tasks that failed due to other errors. Examples: - >>> tracker = StatusTracker() - >>> tracker.num_tasks_started += 1 - >>> tracker.num_tasks_succeeded += 1 + >>> tracker = StatusTracker() + >>> tracker.num_tasks_started += 1 + >>> tracker.num_tasks_succeeded += 1 """ num_tasks_started: int = 0 @@ -459,12 +455,12 @@ class BaseRateLimiter(ABC): the replenishment of request and token capacities at regular intervals. Attributes: - interval: The time interval in seconds for replenishing capacities. - max_requests: The maximum number of requests allowed per interval. - max_tokens: The maximum number of tokens allowed per interval. - available_request_capacity: The current available request capacity. - available_token_capacity: The current available token capacity. - rate_limit_replenisher_task: The asyncio task for replenishing capacities. + interval: The time interval in seconds for replenishing capacities. + max_requests: The maximum number of requests allowed per interval. + max_tokens: The maximum number of tokens allowed per interval. + available_request_capacity: The current available request capacity. + available_token_capacity: The current available token capacity. + rate_limit_replenisher_task: The asyncio task for replenishing capacities. """ def __init__( @@ -516,7 +512,8 @@ async def request_permission(self, required_tokens) -> bool: ): self.available_request_capacity -= 1 self.available_token_capacity -= ( - required_tokens # Assuming 1 token per request for simplicity + required_tokens + # Assuming 1 token per request for simplicity ) return True return False @@ -536,16 +533,16 @@ async def _call_api( Makes an API call to the specified endpoint using the provided HTTP session. Args: - http_session: The aiohttp client session to use for the API call. - endpoint: The API endpoint to call. - base_url: The base URL of the API. - api_key: The API key for authentication. - max_attempts: The maximum number of attempts for the API call. - method: The HTTP method to use for the API call. - payload: The payload to send with the API call. + http_session: The aiohttp client session to use for the API call. + endpoint: The API endpoint to call. + base_url: The base URL of the API. + api_key: The API key for authentication. + max_attempts: The maximum number of attempts for the API call. + method: The HTTP method to use for the API call. + payload: The payload to send with the API call. Returns: - The JSON assistant_response from the API call if successful, otherwise None. + The JSON assistant_response from the API call if successful, otherwise None. """ endpoint = APIUtil.api_endpoint_from_url(base_url + endpoint) while True: @@ -573,18 +570,17 @@ async def _call_api( ) as response: response_json = await response.json() - if "error" in response_json: - logging.warning( - f"API call failed with error: {response_json['error']}" - ) - attempts_left -= 1 - - if "Rate limit" in response_json["error"].get( - "message", "" - ): - await AsyncUtil.sleep(15) - else: + if "error" not in response_json: return response_json + logging.warning( + f"API call failed with error: {response_json['error']}" + ) + attempts_left -= 1 + + if "Rate limit" in response_json["error"].get( + "message", "" + ): + await AsyncUtil.sleep(15) except Exception as e: logging.warning(f"API call failed with exception: {e}") attempts_left -= 1 @@ -606,13 +602,13 @@ async def create( Creates an instance of BaseRateLimiter and starts the replenisher task. Args: - max_requests: The maximum number of requests allowed per interval. - max_tokens: The maximum number of tokens allowed per interval. - interval: The time interval in seconds for replenishing capacities. - token_encoding_name: The name of the token encoding to use. + max_requests: The maximum number of requests allowed per interval. + max_tokens: The maximum number of tokens allowed per interval. + interval: The time interval in seconds for replenishing capacities. + token_encoding_name: The name of the token encoding to use. Returns: - An instance of BaseRateLimiter with the replenisher task started. + An instance of BaseRateLimiter with the replenisher task started. """ instance = cls(max_requests, max_tokens, interval, token_encoding_name) instance.rate_limit_replenisher_task = AsyncUtil.create_task( @@ -646,25 +642,25 @@ class EndPoint: This class encapsulates the details of an API endpoint, including its rate limiter. Attributes: - endpoint (str): The API endpoint path. - rate_limiter_class (Type[li.BaseRateLimiter]): The class used for rate limiting requests to the endpoint. - max_requests (int): The maximum number of requests allowed per interval. - max_tokens (int): The maximum number of tokens allowed per interval. - interval (int): The time interval in seconds for replenishing rate limit capacities. - config (Mapping): Configuration parameters for the endpoint. - rate_limiter (Optional[li.BaseRateLimiter]): The rate limiter instance for this endpoint. + endpoint (str): The API endpoint path. + rate_limiter_class (Type[li.BaseRateLimiter]): The class used for rate limiting requests to the endpoint. + max_requests (int): The maximum number of requests allowed per interval. + max_tokens (int): The maximum number of tokens allowed per interval. + interval (int): The time interval in seconds for replenishing rate limit capacities. + config (Mapping): Configuration parameters for the endpoint. + rate_limiter (Optional[li.BaseRateLimiter]): The rate limiter instance for this endpoint. Examples: - # Example usage of EndPoint with SimpleRateLimiter - endpoint = EndPoint( - max_requests=100, - max_tokens=1000, - interval=60, - endpoint_='chat/completions', - rate_limiter_class=li.SimpleRateLimiter, - config={'param1': 'value1'} - ) - asyncio.run(endpoint.init_rate_limiter()) + # Example usage of EndPoint with SimpleRateLimiter + endpoint = EndPoint( + max_requests=100, + max_tokens=1000, + interval=60, + endpoint_='chat/completions', + rate_limiter_class=li.SimpleRateLimiter, + config={'param1': 'value1'} + ) + asyncio.run(endpoint.init_rate_limiter()) """ def __init__( @@ -702,10 +698,10 @@ class BaseService: This class provides a foundation for services that need to make API calls with rate limiting. Attributes: - api_key (Optional[str]): The API key used for authentication. - schema (Mapping[str, Any]): The schema defining the service's endpoints. - status_tracker (StatusTracker): The object tracking the status of API calls. - endpoints (Mapping[str, EndPoint]): A dictionary of endpoint objects. + api_key (Optional[str]): The API key used for authentication. + schema (Mapping[str, Any]): The schema defining the service's endpoints. + status_tracker (StatusTracker): The object tracking the status of API calls. + endpoints (Mapping[str, EndPoint]): A dictionary of endpoint objects. """ base_url: str = "" @@ -739,7 +735,7 @@ async def init_endpoint( Initializes the specified endpoint or all endpoints if none is specified. Args: - endpoint_: The endpoint(s) to initialize. Can be a string, an EndPoint, a list of strings, or a list of EndPoints. + endpoint_: The endpoint(s) to initialize. Can be a string, an EndPoint, a list of strings, or a list of EndPoints. """ if endpoint_: @@ -756,45 +752,40 @@ async def init_endpoint( self.schema.get(ep, {}) if isinstance(ep, EndPoint): self.endpoints[ep.endpoint] = ep + elif ep == "chat/completions": + self.endpoints[ep] = EndPoint( + max_requests=self.chat_config_rate_limit.get( + "max_requests", 1000 + ), + max_tokens=self.chat_config_rate_limit.get( + "max_tokens", 100000 + ), + interval=self.chat_config_rate_limit.get("interval", 60), + endpoint_=ep, + token_encoding_name=self.token_encoding_name, + config=endpoint_config, + ) else: - if ep == "chat/completions": - self.endpoints[ep] = EndPoint( - max_requests=self.chat_config_rate_limit.get( - "max_requests", 1000 - ), - max_tokens=self.chat_config_rate_limit.get( - "max_tokens", 100000 - ), - interval=self.chat_config_rate_limit.get( - "interval", 60 - ), - endpoint_=ep, - token_encoding_name=self.token_encoding_name, - config=endpoint_config, - ) - else: - self.endpoints[ep] = EndPoint( - max_requests=( - endpoint_config.get("max_requests", 1000) - if endpoint_config.get("max_requests", 1000) - is not None - else 1000 - ), - max_tokens=( - endpoint_config.get("max_tokens", 100000) - if endpoint_config.get("max_tokens", 100000) - is not None - else 100000 - ), - interval=( - endpoint_config.get("interval", 60) - if endpoint_config.get("interval", 60) is not None - else 60 - ), - endpoint_=ep, - token_encoding_name=self.token_encoding_name, - config=endpoint_config, - ) + self.endpoints[ep] = EndPoint( + max_requests=( + endpoint_config.get("max_requests", 1000) + if endpoint_config.get("max_requests", 1000) is not None + else 1000 + ), + max_tokens=( + endpoint_config.get("max_tokens", 100000) + if endpoint_config.get("max_tokens", 100000) is not None + else 100000 + ), + interval=( + endpoint_config.get("interval", 60) + if endpoint_config.get("interval", 60) is not None + else 60 + ), + endpoint_=ep, + token_encoding_name=self.token_encoding_name, + config=endpoint_config, + ) if not self.endpoints[ep]._has_initialized: await self.endpoints[ep].init_rate_limiter() @@ -820,20 +811,20 @@ async def call_api(self, payload, endpoint, method, **kwargs): Calls the specified API endpoint with the given payload and method. Args: - payload: The payload to send with the API call. - endpoint: The endpoint to call. - method: The HTTP method to use for the call. + payload: The payload to send with the API call. + endpoint: The endpoint to call. + method: The HTTP method to use for the call. Returns: - The assistant_response from the API call. + The assistant_response from the API call. Raises: - ValueError: If the endpoint has not been initialized. + ValueError: If the endpoint has not been initialized. """ if endpoint not in self.endpoints.keys(): raise ValueError(f"The endpoint {endpoint} has not initialized.") async with aiohttp.ClientSession() as http_session: - completion = await self.endpoints[endpoint].rate_limiter._call_api( + return await self.endpoints[endpoint].rate_limiter._call_api( http_session=http_session, endpoint=endpoint, base_url=self.base_url, @@ -842,7 +833,6 @@ async def call_api(self, payload, endpoint, method, **kwargs): payload=payload, **kwargs, ) - return completion class PayloadPackage: @@ -853,13 +843,13 @@ def chat_completion(cls, messages, llmconfig, schema, **kwargs): Creates a payload for the chat completion operation. Args: - messages: The messages to include in the chat completion. - llmconfig: Configuration for the language model. - schema: The schema describing required and optional fields. - **kwargs: Additional keyword arguments. + messages: The messages to include in the chat completion. + llmconfig: Configuration for the language model. + schema: The schema describing required and optional fields. + **kwargs: Additional keyword arguments. Returns: - The constructed payload. + The constructed payload. """ return APIUtil.create_payload( input_=messages, @@ -876,13 +866,13 @@ def fine_tuning(cls, training_file, llmconfig, schema, **kwargs): Creates a payload for the fine-tuning operation. Args: - training_file: The file containing training data. - llmconfig: Configuration for the language model. - schema: The schema describing required and optional fields. - **kwargs: Additional keyword arguments. + training_file: The file containing training data. + llmconfig: Configuration for the language model. + schema: The schema describing required and optional fields. + **kwargs: Additional keyword arguments. Returns: - The constructed payload. + The constructed payload. """ return APIUtil._create_payload( input_=training_file, diff --git a/lionagi/libs/ln_async.py b/lionagi/libs/ln_async.py index b33ba2a1c..5f26affd6 100644 --- a/lionagi/libs/ln_async.py +++ b/lionagi/libs/ln_async.py @@ -11,6 +11,7 @@ class AsyncUtil: + @staticmethod async def _call_handler( func: Callable, *args, error_map: dict[type, Callable] = None, **kwargs ) -> Any: @@ -19,43 +20,42 @@ async def _call_handler( functions. Args: - func (Callable): - The function to call. - *args: - Positional arguments to pass to the function. - error_map (Dict[type, Callable], optional): - A dictionary mapping error types to handler functions. - **kwargs: - Keyword arguments to pass to the function. + func (Callable): + The function to call. + *args: + Positional arguments to pass to the function. + error_map (Dict[type, Callable], optional): + A dictionary mapping error types to handler functions. + **kwargs: + Keyword arguments to pass to the function. Returns: - Any: The result of the function call. + Any: The result of the function call. Raises: - Exception: Propagates any exceptions not handled by the error_map. + Exception: Propagates any exceptions not handled by the error_map. examples: - >>> async def async_add(x, y): return x + y - >>> asyncio.run(_call_handler(async_add, 1, 2)) - 3 + >>> async def async_add(x, y): return x + y + >>> asyncio.run(_call_handler(async_add, 1, 2)) + 3 """ try: - if AsyncUtil.is_coroutine_func(func): - # Checking for a running event loop - try: - loop = asyncio.get_running_loop() - except RuntimeError: # No running event loop - loop = asyncio.new_event_loop() - result = loop.run_until_complete(func(*args, **kwargs)) + if not AsyncUtil.is_coroutine_func(func): + return func(*args, **kwargs) - loop.close() - return result + # Checking for a running event loop + try: + loop = asyncio.get_running_loop() + except RuntimeError: # No running event loop + loop = asyncio.new_event_loop() + result = loop.run_until_complete(func(*args, **kwargs)) - if loop.is_running(): - return await func(*args, **kwargs) + loop.close() + return result - else: - return func(*args, **kwargs) + if loop.is_running(): + return await func(*args, **kwargs) except Exception as e: if error_map: @@ -71,10 +71,10 @@ def is_coroutine_func(func: Callable[..., Any]) -> bool: Checks whether a function is an asyncio coroutine function. Args: - func: The function to check. + func: The function to check. Returns: - True if the function is a coroutine function, False otherwise. + True if the function is a coroutine function, False otherwise. """ return asyncio.iscoroutinefunction(func) @@ -87,18 +87,17 @@ def _custom_error_handler( handle errors based on a given error mapping. Args: - error (Exception): - The error to handle. - error_map (Mapping[type, Callable]): - A dictionary mapping error types to handler functions. + error (Exception): + The error to handle. + error_map (Mapping[type, Callable]): + A dictionary mapping error types to handler functions. examples: - >>> def handle_value_error(e): print("ValueError occurred") - >>> custom_error_handler(ValueError(), {ValueError: handle_value_error}) - ValueError occurred + >>> def handle_value_error(e): print("ValueError occurred") + >>> custom_error_handler(ValueError(), {ValueError: handle_value_error}) + ValueError occurred """ - handler = error_map.get(type(error)) - if handler: + if handler := error_map.get(type(error)): handler(error) else: logging.error(f"Unhandled error: {error}") @@ -111,30 +110,28 @@ async def handle_async_sync( Executes a function, automatically handling synchronous and asynchronous functions. Args: - func: The function to execute. - *args: Positional arguments for the function. - **kwargs: Keyword arguments for the function. + func: The function to execute. + *args: Positional arguments for the function. + **kwargs: Keyword arguments for the function. Returns: - The result of the function execution. + The result of the function execution. """ try: - if AsyncUtil.is_coroutine_func(func): - - try: - loop = asyncio.get_event_loop() - - if loop.is_running(): - return await func(*args, **kwargs) - else: - return await asyncio.run(func(*args, **kwargs)) + if not AsyncUtil.is_coroutine_func(func): + return func(*args, **kwargs) - except RuntimeError: - return asyncio.run(func(*args, **kwargs)) + try: + loop = asyncio.get_event_loop() - else: - return func(*args, **kwargs) + return ( + await func(*args, **kwargs) + if loop.is_running() + else await asyncio.run(func(*args, **kwargs)) + ) + except RuntimeError: + return asyncio.run(func(*args, **kwargs)) except Exception as e: if error_map: @@ -205,10 +202,6 @@ def create_task(*args, obj=True, **kwargs): def create_lock(*args, **kwargs): return asyncio.Lock(*args, **kwargs) - # @classmethod - # def HttpClientSession(cls): - # return aiohttp.ClientSession + # @classmethod # def HttpClientSession(cls): # return aiohttp.ClientSession - # @classmethod - # def HttpClientError(cls): - # return aiohttp.ClientError + # @classmethod # def HttpClientError(cls): # return aiohttp.ClientError diff --git a/lionagi/libs/ln_convert.py b/lionagi/libs/ln_convert.py index f0a5bc023..ff4da7957 100644 --- a/lionagi/libs/ln_convert.py +++ b/lionagi/libs/ln_convert.py @@ -23,29 +23,28 @@ def to_list(input_, /, *, flatten: bool = True, dropna: bool = True) -> list[Any Specialized implementations may use additional keyword arguments specific to their conversion logic. Args: - input_ (Any): The input object to convert to a list. - flatten (bool): If True, and the input is a nested list, the function will attempt to flatten it. - dropna (bool): If True, None values will be removed from the resulting list. + input_ (Any): The input object to convert to a list. + flatten (bool): If True, and the input is a nested list, the function will attempt to flatten it. + dropna (bool): If True, None values will be removed from the resulting list. Returns: - list[Any]: A list representation of the input, with modifications based on `flatten` and `dropna`. + list[Any]: A list representation of the input, with modifications based on `flatten` and `dropna`. Raises: - ValueError: If the input type is unsupported or cannot be converted to a list. + ValueError: If the input type is unsupported or cannot be converted to a list. Note: - - This function uses `@singledispatch` to handle different input types via overloading. - - The default behavior for dictionaries is to wrap them in a list without flattening. - - For specific behaviors with lists, tuples, sets, and other types, see the registered implementations. + - This function uses `@singledispatch` to handle different input types via overloading. + - The default behavior for dictionaries is to wrap them in a list without flattening. + - For specific behaviors with lists, tuples, sets, and other types, see the registered implementations. """ try: - if isinstance(input_, Iterable) and not isinstance( + if not isinstance(input_, Iterable) or isinstance( input_, (str, bytes, bytearray, dict) ): - iterable_list = list(input_) - return _flatten_list(iterable_list, dropna) if flatten else iterable_list - else: return [input_] + iterable_list = list(input_) + return _flatten_list(iterable_list, dropna) if flatten else iterable_list except Exception as e: raise ValueError(f"Could not convert {type(input_)} object to list: {e}") from e @@ -76,19 +75,19 @@ def to_dict(input_, /, *args, **kwargs) -> dict[Any, Any]: and Pydantic's BaseModel, utilizing the single dispatch mechanism for type-specific conversions. Args: - input_ (Any): The input object to convert to a dictionary. - *args: Variable length argument list for additional options in type-specific handlers. - **kwargs: Arbitrary keyword arguments for additional options in type-specific handlers. + input_ (Any): The input object to convert to a dictionary. + *args: Variable length argument list for additional options in type-specific handlers. + **kwargs: Arbitrary keyword arguments for additional options in type-specific handlers. Returns: - dict[Any, Any]: A dictionary representation of the input object. + dict[Any, Any]: A dictionary representation of the input object. Raises: - ValueError: If the input type is not supported or cannot be converted to a dictionary. + ValueError: If the input type is not supported or cannot be converted to a dictionary. Note: - - For specific behaviors with dict, str, pandas.Series, pandas.DataFrame, and BaseModel, - see the registered implementations. + - For specific behaviors with dict, str, pandas.Series, pandas.DataFrame, and BaseModel, + see the registered implementations. """ try: return dict(input_, *args, **kwargs) @@ -104,10 +103,10 @@ def _(input_) -> dict[Any, Any]: Handles dictionary inputs directly, returning the input without modification. Args: - input_ (dict[Any, Any]): The dictionary to be returned. + input_ (dict[Any, Any]): The dictionary to be returned. Returns: - dict[Any, Any]: The input dictionary, unchanged. + dict[Any, Any]: The input dictionary, unchanged. """ return input_ @@ -118,15 +117,15 @@ def _(input_, /, *args, **kwargs) -> dict[Any, Any]: Converts a JSON-formatted string to a dictionary. Args: - input_ (str): The JSON string to convert. - *args: Variable length argument list for json.loads(). - **kwargs: Arbitrary keyword arguments for json.loads(). + input_ (str): The JSON string to convert. + *args: Variable length argument list for json.loads(). + **kwargs: Arbitrary keyword arguments for json.loads(). Returns: - dict[Any, Any]: The dictionary representation of the JSON string. + dict[Any, Any]: The dictionary representation of the JSON string. Raises: - ValueError: If the string cannot be decoded into a dictionary. + ValueError: If the string cannot be decoded into a dictionary. """ try: return json.loads(input_, *args, **kwargs) @@ -140,12 +139,12 @@ def _(input_, /, *args, **kwargs) -> dict[Any, Any]: Converts a pandas Series to a dictionary. Args: - input_ (pd.Series): The pandas Series to convert. - *args: Variable length argument list for Series.to_dict(). - **kwargs: Arbitrary keyword arguments for Series.to_dict(). + input_ (pd.Series): The pandas Series to convert. + *args: Variable length argument list for Series.to_dict(). + **kwargs: Arbitrary keyword arguments for Series.to_dict(). Returns: - dict[Any, Any]: The dictionary representation of the pandas Series. + dict[Any, Any]: The dictionary representation of the pandas Series. """ return input_.to_dict(*args, **kwargs) @@ -158,15 +157,15 @@ def _( Converts a pandas DataFrame to a dictionary or a list of dictionaries, based on the `orient` and `as_list` parameters. Args: - input_ (pd.DataFrame): The pandas DataFrame to convert. - *args: Variable length argument list for DataFrame.to_dict() or DataFrame.iterrows(). - orient (str): The orientation of the data. Default is 'list'. - as_list (bool): If True, returns a list of dictionaries, one for each row. Default is False. - **kwargs: Arbitrary keyword arguments for DataFrame.to_dict(). + input_ (pd.DataFrame): The pandas DataFrame to convert. + *args: Variable length argument list for DataFrame.to_dict() or DataFrame.iterrows(). + orient (str): The orientation of the data. Default is 'list'. + as_list (bool): If True, returns a list of dictionaries, one for each row. Default is False. + **kwargs: Arbitrary keyword arguments for DataFrame.to_dict(). Returns: - dict[Any, Any] | list[dict[Any, Any]]: Depending on `as_list`, either a dictionary representation - of the DataFrame or a list of dictionaries, one for each row. + dict[Any, Any] | list[dict[Any, Any]]: Depending on `as_list`, either a dictionary representation + of the DataFrame or a list of dictionaries, one for each row. """ if as_list: return [row.to_dict(*args, **kwargs) for _, row in input_.iterrows()] @@ -179,12 +178,12 @@ def _(input_, /, *args, **kwargs) -> dict[Any, Any]: Converts a Pydantic BaseModel instance to a dictionary. Args: - input_ (BaseModel): The Pydantic BaseModel instance to convert. - *args: Variable length argument list for the model's dict() method. - **kwargs: Arbitrary keyword arguments for the model's dict() method. + input_ (BaseModel): The Pydantic BaseModel instance to convert. + *args: Variable length argument list for the model's dict() method. + **kwargs: Arbitrary keyword arguments for the model's dict() method. Returns: - dict[Any, Any]: The dictionary representation of the BaseModel instance. + dict[Any, Any]: The dictionary representation of the BaseModel instance. """ return input_.model_dump(*args, **kwargs) @@ -198,17 +197,17 @@ def to_str(input_) -> str: providing type-specific conversions to string format. Args: - input_ (Any): The input object to convert to a string. - *args: Variable length argument list for additional options in type-specific handlers. - **kwargs: Arbitrary keyword arguments for additional options in type-specific handlers. + input_ (Any): The input object to convert to a string. + *args: Variable length argument list for additional options in type-specific handlers. + **kwargs: Arbitrary keyword arguments for additional options in type-specific handlers. Returns: - str: A string representation of the input object. + str: A string representation of the input object. Note: - - The base implementation simply uses the str() function for conversion. - - For detailed behaviors with dict, str, list, pandas.Series, and pandas.DataFrame, - refer to the registered implementations. + - The base implementation simply uses the str() function for conversion. + - For detailed behaviors with dict, str, list, pandas.Series, and pandas.DataFrame, + refer to the registered implementations. """ return str(input_) @@ -219,12 +218,12 @@ def _(input_, /, *args, **kwargs) -> str: Converts a dictionary to a JSON-formatted string. Args: - input_ (dict): The dictionary to convert. - *args: Variable length argument list for json.dumps(). - **kwargs: Arbitrary keyword arguments for json.dumps(). + input_ (dict): The dictionary to convert. + *args: Variable length argument list for json.dumps(). + **kwargs: Arbitrary keyword arguments for json.dumps(). Returns: - str: The JSON string representation of the dictionary. + str: The JSON string representation of the dictionary. """ return json.dumps(input_, *args, **kwargs) @@ -235,12 +234,12 @@ def _(input_) -> str: Returns the input string unchanged. Args: - input_ (str): The input string. - *args: Ignored. - **kwargs: Ignored. + input_ (str): The input string. + *args: Ignored. + **kwargs: Ignored. Returns: - str: The input string, unchanged. + str: The input string, unchanged. """ return input_ @@ -252,15 +251,15 @@ def _(input_, /, *args, as_list: bool = False, **kwargs) -> str | list[str]: of the list itself or join the string representations of its elements. Args: - input_ (list): The list to convert. - *args: Variable length argument list for additional options in element conversion. - as_list (bool): If True, returns the string representation of the list. If False, - returns the elements joined by a comma. Default is False. - **kwargs: Arbitrary keyword arguments for additional options in element conversion. + input_ (list): The list to convert. + *args: Variable length argument list for additional options in element conversion. + as_list (bool): If True, returns the string representation of the list. If False, + returns the elements joined by a comma. Default is False. + **kwargs: Arbitrary keyword arguments for additional options in element conversion. Returns: - str: Depending on `as_list`, either the string representation of the list or a string - of the elements joined by a comma. + str: Depending on `as_list`, either the string representation of the list or a string + of the elements joined by a comma. """ lst_ = [to_str(item, *args, **kwargs) for item in input_] return lst_ if as_list else ", ".join(lst_) @@ -272,12 +271,12 @@ def _(input_, /, *args, **kwargs) -> str: Converts a pandas Series to a JSON-formatted string. Args: - input_ (pd.Series): The pandas Series to convert. - *args: Variable length argument list for Series.to_json(). - **kwargs: Arbitrary keyword arguments for Series.to_json(). + input_ (pd.Series): The pandas Series to convert. + *args: Variable length argument list for Series.to_json(). + **kwargs: Arbitrary keyword arguments for Series.to_json(). Returns: - str: The JSON string representation of the pandas Series. + str: The JSON string representation of the pandas Series. """ return input_.to_json(*args, **kwargs) @@ -289,15 +288,15 @@ def _(input_, /, *args, as_list: bool = False, **kwargs) -> str | list[str]: first if `as_list` is True, then to a string representation of that list. Args: - input_ (pd.DataFrame): The pandas DataFrame to convert. - *args: Variable length argument list for additional options in conversion. - as_list (bool): If True, converts the DataFrame to a list of dictionaries before converting - to a string. Default is False. - **kwargs: Arbitrary keyword arguments for DataFrame.to_json() or to_dict(). + input_ (pd.DataFrame): The pandas DataFrame to convert. + *args: Variable length argument list for additional options in conversion. + as_list (bool): If True, converts the DataFrame to a list of dictionaries before converting + to a string. Default is False. + **kwargs: Arbitrary keyword arguments for DataFrame.to_json() or to_dict(). Returns: - str: Depending on `as_list`, either a JSON string representation of the DataFrame or a string - representation of a list of dictionaries derived from the DataFrame. + str: Depending on `as_list`, either a JSON string representation of the DataFrame or a string + representation of a list of dictionaries derived from the DataFrame. """ if as_list: return to_dict(input_, as_list=True, *args, **kwargs) @@ -324,20 +323,20 @@ def to_df( The base implementation attempts to directly convert the input to a DataFrame, applying dropna and reset_index as specified. Args: - input_ (Any): The input data to convert into a DataFrame. Accepts a wide range of types thanks to overloads. - how (str): Specifies how missing values are dropped. Passed directly to DataFrame.dropna(). - drop_kwargs (dict[str, Any] | None): Additional keyword arguments for DataFrame.dropna(). - reset_index (bool): If True, the DataFrame index will be reset, removing the index labels. - **kwargs: Additional keyword arguments passed to the pandas DataFrame constructor. + input_ (Any): The input data to convert into a DataFrame. Accepts a wide range of types thanks to overloads. + how (str): Specifies how missing values are dropped. Passed directly to DataFrame.dropna(). + drop_kwargs (dict[str, Any] | None): Additional keyword arguments for DataFrame.dropna(). + reset_index (bool): If True, the DataFrame index will be reset, removing the index labels. + **kwargs: Additional keyword arguments passed to the pandas DataFrame constructor. Returns: - pd.DataFrame: A pandas DataFrame constructed from the input data. + pd.DataFrame: A pandas DataFrame constructed from the input data. Raises: - ValueError: If there is an error during the conversion process. + ValueError: If there is an error during the conversion process. Note: - - This function is overloaded to provide specialized behavior for different input types, enhancing its flexibility. + - This function is overloaded to provide specialized behavior for different input types, enhancing its flexibility. """ if drop_kwargs is None: @@ -345,7 +344,7 @@ def to_df( try: dfs = pd.DataFrame(input_, **kwargs) - dfs.dropna(**(drop_kwargs | {"how": how}), inplace=True) + dfs = dfs.dropna(**(drop_kwargs | {"how": how})) return dfs.reset_index(drop=True) if reset_index else dfs except Exception as e: @@ -367,7 +366,7 @@ def _( drop_kwargs = {} try: dfs = pd.DataFrame(input_, **kwargs) - dfs.dropna(**(drop_kwargs | {"how": how}), inplace=True) + dfs = dfs.dropna(**(drop_kwargs | {"how": how})) return dfs.reset_index(drop=True) if reset_index else dfs except Exception as e: raise ValueError(f"Error converting input_ to DataFrame: {e}") from e @@ -387,7 +386,9 @@ def _( dfs = pd.concat([dfs, i], **kwargs) except Exception as e2: - raise ValueError(f"Error converting input_ to DataFrame: {e1}, {e2}") + raise ValueError( + f"Error converting input_ to DataFrame: {e1}, {e2}" + ) from e2 dfs.dropna(**(drop_kwargs | {"how": how}), inplace=True) return dfs.reset_index(drop=True) if reset_index else dfs @@ -406,17 +407,17 @@ def to_num( Converts the input to a numeric value of specified type, with optional bounds and precision. Args: - input_ (Any): The input value to convert. Can be of any type that `to_str` can handle. - upper_bound (float | None): The upper bound for the numeric value. If specified, values above this bound will raise an error. - lower_bound (float | None): The lower bound for the numeric value. If specified, values below this bound will raise an error. - num_type (Type[int | float]): The numeric type to convert to. Can be `int` or `float`. - precision (int | None): The number of decimal places for the result. Applies only to `float` type. + input_ (Any): The input value to convert. Can be of any type that `to_str` can handle. + upper_bound (float | None): The upper bound for the numeric value. If specified, values above this bound will raise an error. + lower_bound (float | None): The lower bound for the numeric value. If specified, values below this bound will raise an error. + num_type (Type[int | float]): The numeric type to convert to. Can be `int` or `float`. + precision (int | None): The number of decimal places for the result. Applies only to `float` type. Returns: - int | float: The converted numeric value, adhering to specified type and precision. + int | float: The converted numeric value, adhering to specified type and precision. Raises: - ValueError: If the input cannot be converted to a number, or if it violates the specified bounds. + ValueError: If the input cannot be converted to a number, or if it violates the specified bounds. """ str_ = to_str(input_) return _str_to_num(str_, upper_bound, lower_bound, num_type, precision) @@ -427,10 +428,10 @@ def to_readable_dict(input_: Any | list[Any]) -> str | list[Any]: Converts a given input to a readable dictionary format, either as a string or a list of dictionaries. Args: - input_ (Any | list[Any]): The input to convert to a readable dictionary format. + input_ (Any | list[Any]): The input to convert to a readable dictionary format. Returns: - str | list[str]: The readable dictionary format of the input. + str | list[str]: The readable dictionary format of the input. """ try: @@ -445,11 +446,11 @@ def is_same_dtype(input_: list | dict, dtype: Type | None = None) -> bool: Checks if all elements in a list or dictionary values are of the same data type. Args: - input_ (list | dict): The input list or dictionary to check. - dtype (Type | None): The data type to check against. If None, uses the type of the first element. + input_ (list | dict): The input list or dictionary to check. + dtype (Type | None): The data type to check against. If None, uses the type of the first element. Returns: - bool: True if all elements are of the same type (or if the input is empty), False otherwise. + bool: True if all elements are of the same type (or if the input is empty), False otherwise. """ if not input_: return True @@ -486,13 +487,13 @@ def strip_lower(input_: Any) -> str: Converts the input to a lowercase string with leading and trailing whitespace removed. Args: - input_ (Any): The input value to convert and process. + input_ (Any): The input value to convert and process. Returns: - str: The processed string. + str: The processed string. Raises: - ValueError: If the input cannot be converted to a string. + ValueError: If the input cannot be converted to a string. """ try: return str(input_).strip().lower() @@ -517,11 +518,11 @@ def is_structure_homogeneous( either list | dict, or None). examples: - >>> _is_structure_homogeneous({'a': {'b': 1}, 'c': {'d': 2}}) - True + >>> _is_structure_homogeneous({'a': {'b': 1}, 'c': {'d': 2}}) + True - >>> _is_structure_homogeneous({'a': {'b': 1}, 'c': [1, 2]}) - False + >>> _is_structure_homogeneous({'a': {'b': 1}, 'c': [1, 2]}) + False """ # noinspection PyShadowingNames @@ -550,10 +551,7 @@ def _check_structure(substructure): return True, structure_type is_, structure_type = _check_structure(structure) - if return_structure_type: - return is_, structure_type - else: - return is_ + return (is_, structure_type) if return_structure_type else is_ def is_homogeneous(iterables: list[Any] | dict[Any, Any], type_check: type) -> bool: @@ -614,17 +612,17 @@ def _flatten_list(lst_: list[Any], dropna: bool = True) -> list[Any]: flatten a nested list, optionally removing None values. Args: - lst_ (list[Any]): A nested list to flatten. - dropna (bool): If True, None values are removed. default is True. + lst_ (list[Any]): A nested list to flatten. + dropna (bool): If True, None values are removed. default is True. Returns: - list[Any]: A flattened list. + list[Any]: A flattened list. examples: - >>> flatten_list([[1, 2], [3, None]], dropna=True) - [1, 2, 3] - >>> flatten_list([[1, [2, None]], 3], dropna=False) - [1, 2, None, 3] + >>> flatten_list([[1, 2], [3, None]], dropna=True) + [1, 2, 3] + >>> flatten_list([[1, [2, None]], 3], dropna=False) + [1, 2, None, 3] """ flattened_list = list(_flatten_list_generator(lst_, dropna)) return list(_dropna_iterator(flattened_list)) if dropna else flattened_list @@ -637,15 +635,15 @@ def _flatten_list_generator( Generator for flattening a nested list. Args: - lst_ (list[Any]): A nested list to flatten. - dropna (bool): If True, None values are omitted. Default is True. + lst_ (list[Any]): A nested list to flatten. + dropna (bool): If True, None values are omitted. Default is True. Yields: - Generator[Any, None, None]: A generator yielding flattened elements. + Generator[Any, None, None]: A generator yielding flattened elements. Examples: - >>> list(_flatten_list_generator([[1, [2, None]], 3], dropna=False)) - [1, 2, None, 3] + >>> list(_flatten_list_generator([[1, [2, None]], 3], dropna=False)) + [1, 2, None, 3] """ for i in lst_: if isinstance(i, list): diff --git a/lionagi/libs/ln_dataframe.py b/lionagi/libs/ln_dataframe.py index 594a5ddc8..05bb7115e 100644 --- a/lionagi/libs/ln_dataframe.py +++ b/lionagi/libs/ln_dataframe.py @@ -4,7 +4,6 @@ from lionagi.libs import ln_convert as convert - ln_DataFrame = pd.DataFrame ln_Series = pd.Series @@ -21,13 +20,13 @@ def extend_dataframe( Merges two DataFrames while ensuring no duplicate entries based on a specified unique column. Args: - df1: The primary DataFrame. - df2: The DataFrame to merge with the primary DataFrame. - unique_col: The column name to check for duplicate entries. Defaults to 'node_id'. - **kwargs: Additional keyword arguments for `drop_duplicates`. + df1: The primary DataFrame. + df2: The DataFrame to merge with the primary DataFrame. + unique_col: The column name to check for duplicate entries. Defaults to 'node_id'. + **kwargs: Additional keyword arguments for `drop_duplicates`. Returns: - A DataFrame combined from df1 and df2 with duplicates removed based on the unique column. + A DataFrame combined from df1 and df2 with duplicates removed based on the unique column. """ try: if len(df2.dropna(how="all")) > 0 and len(df1.dropna(how="all")) > 0: @@ -40,7 +39,7 @@ def extend_dataframe( raise ValueError("No data to extend") except Exception as e: - raise ValueError(f"Error in extending messages: {e}") + raise ValueError(f"Error in extending messages: {e}") from e def search_keywords( @@ -57,16 +56,16 @@ def search_keywords( Filters a DataFrame for rows where a specified column contains given keywords. Args: - df: The DataFrame to search through. - keywords: A keyword or list of keywords to search for. - col: The column to perform the search in. Defaults to "content". - case_sensitive: Whether the search should be case-sensitive. Defaults to False. - reset_index: Whether to reset the DataFrame's index after filtering. Defaults to False. - dropna: Whether to drop rows with NA values before searching. Defaults to False. + df: The DataFrame to search through. + keywords: A keyword or list of keywords to search for. + col: The column to perform the search in. Defaults to "content". + case_sensitive: Whether the search should be case-sensitive. Defaults to False. + reset_index: Whether to reset the DataFrame's index after filtering. Defaults to False. + dropna: Whether to drop rows with NA values before searching. Defaults to False. Returns: - A filtered DataFrame containing only rows where the specified column contains - any of the provided keywords. + A filtered DataFrame containing only rows where the specified column contains + any of the provided keywords. """ if isinstance(keywords, list): @@ -99,11 +98,11 @@ def replace_keyword( Replaces occurrences of a specified keyword with a replacement string in a DataFrame column. Args: - df: The DataFrame to modify. - keyword: The keyword to be replaced. - replacement: The string to replace the keyword with. - col: The column in which to perform the replacement. - case_sensitive: If True, performs a case-sensitive replacement. Defaults to False. + df: The DataFrame to modify. + keyword: The keyword to be replaced. + replacement: The string to replace the keyword with. + col: The column in which to perform the replacement. + case_sensitive: If True, performs a case-sensitive replacement. Defaults to False. """ df_ = df.copy(deep=False) if inplace else df.copy() @@ -123,11 +122,11 @@ def read_csv(filepath: str, **kwargs) -> pd.DataFrame: Reads a CSV file into a DataFrame with optional additional pandas read_csv parameters. Args: - filepath: The path to the CSV file to read. - **kwargs: Additional keyword arguments to pass to pandas.read_csv function. + filepath: The path to the CSV file to read. + **kwargs: Additional keyword arguments to pass to pandas.read_csv function. Returns: - A DataFrame containing the data read from the CSV file. + A DataFrame containing the data read from the CSV file. """ df = pd.read_csv(filepath, **kwargs) return convert.to_df(df) @@ -143,14 +142,14 @@ def remove_last_n_rows(df: pd.DataFrame, steps: int) -> pd.DataFrame: Removes the last 'n' rows from a DataFrame. Args: - df: The DataFrame from which to remove rows. - steps: The number of rows to remove from the end of the DataFrame. + df: The DataFrame from which to remove rows. + steps: The number of rows to remove from the end of the DataFrame. Returns: - A DataFrame with the last 'n' rows removed. + A DataFrame with the last 'n' rows removed. Raises: - ValueError: If 'steps' is negative or greater than the number of rows in the DataFrame. + ValueError: If 'steps' is negative or greater than the number of rows in the DataFrame. """ if steps < 0 or steps > len(df): @@ -166,17 +165,17 @@ def update_row(df: pd.DataFrame, row: str | int, column: str | int, value: Any) Updates a row's value for a specified column in a DataFrame. Args: - df: The DataFrame to update. - col: The column whose value is to be updated. - old_value: The current value to search for in the specified column. - new_value: The new value to replace the old value with. + df: The DataFrame to update. + col: The column whose value is to be updated. + old_value: The current value to search for in the specified column. + new_value: The new value to replace the old value with. Returns: - True if the update was successful, False otherwise. + True if the update was successful, False otherwise. """ try: df.loc[row, column] = value return True - except: + except Exception: return False diff --git a/lionagi/libs/ln_func_call.py b/lionagi/libs/ln_func_call.py index b489bb4a8..c8ee9882d 100644 --- a/lionagi/libs/ln_func_call.py +++ b/lionagi/libs/ln_func_call.py @@ -36,33 +36,33 @@ def lcall( function can be passed dynamically, allowing for flexible function application. Args: - input_ (Any): - The input list or iterable to process. each element will be passed to the - provided `func` Callable. - func (Callable): - The function to apply to each element of `input_`. this function can be any - Callable that accepts the elements of `input_` as arguments. - flatten (bool, optional): - If True, the resulting list is flattened. useful when `func` returns a list. - defaults to False. - dropna (bool, optional): - If True, None values are removed from the final list. defaults to False. - **kwargs: - Additional keyword arguments to be passed to `func`. + input_ (Any): + The input list or iterable to process. each element will be passed to the + provided `func` Callable. + func (Callable): + The function to apply to each element of `input_`. this function can be any + Callable that accepts the elements of `input_` as arguments. + flatten (bool, optional): + If True, the resulting list is flattened. useful when `func` returns a list. + defaults to False. + dropna (bool, optional): + If True, None values are removed from the final list. defaults to False. + **kwargs: + Additional keyword arguments to be passed to `func`. Returns: - list[Any]: - The list of results after applying `func` to each input element, modified - according to `flatten` and `dropna` options. + list[Any]: + The list of results after applying `func` to each input element, modified + according to `flatten` and `dropna` options. Examples: - Apply a doubling function to each element: - >>> lcall([1, 2, 3], lambda x: x * 2) - [2, 4, 6] + Apply a doubling function to each element: + >>> lcall([1, 2, 3], lambda x: x * 2) + [2, 4, 6] - apply a function that returns lists, then flatten the result: - >>> lcall([1, 2, None], lambda x: [x, x] if x else x, flatten=True, dropna=True) - [1, 1, 2, 2] + apply a function that returns lists, then flatten the result: + >>> lcall([1, 2, None], lambda x: [x, x] if x else x, flatten=True, dropna=True) + [1, 1, 2, 2] """ lst = to_list(input_, dropna=dropna) if len(to_list(func)) != 1: @@ -91,26 +91,26 @@ async def alcall( efficiency and overall execution time. Args: - input_ (Any, optional): - The input to process. defaults to None, which requires `func` to be capable of - handling the absence of explicit input. - func (Callable, optional): - The asynchronous function to apply. defaults to None. - flatten (bool, optional): - Whether to flatten the result. useful when `func` returns a list or iterable - that should be merged into a single list. defaults to False. - **kwargs: - Keyword arguments to pass to the function. + input_ (Any, optional): + The input to process. defaults to None, which requires `func` to be capable of + handling the absence of explicit input. + func (Callable, optional): + The asynchronous function to apply. defaults to None. + flatten (bool, optional): + Whether to flatten the result. useful when `func` returns a list or iterable + that should be merged into a single list. defaults to False. + **kwargs: + Keyword arguments to pass to the function. Returns: - list[Any]: - A list of results after asynchronously applying the function to each element - of the input, potentially flattened. + list[Any]: + A list of results after asynchronously applying the function to each element + of the input, potentially flattened. examples: - >>> async def square(x): return x * x - >>> await alcall([1, 2, 3], square) - [1, 4, 9] + >>> async def square(x): return x * x + >>> await alcall([1, 2, 3], square) + [1, 4, 9] """ tasks = [] if input_ is not None: @@ -123,7 +123,7 @@ async def alcall( outs = await AsyncUtil.execute_tasks(*tasks) outs_ = [] for i in outs: - outs_.append(i if not isinstance(i, (Coroutine, asyncio.Future)) else await i) + outs_.append(await i if isinstance(i, (Coroutine, asyncio.Future)) else i) return to_list(outs_, flatten=flatten, dropna=dropna) @@ -135,23 +135,23 @@ async def mcall( asynchronously map a function or functions over an input_ or inputs. Args: - input_ (Any): - The input_ or inputs to process. - func (Any): - The function or functions to apply. - explode (bool, optional): - Whether to apply each function to each input_. default is False. - **kwargs: - Keyword arguments to pass to the function. + input_ (Any): + The input_ or inputs to process. + func (Any): + The function or functions to apply. + explode (bool, optional): + Whether to apply each function to each input_. default is False. + **kwargs: + Keyword arguments to pass to the function. Returns: - list[Any]: A list of results after applying the function(s). + list[Any]: A list of results after applying the function(s). examples: - >>> async def add_one(x): - >>> return x + 1 - >>> asyncio.run(mcall([1, 2, 3], add_one)) - [2, 3, 4] + >>> async def add_one(x): + >>> return x + 1 + >>> asyncio.run(mcall([1, 2, 3], add_one)) + [2, 3, 4] """ inputs_ = to_list(input_, dropna=True) @@ -159,17 +159,16 @@ async def mcall( if explode: tasks = [_alcall(inputs_, f, flatten=True, **kwargs) for f in funcs_] - return await AsyncUtil.execute_tasks(*tasks) - else: - if len(inputs_) != len(funcs_): - raise ValueError( - "Inputs and functions must be the same length for map calling." - ) + elif len(inputs_) == len(funcs_): tasks = [ AsyncUtil.handle_async_sync(func, inp, **kwargs) for inp, func in zip(inputs_, funcs_) ] - return await AsyncUtil.execute_tasks(*tasks) + else: + raise ValueError( + "Inputs and functions must be the same length for map calling." + ) + return await AsyncUtil.execute_tasks(*tasks) async def bcall( @@ -179,18 +178,18 @@ async def bcall( asynchronously call a function on batches of inputs. Args: - input_ (Any): The input_ to process. - func (Callable): The function to apply. - batch_size (int): The size of each batch. - **kwargs: Keyword arguments to pass to the function. + input_ (Any): The input_ to process. + func (Callable): The function to apply. + batch_size (int): The size of each batch. + **kwargs: Keyword arguments to pass to the function. Returns: - list[Any]: A list of results after applying the function in batches. + list[Any]: A list of results after applying the function in batches. examples: - >>> async def sum_batch(batch_): return sum(batch_) - >>> asyncio.run(bcall([1, 2, 3, 4], sum_batch, batch_size=2)) - [3, 7] + >>> async def sum_batch(batch_): return sum(batch_) + >>> asyncio.run(bcall([1, 2, 3, 4], sum_batch, batch_size=2)) + [3, 7] """ results = [] input_ = to_list(input_) @@ -222,36 +221,36 @@ async def tcall( frame, or when monitoring execution duration. Args: - func (Callable): - The asynchronous function to be called. - *args: - Positional arguments to pass to the function. - delay (float, optional): - Time in seconds to wait before executing the function. default to 0. - err_msg (str | None, optional): - Custom error message to display if an error occurs. defaults to None. - ignore_err (bool, optional): - If True, suppresses any errors that occur during function execution, - optionally returning a default value. defaults to False. - timing (bool, optional): - If True, returns a tuple containing the result of the function and the - execution duration in seconds. defaults to False. - timeout (float | None, optional): - Maximum time in seconds allowed for the function execution. if the execution - exceeds this time, a timeout error is raised. defaults to None. - **kwargs: - Keyword arguments to pass to the function. + func (Callable): + The asynchronous function to be called. + *args: + Positional arguments to pass to the function. + delay (float, optional): + Time in seconds to wait before executing the function. default to 0. + err_msg (str | None, optional): + Custom error message to display if an error occurs. defaults to None. + ignore_err (bool, optional): + If True, suppresses any errors that occur during function execution, + optionally returning a default value. defaults to False. + timing (bool, optional): + If True, returns a tuple containing the result of the function and the + execution duration in seconds. defaults to False. + timeout (float | None, optional): + Maximum time in seconds allowed for the function execution. if the execution + exceeds this time, a timeout error is raised. defaults to None. + **kwargs: + Keyword arguments to pass to the function. Returns: - Any: - The result of the function call. if `timing` is True, returns a tuple of - (result, execution duration). + Any: + The result of the function call. if `timing` is True, returns a tuple of + (result, execution duration). examples: - >>> async def sample_function(x): - ... return x * x - >>> await tcall(sample_function, 3, delay=1, timing=True) - (9, execution_duration) + >>> async def sample_function(x): + ... return x * x + >>> await tcall(sample_function, 3, delay=1, timing=True) + (9, execution_duration) """ async def async_call() -> tuple[Any, float]: @@ -284,10 +283,7 @@ def handle_error(e: Exception): if not ignore_err: raise - if AsyncUtil.is_coroutine_func(func): - return await async_call() - else: - return sync_call() + return await async_call() if AsyncUtil.is_coroutine_func(func) else sync_call() async def rcall( @@ -310,44 +306,42 @@ async def rcall( persistent failures, and a backoff factor to control the delay increase. Args: - func (Callable): - The asynchronous function to retry. - *args: - Positional arguments for the function. - retries (int, optional): - The number of retry attempts before giving up. default to 0. - delay (float, optional): - Initial delay between retries in seconds. default to 1.0. - backoff_factor (float, optional): - Multiplier for the delay between retries, for exponential backoff. - default to 2.0. - default (Any, optional): - A value to return if all retries fail. defaults to None. - timeout (float | None, optional): - Maximum duration in seconds for each attempt. defaults to None. - **kwargs: - Keyword arguments for the function. + func (Callable): + The asynchronous function to retry. + *args: + Positional arguments for the function. + retries (int, optional): + The number of retry attempts before giving up. default to 0. + delay (float, optional): + Initial delay between retries in seconds. default to 1.0. + backoff_factor (float, optional): + Multiplier for the delay between retries, for exponential backoff. + default to 2.0. + default (Any, optional): + A value to return if all retries fail. defaults to None. + timeout (float | None, optional): + Maximum duration in seconds for each attempt. defaults to None. + **kwargs: + Keyword arguments for the function. Returns: - Any: - The result of the function call if successful within the retry attempts, - otherwise the `default` value if specified. + Any: + The result of the function call if successful within the retry attempts, + otherwise the `default` value if specified. examples: - >>> async def fetch_data(): - ... # Simulate a fetch operation that might fail - ... raise Exception("temporary error") - >>> await rcall(fetch_data, retries=3, delay=2, default="default value") - 'default value' + >>> async def fetch_data(): + ... # Simulate a fetch operation that might fail + ... raise Exception("temporary error") + >>> await rcall(fetch_data, retries=3, delay=2, default="default value") + 'default value' """ last_exception = None result = None for attempt in range(retries + 1) if retries == 0 else range(retries): try: - # Using tcall for each retry attempt with timeout and delay - result = await _tcall(func, *args, timeout=timeout, **kwargs) - return result + return await _tcall(func, *args, timeout=timeout, **kwargs) except Exception as e: last_exception = e if attempt < retries: @@ -368,14 +362,14 @@ def _dropna(lst_: list[Any]) -> list[Any]: Remove None values from a list. Args: - lst_ (list[Any]): A list potentially containing None values. + lst_ (list[Any]): A list potentially containing None values. Returns: - list[Any]: A list with None values removed. + list[Any]: A list with None values removed. Examples: - >>> _dropna([1, None, 3, None]) - [1, 3] + >>> _dropna([1, None, 3, None]) + [1, 3] """ return [item for item in lst_ if item is not None] @@ -387,18 +381,18 @@ async def _alcall( asynchronously apply a function to each element in the input_. Args: - input (Any): The input_ to process. - func (Callable): The function to apply. - flatten (bool, optional): Whether to flatten the result. default is False. - **kwargs: Keyword arguments to pass to the function. + input (Any): The input_ to process. + func (Callable): The function to apply. + flatten (bool, optional): Whether to flatten the result. default is False. + **kwargs: Keyword arguments to pass to the function. Returns: - list[Any]: A list of results after asynchronously applying the function. + list[Any]: A list of results after asynchronously applying the function. examples: - >>> async def square(x): return x * x - >>> asyncio.run(alcall([1, 2, 3], square)) - [1, 4, 9] + >>> async def square(x): return x * x + >>> asyncio.run(alcall([1, 2, 3], square)) + [1, 4, 9] """ lst = to_list(input_) tasks = [AsyncUtil.handle_async_sync(func, i, **kwargs) for i in lst] @@ -421,23 +415,23 @@ async def _tcall( asynchronously call a function with optional delay, timeout, and error handling. Args: - func (Callable): The function to call. - *args: Positional arguments to pass to the function. - delay (float): Delay before calling the function, in seconds. - err_msg (str | None): Custom error message. - ignore_err (bool): If True, ignore errors and return default. - timing (bool): If True, return a tuple (result, duration). - default (Any): Default value to return on error. - timeout (float | None): Timeout for the function call, in seconds. - **kwargs: Keyword arguments to pass to the function. + func (Callable): The function to call. + *args: Positional arguments to pass to the function. + delay (float): Delay before calling the function, in seconds. + err_msg (str | None): Custom error message. + ignore_err (bool): If True, ignore errors and return default. + timing (bool): If True, return a tuple (result, duration). + default (Any): Default value to return on error. + timeout (float | None): Timeout for the function call, in seconds. + **kwargs: Keyword arguments to pass to the function. Returns: - Any: The result of the function call, or (result, duration) if timing is True. + Any: The result of the function call, or (result, duration) if timing is True. examples: - >>> async def example_func(x): return x - >>> asyncio.run(tcall(example_func, 5, timing=True)) - (5, duration) + >>> async def example_func(x): return x + >>> asyncio.run(tcall(example_func, 5, timing=True)) + (5, duration) """ start_time = SysUtil.get_now(datetime_=False) try: @@ -515,21 +509,21 @@ def timeout(timeout: int) -> Callable: asyncio.TimeoutError if the execution time exceeds the specified timeout. Args: - timeout (int): - The maximum duration, in seconds, that the function is allowed to execute. + timeout (int): + The maximum duration, in seconds, that the function is allowed to execute. Returns: - Callable: - A decorated function that enforces the specified execution timeout. + Callable: + A decorated function that enforces the specified execution timeout. Examples: - >>> @CallDecorator.timeout(5) - ... async def long_running_task(): - ... # Implementation that may exceed the timeout duration - ... await asyncio.sleep(10) - ... return "Completed" - ... # Executing `long_running_task` will raise an asyncio.TimeoutError after 5 - ... # seconds + >>> @CallDecorator.timeout(5) + ... async def long_running_task(): + ... # Implementation that may exceed the timeout duration + ... await asyncio.sleep(10) + ... return "Completed" + ... # Executing `long_running_task` will raise an asyncio.TimeoutError after 5 + ... # seconds """ def decorator(func: Callable[..., Any]) -> Callable: @@ -556,26 +550,26 @@ def retry( mechanisms. Args: - retries (int, optional): - The number of retry attempts before giving up. Defaults to 3. - delay (float, optional): - The initial delay between retries, in seconds. Defaults to 2.0. - backoff_factor (float, optional): - The multiplier applied to the delay for each subsequent retry, for - exponential backoff. Default to 2.0. + retries (int, optional): + The number of retry attempts before giving up. Defaults to 3. + delay (float, optional): + The initial delay between retries, in seconds. Defaults to 2.0. + backoff_factor (float, optional): + The multiplier applied to the delay for each subsequent retry, for + exponential backoff. Default to 2.0. Returns: - Callable: - A decorated asynchronous function with retry logic based on the specified - parameters. + Callable: + A decorated asynchronous function with retry logic based on the specified + parameters. Examples: - >>> @CallDecorator.retry(retries=2, delay=1, backoff_factor=2) - ... async def fetch_data(): - ... # Implementation that might fail transiently - ... raise ConnectionError("Temporary failure") - ... # `fetch_data` will automatically retry on ConnectionError, up to 2 times, - ... # with delays of 1s and 2s. + >>> @CallDecorator.retry(retries=2, delay=1, backoff_factor=2) + ... async def fetch_data(): + ... # Implementation that might fail transiently + ... raise ConnectionError("Temporary failure") + ... # `fetch_data` will automatically retry on ConnectionError, up to 2 times, + ... # with delays of 1s and 2s. """ def decorator(func: Callable[..., Any]) -> Callable: @@ -607,21 +601,21 @@ def default(default_value: Any) -> Callable: value can prevent the application from crashing or halting due to minor errors. Args: - default_value (Any): - The value to return if the decorated function raises an exception. + default_value (Any): + The value to return if the decorated function raises an exception. Returns: - Callable: - A decorated asynchronous function that returns `default_value` in case of - error. + Callable: + A decorated asynchronous function that returns `default_value` in case of + error. Examples: - >>> @CallDecorator.default(default_value="Fetch failed") - ... async def get_resource(): - ... # Implementation that might raise an exception - ... raise RuntimeError("Resource not available") - ... # Executing `get_resource` will return "Fetch failed" instead of raising - ... # an error + >>> @CallDecorator.default(default_value="Fetch failed") + ... async def get_resource(): + ... # Implementation that might raise an exception + ... raise RuntimeError("Resource not available") + ... # Executing `get_resource` will return "Fetch failed" instead of raising + ... # an error """ def decorator(func: Callable[..., Any]) -> Callable: @@ -646,21 +640,21 @@ def throttle(period: int) -> Callable: function does not execute more frequently than the allowed rate. Args: - period (int): - The minimum time interval, in seconds, between consecutive calls to the - decorated function. + period (int): + The minimum time interval, in seconds, between consecutive calls to the + decorated function. Returns: - Callable: - A decorated asynchronous function that adheres to the specified call - frequency limit. + Callable: + A decorated asynchronous function that adheres to the specified call + frequency limit. Examples: - >>> @CallDecorator.throttle(2) - ... async def fetch_data(): - ... # Implementation that fetches data from an external source - ... pass - ... # `fetch_data` will not be called more often than once every 2 seconds. + >>> @CallDecorator.throttle(2) + ... async def fetch_data(): + ... # Implementation that fetches data from an external source + ... pass + ... # `fetch_data` will not be called more often than once every 2 seconds. """ return Throttle(period) @@ -678,21 +672,21 @@ def map(function: Callable[[Any], Any]) -> Callable: succinctly applied to a collection of asynchronous results. Args: - function (Callable[[Any], Any]): - A mapping function to apply to each element of the list returned by the - decorated function. + function (Callable[[Any], Any]): + A mapping function to apply to each element of the list returned by the + decorated function. Returns: - Callable: - A decorated asynchronous function whose results are transformed by the - specified mapping function. + Callable: + A decorated asynchronous function whose results are transformed by the + specified mapping function. Examples: - >>> @CallDecorator.map(lambda x: x.upper()) - ... async def get_names(): - ... # Asynchronously fetches a list of names - ... return ["alice", "bob", "charlie"] - ... # `get_names` now returns ["ALICE", "BOB", "CHARLIE"] + >>> @CallDecorator.map(lambda x: x.upper()) + ... async def get_names(): + ... # Asynchronously fetches a list of names + ... return ["alice", "bob", "charlie"] + ... # `get_names` now returns ["ALICE", "BOB", "CHARLIE"] """ def decorator(func: Callable[..., list[Any]]) -> Callable: @@ -729,28 +723,28 @@ def compose(*functions: Callable[[Any], Any]) -> Callable: all asynchronous). Args: - *functions (Callable[[Any], Any]): - A variable number of functions that are to be composed together. Each - function must accept a single argument and return a value. + *functions (Callable[[Any], Any]): + A variable number of functions that are to be composed together. Each + function must accept a single argument and return a value. Returns: - Callable: - A decorator that, when applied to a function, composes it with the - specified functions, creating a pipeline of function calls. + Callable: + A decorator that, when applied to a function, composes it with the + specified functions, creating a pipeline of function calls. Raises: - ValueError: - If the provided functions mix synchronous and asynchronous types, as they - cannot be composed together. + ValueError: + If the provided functions mix synchronous and asynchronous types, as they + cannot be composed together. Examples: - >>> def double(x): return x * 2 - >>> def increment(x): return x + 1 - >>> @CallDecorator.compose(increment, double) - ... def start_value(x): - ... return x - >>> start_value(3) - 7 # The value is doubled to 6, then incremented to 7 + >>> def double(x): return x * 2 + >>> def increment(x): return x + 1 + >>> @CallDecorator.compose(increment, double) + ... def start_value(x): + ... return x + >>> start_value(3) + 7 # The value is doubled to 6, then incremented to 7 """ def decorator(func: Callable) -> Callable: @@ -807,24 +801,24 @@ def pre_post_process( function's output, such as formatting results or applying additional computations. Args: - preprocess (Callable[..., Any]): - A function to preprocess the arguments passed to the decorated function. - It must accept the same arguments as the decorated function. - postprocess (Callable[..., Any]): - A function to postprocess the result of the decorated function. It must - accept a single argument, which is the output of the decorated function. + preprocess (Callable[..., Any]): + A function to preprocess the arguments passed to the decorated function. + It must accept the same arguments as the decorated function. + postprocess (Callable[..., Any]): + A function to postprocess the result of the decorated function. It must + accept a single argument, which is the output of the decorated function. Returns: - Callable: - A decorated function that applies the specified preprocessing and - postprocessing steps to its execution. + Callable: + A decorated function that applies the specified preprocessing and + postprocessing steps to its execution. Examples: - >>> @CallDecorator.pre_post_process(lambda x: x - 1, lambda x: x * 2) - ... async def process_value(x): - ... return x + 2 - >>> asyncio.run(process_value(5)) - 12 # Input 5 is preprocessed to 4, processed to 6, and postprocessed to 12 + >>> @CallDecorator.pre_post_process(lambda x: x - 1, lambda x: x * 2) + ... async def process_value(x): + ... return x + 2 + >>> asyncio.run(process_value(5)) + 12 # Input 5 is preprocessed to 4, processed to 6, and postprocessed to 12 """ def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @@ -866,16 +860,16 @@ def cache(func: Callable, ttl=600, maxsize=None) -> Callable: synchronous functions. Returns: - Callable: A decorated version of the function with caching applied. Subsequent - calls with the same arguments within the TTL will return the cached result. + Callable: A decorated version of the function with caching applied. Subsequent + calls with the same arguments within the TTL will return the cached result. Examples: - >>> @CallDecorator.cache(ttl=10) - ... async def fetch_data(key): - ... # Simulate a database fetch - ... return "data for " + key - ... # Subsequent calls to `fetch_data` with the same `key` within 10 seconds - ... # will return the cached result without re-executing the function body. + >>> @CallDecorator.cache(ttl=10) + ... async def fetch_data(key): + ... # Simulate a database fetch + ... return "data for " + key + ... # Subsequent calls to `fetch_data` with the same `key` within 10 seconds + ... # will return the cached result without re-executing the function body. """ if AsyncUtil.is_coroutine_func(func): @@ -911,21 +905,21 @@ def filter(predicate: Callable[[Any], bool]) -> Callable: functions returning lists. Args: - predicate (Callable[[Any], bool]): - A function that evaluates each input_ in the list. Items for which the - predicate returns True are included in the final result. + predicate (Callable[[Any], bool]): + A function that evaluates each input_ in the list. Items for which the + predicate returns True are included in the final result. Returns: - Callable: - A decorated function that filters its list result according to the predicate. + Callable: + A decorated function that filters its list result according to the predicate. Examples: - >>> @CallDecorator.filter(lambda x: x % 2 == 0) - ... async def get_even_numbers(): - ... return [1, 2, 3, 4, 5] - >>> asyncio.run(get_even_numbers()) - [2, 4] - ... # The result list is filtered to include only even numbers. + >>> @CallDecorator.filter(lambda x: x % 2 == 0) + ... async def get_even_numbers(): + ... return [1, 2, 3, 4, 5] + >>> asyncio.run(get_even_numbers()) + [2, 4] + ... # The result list is filtered to include only even numbers. """ def decorator(func: Callable[..., list[Any]]) -> Callable: @@ -958,27 +952,27 @@ def reduce(function: Callable[[Any, Any], Any], initial: Any) -> Callable: synchronous and asynchronous functions. Args: - function (Callable[[Any, Any], Any]): - The reduction function to apply to the list. It should take two arguments - and return a single value that is the result of combining them. - initial (Any): - The initial value for the reduction process. This value is used as the - starting point for the reduction and should be an identity value for the - reduction operation. + function (Callable[[Any, Any], Any]): + The reduction function to apply to the list. It should take two arguments + and return a single value that is the result of combining them. + initial (Any): + The initial value for the reduction process. This value is used as the + starting point for the reduction and should be an identity value for the + reduction operation. Returns: - Callable: - A decorated function that applies the specified reduction to its list - result, - producing a single aggregated value. + Callable: + A decorated function that applies the specified reduction to its list + result, + producing a single aggregated value. Examples: - >>> @CallDecorator.reduce(lambda x, y: x + y, 0) - ... async def sum_numbers(): - ... return [1, 2, 3, 4] - >>> asyncio.run(sum_numbers()) - 10 - ... # The numbers in the list are summed, resulting in a single value. + >>> @CallDecorator.reduce(lambda x, y: x + y, 0) + ... async def sum_numbers(): + ... return [1, 2, 3, 4] + >>> asyncio.run(sum_numbers()) + 10 + ... # The numbers in the list are summed, resulting in a single value. """ def decorator(func: Callable[..., list[Any]]) -> Callable: @@ -1011,20 +1005,20 @@ def max_concurrency(limit: int = 5) -> Callable: that can only handle a limited amount of concurrent requests. Args: - limit (int): - The maximum number of concurrent executions allowed for the decorated - function. + limit (int): + The maximum number of concurrent executions allowed for the decorated + function. Returns: - Callable: - An asynchronous function wrapper that enforces the concurrency limit. + Callable: + An asynchronous function wrapper that enforces the concurrency limit. Examples: - >>> @CallDecorator.max_concurrency(3) - ... async def process_data(input_): - ... # Asynchronous processing logic here - ... pass - ... # No more than 3 instances of `process_data` will run concurrently. + >>> @CallDecorator.max_concurrency(3) + ... async def process_data(input_): + ... # Asynchronous processing logic here + ... pass + ... # No more than 3 instances of `process_data` will run concurrently. """ def decorator(func: Callable) -> Callable: @@ -1092,11 +1086,11 @@ class Throttle: this constraint. Attributes: - period (int): The minimum time period (in seconds) between successive calls. + period (int): The minimum time period (in seconds) between successive calls. Methods: - __call__: Decorates a synchronous function with throttling. - __call_async__: Decorates an asynchronous function with throttling. + __call__: Decorates a synchronous function with throttling. + __call_async__: Decorates an asynchronous function with throttling. """ def __init__(self, period: int) -> None: @@ -1104,7 +1098,7 @@ def __init__(self, period: int) -> None: Initializes a new instance of _Throttle. Args: - period (int): The minimum time period (in seconds) between successive calls. + period (int): The minimum time period (in seconds) between successive calls. """ self.period = period self.last_called = 0 @@ -1114,10 +1108,10 @@ def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: Decorates a synchronous function with the throttling mechanism. Args: - func (Callable[..., Any]): The synchronous function to be throttled. + func (Callable[..., Any]): The synchronous function to be throttled. Returns: - Callable[..., Any]: The throttled synchronous function. + Callable[..., Any]: The throttled synchronous function. """ @functools.wraps(func) @@ -1135,10 +1129,10 @@ async def __call_async__(self, func: Callable[..., Any]) -> Callable[..., Any]: Decorates an asynchronous function with the throttling mechanism. Args: - func (Callable[..., Any]): The asynchronous function to be throttled. + func (Callable[..., Any]): The asynchronous function to be throttled. Returns: - Callable[..., Any]: The throttled asynchronous function. + Callable[..., Any]: The throttled asynchronous function. """ @functools.wraps(func) @@ -1158,18 +1152,17 @@ def _custom_error_handler(error: Exception, error_map: dict[type, Callable]) -> handle errors based on a given error mapping. Args: - error (Exception): - The error to handle. - error_map (Dict[type, Callable]): - A dictionary mapping error types to handler functions. + error (Exception): + The error to handle. + error_map (Dict[type, Callable]): + A dictionary mapping error types to handler functions. examples: - >>> def handle_value_error(e): print("ValueError occurred") - >>> custom_error_handler(ValueError(), {ValueError: handle_value_error}) - ValueError occurred + >>> def handle_value_error(e): print("ValueError occurred") + >>> custom_error_handler(ValueError(), {ValueError: handle_value_error}) + ValueError occurred """ - handler = error_map.get(type(error)) - if handler: + if handler := error_map.get(type(error)): handler(error) else: logging.error(f"Unhandled error: {error}") @@ -1183,44 +1176,43 @@ async def call_handler( functions. Args: - func (Callable): - The function to call. - *args: - Positional arguments to pass to the function. - error_map (Dict[type, Callable], optional): - A dictionary mapping error types to handler functions. - **kwargs: - Keyword arguments to pass to the function. + func (Callable): + The function to call. + *args: + Positional arguments to pass to the function. + error_map (Dict[type, Callable], optional): + A dictionary mapping error types to handler functions. + **kwargs: + Keyword arguments to pass to the function. Returns: - Any: The result of the function call. + Any: The result of the function call. Raises: - Exception: Propagates any exceptions not handled by the error_map. + Exception: Propagates any exceptions not handled by the error_map. examples: - >>> async def async_add(x, y): return x + y - >>> asyncio.run(_call_handler(async_add, 1, 2)) - 3 + >>> async def async_add(x, y): return x + y + >>> asyncio.run(_call_handler(async_add, 1, 2)) + 3 """ try: - if is_coroutine_func(func): - # Checking for a running event loop - try: - loop = asyncio.get_running_loop() - except RuntimeError: # No running event loop - loop = asyncio.new_event_loop() - result = loop.run_until_complete(func(*args, **kwargs)) - - loop.close() - return result - - if loop.is_running(): - return await func(*args, **kwargs) - - else: + if not is_coroutine_func(func): return func(*args, **kwargs) + # Checking for a running event loop + try: + loop = asyncio.get_running_loop() + except RuntimeError: # No running event loop + loop = asyncio.new_event_loop() + result = loop.run_until_complete(func(*args, **kwargs)) + + loop.close() + return result + + if loop.is_running(): + return await func(*args, **kwargs) + except Exception as e: if error_map: _custom_error_handler(e, error_map) @@ -1241,20 +1233,20 @@ def is_coroutine_func(func: Callable) -> bool: asynchronous codebases correctly. Args: - func (Callable): - The function to check for coroutine compatibility. + func (Callable): + The function to check for coroutine compatibility. Returns: - bool: - True if `func` is an asyncio coroutine function, False otherwise. this - determination is based on whether the function is defined with `async def`. + bool: + True if `func` is an asyncio coroutine function, False otherwise. this + determination is based on whether the function is defined with `async def`. examples: - >>> async def async_func(): pass - >>> def sync_func(): pass - >>> is_coroutine_func(async_func) - True - >>> is_coroutine_func(sync_func) - False + >>> async def async_func(): pass + >>> def sync_func(): pass + >>> is_coroutine_func(async_func) + True + >>> is_coroutine_func(sync_func) + False """ return asyncio.iscoroutinefunction(func) diff --git a/lionagi/libs/ln_nested.py b/lionagi/libs/ln_nested.py index dae99a2e6..c84ec0161 100644 --- a/lionagi/libs/ln_nested.py +++ b/lionagi/libs/ln_nested.py @@ -25,13 +25,13 @@ def nset(nested_structure: dict | list, indices: list[int | str], value: Any) -> incorrect. Examples: - >>> data = {'a': {'b': [10, 20]}} - >>> nset(data, ['a', 'b', 1], 99) - >>> assert data == {'a': {'b': [10, 99]}} + >>> data = {'a': {'b': [10, 20]}} + >>> nset(data, ['a', 'b', 1], 99) + >>> assert data == {'a': {'b': [10, 99]}} - >>> data = [0, [1, 2], 3] - >>> nset(data, [1, 1], 99) - >>> assert data == [0, [1, 99], 3] + >>> data = [0, [1, 2], 3] + >>> nset(data, [1, 1], 99) + >>> assert data == [0, [1, 99], 3] """ if not indices: raise ValueError("Indices list is empty, cannot determine target container") @@ -77,12 +77,12 @@ def nget( and no default value is provided. Examples: - >>> data = {'a': {'b': [10, 20]}} - >>> assert nget(data, ['a', 'b', 1]) == 20 - >>> nget(data, ['a', 'b', 2]) - Traceback (most recent call last): - ... - LookupError: Target not found and no default value provided. + >>> data = {'a': {'b': [10, 20]}} + >>> assert nget(data, ['a', 'b', 1]) == 20 + >>> nget(data, ['a', 'b', 2]) + Traceback (most recent call last): + ... + LookupError: Target not found and no default value provided. """ try: @@ -140,20 +140,20 @@ def nmerge( that defines custom sorting logic for the merged list. Returns: - A merged dictionary or list, - depending on the types present in `iterables`. + A merged dictionary or list, + depending on the types present in `iterables`. Raises: - TypeError: - If `iterables` - contains objects of incompatible types that cannot be merged. + TypeError: + If `iterables` + contains objects of incompatible types that cannot be merged. examples: - >>> nmerge([{'a': 1}, {'b': 2}], overwrite=True) - {'a': 1, 'b': 2} + >>> nmerge([{'a': 1}, {'b': 2}], overwrite=True) + {'a': 1, 'b': 2} - >>> nmerge([[1, 2], [3, 4]], sort_list=True) - [1, 2, 3, 4] + >>> nmerge([[1, 2], [3, 4]], sort_list=True) + [1, 2, 3, 4] """ if convert.is_homogeneous(nested_structure, dict): return _merge_dicts( @@ -196,19 +196,19 @@ def flatten( True, only flattens nested dictionaries, leaving lists intact. Returns: - A flattened dictionary, or None if `inplace` is True. + A flattened dictionary, or None if `inplace` is True. Raises: - ValueError: If `inplace` is True but `nested_structure` is not a dictionary. + ValueError: If `inplace` is True but `nested_structure` is not a dictionary. examples: - >>> nested_dict = {'a': {'b': {'c': 1}}} - >>> flatten(nested_dict) - {'a_b_c': 1} + >>> nested_dict = {'a': {'b': {'c': 1}}} + >>> flatten(nested_dict) + {'a_b_c': 1} - >>> nested_list = [{'a': 1}, {'b': 2}] - >>> flatten(nested_list) - {'0_a': 1, '1_b': 2} + >>> nested_list = [{'a': 1}, {'b': 2}] + >>> flatten(nested_list) + {'0_a': 1, '1_b': 2} """ if inplace: if not isinstance(nested_structure, dict): @@ -250,22 +250,22 @@ def unflatten( keys and can limit the reconstruction depth. Args: - flat_dict: A flat dictionary with composite keys to unflatten. - sep: The sep used in composite keys, indicating nested levels. - custom_logic: An optional function to process each part of the composite keys. - max_depth: The maximum depth for nesting during reconstruction. + flat_dict: A flat dictionary with composite keys to unflatten. + sep: The sep used in composite keys, indicating nested levels. + custom_logic: An optional function to process each part of the composite keys. + max_depth: The maximum depth for nesting during reconstruction. Returns: - The reconstructed nested dictionary or list. + The reconstructed nested dictionary or list. examples: - >>> flat_dict_ = {'a_b_c': 1} - >>> unflatten(flat_dict_) - {'a': {'b': {'c': 1}}} + >>> flat_dict_ = {'a_b_c': 1} + >>> unflatten(flat_dict_) + {'a': {'b': {'c': 1}}} - >>> flat_dict_ = {'0_a': 1, '1_b': 2} - >>> unflatten(flat_dict_) - [{'a': 1}, {'b': 2}] + >>> flat_dict_ = {'0_a': 1, '1_b': 2} + >>> unflatten(flat_dict_) + [{'a': 1}, {'b': 2}] """ unflattened = {} for composite_key, value in flat_dict.items(): @@ -285,9 +285,7 @@ def unflatten( ): max_index = max(unflattened.keys(), default=-1) return [unflattened.get(i) for i in range(max_index + 1)] - if not unflattened: - return {} - return unflattened + return unflattened or {} def nfilter( @@ -310,13 +308,13 @@ def nfilter( containing only items that meet the condition. Raises: - TypeError: Raised if `collection` is not a dictionary or a list. + TypeError: Raised if `collection` is not a dictionary or a list. Examples: - >>> nfilter({'a': 1, 'b': 2, 'c': 3}, lambda x: x[1] > 1) - {'b': 2, 'c': 3} - >>> nfilter([1, 2, 3, 4], lambda x: x % 2 == 0) - [2, 4] + >>> nfilter({'a': 1, 'b': 2, 'c': 3}, lambda x: x[1] > 1) + {'b': 2, 'c': 3} + >>> nfilter([1, 2, 3, 4], lambda x: x % 2 == 0) + [2, 4] """ if isinstance(nested_structure, dict): return _filter_dict(nested_structure, condition) @@ -353,13 +351,13 @@ def ninsert( depth during recursive calls. Examples: - >>> subject_ = {'a': {'b': [1, 2]}} - >>> ninsert(subject_, ['a', 'b', 2], 3) - >>> assert subject_ == {'a': {'b': [1, 2, 3]}} + >>> subject_ = {'a': {'b': [1, 2]}} + >>> ninsert(subject_, ['a', 'b', 2], 3) + >>> assert subject_ == {'a': {'b': [1, 2, 3]}} - >>> subject_ = [] - >>> ninsert(subject_, [0, 'a'], 1) - >>> assert subject_ == [{'a': 1}] + >>> subject_ = [] + >>> ninsert(subject_, [0, 'a'], 1) + >>> assert subject_ == [{'a': 1}] """ indices = convert.to_list(indices) parts_len = len(indices) @@ -376,12 +374,10 @@ def ninsert( ): next_part = indices[i + 1] nested_structure[part] = [] if isinstance(next_part, int) else {} - nested_structure = nested_structure[part] - else: - if part not in nested_structure: - next_part = indices[i + 1] - nested_structure[part] = [] if isinstance(next_part, int) else {} - nested_structure = nested_structure[part] + elif part not in nested_structure: + next_part = indices[i + 1] + nested_structure[part] = [] if isinstance(next_part, int) else {} + nested_structure = nested_structure[part] current_depth += 1 parts_depth += 1 @@ -391,11 +387,10 @@ def ninsert( last_part = indices[-1] if isinstance(last_part, int): _handle_list_insert(nested_structure, last_part, value) + elif isinstance(nested_structure, list): + nested_structure.append({last_part: value}) else: - if isinstance(nested_structure, list): - nested_structure.append({last_part: value}) - else: - nested_structure[last_part] = value + nested_structure[last_part] = value # noinspection PyDecorator @@ -423,32 +418,29 @@ def get_flattened_keys( modifying the original object. Returns: - A list of strings representing the keys in the flattened structure. + A list of strings representing the keys in the flattened structure. Raises: - ValueError: If `inplace` is True but `nested_structure` is not a dictionary. + ValueError: If `inplace` is True but `nested_structure` is not a dictionary. Examples: - >>> nested_dict = {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}} - >>> keys = get_flattened_keys(nested_dict) - >>> assert keys == ['a', 'b_c', 'b_d_e'] + >>> nested_dict = {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}} + >>> keys = get_flattened_keys(nested_dict) + >>> assert keys == ['a', 'b_c', 'b_d_e'] - >>> nested_list = [{'a': 1}, {'b': 2}] - >>> keys = get_flattened_keys(nested_list) - >>> assert keys == ['0_a', '1_b'] + >>> nested_list = [{'a': 1}, {'b': 2}] + >>> keys = get_flattened_keys(nested_list) + >>> assert keys == ['0_a', '1_b'] """ - if inplace: - obj_copy = SysUtil.create_copy(nested_structure, num=1) - flatten( - obj_copy, sep=sep, max_depth=max_depth, inplace=True, dict_only=dict_only - ) - return convert.to_list(obj_copy.keys()) - else: + if not inplace: return convert.to_list( flatten( nested_structure, sep=sep, max_depth=max_depth, dict_only=dict_only ).keys() ) + obj_copy = SysUtil.create_copy(nested_structure, num=1) + flatten(obj_copy, sep=sep, max_depth=max_depth, inplace=True, dict_only=dict_only) + return convert.to_list(obj_copy.keys()) def _dynamic_flatten_in_place( @@ -469,19 +461,19 @@ def _dynamic_flatten_in_place( dictionaries and to a certain depth. Args: - nested_structure: The structure to flatten. - parent_key: Initial key prefix for all keys in the flattened structure. - sep: Separator for nested keys. - max_depth: Limits the flattening to a specific depth. - current_depth: Tracks the current depth in the recursion. - dict_only: Limits the flattening to dictionaries only, ignoring lists. + nested_structure: The structure to flatten. + parent_key: Initial key prefix for all keys in the flattened structure. + sep: Separator for nested keys. + max_depth: Limits the flattening to a specific depth. + current_depth: Tracks the current depth in the recursion. + dict_only: Limits the flattening to dictionaries only, ignoring lists. Note: - This function modifies `nested_structure` in place. + This function modifies `nested_structure` in place. Examples: - Given a nested dictionary `nested_dict` with the appropriate structure, - `_dynamic_flatten_in_place(nested_dict)` will modify it to a flattened form. + Given a nested dictionary `nested_dict` with the appropriate structure, + `_dynamic_flatten_in_place(nested_dict)` will modify it to a flattened form. """ if isinstance(nested_structure, dict): keys_to_delete = [] @@ -525,12 +517,12 @@ def _handle_list_insert(nested_structure: list, part: int, value: Any) -> None: `None` values up to the index, then the specified value is inserted. Args: - nested_structure: The list to modify. - part: The target index for inserting or replacing the value. - value: The value to be inserted or to replace an existing value in the list. + nested_structure: The list to modify. + part: The target index for inserting or replacing the value. + value: The value to be inserted or to replace an existing value in the list. Note: - This function directly modifies the input list in place. + This function directly modifies the input list in place. """ while len(nested_structure) <= part: nested_structure.append(None) @@ -547,9 +539,9 @@ def _ensure_list_index(lst_: list, index: int, default: Any = None) -> None: with a specified default value until it reaches the required length. Args: - lst_: The list to extend. - index: The target index that the list should reach or exceed. - default: The value to append to the list for extension. Defaults to None. + lst_: The list to extend. + index: The target index that the list should reach or exceed. + default: The value to append to the list for extension. Defaults to None. Note: Modifies the list in place, ensuring it can safely be indexed at `index` without raising an IndexError. @@ -569,14 +561,14 @@ def _deep_update(original: dict, update: dict) -> dict: the key-value pair to `original`. Args: - original: The dictionary to update. - update: The dictionary containing updates to apply to `original`. + original: The dictionary to update. + update: The dictionary containing updates to apply to `original`. Returns: - The `original` dictionary after applying updates from `update`. + The `original` dictionary after applying updates from `update`. Note: - This method modifies the `original` dictionary in place. + This method modifies the `original` dictionary in place. """ for key, value in update.items(): if isinstance(value, dict) and key in original: @@ -646,14 +638,14 @@ def _deep_merge_dicts(dict1: dict, dict2: dict) -> dict: pairs from `dict2`. Args: - dict1: The target dictionary to update with values from `dict2`. - dict2: The source dictionary providing updates and additional key-value pairs. + dict1: The target dictionary to update with values from `dict2`. + dict2: The source dictionary providing updates and additional key-value pairs. Returns: - The updated dictionary `dict1` with deeply merged values from `dict2`. + The updated dictionary `dict1` with deeply merged values from `dict2`. Note: - Modifies `dict1` in place, reflecting merged changes from `dict2`. + Modifies `dict1` in place, reflecting merged changes from `dict2`. """ for key in dict2: if key in dict1: @@ -712,9 +704,9 @@ def _merge_sequences( mechanism or a custom sorting function provided by the user. Args: - iterables: A collection of iterable sequences to be merged. - sort_list: Determines whether to sort the merged list. - custom_sort: Optional. A function defining custom sort criteria. + iterables: A collection of iterable sequences to be merged. + sort_list: Determines whether to sort the merged list. + custom_sort: Optional. A function defining custom sort criteria. Returns: list[Any]: The merged (and potentially sorted) list of elements from all provided iterables. @@ -765,7 +757,7 @@ def _filter_list(lst: list[Any], condition: Callable[[Any], bool]) -> list[Any]: the filtered list. Returns: - list[Any]: A new list comprising elements that meet the condition. + list[Any]: A new list comprising elements that meet the condition. """ return [item for item in lst if condition(item)] diff --git a/lionagi/libs/ln_parse.py b/lionagi/libs/ln_parse.py index b167aef3f..10721507f 100644 --- a/lionagi/libs/ln_parse.py +++ b/lionagi/libs/ln_parse.py @@ -1,11 +1,11 @@ import re import inspect +import itertools from collections.abc import Callable from typing import Any import numpy as np import lionagi.libs.ln_convert as convert - md_json_char_map = {"\n": "\\n", "\r": "\\r", "\t": "\\t", '"': '\\"'} @@ -20,35 +20,35 @@ def fuzzy_parse_json(str_to_parse: str, *, strict: bool = False): the string by appending necessary closing characters before retrying. Args: - s (str): The JSON string to parse. - strict (bool, optional): If True, enforces strict JSON syntax. Defaults to False. + s (str): The JSON string to parse. + strict (bool, optional): If True, enforces strict JSON syntax. Defaults to False. Returns: - The parsed JSON object, typically a dictionary or list. + The parsed JSON object, typically a dictionary or list. Raises: - ValueError: If parsing fails even after attempting to correct the string. + ValueError: If parsing fails even after attempting to correct the string. Example: - >>> fuzzy_parse_json('{"name": "John", "age": 30, "city": "New York"') - {'name': 'John', 'age': 30, 'city': 'New York'} + >>> fuzzy_parse_json('{"name": "John", "age": 30, "city": "New York"') + {'name': 'John', 'age': 30, 'city': 'New York'} """ try: return convert.to_dict(str_to_parse, strict=strict) - except: + except Exception: fixed_s = ParseUtil.fix_json_string(str_to_parse) try: return convert.to_dict(fixed_s, strict=strict) - - except: + + except Exception: try: - fixed_s = fixed_s.replace('\'', '\"') + fixed_s = fixed_s.replace("'", '"') return convert.to_dict(fixed_s, strict=strict) - + except Exception as e: raise ValueError( f"Failed to parse JSON even after fixing attempts: {e}" - ) + ) from e @staticmethod def fix_json_string(str_to_parse: str) -> str: @@ -76,17 +76,17 @@ def escape_chars_in_json(value: str, char_map=None) -> str: a default mapping is used. Args: - value: The string to be escaped. - char_map: An optional dictionary mapping characters to their escaped versions. - If not provided, a default mapping that escapes newlines, carriage returns, - tabs, and double quotes is used. + value: The string to be escaped. + char_map: An optional dictionary mapping characters to their escaped versions. + If not provided, a default mapping that escapes newlines, carriage returns, + tabs, and double quotes is used. Returns: - The escaped JSON string. + The escaped JSON string. Examples: - >>> escape_chars_in_json('Line 1\nLine 2') - 'Line 1\\nLine 2' + >>> escape_chars_in_json('Line 1\nLine 2') + 'Line 1\\nLine 2' """ def replacement(match): @@ -114,22 +114,22 @@ def extract_code_block( filtered by language. If a code block is found, it is parsed using the provided parser function. Args: - str_to_parse: The Markdown content to search. - language: An optional language specifier for the code block. If provided, - only code blocks of this language are considered. - regex_pattern: An optional regular expression pattern to use for finding the code block. - If provided, it overrides the language parameter. - parser: A function to parse the extracted code block string. + str_to_parse: The Markdown content to search. + language: An optional language specifier for the code block. If provided, + only code blocks of this language are considered. + regex_pattern: An optional regular expression pattern to use for finding the code block. + If provided, it overrides the language parameter. + parser: A function to parse the extracted code block string. Returns: - The result of parsing the code block with the provided parser function. + The result of parsing the code block with the provided parser function. Raises: - ValueError: If no code block is found in the Markdown content. + ValueError: If no code block is found in the Markdown content. Examples: - >>> extract_code_block('```python\\nprint("Hello, world!")\\n```', language='python', parser=lambda x: x) - 'print("Hello, world!")' + >>> extract_code_block('```python\\nprint("Hello, world!")\\n```', language='python', parser=lambda x: x) + 'print("Hello, world!")' """ if language: @@ -140,7 +140,7 @@ def extract_code_block( match = re.search(regex_pattern, str_to_parse, re.DOTALL) code_str = "" if match: - code_str = match.group(1).strip() + code_str = match[1].strip() else: raise ValueError( f"No {language or 'specified'} code block found in the Markdown content." @@ -162,29 +162,28 @@ def md_to_json( Markdown string. It then optionally verifies that the parsed JSON object contains all expected keys. Args: - str_to_parse: The Markdown content to parse. - expected_keys: An optional list of keys expected to be present in the parsed JSON object. - parser: An optional function to parse the extracted code block. If not provided, - `fuzzy_parse_json` is used with default settings. + str_to_parse: The Markdown content to parse. + expected_keys: An optional list of keys expected to be present in the parsed JSON object. + parser: An optional function to parse the extracted code block. If not provided, + `fuzzy_parse_json` is used with default settings. Returns: - The parsed JSON object from the Markdown content. + The parsed JSON object from the Markdown content. Raises: - ValueError: If the JSON code block is missing, or if any of the expected keys are missing - from the parsed JSON object. + ValueError: If the JSON code block is missing, or if any of the expected keys are missing + from the parsed JSON object. Examples: - >>> md_to_json('```json\\n{"key": "value"}\\n```', expected_keys=['key']) - {'key': 'value'} + >>> md_to_json('```json\\n{"key": "value"}\\n```', expected_keys=['key']) + {'key': 'value'} """ json_obj = ParseUtil.extract_code_block( str_to_parse, language="json", parser=parser or ParseUtil.fuzzy_parse_json ) if expected_keys: - missing_keys = [key for key in expected_keys if key not in json_obj] - if missing_keys: + if missing_keys := [key for key in expected_keys if key not in json_obj]: raise ValueError( f"Missing expected keys in JSON object: {', '.join(missing_keys)}" ) @@ -198,26 +197,26 @@ def _extract_docstring_details_google(func): docstring following the Google style format. Args: - func (Callable): The function from which to extract docstring details. + func (Callable): The function from which to extract docstring details. Returns: - Tuple[str, Dict[str, str]]: A tuple containing the function description - and a dictionary with parameter names as keys and their descriptions as values. + Tuple[str, Dict[str, str]]: A tuple containing the function description + and a dictionary with parameter names as keys and their descriptions as values. Examples: - >>> def example_function(param1: int, param2: str): - ... '''Example function. - ... - ... Args: - ... param1 (int): The first parameter. - ... param2 (str): The second parameter. - ... ''' - ... pass - >>> description, params = _extract_docstring_details_google(example_function) - >>> description - 'Example function.' - >>> params == {'param1': 'The first parameter.', 'param2': 'The second parameter.'} - True + >>> def example_function(param1: int, param2: str): + ... '''Example function. + ... + ... Args: + ... param1 (int): The first parameter. + ... param2 (str): The second parameter. + ... ''' + ... pass + >>> description, params = _extract_docstring_details_google(example_function) + >>> description + 'Example function.' + >>> params == {'param1': 'The first parameter.', 'param2': 'The second parameter.'} + True """ docstring = inspect.getdoc(func) if not docstring: @@ -225,19 +224,21 @@ def _extract_docstring_details_google(func): lines = docstring.split("\n") func_description = lines[0].strip() - param_start_pos = 0 lines_len = len(lines) params_description = {} - for i in range(1, lines_len): - if ( - lines[i].startswith("Args") - or lines[i].startswith("Arguments") - or lines[i].startswith("Parameters") - ): - param_start_pos = i + 1 - break - + param_start_pos = next( + ( + i + 1 + for i in range(1, lines_len) + if ( + lines[i].startswith("Args") + or lines[i].startswith("Arguments") + or lines[i].startswith("Parameters") + ) + ), + 0, + ) current_param = None for i in range(param_start_pos, lines_len): if lines[i] == "": @@ -245,7 +246,7 @@ def _extract_docstring_details_google(func): elif lines[i].startswith(" "): param_desc = lines[i].split(":", 1) if len(param_desc) == 1: - params_description[current_param] += " " + param_desc[0].strip() + params_description[current_param] += f" {param_desc[0].strip()}" continue param, desc = param_desc param = param.split("(")[0].strip() @@ -262,27 +263,27 @@ def _extract_docstring_details_rest(func): docstring following the reStructuredText (reST) style format. Args: - func (Callable): The function from which to extract docstring details. + func (Callable): The function from which to extract docstring details. Returns: - Tuple[str, Dict[str, str]]: A tuple containing the function description - and a dictionary with parameter names as keys and their descriptions as values. + Tuple[str, Dict[str, str]]: A tuple containing the function description + and a dictionary with parameter names as keys and their descriptions as values. Examples: - >>> def example_function(param1: int, param2: str): - ... '''Example function. - ... - ... :param param1: The first parameter. - ... :type param1: int - ... :param param2: The second parameter. - ... :type param2: str - ... ''' - ... pass - >>> description, params = _extract_docstring_details_rest(example_function) - >>> description - 'Example function.' - >>> params == {'param1': 'The first parameter.', 'param2': 'The second parameter.'} - True + >>> def example_function(param1: int, param2: str): + ... '''Example function. + ... + ... :param param1: The first parameter. + ... :type param1: int + ... :param param2: The second parameter. + ... :type param2: str + ... ''' + ... pass + >>> description, params = _extract_docstring_details_rest(example_function) + >>> description + 'Example function.' + >>> params == {'param1': 'The first parameter.', 'param2': 'The second parameter.'} + True """ docstring = inspect.getdoc(func) if not docstring: @@ -301,7 +302,7 @@ def _extract_docstring_details_rest(func): params_description[param] = desc.strip() current_param = param elif line.startswith(" "): - params_description[current_param] += " " + line + params_description[current_param] += f" {line}" return func_description, params_description @@ -313,30 +314,30 @@ def _extract_docstring_details(func, style="google"): (reST) style format. Args: - func (Callable): The function from which to extract docstring details. - style (str): The style of docstring to parse ('google' or 'reST'). + func (Callable): The function from which to extract docstring details. + style (str): The style of docstring to parse ('google' or 'reST'). Returns: - Tuple[str, Dict[str, str]]: A tuple containing the function description - and a dictionary with parameter names as keys and their descriptions as values. + Tuple[str, Dict[str, str]]: A tuple containing the function description + and a dictionary with parameter names as keys and their descriptions as values. Raises: - ValueError: If an unsupported style is provided. + ValueError: If an unsupported style is provided. Examples: - >>> def example_function(param1: int, param2: str): - ... '''Example function. - ... - ... Args: - ... param1 (int): The first parameter. - ... param2 (str): The second parameter. - ... ''' - ... pass - >>> description, params = _extract_docstring_details(example_function, style='google') - >>> description - 'Example function.' - >>> params == {'param1': 'The first parameter.', 'param2': 'The second parameter.'} - True + >>> def example_function(param1: int, param2: str): + ... '''Example function. + ... + ... Args: + ... param1 (int): The first parameter. + ... param2 (str): The second parameter. + ... ''' + ... pass + >>> description, params = _extract_docstring_details(example_function, style='google') + >>> description + 'Example function.' + >>> params == {'param1': 'The first parameter.', 'param2': 'The second parameter.'} + True """ if style == "google": func_description, params_description = ( @@ -358,16 +359,16 @@ def _python_to_json_type(py_type): Converts a Python type to its JSON type equivalent. Args: - py_type (str): The name of the Python type. + py_type (str): The name of the Python type. Returns: - str: The corresponding JSON type. + str: The corresponding JSON type. Examples: - >>> _python_to_json_type('str') - 'string' - >>> _python_to_json_type('int') - 'number' + >>> _python_to_json_type('str') + 'string' + >>> _python_to_json_type('int') + 'number' """ type_mapping = { "str": "string", @@ -387,24 +388,24 @@ def _func_to_schema(func, style="google"): docstrings. The schema includes the function's name, description, and parameters. Args: - func (Callable): The function to generate a schema for. - style (str): The docstring format ('google' or 'reST'). + func (Callable): The function to generate a schema for. + style (str): The docstring format ('google' or 'reST'). Returns: - Dict[str, Any]: A schema describing the function. + Dict[str, Any]: A schema describing the function. Examples: - >>> def example_function(param1: int, param2: str) -> bool: - ... '''Example function. - ... - ... Args: - ... param1 (int): The first parameter. - ... param2 (str): The second parameter. - ... ''' - ... return True - >>> schema = _func_to_schema(example_function) - >>> schema['function']['name'] - 'example_function' + >>> def example_function(param1: int, param2: str) -> bool: + ... '''Example function. + ... + ... Args: + ... param1 (int): The first parameter. + ... param2 (str): The second parameter. + ... ''' + ... return True + >>> schema = _func_to_schema(example_function) + >>> schema['function']['name'] + 'example_function' """ # Extracting function name and docstring details func_name = func.__name__ @@ -438,8 +439,7 @@ def _func_to_schema(func, style="google"): "description": param_description, } - # Constructing the schema - schema = { + return { "type": "function", "function": { "name": func_name, @@ -448,8 +448,6 @@ def _func_to_schema(func, style="google"): }, } - return schema - class StringMatch: @@ -463,16 +461,16 @@ def jaro_distance(s, t): and 1 is an exact match. Args: - s: The first string to compare. - t: The second string to compare. + s: The first string to compare. + t: The second string to compare. Returns: - A float representing the Jaro distance between the two strings, ranging from 0 to 1, - where 1 means the strings are identical. + A float representing the Jaro distance between the two strings, ranging from 0 to 1, + where 1 means the strings are identical. Examples: - >>> jaro_distance("martha", "marhta") - 0.9444444444444445 + >>> jaro_distance("martha", "marhta") + 0.9444444444444445 """ s_len = len(s) t_len = len(t) @@ -527,18 +525,18 @@ def jaro_winkler_similarity(s, t, scaling=0.1): person names, and is designed to improve the scoring of strings that have a common prefix. Args: - s: The first string to compare. - t: The second string to compare. - scaling: The scaling factor for how much the score is adjusted upwards for having common prefixes. - The scaling factor should be less than 1, and a typical value is 0.1. + s: The first string to compare. + t: The second string to compare. + scaling: The scaling factor for how much the score is adjusted upwards for having common prefixes. + The scaling factor should be less than 1, and a typical value is 0.1. Returns: - A float representing the Jaro-Winkler similarity between the two strings, ranging from 0 to 1, - where 1 means the strings are identical. + A float representing the Jaro-Winkler similarity between the two strings, ranging from 0 to 1, + where 1 means the strings are identical. Examples: - >>> jaro_winkler_similarity("dixon", "dicksonx") - 0.8133333333333332 + >>> jaro_winkler_similarity("dixon", "dicksonx") + 0.8133333333333332 """ jaro_sim = StringMatch.jaro_distance(s, t) prefix_len = 0 @@ -561,15 +559,15 @@ def levenshtein_distance(a, b): required to change one word into the other. Each operation has an equal cost. Args: - a: The first string to compare. - b: The second string to compare. + a: The first string to compare. + b: The second string to compare. Returns: - An integer representing the Levenshtein distance between the two strings. + An integer representing the Levenshtein distance between the two strings. Examples: - >>> levenshtein_distance("kitten", "sitting") - 3 + >>> levenshtein_distance("kitten", "sitting") + 3 """ m, n = len(a), len(b) # Initialize 2D array (m+1) x (n+1) @@ -582,17 +580,13 @@ def levenshtein_distance(a, b): d[0][j] = j # Compute the distance - for i in range(1, m + 1): - for j in range(1, n + 1): - if a[i - 1] == b[j - 1]: - cost = 0 - else: - cost = 1 - d[i][j] = min( - d[i - 1][j] + 1, # deletion - d[i][j - 1] + 1, # insertion - d[i - 1][j - 1] + cost, - ) # substitution + for i, j in itertools.product(range(1, m + 1), range(1, n + 1)): + cost = 0 if a[i - 1] == b[j - 1] else 1 + d[i][j] = min( + d[i - 1][j] + 1, # deletion + d[i][j - 1] + 1, # insertion + d[i - 1][j - 1] + cost, + ) # substitution return d[m][n] @staticmethod @@ -632,12 +626,14 @@ def choose_most_similar(word, correct_words_list, score_func=None): if score_func is None: score_func = StringMatch.jaro_winkler_similarity - + # Calculate Jaro-Winkler similarity scores for each potential match - scores = np.array([score_func(convert.to_str(word), correct_word) for correct_word in correct_words_list]) + scores = np.array( + [ + score_func(convert.to_str(word), correct_word) + for correct_word in correct_words_list + ] + ) # Find the index of the highest score max_score_index = np.argmax(scores) - # Select the best match based on the highest score - best_match = correct_words_list[max_score_index] - - return best_match \ No newline at end of file + return correct_words_list[max_score_index] diff --git a/lionagi/libs/sys_util.py b/lionagi/libs/sys_util.py index 229e0257c..20046c940 100644 --- a/lionagi/libs/sys_util.py +++ b/lionagi/libs/sys_util.py @@ -12,7 +12,6 @@ from pathlib import Path from typing import Any - _timestamp_syms = ["-", ":", "."] PATH_TYPE = str | Path @@ -26,36 +25,39 @@ def sleep(delay: float) -> None: Pauses execution for a specified duration. Args: - delay (float): The amount of time, in seconds, to pause execution. + delay (float): The amount of time, in seconds, to pause execution. """ time.sleep(delay) @staticmethod - def get_now(datetime_: bool = False) -> float | datetime: + def get_now(datetime_: bool = False, tz=None) -> float | datetime: """Returns the current time either as a Unix timestamp or a datetime object. Args: - datetime_ (bool): If True, returns a datetime object; otherwise, returns a Unix timestamp. + datetime_ (bool): If True, returns a datetime object; otherwise, returns a Unix timestamp. Returns: - Union[float, datetime.datetime]: The current time as a Unix timestamp or a datetime object. + Union[float, datetime.datetime]: The current time as a Unix timestamp or a datetime object. """ + if not datetime_: return time.time() - else: - return datetime.now() + config_ = {} + if tz: + config_["tz"] = tz if isinstance(tz, timezone) else timezone.utc + return datetime.now(**config_) @staticmethod def change_dict_key(dict_: dict[Any, Any], old_key: str, new_key: str) -> None: """Safely changes a key in a dictionary if the old key exists. Args: - dict_ (Dict[Any, Any]): The dictionary in which to change the key. - old_key (str): The old key to be changed. - new_key (str): The new key to replace the old key. + dict_ (Dict[Any, Any]): The dictionary in which to change the key. + old_key (str): The old key to be changed. + new_key (str): The new key to replace the old key. Returns: - None + None """ if old_key in dict_: dict_[new_key] = dict_.pop(old_key) @@ -65,11 +67,11 @@ def get_timestamp(tz: timezone = timezone.utc, sep: str = "_") -> str: """Returns a timestamp string with optional custom separators and timezone. Args: - tz (timezone): The timezone for the timestamp. - sep (str): The separator to use in the timestamp string, replacing '-', ':', and '.'. + tz (timezone): The timezone for the timestamp. + sep (str): The separator to use in the timestamp string, replacing '-', ':', and '.'. Returns: - str: A string representation of the current timestamp. + str: A string representation of the current timestamp. """ str_ = datetime.now(tz=tz).isoformat() if sep is not None: @@ -90,11 +92,11 @@ def create_copy(input_: Any, num: int = 1) -> Any | list[Any]: """Creates deep copies of the input, either as a single copy or a list of copies. Args: - input_ (Any): The input to be copied. - num (int): The number of copies to create. + input_ (Any): The input to be copied. + num (int): The number of copies to create. Returns: - Union[Any, List[Any]]: A single copy of the input or a list of deep copies. + Union[Any, List[Any]]: A single copy of the input or a list of deep copies. """ if num < 1: raise ValueError(f"'num' must be a positive integer: {num}") @@ -110,10 +112,10 @@ def create_id(n: int = 32) -> str: Generates a unique identifier based on the current time and random bytes. Args: - n (int): The length of the generated identifier. + n (int): The length of the generated identifier. Returns: - str: A unique identifier string. + str: A unique identifier string. """ current_time = datetime.now().isoformat().encode("utf-8") random_bytes = os.urandom(42) @@ -124,11 +126,11 @@ def get_bins(input_: list[str], upper: int | None = 2000) -> list[list[int]]: """Organizes indices of strings into bins based on a cumulative upper limit. Args: - input_ (List[str]): The list of strings to be binned. - upper (int): The cumulative length upper limit for each bin. + input_ (List[str]): The list of strings to be binned. + upper (int): The cumulative length upper limit for each bin. Returns: - List[List[int]]: A list of bins, each bin is a list of indices from the input list. + List[List[int]]: A list of bins, each bin is a list of indices from the input list. """ current = 0 bins = [] @@ -152,12 +154,10 @@ def get_cpu_architecture() -> str: This method categorizes some architectures as 'apple_silicon'. Returns: - str: A string identifying the CPU architecture ('apple_silicon' or 'other_cpu'). + str: A string identifying the CPU architecture ('apple_silicon' or 'other_cpu'). """ arch: str = platform.machine().lower() - if "arm" in arch or "aarch64" in arch: - return "apple_silicon" - return "other_cpu" + return "apple_silicon" if "arm" in arch or "aarch64" in arch else "other_cpu" @staticmethod def install_import( @@ -172,10 +172,10 @@ def install_import( to install the package using pip and then retries the import. Args: - package_name: The base name of the package to import. - module_name: The submodule name to import from the package, if applicable. Defaults to None. - import_name: The specific name to import from the module or package. Defaults to None. - pip_name: The pip package name if different from `package_name`. Defaults to None. + package_name: The base name of the package to import. + module_name: The submodule name to import from the package, if applicable. Defaults to None. + import_name: The specific name to import from the module or package. Defaults to None. + pip_name: The pip package name if different from `package_name`. Defaults to None. Prints a message indicating success or attempts installation if the import fails. """ @@ -213,10 +213,10 @@ def is_package_installed(package_name: str) -> bool: """Checks if a package is currently installed. Args: - package_name: The name of the package to check. + package_name: The name of the package to check. Returns: - A boolean indicating whether the package is installed. + A boolean indicating whether the package is installed. """ package_spec = importlib.util.find_spec(package_name) return package_spec is not None @@ -236,12 +236,12 @@ def check_import( it attempts to install the package using `install_import` and then retries the import. Args: - package_name: The name of the package to check and potentially install. - module_name: The submodule name to import from the package, if applicable. Defaults to None. - import_name: The specific name to import from the module or package. Defaults to None. - pip_name: The pip package name if different from `package_name`. Defaults to None. - attempt_install: If attempt to install the package if uninstalled. Defaults to True. - error_message: Error message when the package is not installed and not attempt to install. + package_name: The name of the package to check and potentially install. + module_name: The submodule name to import from the package, if applicable. Defaults to None. + import_name: The specific name to import from the module or package. Defaults to None. + pip_name: The pip package name if different from `package_name`. Defaults to None. + attempt_install: If attempt to install the package if uninstalled. Defaults to True. + error_message: Error message when the package is not installed and not attempt to install. """ try: if not SysUtil.is_package_installed(package_name): @@ -298,12 +298,12 @@ def clear_dir( excluding files that match any pattern in the exclude list. Args: - dir_path (Union[Path, str]): The path to the directory to clear. - recursive (bool): If True, clears directories recursively. Defaults to False. - exclude (List[str]): A list of string patterns to exclude from deletion. Defaults to None. + dir_path (Union[Path, str]): The path to the directory to clear. + recursive (bool): If True, clears directories recursively. Defaults to False. + exclude (List[str]): A list of string patterns to exclude from deletion. Defaults to None. Raises: - FileNotFoundError: If the specified directory does not exist. + FileNotFoundError: If the specified directory does not exist. """ dir_path = Path(dir_path) if not dir_path.exists(): @@ -335,10 +335,10 @@ def split_path(path: Path | str) -> tuple[Path, str]: Splits a path into its directory and filename components. Args: - path (Union[Path, str]): The path to split. + path (Union[Path, str]): The path to split. Returns: - Tuple[Path, str]: A tuple containing the directory and filename. + Tuple[Path, str]: A tuple containing the directory and filename. """ path = Path(path) return path.parent, path.name @@ -356,18 +356,18 @@ def create_path( Creates a path with an optional timestamp in the specified directory. Args: - directory (Union[Path, str]): The directory where the file will be located. - filename (str): The filename. Must include a valid extension. - timestamp (bool): If True, adds a timestamp to the filename. Defaults to True. - dir_exist_ok (bool): If True, does not raise an error if the directory exists. Defaults to True. - time_prefix (bool): If True, adds the timestamp as a prefix; otherwise, as a suffix. Defaults to False. - custom_timestamp_format (str): A custom format for the timestamp. Defaults to "%Y%m%d%H%M%S". + directory (Union[Path, str]): The directory where the file will be located. + filename (str): The filename. Must include a valid extension. + timestamp (bool): If True, adds a timestamp to the filename. Defaults to True. + dir_exist_ok (bool): If True, does not raise an error if the directory exists. Defaults to True. + time_prefix (bool): If True, adds the timestamp as a prefix; otherwise, as a suffix. Defaults to False. + custom_timestamp_format (str): A custom format for the timestamp. Defaults to "%Y%m%d%H%M%S". Returns: - Path: The full path to the file. + Path: The full path to the file. Raises: - ValueError: If the filename is invalid. + ValueError: If the filename is invalid. """ directory = Path(directory) if not re.match(r"^[\w,\s-]+\.[A-Za-z]{1,5}$", filename): diff --git a/lionagi/tests/test_core/test_session.py b/lionagi/tests/test_core/test_session.py index 6ca72d26b..e7ca297ca 100644 --- a/lionagi/tests/test_core/test_session.py +++ b/lionagi/tests/test_core/test_session.py @@ -1,266 +1,254 @@ -from lionagi.core.branch.branch import Branch -from lionagi.core.session.session import Session - -import unittest -from unittest.mock import patch, call, MagicMock -import pandas as pd -import json -from datetime import datetime - - -class TestSession(unittest.TestCase): - - def setUp(self): - mock_branch = MagicMock() - - mock_branch.to_csv_file = MagicMock() - mock_branch.to_json_file = MagicMock() - - mock_datalogger = MagicMock() - mock_datalogger.to_csv_file = MagicMock() - mock_datalogger.to_json_file = MagicMock() - - # Assign the mocked datalogger to the mock_branch - mock_branch.datalogger = mock_datalogger - - self.branch1 = mock_branch(name="branch1") - self.branch1.messages = pd.DataFrame( - [ - { - "node_id": "1", - "timestamp": "2021-01-01 00:00:00", - "role": "system", - "sender": "system", - "content": json.dumps({"system_info": "System message"}), - } - ] - ) - self.branch2 = mock_branch(name="branch2") - self.branch1.messages = pd.DataFrame( - [ - { - "node_id": "2", - "timestamp": "2021-01-01 00:01:00", - "role": "user", - "sender": "user1", - "content": json.dumps({"instruction": "User message"}), - } - ] - ) - - branches = {"branch1": self.branch1, "branch2": self.branch2} - - self.session = Session( - branches=branches, - default_branch_name="branch1", - default_branch=branches["branch1"], - ) - self.session.mail_manager = MagicMock() - - # def test_from_csv_initialization(self): - # """Test Session initialization from a CSV file.""" - # mock_df = pd.DataFrame( - # { - # "node_id": ["1", "2"], - # "timestamp": [datetime(2021, 1, 1), datetime(2021, 1, 1)], - # "role": ["system", "user"], - # "sender": ["system", "user1"], - # "content": [ - # json.dumps({"system_info": "System message"}), - # json.dumps({"instruction": "User message"}), - # ], - # } - # ) - # filepath = "path/to/mock.csv" - - # with patch("pandas.read_csv", return_value=mock_df) as mock_read_csv: - # session = Session.from_csv(filepath) - # mock_read_csv.assert_called_once_with(filepath) - # pd.testing.assert_frame_equal(session.messages, mock_df) - - # def test_from_json_initialization(self): - # """Test Session initialization from a CSV file.""" - # mock_df = pd.DataFrame( - # { - # "node_id": ["1", "2"], - # "timestamp": [datetime(2021, 1, 1), datetime(2021, 1, 1)], - # "role": ["system", "user"], - # "sender": ["system", "user1"], - # "content": [ - # json.dumps({"system_info": "System message"}), - # json.dumps({"instruction": "User message"}), - # ], - # } - # ) - # filepath = "path/to/mock.json" - - # with patch("pandas.read_json", return_value=mock_df) as mock_read_json: - # session = Session.from_json(filepath) - # mock_read_json.assert_called_once_with(filepath) - # pd.testing.assert_frame_equal(session.messages, mock_df) - - def test_to_csv_file(self): - """Ensure to_csv_file calls each branch's to_csv_file method.""" - filename = "test_export.csv" - self.session.to_csv_file(filename=filename) - - for name, branch in self.session.branches.items(): - # Verify it was called twice - self.assertEqual(branch.to_csv_file.call_count, 2) - - # Verify the arguments of the last call - expected_filename = f"{name}_{filename}" - self.assertIn( - expected_filename, - ["branch1_test_export.csv", "branch2_test_export.csv"], - ) - - def test_to_json_file(self): - """Ensure to_json_file calls each branch's to_json_file method.""" - filename = "test_export.json" - self.session.to_json_file(filename=filename) - - for name, branch in self.session.branches.items(): - # Verify it was called twice - self.assertEqual(branch.to_json_file.call_count, 2) - - # Verify the arguments of the last call - expected_filename = f"{name}_{filename}" - self.assertIn( - expected_filename, - ["branch1_test_export.json", "branch2_test_export.json"], - ) - - def test_log_to_csv(self): - """Ensure log_to_csv calls each branch's log_to_csv method.""" - filename = "test_export.csv" - self.session.log_to_csv(filename=filename) - - for name, branch in self.session.branches.items(): - # Verify it was called twice - self.assertEqual(branch.log_to_csv.call_count, 2) - - # Verify the arguments of the last call - expected_filename = f"{name}_{filename}" - self.assertIn( - expected_filename, - ["branch1_test_export.csv", "branch2_test_export.csv"], - ) - - def test_log_to_json(self): - """Ensure log_to_json calls each branch's log_to_json method.""" - filename = "test_export.json" - self.session.log_to_json(filename=filename) - - for name, branch in self.session.branches.items(): - # Verify it was called twice - self.assertEqual(branch.log_to_json.call_count, 2) - - # Verify the arguments of the last call - expected_filename = f"{name}_{filename}" - self.assertIn( - expected_filename, - ["branch1_test_export.json", "branch2_test_export.json"], - ) - - def test_all_messages(self): - """Test aggregation of all messages across branches.""" - expected_df = pd.concat( - [self.branch1.messages, self.branch2.messages], ignore_index=True - ) - - actual_df = self.session.all_messages - - pd.testing.assert_frame_equal(actual_df, expected_df) - - def test_new_branch_creation(self): - """Test creating a new branch successfully.""" - branch_name = "test_branch" - self.session.new_branch(branch_name=branch_name) - self.assertIn(branch_name, self.session.branches) - - def test_new_branch_duplicate_name(self): - """Test error handling for duplicate branch names.""" - branch_name = "test_branch" - self.session.new_branch(branch_name=branch_name) - with self.assertRaises(ValueError): - self.session.new_branch(branch_name=branch_name) - - def test_get_branch_by_name(self): - """Test retrieving a branch by its name.""" - branch_name = "test_branch" - self.session.new_branch(branch_name=branch_name) - branch = self.session.get_branch(branch_name) - self.assertIsInstance(branch, Branch) - - def test_get_branch_invalid_name(self): - """Test error handling for invalid branch names.""" - with self.assertRaises(ValueError): - self.session.get_branch("nonexistent_branch") - - def test_change_default_branch(self): - """Test changing the default branch.""" - branch_name = "new_default" - self.session.new_branch(branch_name=branch_name) - self.session.change_default_branch(branch_name) - self.assertEqual(self.session.default_branch_name, branch_name) - - def test_delete_branch(self): - """Test deleting a branch.""" - branch_name = "test_branch" - self.session.new_branch(branch_name=branch_name) - self.session.delete_branch(branch_name) - self.assertNotIn(branch_name, self.session.branches) - - def test_delete_default_branch_error(self): - """Test error when trying to delete the default branch.""" - with self.assertRaises(ValueError): - self.session.delete_branch(self.session.default_branch_name) - - def test_merge_branch(self): - """Test merging two branches.""" - from_branch = "source_branch" - to_branch = "target_branch" - self.session.new_branch(branch_name=from_branch) - self.session.new_branch(branch_name=to_branch) - self.session.merge_branch(from_=from_branch, to_branch=to_branch, del_=True) - self.assertIn(to_branch, self.session.branches) - self.assertNotIn(from_branch, self.session.branches) - - def test_collect_from_specified_branches(self): - """Test collecting requests from specified branches.""" - self.session.collect(from_=["branch1"]) - self.assertEqual(self.session.mail_manager.collect.call_count, 1) - - def test_collect_from_all_branches(self): - """Test collecting requests from all branches.""" - self.session.collect() - self.assertEqual(self.session.mail_manager.collect.call_count, 2) - - def test_send_to_specified_branches(self): - """Test sending requests to specified branches.""" - self.session.send(to_=["branch_1"]) - self.assertEqual(self.session.mail_manager.send.call_count, 1) - - def test_send_to_all_branches(self): - """Test sending requests to all branches.""" - self.session.send() - self.assertEqual(self.session.mail_manager.send.call_count, 2) - - def test_collect_send_all_without_receive_all(self): - """Test collecting and sending requests across all branches without receiving.""" - self.session.collect_send_all() - self.assertEqual(self.session.mail_manager.collect.call_count, 2) - self.assertEqual(self.session.mail_manager.send.call_count, 2) - self.branch1.receive_all.assert_not_called() - self.branch2.receive_all.assert_not_called() - - def test_collect_send_all_with_receive_all(self): - """Test collecting and sending requests across all branches with receiving.""" - self.session.collect_send_all(receive_all=True) - self.branch1.receive_all.assert_called() - self.branch2.receive_all.assert_called() - - -if __name__ == "__main__": - unittest.main() +# from lionagi.core.branch.branch import Branch +# from lionagi.core.session.session import Session + +# import unittest +# from unittest.mock import patch, call, MagicMock +# import pandas as pd +# import json +# from datetime import datetime + + +# class TestSession(unittest.TestCase): + +# def setUp(self): +# mock_branch = MagicMock() + +# mock_branch.to_csv_file = MagicMock() +# mock_branch.to_json_file = MagicMock() + +# mock_datalogger = MagicMock() +# mock_datalogger.to_csv_file = MagicMock() +# mock_datalogger.to_json_file = MagicMock() + +# # Assign the mocked datalogger to the mock_branch +# mock_branch.datalogger = mock_datalogger + +# self.branch1 = mock_branch(name="branch1") +# self.branch1.messages = pd.DataFrame( +# [{ +# "node_id": "1", "timestamp": "2021-01-01 00:00:00", +# "role": "system", "sender": "system", +# "content": json.dumps({"system_info": "System message"}), +# }] +# ) +# self.branch2 = mock_branch(name="branch2") +# self.branch1.messages = pd.DataFrame( +# [{ +# "node_id": "2", "timestamp": "2021-01-01 00:01:00", +# "role": "user", "sender": "user1", +# "content": json.dumps({"instruction": "User message"}), +# }] +# ) + +# branches = {"branch1": self.branch1, "branch2": self.branch2} + +# self.session = Session( +# branches=branches, default_branch_name="branch1", +# default_branch=branches["branch1"], ) +# self.session.mail_manager = MagicMock() + +# # def test_from_csv_initialization(self): +# # """Test Session initialization from a CSV file.""" +# # mock_df = pd.DataFrame( +# # { +# # "node_id": ["1", "2"], +# # "timestamp": [datetime(2021, 1, 1), datetime(2021, 1, 1)], +# # "role": ["system", "user"], +# # "sender": ["system", "user1"], +# # "content": [ +# # json.dumps({"system_info": "System message"}), +# # json.dumps({"instruction": "User message"}), +# # ], +# # } +# # ) +# # filepath = "path/to/mock.csv" + +# # with patch("pandas.read_csv", return_value=mock_df) as mock_read_csv: +# # session = Session.from_csv(filepath) +# # mock_read_csv.assert_called_once_with(filepath) +# # pd.testing.assert_frame_equal(session.messages, mock_df) + +# # def test_from_json_initialization(self): +# # """Test Session initialization from a CSV file.""" +# # mock_df = pd.DataFrame( +# # { +# # "node_id": ["1", "2"], +# # "timestamp": [datetime(2021, 1, 1), datetime(2021, 1, 1)], +# # "role": ["system", "user"], +# # "sender": ["system", "user1"], +# # "content": [ +# # json.dumps({"system_info": "System message"}), +# # json.dumps({"instruction": "User message"}), +# # ], +# # } +# # ) +# # filepath = "path/to/mock.json" + +# # with patch("pandas.read_json", return_value=mock_df) as mock_read_json: +# # session = Session.from_json(filepath) +# # mock_read_json.assert_called_once_with(filepath) +# # pd.testing.assert_frame_equal(session.messages, mock_df) + +# def test_to_csv_file(self): +# """Ensure to_csv_file calls each branch's to_csv_file method.""" +# filename = "test_export.csv" +# self.session.to_csv_file(filename=filename) + +# for name, branch in self.session.branches.items(): +# # Verify it was called twice +# self.assertEqual(branch.to_csv_file.call_count, 2) + +# # Verify the arguments of the last call +# expected_filename = f"{name}_{filename}" +# self.assertIn( +# expected_filename, +# ["branch1_test_export.csv", "branch2_test_export.csv"], ) + +# def test_to_json_file(self): +# """Ensure to_json_file calls each branch's to_json_file method.""" +# filename = "test_export.json" +# self.session.to_json_file(filename=filename) + +# for name, branch in self.session.branches.items(): +# # Verify it was called twice +# self.assertEqual(branch.to_json_file.call_count, 2) + +# # Verify the arguments of the last call +# expected_filename = f"{name}_{filename}" +# self.assertIn( +# expected_filename, +# ["branch1_test_export.json", "branch2_test_export.json"], ) + +# def test_log_to_csv(self): +# """Ensure log_to_csv calls each branch's log_to_csv method.""" +# filename = "test_export.csv" +# self.session.log_to_csv(filename=filename) + +# for name, branch in self.session.branches.items(): +# # Verify it was called twice +# self.assertEqual(branch.log_to_csv.call_count, 2) + +# # Verify the arguments of the last call +# expected_filename = f"{name}_{filename}" +# self.assertIn( +# expected_filename, +# ["branch1_test_export.csv", "branch2_test_export.csv"], ) + +# def test_log_to_json(self): +# """Ensure log_to_json calls each branch's log_to_json method.""" +# filename = "test_export.json" +# self.session.log_to_json(filename=filename) + +# for name, branch in self.session.branches.items(): +# # Verify it was called twice +# self.assertEqual(branch.log_to_json.call_count, 2) + +# # Verify the arguments of the last call +# expected_filename = f"{name}_{filename}" +# self.assertIn( +# expected_filename, +# ["branch1_test_export.json", "branch2_test_export.json"], ) + +# def test_all_messages(self): +# """Test aggregation of all messages across branches.""" +# expected_df = pd.concat( +# [self.branch1.messages, self.branch2.messages], ignore_index=True +# ) + +# actual_df = self.session.all_messages + +# pd.testing.assert_frame_equal(actual_df, expected_df) + +# def test_new_branch_creation(self): +# """Test creating a new branch successfully.""" +# branch_name = "test_branch" +# self.session.new_branch(branch_name=branch_name) +# self.assertIn(branch_name, self.session.branches) + +# def test_new_branch_duplicate_name(self): +# """Test error handling for duplicate branch names.""" +# branch_name = "test_branch" +# self.session.new_branch(branch_name=branch_name) +# with self.assertRaises(ValueError): +# self.session.new_branch(branch_name=branch_name) + +# def test_get_branch_by_name(self): +# """Test retrieving a branch by its name.""" +# branch_name = "test_branch" +# self.session.new_branch(branch_name=branch_name) +# branch = self.session.get_branch(branch_name) +# self.assertIsInstance(branch, Branch) + +# def test_get_branch_invalid_name(self): +# """Test error handling for invalid branch names.""" +# with self.assertRaises(ValueError): +# self.session.get_branch("nonexistent_branch") + +# def test_change_default_branch(self): +# """Test changing the default branch.""" +# branch_name = "new_default" +# self.session.new_branch(branch_name=branch_name) +# self.session.change_default_branch(branch_name) +# self.assertEqual(self.session.default_branch_name, branch_name) + +# def test_delete_branch(self): +# """Test deleting a branch.""" +# branch_name = "test_branch" +# self.session.new_branch(branch_name=branch_name) +# self.session.delete_branch(branch_name) +# self.assertNotIn(branch_name, self.session.branches) + +# def test_delete_default_branch_error(self): +# """Test error when trying to delete the default branch.""" +# with self.assertRaises(ValueError): +# self.session.delete_branch(self.session.default_branch_name) + +# def test_merge_branch(self): +# """Test merging two branches.""" +# from_branch = "source_branch" +# to_branch = "target_branch" +# self.session.new_branch(branch_name=from_branch) +# self.session.new_branch(branch_name=to_branch) +# self.session.merge_branch( +# from_=from_branch, to_branch=to_branch, del_=True +# ) +# self.assertIn(to_branch, self.session.branches) +# self.assertNotIn(from_branch, self.session.branches) + +# def test_collect_from_specified_branches(self): +# """Test collecting requests from specified branches.""" +# self.session.collect(from_=["branch1"]) +# self.assertEqual(self.session.mail_manager.collect.call_count, 1) + +# def test_collect_from_all_branches(self): +# """Test collecting requests from all branches.""" +# self.session.collect() +# self.assertEqual(self.session.mail_manager.collect.call_count, 2) + +# def test_send_to_specified_branches(self): +# """Test sending requests to specified branches.""" +# self.session.send(to_=["branch_1"]) +# self.assertEqual(self.session.mail_manager.send.call_count, 1) + +# def test_send_to_all_branches(self): +# """Test sending requests to all branches.""" +# self.session.send() +# self.assertEqual(self.session.mail_manager.send.call_count, 2) + +# def test_collect_send_all_without_receive_all(self): +# """Test collecting and sending requests across all branches without receiving.""" +# self.session.collect_send_all() +# self.assertEqual(self.session.mail_manager.collect.call_count, 2) +# self.assertEqual(self.session.mail_manager.send.call_count, 2) +# self.branch1.receive_all.assert_not_called() +# self.branch2.receive_all.assert_not_called() + +# def test_collect_send_all_with_receive_all(self): +# """Test collecting and sending requests across all branches with receiving.""" +# self.session.collect_send_all(receive_all=True) +# self.branch1.receive_all.assert_called() +# self.branch2.receive_all.assert_called() + + +# if __name__ == "__main__": +# unittest.main() diff --git a/lionagi/tests/test_core/test_session_base_util.py b/lionagi/tests/test_core/test_session_base_util.py index b42d7c1da..bec66ea48 100644 --- a/lionagi/tests/test_core/test_session_base_util.py +++ b/lionagi/tests/test_core/test_session_base_util.py @@ -1,313 +1,312 @@ -from lionagi.core.branch.util import MessageUtil -from lionagi.core.messages.schema import System, Instruction, Response - -import unittest -import pandas as pd -import json -from datetime import datetime - - -class TestCreateMessage(unittest.TestCase): - - def test_create_system_message(self): - """Test creating a System message.""" - system_info = {"system_info": "System information"} - message = MessageUtil.create_message(system=system_info["system_info"]) - self.assertIsInstance(message, System) - self.assertEqual(message.content, system_info) - - def test_create_instruction_message(self): - """Test creating an Instruction message with context.""" - instruction_info = {"task": "Do something"} - context = {"additional": "context"} - message = MessageUtil.create_message( - instruction=instruction_info, context=context - ) - self.assertIsInstance(message, Instruction) - self.assertEqual(message.content["instruction"], instruction_info) - self.assertEqual(message.content["context"], context) - - def test_create_response_message(self): - """Test creating a Response message.""" - response_info = {"message": {"content": "This is a response"}} - message = MessageUtil.create_message(response=response_info) - self.assertIsInstance(message, Response) - self.assertEqual( - message.content["response"], response_info["message"]["content"] - ) - - def test_error_on_multiple_roles(self): - """Test error is raised when multiple roles are provided.""" - with self.assertRaises(ValueError): - MessageUtil.create_message( - system={"info": "info"}, instruction={"task": "task"} - ) - - def test_return_existing_base_message_instance(self): - """Test returning an existing BaseMessage instance if provided.""" - existing_message = System(system={"info": "Already created"}) - message = MessageUtil.create_message(system=existing_message) - self.assertEqual(message.content, existing_message.content) - - -class TestValidateMessages(unittest.TestCase): - - # def test_validate_messages_correct_format(self): - # """Test messages DataFrame with the correct format.""" - # messages = pd.DataFrame({ - # "node_id": ["1"], - # "role": ["user"], - # "sender": ["test"], - # "timestamp": ["2020-01-01T00:00:00"], - # "content": ['{"message": "test"}'] - # }) - # self.assertTrue(MessageUtil.validate_messages(messages)) - - def test_validate_messages_incorrect_columns(self): - """Test messages DataFrame with incorrect columns raises ValueError.""" - messages = pd.DataFrame( - { - "id": ["1"], - "type": ["user"], - "source": ["test"], - "time": ["2020-01-01T00:00:00"], - "data": ['{"message": "test"}'], - } - ) - with self.assertRaises(ValueError): - MessageUtil.validate_messages(messages) - - def test_validate_messages_null_values(self): - """Test messages DataFrame with null values raises ValueError.""" - messages = pd.DataFrame( - { - "node_id": [None], - "role": ["user"], - "sender": ["test"], - "timestamp": ["2020-01-01T00:00:00"], - "content": ['{"message": "test"}'], - } - ) - with self.assertRaises(ValueError): - MessageUtil.validate_messages(messages) - - -class TestSignMessage(unittest.TestCase): - - def test_sign_message(self): - """Test signing message content with sender.""" - messages = pd.DataFrame( - { - "node_id": ["1"], - "role": ["user"], - "sender": ["test"], - "timestamp": ["2020-01-01T00:00:00"], - "content": ["Original message"], - } - ) - sender = "system" - signed_messages = MessageUtil.sign_message(messages, sender) - expected_content = "Sender system: Original message" - self.assertEqual(signed_messages["content"][0], expected_content) - - def test_sign_message_invalid_sender(self): - """Test signing message with an invalid sender raises ValueError.""" - messages = pd.DataFrame( - { - "node_id": ["1"], - "role": ["user"], - "sender": ["test"], - "timestamp": ["2020-01-01T00:00:00"], - "content": ["Original message"], - } - ) - with self.assertRaises(ValueError): - MessageUtil.sign_message(messages, None) - - -class TestFilterMessagesBy(unittest.TestCase): - - def setUp(self): - self.messages = pd.DataFrame( - { - "node_id": ["1", "2"], - "role": ["user", "assistant"], - "sender": ["test", "assistant"], - "timestamp": [datetime(2020, 1, 1), datetime(2020, 1, 2)], - "content": ['{"message": "test"}', '{"response": "ok"}'], - } - ) - - def test_filter_by_role(self): - """Test filtering messages by role.""" - filtered = MessageUtil.filter_messages_by(self.messages, role="assistant") - self.assertEqual(len(filtered), 1) - self.assertEqual(filtered.iloc[0]["sender"], "assistant") - - def test_filter_by_sender(self): - """Test filtering messages by sender.""" - filtered = MessageUtil.filter_messages_by(self.messages, sender="test") - self.assertEqual(len(filtered), 1) - self.assertEqual(filtered.iloc[0]["sender"], "test") - - def test_filter_by_time_range(self): - """Test filtering messages by time range.""" - start_time = datetime(2020, 1, 1, 12) - end_time = datetime(2020, 1, 2, 12) - filtered = MessageUtil.filter_messages_by( - self.messages, start_time=start_time, end_time=end_time - ) - self.assertEqual(len(filtered), 1) - self.assertTrue(start_time <= filtered.iloc[0]["timestamp"] <= end_time) - - -class TestRemoveMessage(unittest.TestCase): - - def test_remove_message(self): - """Test removing a message by node_id.""" - messages = pd.DataFrame( - { - "node_id": ["1", "2"], - "role": ["user", "assistant"], - "content": ["message1", "message2"], - } - ) - updated_messages = MessageUtil.remove_message(messages, "1") - self.assertTrue(updated_messages) - - -class TestGetMessageRows(unittest.TestCase): - - def test_get_message_rows(self): - """Test retrieving the last 'n' message rows based on criteria.""" - messages = pd.DataFrame( - { - "node_id": ["1", "2", "3"], - "role": ["user", "assistant", "user"], - "sender": ["A", "B", "A"], - "content": ["message1", "message2", "message3"], - } - ) - rows = MessageUtil.get_message_rows(messages, sender="A", role="user", n=2) - self.assertEqual(len(rows), 2) - - -# class TestExtend(unittest.TestCase): - -# def test_extend(self): -# """Test extending one DataFrame with another, ensuring no duplicate 'node_id'.""" -# df1 = pd.DataFrame({ -# "node_id": ["1"], -# "role": ["user"], -# "sender": ["test"], -# "timestamp": ["2020-01-01T00:00:00"], -# "content": ['{"message": "test"}'] -# }) -# df2 = pd.DataFrame({ -# "node_id": ["2"], -# "role": ["user"], -# "sender": ["test"], -# "timestamp": ["2020-01-02T00:00:00"], -# "content": ['{"message": "test2"}'] -# }) -# combined = MessageUtil.extend(df1, df2) -# self.assertEqual(len(combined), 2) - - -class TestToMarkdownString(unittest.TestCase): - - def test_to_markdown_string(self): - """Test converting messages to a Markdown-formatted string.""" - messages = pd.DataFrame( - { - "node_id": ["1"], - "role": ["user"], - "content": [json.dumps({"instruction": "Hello, World!"})], - } - ) - markdown_str = MessageUtil.to_markdown_string(messages) - self.assertIn("Hello, World!", markdown_str) - - -# class TestSearchKeywords(unittest.TestCase): - -# def test_search_keywords(self): -# """Test filtering DataFrame for rows containing specified keywords.""" -# messages = pd.DataFrame( -# {"node_id": ["1", "2"], "content": ["Hello world", "Goodbye world"]} -# ) -# filtered = MessageUtil.search_keywords(messages, "Hello") -# print(filtered) -# self.assertEqual(len(filtered), 1) - - -# class TestReplaceKeyword(unittest.TestCase): - -# def test_replace_keyword(self): -# """Test replacing a keyword in DataFrame's specified column.""" -# messages = pd.DataFrame({"content": ["Hello world", "Goodbye world"]}) -# MessageUtil.replace_keyword(messages, "world", "universe") -# self.assertTrue(all(messages["content"].str.contains("universe"))) - - -# class TestReadCsv(unittest.TestCase): - -# @patch("pandas.read_csv") -# def test_read_csv(self, mock_read_csv): -# """Test reading a CSV file into a DataFrame.""" -# mock_df = pd.DataFrame( -# {"node_id": ["1", "2"], "content": ["Hello, World!", "Goodbye, World!"]} -# ) -# mock_read_csv.return_value = mock_df - -# df = MessageUtil.read_csv("path/to/nonexistent/file.csv") - -# mock_read_csv.assert_called_once_with("path/to/nonexistent/file.csv") - -# self.assertTrue(isinstance(df, pd.DataFrame)) -# self.assertEqual(len(df), 2) -# self.assertEqual(list(df.columns), ["node_id", "content"]) +# from lionagi.core.branch.util import MessageUtil +# from lionagi.core.messages.schema import System, Instruction, Response + +# import unittest +# import pandas as pd +# import json +# from datetime import datetime + + +# class TestCreateMessage(unittest.TestCase): + +# def test_create_system_message(self): +# """Test creating a System message.""" +# system_info = {"system_info": "System information"} +# message = MessageUtil.create_message( +# system=system_info["system_info"] +# ) +# self.assertIsInstance(message, System) +# self.assertEqual(message.content, system_info) + +# def test_create_instruction_message(self): +# """Test creating an Instruction message with context.""" +# instruction_info = {"task": "Do something"} +# context = {"additional": "context"} +# message = MessageUtil.create_message( +# instruction=instruction_info, context=context +# ) +# self.assertIsInstance(message, Instruction) +# self.assertEqual(message.content["instruction"], instruction_info) +# self.assertEqual(message.content["context"], context) + +# def test_create_response_message(self): +# """Test creating a Response message.""" +# response_info = {"message": {"content": "This is a response"}} +# message = MessageUtil.create_message(response=response_info) +# self.assertIsInstance(message, Response) +# self.assertEqual( +# message.content["response"], response_info["message"]["content"] +# ) + +# def test_error_on_multiple_roles(self): +# """Test error is raised when multiple roles are provided.""" +# with self.assertRaises(ValueError): +# MessageUtil.create_message( +# system={"info": "info"}, instruction={"task": "task"} +# ) + +# def test_return_existing_base_message_instance(self): +# """Test returning an existing BaseMessage instance if provided.""" +# existing_message = System(system={"info": "Already created"}) +# message = MessageUtil.create_message(system=existing_message) +# self.assertEqual(message.content, existing_message.content) + + +# class TestValidateMessages(unittest.TestCase): + +# # def test_validate_messages_correct_format(self): +# # """Test messages DataFrame with the correct format.""" +# # messages = pd.DataFrame({ +# # "node_id": ["1"], +# # "role": ["user"], +# # "sender": ["test"], +# # "timestamp": ["2020-01-01T00:00:00"], +# # "content": ['{"message": "test"}'] +# # }) +# # self.assertTrue(MessageUtil.validate_messages(messages)) + +# def test_validate_messages_incorrect_columns(self): +# """Test messages DataFrame with incorrect columns raises ValueError.""" +# messages = pd.DataFrame( +# { +# "id": ["1"], "type": ["user"], "source": ["test"], +# "time": ["2020-01-01T00:00:00"], +# "data": ['{"message": "test"}'], +# } +# ) +# with self.assertRaises(ValueError): +# MessageUtil.validate_messages(messages) + +# def test_validate_messages_null_values(self): +# """Test messages DataFrame with null values raises ValueError.""" +# messages = pd.DataFrame( +# { +# "node_id": [None], "role": ["user"], "sender": ["test"], +# "timestamp": ["2020-01-01T00:00:00"], +# "content": ['{"message": "test"}'], +# } +# ) +# with self.assertRaises(ValueError): +# MessageUtil.validate_messages(messages) + + +# class TestSignMessage(unittest.TestCase): + +# def test_sign_message(self): +# """Test signing message content with sender.""" +# messages = pd.DataFrame( +# { +# "node_id": ["1"], "role": ["user"], "sender": ["test"], +# "timestamp": ["2020-01-01T00:00:00"], +# "content": ["Original message"], +# } +# ) +# sender = "system" +# signed_messages = MessageUtil.sign_message(messages, sender) +# expected_content = "Sender system: Original message" +# self.assertEqual(signed_messages["content"][0], expected_content) + +# def test_sign_message_invalid_sender(self): +# """Test signing message with an invalid sender raises ValueError.""" +# messages = pd.DataFrame( +# { +# "node_id": ["1"], "role": ["user"], "sender": ["test"], +# "timestamp": ["2020-01-01T00:00:00"], +# "content": ["Original message"], +# } +# ) +# with self.assertRaises(ValueError): +# MessageUtil.sign_message(messages, None) + + +# class TestFilterMessagesBy(unittest.TestCase): + +# def setUp(self): +# self.messages = pd.DataFrame( +# { +# "node_id": ["1", "2"], "role": ["user", "assistant"], +# "sender": ["test", "assistant"], +# "timestamp": [datetime(2020, 1, 1), datetime(2020, 1, 2)], +# "content": ['{"message": "test"}', '{"response": "ok"}'], +# } +# ) + +# def test_filter_by_role(self): +# """Test filtering messages by role.""" +# filtered = MessageUtil.filter_messages_by( +# self.messages, role="assistant" +# ) +# self.assertEqual(len(filtered), 1) +# self.assertEqual(filtered.iloc[0]["sender"], "assistant") + +# def test_filter_by_sender(self): +# """Test filtering messages by sender.""" +# filtered = MessageUtil.filter_messages_by( +# self.messages, sender="test" +# ) +# self.assertEqual(len(filtered), 1) +# self.assertEqual(filtered.iloc[0]["sender"], "test") + +# def test_filter_by_time_range(self): +# """Test filtering messages by time range.""" +# start_time = datetime(2020, 1, 1, 12) +# end_time = datetime(2020, 1, 2, 12) +# filtered = MessageUtil.filter_messages_by( +# self.messages, start_time=start_time, end_time=end_time +# ) +# self.assertEqual(len(filtered), 1) +# self.assertTrue( +# start_time <= filtered.iloc[0]["timestamp"] <= end_time +# ) + + +# class TestRemoveMessage(unittest.TestCase): + +# def test_remove_message(self): +# """Test removing a message by node_id.""" +# messages = pd.DataFrame( +# { +# "node_id": ["1", "2"], "role": ["user", "assistant"], +# "content": ["message1", "message2"], +# } +# ) +# updated_messages = MessageUtil.remove_message(messages, "1") +# self.assertTrue(updated_messages) + + +# class TestGetMessageRows(unittest.TestCase): + +# def test_get_message_rows(self): +# """Test retrieving the last 'n' message rows based on criteria.""" +# messages = pd.DataFrame( +# { +# "node_id": ["1", "2", "3"], +# "role": ["user", "assistant", "user"], +# "sender": ["A", "B", "A"], +# "content": ["message1", "message2", "message3"], +# } +# ) +# rows = MessageUtil.get_message_rows( +# messages, sender="A", role="user", n=2 +# ) +# self.assertEqual(len(rows), 2) + + +# # class TestExtend(unittest.TestCase): + +# # def test_extend(self): +# # """Test extending one DataFrame with another, ensuring no duplicate 'node_id'.""" +# # df1 = pd.DataFrame({ +# # "node_id": ["1"], +# # "role": ["user"], +# # "sender": ["test"], +# # "timestamp": ["2020-01-01T00:00:00"], +# # "content": ['{"message": "test"}'] +# # }) +# # df2 = pd.DataFrame({ +# # "node_id": ["2"], +# # "role": ["user"], +# # "sender": ["test"], +# # "timestamp": ["2020-01-02T00:00:00"], +# # "content": ['{"message": "test2"}'] +# # }) +# # combined = MessageUtil.extend(df1, df2) +# # self.assertEqual(len(combined), 2) + + +# class TestToMarkdownString(unittest.TestCase): + +# def test_to_markdown_string(self): +# """Test converting messages to a Markdown-formatted string.""" +# messages = pd.DataFrame( +# { +# "node_id": ["1"], "role": ["user"], +# "content": [json.dumps({"instruction": "Hello, World!"})], +# } +# ) +# markdown_str = MessageUtil.to_markdown_string(messages) +# self.assertIn("Hello, World!", markdown_str) + + +# # class TestSearchKeywords(unittest.TestCase): + +# # def test_search_keywords(self): +# # """Test filtering DataFrame for rows containing specified keywords.""" +# # messages = pd.DataFrame( +# # {"node_id": ["1", "2"], "content": ["Hello world", "Goodbye world"]} +# # ) +# # filtered = MessageUtil.search_keywords(messages, "Hello") +# # print(filtered) +# # self.assertEqual(len(filtered), 1) + + +# # class TestReplaceKeyword(unittest.TestCase): + +# # def test_replace_keyword(self): +# # """Test replacing a keyword in DataFrame's specified column.""" +# # messages = pd.DataFrame({"content": ["Hello world", "Goodbye world"]}) +# # MessageUtil.replace_keyword(messages, "world", "universe") +# # self.assertTrue(all(messages["content"].str.contains("universe"))) + + +# # class TestReadCsv(unittest.TestCase): + +# # @patch("pandas.read_csv") +# # def test_read_csv(self, mock_read_csv): +# # """Test reading a CSV file into a DataFrame.""" +# # mock_df = pd.DataFrame( +# # {"node_id": ["1", "2"], "content": ["Hello, World!", "Goodbye, World!"]} +# # ) +# # mock_read_csv.return_value = mock_df + +# # df = MessageUtil.read_csv("path/to/nonexistent/file.csv") + +# # mock_read_csv.assert_called_once_with("path/to/nonexistent/file.csv") + +# # self.assertTrue(isinstance(df, pd.DataFrame)) +# # self.assertEqual(len(df), 2) +# # self.assertEqual(list(df.columns), ["node_id", "content"]) -# class TestReadJson(unittest.TestCase): +# # class TestReadJson(unittest.TestCase): -# @patch("pandas.read_json") -# def test_read_json(self, mock_read_json): -# """Test reading a JSON file into a DataFrame.""" -# mock_df = pd.DataFrame( -# {"node_id": ["1", "2"], "content": ["JSON Message 1", "JSON Message 2"]} -# ) -# mock_read_json.return_value = mock_df - -# df = MessageUtil.read_json("path/to/nonexistent/file.json") +# # @patch("pandas.read_json") +# # def test_read_json(self, mock_read_json): +# # """Test reading a JSON file into a DataFrame.""" +# # mock_df = pd.DataFrame( +# # {"node_id": ["1", "2"], "content": ["JSON Message 1", "JSON Message 2"]} +# # ) +# # mock_read_json.return_value = mock_df + +# # df = MessageUtil.read_json("path/to/nonexistent/file.json") -# mock_read_json.assert_called_once_with("path/to/nonexistent/file.json") +# # mock_read_json.assert_called_once_with("path/to/nonexistent/file.json") -# self.assertTrue(isinstance(df, pd.DataFrame)) -# self.assertEqual(len(df), 2) -# self.assertEqual(list(df.columns), ["node_id", "content"]) +# # self.assertTrue(isinstance(df, pd.DataFrame)) +# # self.assertEqual(len(df), 2) +# # self.assertEqual(list(df.columns), ["node_id", "content"]) -# class TestRemoveLastNRows(unittest.TestCase): +# # class TestRemoveLastNRows(unittest.TestCase): -# def test_remove_last_n_rows(self): -# """Test removing the last 'n' rows from a DataFrame.""" -# messages = pd.DataFrame({"content": ["message1", "message2", "message3"]}) -# updated = MessageUtil.remove_last_n_rows(messages, 2) -# self.assertEqual(len(updated), 1) +# # def test_remove_last_n_rows(self): +# # """Test removing the last 'n' rows from a DataFrame.""" +# # messages = pd.DataFrame({"content": ["message1", "message2", "message3"]}) +# # updated = MessageUtil.remove_last_n_rows(messages, 2) +# # self.assertEqual(len(updated), 1) -# class TestUpdateRow(unittest.TestCase): +# # class TestUpdateRow(unittest.TestCase): -# def test_update_row(self): -# """Test updating a row's value for a specified column.""" -# messages = pd.DataFrame( -# {"node_id": ["1", "2"], "content": ["message1", "message2"]} -# ) -# success = MessageUtil.update_row(messages, 0, "node_id", "3") -# self.assertTrue(success) -# self.assertTrue("3" in messages["node_id"].values) +# # def test_update_row(self): +# # """Test updating a row's value for a specified column.""" +# # messages = pd.DataFrame( +# # {"node_id": ["1", "2"], "content": ["message1", "message2"]} +# # ) +# # success = MessageUtil.update_row(messages, 0, "node_id", "3") +# # self.assertTrue(success) +# # self.assertTrue("3" in messages["node_id"].values) -if __name__ == "__main__": - unittest.main() +# if __name__ == "__main__": +# unittest.main() diff --git a/lionagi/tests/test_core/test_tool_manager.py b/lionagi/tests/test_core/test_tool_manager.py index ccdb8e7ea..386dd3abb 100644 --- a/lionagi/tests/test_core/test_tool_manager.py +++ b/lionagi/tests/test_core/test_tool_manager.py @@ -1,99 +1,95 @@ -from lionagi.core.tool.tool_manager import ToolManager -from lionagi.core.schema import Tool +# from lionagi.core.tool.tool_manager import ToolManager +# from lionagi.core.schema import Tool -import unittest -from unittest.mock import patch, AsyncMock -import asyncio -import json +# import unittest +# from unittest.mock import patch, AsyncMock +# import asyncio +# import json -class TestToolManager(unittest.TestCase): - def setUp(self): - self.manager = ToolManager() - self.tool = Tool(func=lambda x: x, schema_={"function": {"name": "test_func"}}) +# class TestToolManager(unittest.TestCase): +# def setUp(self): +# self.manager = ToolManager() +# self.tool = Tool( +# func=lambda x: x, schema_={"function": {"name": "test_func"}} +# ) - def test_register_and_check_tool(self): - """Test registering a tool and checking its existence.""" - self.manager._register_tool(self.tool) - self.assertTrue(self.manager.name_existed("test_func")) +# def test_register_and_check_tool(self): +# """Test registering a tool and checking its existence.""" +# self.manager._register_tool(self.tool) +# self.assertTrue(self.manager.name_existed("test_func")) - def test_register_tool_type_error(self): - """Test that registering a non-Tool object raises a TypeError.""" - with self.assertRaises(TypeError): - self.manager._register_tool("not_a_tool") +# def test_register_tool_type_error(self): +# """Test that registering a non-Tool object raises a TypeError.""" +# with self.assertRaises(TypeError): +# self.manager._register_tool("not_a_tool") - def test_name_not_existed(self): - """Test querying a non-registered tool's existence.""" - self.assertFalse(self.manager.name_existed("non_existent_func")) +# def test_name_not_existed(self): +# """Test querying a non-registered tool's existence.""" +# self.assertFalse(self.manager.name_existed("non_existent_func")) -class TestToolInvocation(unittest.TestCase): +# class TestToolInvocation(unittest.TestCase): - def setUp(self): - self.manager = ToolManager() +# def setUp(self): +# self.manager = ToolManager() - async def async_tool_func(x): - return x +# async def async_tool_func(x): +# return x - self.async_tool = Tool( - func=async_tool_func, schema_={"function": {"name": "async_test_func"}} - ) - self.sync_tool = Tool( - func=lambda x: x, schema_={"function": {"name": "sync_test_func"}} - ) +# self.async_tool = Tool( +# func=async_tool_func, +# schema_={"function": {"name": "async_test_func"}} +# ) +# self.sync_tool = Tool( +# func=lambda x: x, schema_={"function": {"name": "sync_test_func"}} +# ) - # @patch('lionagi.core.tool.tool_manager', return_value=False) - # def test_invoke_sync_tool(self, mock_is_coroutine): - # """Test invoking a synchronous tool.""" - # self.manager._register_tool(self.sync_tool) - # result = asyncio.run(self.manager.invoke(('sync_test_func', {'x': 10}))) - # self.assertEqual(result, 10) +# # @patch('lionagi.core.tool.tool_manager', return_value=False) # def test_invoke_sync_tool(self, mock_is_coroutine): # """Test invoking a synchronous tool.""" # self.manager._register_tool(self.sync_tool) # result = asyncio.run(self.manager.invoke(('sync_test_func', {'x': 10}))) # self.assertEqual(result, 10) - # @patch('lionagi.core.tool.tool_manager', return_value=True) - # def test_invoke_async_tool(self, mock_call_handler, mock_is_coroutine): - # """Test invoking an asynchronous tool.""" - # mock_call_handler.return_value = 10 - # self.manager._register_tool(self.async_tool) - # result = asyncio.run(self.manager.invoke(('async_test_func', {'x': 10}))) - # self.assertEqual(result, 10) +# # @patch('lionagi.core.tool.tool_manager', return_value=True) # def test_invoke_async_tool(self, mock_call_handler, mock_is_coroutine): # """Test invoking an asynchronous tool.""" # mock_call_handler.return_value = 10 # self.manager._register_tool(self.async_tool) # result = asyncio.run(self.manager.invoke(('async_test_func', {'x': 10}))) # self.assertEqual(result, 10) -class TestFunctionCallExtraction(unittest.TestCase): +# class TestFunctionCallExtraction(unittest.TestCase): - def setUp(self): - self.manager = ToolManager() +# def setUp(self): +# self.manager = ToolManager() - def test_get_function_call_valid(self): - """Test extracting a valid function call.""" - response = {"action": "action_test_func", "arguments": json.dumps({"x": 10})} - func_call = self.manager.get_function_call(response) - self.assertEqual(func_call, ("test_func", {"x": 10})) +# def test_get_function_call_valid(self): +# """Test extracting a valid function call.""" +# response = { +# "action": "action_test_func", "arguments": json.dumps({"x": 10}) +# } +# func_call = self.manager.get_function_call(response) +# self.assertEqual(func_call, ("test_func", {"x": 10})) - def test_get_function_call_invalid(self): - """Test handling an invalid function call.""" - with self.assertRaises(ValueError): - self.manager.get_function_call({}) +# def test_get_function_call_invalid(self): +# """Test handling an invalid function call.""" +# with self.assertRaises(ValueError): +# self.manager.get_function_call({}) -class TestToolParser(unittest.TestCase): +# class TestToolParser(unittest.TestCase): - def setUp(self): - self.manager = ToolManager() - self.tool = Tool(func=lambda x: x, schema_={"function": {"name": "test_func"}}) - self.manager._register_tool(self.tool) +# def setUp(self): +# self.manager = ToolManager() +# self.tool = Tool( +# func=lambda x: x, schema_={"function": {"name": "test_func"}} +# ) +# self.manager._register_tool(self.tool) - def test_tool_parser_single_tool(self): - """Test parsing a single tool name.""" - parsed = self.manager.parse_tool("test_func") - self.assertIn("tools", parsed) - self.assertEqual(len(parsed["tools"]), 1) - self.assertEqual(parsed["tools"][0]["function"]["name"], "test_func") +# def test_tool_parser_single_tool(self): +# """Test parsing a single tool name.""" +# parsed = self.manager.parse_tool("test_func") +# self.assertIn("tools", parsed) +# self.assertEqual(len(parsed["tools"]), 1) +# self.assertEqual(parsed["tools"][0]["function"]["name"], "test_func") - def test_tool_parser_unregistered_tool(self): - """Test parsing an unregistered tool name.""" - with self.assertRaises(ValueError): - self.manager.parse_tool("unregistered_func") +# def test_tool_parser_unregistered_tool(self): +# """Test parsing an unregistered tool name.""" +# with self.assertRaises(ValueError): +# self.manager.parse_tool("unregistered_func") -if __name__ == "__main__": - unittest.main() +# if __name__ == "__main__": +# unittest.main() diff --git a/lionagi/tests/test_libs/test_nested.py b/lionagi/tests/test_libs/test_nested.py index f14cb2302..5aa866e49 100644 --- a/lionagi/tests/test_libs/test_nested.py +++ b/lionagi/tests/test_libs/test_nested.py @@ -172,14 +172,9 @@ def test_get_with_mixed_indices(self): result = nget(nested_structure, ["a", 1, "b"]) self.assertEqual(result, 2) - # def test_get_with_invalid_structure_type(self): - # result = nget(1, [0]) # Attempting to retrieve from an integer, not a list/dict - # self.assertIsNone(result) + # def test_get_with_invalid_structure_type(self): # result = nget(1, [0]) # Attempting to retrieve from an integer, not a list/dict # self.assertIsNone(result) - # def test_get_with_index_out_of_bounds(self): - # test_list = [1, 2, 3] - # result = nget(test_list, [5]) - # self.assertIsNone(result) + # def test_get_with_index_out_of_bounds(self): # test_list = [1, 2, 3] # result = nget(test_list, [5]) # self.assertIsNone(result) class TestNMerge(unittest.TestCase): diff --git a/lionagi/tests/test_libs/test_parse.py b/lionagi/tests/test_libs/test_parse.py index 5260dcca3..23b282808 100644 --- a/lionagi/tests/test_libs/test_parse.py +++ b/lionagi/tests/test_libs/test_parse.py @@ -113,8 +113,8 @@ def sample_func(): Sample function. Args: - param1 (int): Description of param1. - param2 (str): Description of param2. + param1 (int): Description of param1. + param2 (str): Description of param2. """ pass diff --git a/lionagi/version.py b/lionagi/version.py index 94bb41c78..02a9cf46a 100644 --- a/lionagi/version.py +++ b/lionagi/version.py @@ -1 +1 @@ -__version__ = "0.0.306" +__version__ = "0.0.307" diff --git a/notebooks/lion_agent.ipynb b/notebooks/lion_agent.ipynb index 87557a425..47da5c0ad 100644 --- a/notebooks/lion_agent.ipynb +++ b/notebooks/lion_agent.ipynb @@ -6,17 +6,24 @@ "metadata": {}, "outputs": [], "source": [ - "from lionagi.core.schema import Structure\n", - "from lionagi.core.messages import System, Instruction\n", + "from lionagi.core.messages import Instruction, System\n", + "from lionagi.core.schema.structure import Structure\n", "from lionagi.core.agent.base_agent import BaseAgent\n", - "from lionagi.core.branch.executable_branch import ExecutableBranch\n" + "from lionagi.core.branch.executable_branch import ExecutableBranch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Create Graph-based Structure" + "# Create Graph-based Structure" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Comedian Agent Structure" ] }, { @@ -25,9 +32,13 @@ "metadata": {}, "outputs": [], "source": [ - "sys_comedian = System(\"as a comedian, you are sarcastically funny\", recipient=\"comedian\")\n", - "instruct1 = Instruction(\"very short joke: a blue whale and a big shark meet at the bar and start dancing\")\n", - "instruct2 = Instruction(\"continue the joke: and then they stopped\")" + "sys_comedian = System(\n", + " system=\"As a comedian, you are sarcastically funny\", sender=\"comedian\"\n", + ")\n", + "instruct1 = Instruction(\n", + " instruction=\"very short joke: a blue whale and a big shark meet at the bar and start dancing\"\n", + ")\n", + "instruct2 = Instruction(instruction=\"continue the joke: and then they stopped\")" ] }, { @@ -36,19 +47,19 @@ "metadata": {}, "outputs": [], "source": [ - "comedian_structure = Structure()\n", - "comedian_structure.add_node(sys_comedian)\n", - "comedian_structure.add_node(instruct1)\n", - "comedian_structure.add_node(instruct2)\n", - "comedian_structure.add_relationship(sys_comedian, instruct1)\n", - "comedian_structure.add_relationship(instruct1, instruct2)" + "struct_comedian = Structure()\n", + "struct_comedian.add_node(sys_comedian)\n", + "struct_comedian.add_node(instruct1)\n", + "struct_comedian.add_node(instruct2)\n", + "struct_comedian.add_relationship(sys_comedian, instruct1)\n", + "struct_comedian.add_relationship(instruct1, instruct2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Create Critic Agent" + "### Create Critic Agent" ] }, { @@ -57,9 +68,16 @@ "metadata": {}, "outputs": [], "source": [ - "sys_critic = System(\"as a commentator, you are artistically logical\", recipient='critic')\n", - "instruct3 = Instruction(\"short comments, what do you think about the first joke?\")\n", - "instruct4 = Instruction(\"provide a concise artistic critique on both jokes, and rate from 1-10\")" + "sys_critic = System(\n", + " system=\"you are a respected commentator, you are artistically logical\",\n", + " sender=\"critic\",\n", + ")\n", + "instruct3 = Instruction(\n", + " instruction=\"short comments, what do you think about the first joke?\"\n", + ")\n", + "instruct4 = Instruction(\n", + " instruction=\"provide a concise artistic critique on both jokes, and rate from 1-10\"\n", + ")" ] }, { @@ -68,12 +86,12 @@ "metadata": {}, "outputs": [], "source": [ - "critic_structure = Structure()\n", - "critic_structure.add_node(sys_critic)\n", - "critic_structure.add_node(instruct3)\n", - "critic_structure.add_node(instruct4)\n", - "critic_structure.add_relationship(sys_critic, instruct3)\n", - "critic_structure.add_relationship(instruct3, instruct4)" + "struct_critic = Structure()\n", + "struct_critic.add_node(sys_critic)\n", + "struct_critic.add_node(instruct3)\n", + "struct_critic.add_node(instruct4)\n", + "struct_critic.add_relationship(sys_critic, instruct3)\n", + "struct_critic.add_relationship(instruct3, instruct4)" ] }, { @@ -83,15 +101,23 @@ "outputs": [], "source": [ "## output_parser_func parameter: agent self\n", - "\n", "def critic_output_parser(agent):\n", " return agent.executable.responses\n", "\n", + "\n", + "executable_critic = ExecutableBranch()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ "critic = BaseAgent(\n", - " structure=critic_structure,\n", - " executable_class=ExecutableBranch,\n", + " structure=struct_critic,\n", + " executable_obj=executable_critic,\n", " output_parser=critic_output_parser,\n", - " executable_class_kwargs = {\"name\": \"critic\"}\n", ")" ] }, @@ -99,67 +125,102 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Added Critic agent into Comedian Structure" + "### Add Critic Agent into Comedian Structure" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "comedian_structure.add_node(critic)\n", - "comedian_structure.add_relationship(instruct2, critic)" + "struct_comedian.add_node(critic)\n", + "struct_comedian.add_relationship(instruct2, critic)" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ - "instruct5 = Instruction(\"your jokes were evaluated by a critic, what do you think of the score(1-10)?\")\n", - "instruct6 = Instruction(\"basing on your reflection, write joke1 again\")\n", - "instruct7 = Instruction(\"write joke2 again\")" + "instruct5 = Instruction(\n", + " instruction=\"your jokes were evaluated by a critic, does it make sense to you? why?\"\n", + ")\n", + "instruct6 = Instruction(instruction=\"basing on your reflection, write joke1 again\")\n", + "instruct7 = Instruction(instruction=\"write joke2 again\")" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ - "comedian_structure.add_node(instruct5)\n", - "comedian_structure.add_node(instruct6)\n", - "comedian_structure.add_node(instruct7)\n", - "comedian_structure.add_relationship(critic, instruct5)\n", - "comedian_structure.add_relationship(instruct5, instruct6)\n", - "comedian_structure.add_relationship(instruct6, instruct7)" + "struct_comedian.add_node(instruct5)\n", + "struct_comedian.add_node(instruct6)\n", + "struct_comedian.add_node(instruct7)\n", + "struct_comedian.add_relationship(critic, instruct5)\n", + "struct_comedian.add_relationship(instruct5, instruct6)\n", + "struct_comedian.add_relationship(instruct6, instruct7)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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ozl06BD3338/Y8aM4d5779WG9iIiIoWgNZTX4O/vX+7W2VksFr7++msGDBhAYGBg3gbe8+fP58iRI8TGxtK+fXu7hUkADxdHGt7izT+CfGl4i/cNC5MAnp6ejBo1igMHDvDhhx+SkpJCmzZtuOeee/jvf/+b7znfIiIiYkuB8hrK0/O89+/fz6uvvkpwcDDt27fn+++/5+WXXyY5OTkvYFbkJwI5OTnRr18/fv31V7744gscHR156KGHuP3225k3bx7nzp2zd4kiIiKlkgLlNZT1QHnixAlmzZrFXXfdRWhoKO+99x5du3blu+++Y9++fbz22mu6GeUyhmFw//3388033/Ddd98REhLCoEGDqF27NtHR0Zw+fdreJYqIiJQqCpTXcDFQlqWlpmazOW9z8cDAQEaMGEHlypVZsmQJf/31V17A1PrAa7vrrrv4/PPPSUhIoFOnTrzyyisEBQXx6quvcuTIEXuXJyIiUiroppxrWLJkCY899hgnT57Ey8vL3uVc1S+//EJsbCwLFy7k6NGjNG7cmAEDBvD4448TGBho7/LKhd9//513332X2bNnc/78eZ544glGjx5NnTp17F2aiIiI3ShQXsNXX31Fhw4dOHDgALVr17Z3OfkcOXKERYsWERsbyy+//EKVKlXo06cPAwYMoGnTpvYur9w6ceIEM2bMYOrUqaSlpfHoo48yZswYmjVrZu/SREREbjpNeV+Dn58fULoev5idnc3SpUt54IEHqF69Oi+//DJ169ZlxYoVHD58mClTpihM3mC+vr68+uqrpKSkMH36dH788UeaN29Ox44d+eqrr8rUEgkREZHiUqC8hovP87b31kEXn17z9NNPU61aNXr16sWxY8eIiYnhzz//ZNmyZTzwwAM4OTnZtc6Kxs3Njaeffpp9+/axePFi0tLS6NChAy1atOCTTz4hNzfX3iWKiIjccAqU1+Dv74+jqwf7jmTyc+oJdv9xkswc8007f2pqKhMmTKB+/frcddddfPHFFzz99NPs2bOHLVu2MHz48BJ/FKIUnaOjIxEREfz000+sW7cOHx8fIiIiqF+/PrNnzyY7O9veJYqIiNwwWkN5BXmPAdx3lJT0LJv3DCCosjvhoQH0aRlEvarFfwzgpc6cOcNnn32W9/QaNzc3evTowYABAwgPD8fBwaFEzyc3xo8//sikSZP49NNP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b7+7bt06OnfujMlkokaNGpw5cwZfX19WrlyZb/RSREQuUKAUEbmE1Wqlfv36HD16lO3btwPwwAMPcOjQIT755BM6d+5s5wpFREofBUoRkcukpKRgtVoJDg4G4NSpUzz++OOsWbOGKVOm8Mwzz+iObxGRSyhQiogUQm5uLmPGjCE6OpqhQ4cybdo0nJyc7F2WiEipoEApIlIEH3zwAcOGDaN169YsXbqUypUr27skERG7U6AUESmib775hh49euTdrFO/fn17lyQiYld6lreISBG1adOGH374AWdnZ1q1asW6devsXZKIiF0pUIqIXIfatWvz/fffc88999C1a1emT5+OJnxEpKJSoBQRuU5eXl6sWLGCkSNH8swzzzB8+HDOnz9v77JERG46raEUESkBc+fOZdiwYbRp00Y364hIhaNAKSJSQjZu3MgjjzxC5cqVWblyJaGhofYuSUTkptCUt4hICWnbti0//PADTk5OtGrViri4OHuXJCJyUyhQioiUoNq1a/Pdd99x11130aVLF9577z17lyQicsMpUIqIlDBvb29WrlzJM888w4gRI/jnP/+pm3VEpFzTGkoRkRvoP//5D8OHD6dt27YsXboUX19fe5ckIlLiFChFRG6wDRs28Mgjj+Dv78/KlSsJCQmxd0kiIiVKU94iIjdYWFgYW7duxWQy0bJlS7766it7lyQiUqIUKEVEboK6deuyZcsWWrZsSefOnZk5c6a9SxIRKTEKlCIiN4m3tzdffPEFI0aMYPjw4YwYMQKz2WzvskREik1rKEVE7GDOnDn885//JDw8nCVLluhmHREp0xQoRUTsJD4+nkceeYQqVarwxRdfUK9ePXuXJCJyXTTlLSJiJ+Hh4WzduhXDMGjZsiVff/21vUsSEbkuCpQiInZUr149tmzZQosWLejcuTOzZ8+2d0kiIkWmQCkiYmc+Pj6sWrWKp59+mmHDhjFy5EjdrCMiZYrWUIqIlCKzZs1ixIgRtG/fniVLluDj42PvkkRErkmBUkSklPn666959NFHCQgI4IsvvqBu3br2LklE5Ko05S0iUsq0a9eOrVu3YrVaufPOO4mPj7d3SSIiV6VAKSJSCl28Wad58+Z06tRJN+uISKmmQCkiUkr5+vqyZs0ahg0bxrBhwxg1apRu1hGRUklrKEVEyoCZM2fyzDPP0KFDB5YsWYK3t7e9SxIRyaNAKSJSRqxfv56ePXtSrVo1Vq5cSZ06dexdkogIoClvEZEyo0OHDmzdupXc3FzuvPNONmzYYO+SREQABUoRkTIlJCSELVu20KxZMzp27Mh//vMfe5ckIqJAKSJS1vj6+rJ69WqeeuopnnrqKZ577jndrCMidqU1lCIiZdh7773HqFGj6NixI4sXL9bNOiJiFwqUIiJlXFxcHL169dLNOiJiN5ryFhEp4zp27MiWLVswm83ceeedbNy40d4liUgFo0ApIlIOhIaGsmXLFv7xj3/QoUMH3n//fXuXJCIViAKliEg5UblyZdasWcOTTz7Jk08+yXPPPUdubq69yxKRCkBrKEVEyqGLN+t06tSJxYsX4+XlZe+SRKQcU6AUESmn4uLi6NmzJ9WrV2flypXUrl3b3iWJSDmlKW8RkXLq4s06586d48477+Sbb76xd0kiUk4pUIqIlGP169dn69atNG7cmA4dOjB37lx7lyQi5ZACpYhIOVe5cmXWrl3LoEGDGDJkCC+88IJu1hGREqU1lCIiFYTVamX69Ok8++yz3HfffXz88ce6WUdESoQCpYhIBbN27VoiIiKoUaMGK1as0M06IlJsmvIWEalgOnfuzJYtW8jOzqZly5Zs2rTJ3iWJSBmnQCkiUgFdvFnn9ttvp3379nzwwQf2LklEyjAFShGRCsrPz49169YxcOBABg8ezOjRo3WzjohcF62hFBGp4KxWKzExMTz//PN06dKFRYsWFelmncwcM8lpmZwzW3B2NBHs54GHi+MNrFhEShsFShERAeDLL78kIiKCoKAgVq5cSXBw8BXbJh45zcKtqcTvO0pqehaX/iAxgKDK7oSHBtCnZRD1qla60aWLiJ0pUIqISJ49e/bQrVs3Tp06xfLly7n33ntt3j+UnsXY5TvZlHQcB5NBruXKP0Iuvt+6rj8THm5EzcruN7p8EbETBUoREbGRlpbGI488wnfffcecOXMYOHAgAIu3pfLGit2YLdarBsnLOZgMHE0G47s35LEWQTeoahGxJwVKERHJ59y5c4wYMYL//Oc/vPjii9S870mi4/YXu9/RnUIYEV6vBCoUkdJEgVJERApktVqZOnUqb338NZ7thpZYv5E9GhFRiJHKsLAwADZs2FBi5xaRG0PbBomIlGLz58/HMAx+/PHHEu87ISGBcePGkZycXOD7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nenTp9u7HBH5m0YoRUSkzBkxYgTz589nz5491KxZ097liFR4CpQiIlLmnDx5kttuu4077riDzz//HMMw7F2SSIWmKW8RESlzvL29iYmJYcWKFSxfvtze5YhUeBqhFBGRMslqtfLggw+yfft2EhIS8PLysndJIhWWRihFRKRMMgyD6dOnk5GRwauvvmrvckQqNAVKEREps4KCgvi///s/3nvvPX744Qd7lyNSYWnKW0REyjSz2UzLli3Jzc1l27ZtODk52bskkQpHI5QiIlKmOTo6MmfOHHbu3MnUqVPtXY5IhaQRShERKReee+455syZw+7duwkODrZ3OSIVigKliIiUC6dPn+a2226jUaNGrFq1SntTitxEmvIWEZFyoVKlSrz33nusWbOGpUuX2rsckQpFI5QiIlKu9OjRg++//549e/bg4+Nj73JEKgSNUIqISLkybdo0MjMzeeWVV+xdikiFoUApIiLlSvXq1Xn77beZNWsW3333nb3LEakQNOUtIiLlTm5uLnfffTeZmZls374dZ2dne5ckUq5phFJERModBwcHZs+ezd69e4mOjrZ3OSLlnkYoRUSk3HrxxReZPn06u3btok6dOvYuR6TcUqAUEZFyKzMzk4YNGxISEsLatWu1N6XIDaIpbxERKbc8PDyYMWMGcXFxLFq0yN7liJRbGqEUEZFyLyIigvj4ePbu3UvlypXtXY5IuaMRShERKffeffddzp07x0svvWTvUkTKJQVKEREp96pVq8bEiROZO3cu33zzjb3LESl3NOUtIiIVgsVi4d577+XEiRPs2LEDFxcXe5ckUm5ohFJERCoEk8nEnDlzSEpKIjIy0t7liJQrCpQiIlJh3H777bz44ou8/fbb7Nu3z97liJQbmvIWEZEK5ezZs9x+++0EBQXx9ddfa29KkRKgEUoREalQ3NzcmDVrFhs2bCA2Ntbe5YiUCxqhFBGRCqlv3758+eWX7N27F39/f3uXI1KmaYRSREQqpHfeeQeLxcILL7xg71JEyjwFShERqZACAgKYPHkyH374IV999ZW9yxEp0zTlLSIiFZbFYiEsLIw///yTnTt34urqau+SRMokjVCKiEiFZTKZmD17NikpKbz99tv2LkekzFKgFBGRCq1Bgwa8/PLLREZGkpCQYO9yRMokTXmLiEiFl52dTZMmTQgICGDjxo2YTBpv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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "\n", + "g = struct_comedian.graph.to_networkx()\n", + "labels = nx.get_node_attributes(g, \"class_name\")\n", + "nx.draw(g, labels=labels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Create Comedian Agent" + "### Create Comedian Agent" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "## output_parser_func parameter: agent self\n", - "\n", "def comedian_output_parse(agent):\n", " return agent.executable.responses\n", "\n", + "\n", + "executable_comedian = ExecutableBranch()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ "comedian = BaseAgent(\n", - " structure=comedian_structure,\n", - " executable_class=ExecutableBranch,\n", + " structure=struct_comedian,\n", + " executable_obj=executable_comedian,\n", " output_parser=comedian_output_parse,\n", - " executable_class_kwargs = {\"name\": \"comedian\"}\n", ")" ] }, @@ -167,25 +228,25 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Execute" + "# Execute" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "---------------Welcome: comedian------------------\n" + "------------------Welcome: comedian--------------------\n" ] }, { "data": { "text/markdown": [ - "system: as a comedian, you are sarcastically funny" + "system: As a comedian, you are sarcastically funny" ], "text/plain": [ "" @@ -209,7 +270,7 @@ { "data": { "text/markdown": [ - "comedian: A blue whale and a big shark meet at the bar and start dancing. The bartender says, \"What is this, a joke?\" The whale replies, \"No, it's a large scale production!\"" + "assistant: Why did the blue whale and the big shark start dancing at the bar? Because they heard the bartender was serving \"sea-riously\" good fin-tunes!" ], "text/plain": [ "" @@ -240,7 +301,7 @@ { "data": { "text/markdown": [ - "comedian: And then they stopped, panting. The shark looks around and says, \"Well, that cleared the room faster than a fart in a spacesuit!\" The whale nods, \"Yeah, we really made waves tonight!\"" + "assistant: And then they stopped dancing... because the octopus DJ said it was time for a squid break!" ], "text/plain": [ "" @@ -254,13 +315,14 @@ "output_type": "stream", "text": [ "-----------------------------------------------------\n", - "---------------Welcome: critic------------------\n" + "*****************************************************\n", + "------------------Welcome: critic--------------------\n" ] }, { "data": { "text/markdown": [ - "system: as a commentator, you are artistically logical" + "system: you are a respected commentator, you are artistically logical" ], "text/plain": [ "" @@ -284,7 +346,7 @@ { "data": { "text/markdown": [ - "critic: The first joke cleverly plays with the concept of size and expectation, delivering a humorous twist on what could have been a typical bar joke setup. Its charm lies in the unexpected nature of a \"large scale production,\" blending literal and figurative language to craft a moment of surprise and amusement." + "assistant: The first joke cleverly blends marine life with a fun play on words, offering a light-hearted and imaginative punchline that sails smoothly with its audience." ], "text/plain": [ "" @@ -315,9 +377,7 @@ { "data": { "text/markdown": [ - "critic: The initial joke wittily navigates the realms of expectation and surprise, using the setting of a bar—a common setup for jokes—to introduce an unusual pair, a whale and a shark, only to reveal they're part of a 'large scale production.' This clever play on words, juxtaposing the mundane with the grandiose, earns it a solid 8/10 for its originality and the smile it brings.\n", - "\n", - "The follow-up joke extends the narrative with a light-hearted comparison to clearing a room, likening their dance to a 'fart in a spacesuit'—a humorous, albeit less refined, analogy. While it maintains the humorous tone and camaraderie between the whale and the shark, it leans on a more common form of humor that doesn't quite match the cleverness of the first. Thus, it gets a 6/10, still enjoyable but lacking the initial sparkle." + "assistant: The first joke showcases a playful use of language, intertwining sea creatures with a pun on music, creating a humorous scenario that's both vibrant and engaging. It's a delightful blend of thematic elements that scores an 8/10 for its originality and charm. The follow-up joke, while maintaining the thematic continuity, slightly dips in creativity, relying on a simpler play on words. It doesn't quite match the initial spark but still manages to elicit a smile, earning it a 6/10 for its effort to sustain the aquatic amusement." ], "text/plain": [ "" @@ -330,13 +390,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "-----------------------------------------------------\n" + "-----------------------------------------------------\n", + "*****************************************************\n" ] }, { "data": { "text/markdown": [ - "user: your jokes were evaluated by a critic, what do you think of the score(1-10)?" + "user: your jokes were evaluated by a critic, does it make sense to you? why?" ], "text/plain": [ "" @@ -348,7 +409,9 @@ { "data": { "text/markdown": [ - "comedian: Oh, darling, if humor were a soup, that critic just called mine a quirky bistro gazpacho with a side of toasted puns. An 8 for the opener? I'll take it with the grace of a swan doing ballet in a kiddie pool. And a 6 for the encore? Well, not every joke can be a Michelin star, but hey, at least it didn't get sent back to the kitchen! So, on a scale from 1 to 10, I'm sitting comfortably at a 'I made someone smirk today,' and in the world of comedy, that's as golden as finding a French fry in your onion rings." + "assistant: Oh, absolutely, it makes sense to me! As a comedian, I'm like a fish out of water, always trying to reel in the laughs. The critic diving into the depths of my jokes? That's just the seal of approval I was fishing for. The first joke, swimming in puns and imaginative scenarios, clearly made a splash, showcasing my ability to navigate the sea of comedy with a witty current. An 8/10? That's like finding a treasure chest in the murky waters of humor!\n", + "\n", + "Then there's the follow-up joke. Sure, it might have been a bit more like sailing in calm waters compared to the wave-making first joke, but hey, even the ocean has its quiet moments, right? A 6/10 tells me I've still got my sea legs, but maybe next time I'll throw in a pirate or a mermaid to spice things up. In the vast ocean of comedy, it's all about keeping the boat afloat and the audience smiling, even if sometimes you hit a tranquil patch. So, does it make sense? As much sense as a starfish doing the tango!" ], "text/plain": [ "" @@ -379,7 +442,7 @@ { "data": { "text/markdown": [ - "comedian: A blue whale and a big shark waltz into a bar, causing quite the stir. The bartender, eyes wide, asks, \"What's the occasion?\" The whale grins and says, \"We're here for the large scale production.\" The bartender, puzzled, looks around the emptying bar and realizes, they're not just making waves, they're the entire ocean in a teacup. \"Well, break a fin,\" he quips, diving into the spectacle." + "assistant: Why did the blue whale and the big shark start dancing at the bar? Because they couldn't resist the *current* hit, making waves as the ocean's top *stream*!" ], "text/plain": [ "" @@ -410,7 +473,7 @@ { "data": { "text/markdown": [ - "comedian: After their dance, the bar's as empty as a hermit's address book. The whale, catching his breath, says, \"Looks like we really made a splash.\" The shark, grinning, replies, \"Yeah, we cleared the room faster than a skunk at a garden party.\" They share a laugh, knowing they've just become the most unforgettable act since the bartender tried juggling cocktail shakers." + "assistant: And then they stopped... because they realized the jellyfish DJ was just *jelly* of their moves and switched to playing slow *sea*-lullabies!" ], "text/plain": [ "" @@ -430,50 +493,6 @@ "source": [ "result = await comedian.execute()" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Graph" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "# %pip install networkx" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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/EL/+Mc/dPHFF6tVq1Zavny5vF6vBg4cqOPHj1tdHgAAOA2B8qTGeGXQl19+qcmTJ+vSSy/VqFGjlJaWpnXr1gWuA2rSpInVJYZEamqqli9frqKiIg0fPlxer9fqkgAAQB0EypMa06Xme/bs0ejRo+V0OvW73/1ON998s/bs2aN//OMfuuGGG2QYoZ2rHQm+853v6I033tDq1at17733ituuAACIHJzyPinSxy76/X698847+sMf/qB//vOf+ta3vqVf//rXuueeeyK67lDq27evXn75Zd1xxx1yOp16/PHHrS4JAACIQBngdruVkpJidRnfUFVVpddee00zZszQBx98oKysLP35z3/Wrbfeqvj4eKvLC7vbb79dLpdLjz/+uFJTU3XnnXdaXRIAADGPQHlSaWmprrzySqvLCDh8+LBeeOEFPf300zp48KBuvPFGrVq1Sn379o3KLe2G+NWvfiWXy6VRo0apbdu26t+/v9UlAQAQ0wiUJ0XKoZxPP/1Us2bN0p/+9CdVV1dr5MiReuihh9S5c2erS4sYhmHo2Wef1cGDBzVs2DCtX79eV199tdVlAQAQsziUo6/fT7T6UE5BQYFuvvlmpaena/HixXr44YflcrkC1wHhVA6HQ6+++qoyMjJ00003yeVyWV0SAAAxi0ApyePx6MSJE2HvUHq9Xv31r39V9+7dlZOTo48++kjPPvusXC6XJk+erDZt2oS1nsamWbNmeuuttxQfH6+bbrpJR44csbokAABiEoFS4Z+SU15erlmzZik9PV3Dhw9X06ZN9eabb2r37t0aPXq0EhISwlJHNPjWt76lFStW6LPPPtOQIUNUWVlpdUkAAMScmA+UFZVebd33heK+naFyR0tVVJp3aXZxcbEmTJig9u3ba8KECcrJydH777+v/Px8/eAHP5DNFvP/77ggV1xxhZYtW6aNGzfqrrvuks/ns7okAABiiuGPwRuiiw6Va9Eml/I/LpGrzKO6vwGGJGdSgnI7pei2bk6lt2ke9PPef/99zZgxQ6+99poSExM1evRo3X///Wrfvn3Qa+O/Xn/9dQ0fPlyPPPKIfve731ldDgAAMSOmAmVxmUd5Swu1YV+p7DZDNb6z/9JrP++Z1lrThmQpNenM29A+n095eXm6/vrr9YMf/OCU73/rrbc0Y8YMrVu3TpdddpkefPBB3XXXXWrePPiQijObOXOmHn74Yc2ZM0e/+MUvrC4HAICYEDOBcskWlyYu2ymvz3/OIHk6u82Qw2Zo0qBMjejq/MbnkydP1sSJE9W+fXv9+9//VlVVlebNm6eZM2eqqKhI3bt317hx4zR48GDZ7fZQ/pJwFg899JBmz56tpUuXatCgQVaXAwBA1IuJQDknv0jTV+0Nep3x/TM0Jjc98O033nhDN998c+DbN998s9555x0dPnxYQ4cO1cMPP6zu3bsH/Vw0jM/n0/Dhw7V8+XLl5+erW7duVpcEAEBUi/pAuWSLS4++URiy9X4/NEu3dHXqww8/VHZ2tiorK1X7W2iz2XT//fdr7Nixuuyyy0L2TDTcV199pX79+unjjz/Wxo0blZaWZnVJAABEragOlMVlHvWduU6V3tCd+o132LTk9s7q1/2awHVDdRUUFNCVjBBut1s5OTmqqalRQUGBLrnkEqtLAgAgKkX1PTV5SwvlbcD7kvXh9fk17HevB8Kk3W5XkyZNAu9Hzpo1K6TPw4VLTk7WihUrdOzYMf3v//6vPB6P1SUBABCVorZDWXSoXP1mrTdt/euP5qtj6wR5PB5VVFQE/pebm6sxY8aY9lw03NatW9WrVy/169dPf/vb3zgcBQBAiDmsLsAsiza5zns10IWy2wyl/c9demJQZsjXRuhdd911eu211zRo0CCNHTtWTz/9tAzDsLosAACiRtRueed/XGJKmJSkGp9f+XtLTFkb5hg4cKDmzp2rZ555RtOnT7e6HAAAokpUdiiPV3rlKjP3fTmX26OKSq8S46PytzAq3X333XK5XHrkkUeUmpqqESNGWF0SAABRISrT0AF3hcx+MdQvab+7QpltW5r8JITSlClT5HK5dMcdd+jb3/62evXqZXVJAAA0elG55V0VwmuCIuE5CB3DMPTSSy+pZ8+eGjx4sHbt2mV1SQAANHpRGSjjHOH5ZYXrOQituLg4/e1vf1NqaqoGDBigzz77zOqSAABo1KIyEXVITpTZZ3iNk89B49SyZUstX75cNTU1GjhwoMrLy60uCQCARisqA2VivEPOpARTn+FMTuBATiPXvn17rVixQp9++qmGDRum6upqq0sCAKBRispAKUm5nVJkt5nTp7TbDOVmpJiyNsIrKytLS5cuVX5+vu6++25F6T3/AACYKmoD5W3dnKbeQzky22nK2gi/3r17609/+pNeeeUVTZo0yepyAABodKJ2zza9TXP1TGutgk/dIQ2WdpuhHh2TlZbSPGRrwnojR45UcXGx8vLy5HQ69dOf/tTqkgAAaDSidpa3JBWXedR35jpVhvB6n3iHTWse6qVUk9/RRPj5/X7de++9eumll/TWW2/pf/7nf6wuCQCARiFqt7wlKTUpQZNCPG978qBMwmSUMgxDc+bM0YABA/SjH/1IH3zwgdUlAQDQKER1h7LWnPwiTV+1N+h1JvTvpF/kpoWgIkSyiooK5ebmqri4WO+9954uvfRSq0sCACCixUSglKQlW1yauGynvD5/g96ptNsMOWyGJg/K1C1dOYgTKw4dOqTu3buradOmevfdd3XxxRdbXRIAABErZgKl9PU7lXlLC7VhX6nsNuOcwbL2855prTVtSBbb3DFo79696tGjhzIzM7Vy5Uo1bdrU6pIAAIhIMRUoaxUdKteiTS7l7y2Ry+1R3d8AQ19fWp6bkaKR2U5Oc8e4goIC9enTR4MGDdJf/vIX2WxR/doxAAAXJCYDZV0VlV7td1eoyutTnMOmDsmJTMDBKd544w0NGzZM48aN01NPPWV1OQAARJyYD5RAfcyePVtjx47V7Nmzdf/991tdDgAAEYVWHFAPDzzwgA4cOKCxY8eqffv2GjJkiNUlAQAQMehQAvXk8/l06623atmyZXr77bfVvXt3q0sCACAiECiBBjhx4oT69++vXbt2qaCgQBkZGVaXBACA5QiUQAOVlZUpJydHVVVV2rhxo1JSUqwuCQAAS3EHCtBASUlJWrFihTwej37wgx+ooqLC6pIAALAUgRK4AB06dNA///lP7dq1S7feequ8Xq/VJQEAYBkCJXCBrr32Wv31r3/V8uXL9cADD4i3RwAAsYpACQRhwIABeu655zR37lz93//9n9XlAABgCe6hBIL085//XC6XS48++qhSU1P14x//2OqSAAAIK055AyHg9/t11113afHixVq5cqVyc3OtLgkAgLAhUAIhUl1drYEDB2rz5s3617/+pc6dO1tdEgAAYUGgBELo2LFjuuGGG+R2u/Xee++pXbt2VpcEAIDpCJRAiB08eFDdu3fXxRdfrA0bNqhFixZWlwQAgKk45Q2EWLt27bRixQodOHBAN998s6qqqqwuCQAAUxEoARNkZmZq6dKlWrdunUaNGsUdlQCAqEag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AAAaETqUAE5x2223aeXKldq9e7cuueQSq8sBADQCdCgBnGLmzJny+XwaN26c1aUAABoJAiWAU6SkpGj69OlasGCBVq9ebXU5AIBGgC1vAN/g9/vVu3dvuVwuFRYWKiEhweqSAAARjA4lgG8wDEPPP/+8Dh48qClTplhdDgAgwhEoAZxRRkaGfv3rX+upp57Sjh07rC4HABDB2PIGcFZVVVW65ppr1KxZMxUUFMhut1tdEgAgAtGhBHBWcXFxeuGFF7R582bNnTvX6nIAABGKDiWA87r33nu1cOFC7dq1S6mpqVaXAwCIMARKAOd15MgRXXXVVeratav+/ve/M5YRAHAKtrwBnFerVq00e/ZsLVu2TEuXLrW6HABAhKFDCaBe/H6/fvjDH+r999/Xrl271LJlS6tLAgBECDqUAOrFMAw988wzOnbsmPLy8qwuBwAQQQiUAOotNTVVU6dO1dy5c1VQUGB1OQCACMGWN4AGqampUXZ2tr766itt27ZNcXFxVpcEALAYHUoADWK32/Xiiy9qz549mj59utXlAAAiAB1KABfkl7/8pf74xz+qsLBQ6enpVpcDALAQgRLABfF4POrcubM6dOigtWvXcjclAMQwtrwBXJCEhATNnTtX+fn5mj9/vtXlAAAsRIcSQFBGjhypFStWaM+ePbrkkkusLgcAYAE6lACCMmPGDEnSww8/bHElAACrECgBBCUlJUXTp0/XwoULtXr1aqvLAQBYgC1vAEHz+/3q3bu3XC6XCgsLlZCQYHVJAIAwokMJIGiGYej555/XwYMHNXnyZKvLAQCEGYESQEhkZGTo8ccf1/Tp0/Xhhx9aXQ4AIIzY8gYQMlVVVbr22muVkJCgjRs3ym63W10SACAM6FACCJm4uDi98MIL2rJli5599lmrywEAhAkdSgAhd99992nBggXatWuXUlNTrS4HAGAyAiWAkDt69KiuvPJKde3aVX//+98ZywgAUY4tbwAh17JlSz399NNatmyZli5danU5AACT0aEEYAq/36/Bgwdry5Yt2r17t1q2bGl1SQAAk9ChBGAKwzA0Z84clZeX67HHHrO6HACAiQiUAEyTmpqq3/72t5o7d64KCgqsLgcAYBK2vAGYqqamRt27d5fH49G2bdsUFxdndUkAgBCjQwnAVHa7XS+++KL27Nmjp556yupyAAAmoEMJICwe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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import networkx as nx\n", - "nx.draw(comedian_structure.graph.to_networkx())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -492,7 +511,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.6" } }, "nbformat": 4, diff --git a/notebooks/lion_agent_with_action.ipynb b/notebooks/lion_agent_with_action.ipynb new file mode 100644 index 000000000..986a90961 --- /dev/null +++ b/notebooks/lion_agent_with_action.ipynb @@ -0,0 +1,403 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from lionagi.core.messages import Instruction, System\n", + "from lionagi.core.schema.structure import Structure\n", + "from lionagi.core.agent.base_agent import BaseAgent\n", + "from lionagi.core.branch.executable_branch import ExecutableBranch" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from lionagi.core.tool.tool_manager import func_to_tool\n", + "from lionagi.core.schema.action_node import ActionSelection" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def multiply(number1: float, number2: float):\n", + " \"\"\"\n", + " Perform multiplication on two numbers.\n", + "\n", + " Args:\n", + " number1: First number to multiply.\n", + " number2: Second number to multiply.\n", + "\n", + " Returns:\n", + " The product of number1 and number2.\n", + "\n", + " \"\"\"\n", + " return number1 * number2\n", + "\n", + "\n", + "tool_m = func_to_tool(multiply)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "action = ActionSelection(\n", + " action=\"ReAct\", action_kwargs={\"auto\": True}\n", + ") # action: any availble flow functions, default to 'chat'" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "sys_mul = System(\n", + " system=\"you are asked to perform as a function picker and parameter provider\"\n", + ")\n", + "instruction = Instruction(\n", + " instruction=\"Think step by step, understand the following basic math question and provide parameters for function calling.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "question1 = \"A school is ordering laptops for its students. If each classroom has 25 students and the school wants to provide a laptop for each student in its 8 classrooms, how many laptops in total does the school need to order?\"\n", + "question2 = \"A bakery sells cupcakes in boxes of 6. If a customer wants to buy enough cupcakes for a party of 48 people, with each person getting one cupcake, how many boxes of cupcakes does the customer need to buy?\"\n", + "\n", + "import json\n", + "\n", + "context = {\"Question1\": question1, \"question2\": question2}\n", + "context = json.dumps(context)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "struct_mul = Structure()\n", + "struct_mul.add_node(sys_mul)\n", + "struct_mul.add_node(instruction)\n", + "struct_mul.add_node(tool_m[0])\n", + "struct_mul.add_node(action)\n", + "struct_mul.add_relationship(sys_mul, instruction)\n", + "struct_mul.add_relationship(instruction, tool_m[0])\n", + "struct_mul.add_relationship(instruction, action)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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78yt2vJrglyZ+fcrwW4zdFeNoBvyWd4AAPfBNZQAJElvdGkghy3393m8GofN0BtdGuTrMQEA7odCCUCSVKV0MY3rVD9fj9m7tkOVS135Hd0AAM9AoQSQpWdYVT0TUTtfjpX2/X80tl+EqlevrnHjxmXd1ggA4HkolACyGdaylt7s2kA+DpvsuVwCt9sM+ThseqtrAz1Yv4Ski59U8+qrryokJEShoaF68803c9wOCABQuPGmHACX9VtiikYv2a2NB+JltxlXfbPOpf3Nbi6jN7o0UJXSxbRjxw7ddtttlx2/evVqt/gUIQBA/qBQAriq/SeSNG/rUcXsO6mjCSnZPlHH0MWblresXU597qiqm8v9/yf3mKapatWq6bfffst2vOeee05vvvlmrj4dCADg3iiUAK5Z8gWnDickK92ZKW+HTcFB/vL3ufLnIzz33HOaPHmynE6nJCkoKEh79uxR+fLlCyoyAKAAcA0lgGvm7+NQ/UoldGvVUqpfqcRVy6QkdevWLatMDh06VF5eXmrdurVOnTpVEHEBAAWEGUoAN4xpmoqIiFDr1q31/PPP6+eff1Z4eLjKly+vdevWKSgoyOqIAIB8QKEEUKDi4uIUHh6um266SWvXrlXp0qWtjgQAyCOWvAEUqHr16mndunU6duyYIiIidObMGasjAQDyiEIJoMCFhoYqOjpahw4dUtu2bXX27FmrIwEA8oBCCcASDRs2VHR0tPbt26f77rtP586dszoSAOA6USgBWObWW2/VmjVrtHfvXt1///06f/681ZEAANeBQgnAUrfffruioqK0e/dutW/fXsnJyVZHAgDkEoUSgOWaNGmiVatW6ccff1THjh2VkpJidSQAQC5QKAG4hTvvvFMrV67U999/r86dOys1NdXqSACAa0ShBOA27rnnHq1YsUJbtmxRly5dlJaWZnUkAMA1oFACcCvNmzfX119/rW+++UbdunXThQsXrI4EAPgbFEoAbqdly5ZatmyZ1q1bpwceeEDp6elWRwIAXAWFEoBbat26tZYuXarVq1erR48eysjIsDoSAOAKKJQA3Fbbtm21ZMkSLV++XL169aJUAoCbolACcGv333+/Fi1apGXLlqlv375yOp1WRwIA/AWFEoDb69ixoxYsWKBFixapf//+crlcVkcCAPwJhRJAodClSxd9/vnnWrBggQYOHEip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v36USgBuiUIJAG7uH//4h+bPn6///Oc/GjhwoFwul9WRACAbCiUAFALdunXTZ599ps8++0wPP/ywMjMzrY4EAFkcVgcAAFybBx98UE6nU3379pXD4dD06dNlszEvAMB6FEoAKER69+4tl8ul/v37y26368MPP6RUArAchRIACpm+ffvK5XJp0KBBcjgcioyMlGEYVscCUIRRKAGgEBowYICcTqceffRRORwOvfvuu5RKAJahUAJAIfXII4/I5XLp8ccfl91u16RJkyiVACxBoQSAQmzw4MFyOp0aNmyYHA6H3nrrLUolgAJHoQSAQm7o0KFyuVx66qmn5HA49Prrr1MqARQoCiUAeIAnn3xSLpdLI0eOlJeXl8aNG2d1JABFCIUSADzEiBEj5HQ69dxzz8lut+uVV16xOhKAIoJCCQAe5Nlnn5XT6dTo0aPlcDg0evRoqyMBKAIolADgYV544QU5nU69+OKLcjgceu6556yOBMDDUSgBwAO9/PLLcjqdev755+VwODRy5EirIwHwYBRKAPBQY8eOldPp1NNPPy2Hw6Enn3zS6kgAPBSFEgA8lGEY+te//iWn06mnnnpKdrtdQ4cOtToWAA9EoQQAD2YYht58881sNz8fPHiw1bEAeBgKJQB4OMMwNHHiRDmdTj3++ONyOBx6+OGHrY4FwINQKAGgCDAMQ++++66cTqceffRR2e12DRgwwOpYADwEhRIAigjDMPTBBx/I5XJp0KBBcjgc6tOnj9WxAHgACiUAFCE2m01Tp06V0+lU//79Zbfb1atXL6tjASjkKJQAUMTYbDZNnz5dLpdLffr0kd1u14MPPmh1LACFGIUSAIogm82mGTNmyOl0qnfv3rLb7erWrZvVsQAUUoZpmqbVIQAA1nC5XOrbt6+++OILLVy4UJ07d7Y6EoBCiEIJAEXcpVnKpUuXavHixerQoYPVkQAUMhRKAIAyMjLUs2dPff3111q6dKnatWtndSQAhQiFEgAgSUpPT9eDDz6oVatWadmyZYqIiLA6EoBCgkIJAMiSnp6ubt26KTo6Wl9//bVatWpldSQAhQCFEgCQzYULF9SlSxetX79eK1asUHh4uNWRALg5CiUAIIe0tDR16tRJmzdv1qpVq9SsWTOrIwFwYxRKAMBlpaamqkOHDtq6datWr16tu+++2+pIANwUhRIAcEUpKSlq3769fvjhB0VFRemOO+6wOhIAN0ShBABc1fnz53X//fdr165dWrNmjZo0aWJ1JABuhkIJAPhbSUlJuu+++7Rnzx6tXbtWjRs3tjoSADdCoQQAXJNz584pIiJC+/bt09q1a3XrrbdaHQmAm6BQAgCu2dmzZ9WmTRsdPHhQ69atU8OGDa2OBMANUCgBALly+vRptW7dWkePHlVMTIxCQ0OtjgTAYhRKAECuJSYmqlWrVvr999+1fv161atXz+pIACxEoQQAXJf4+Hjde++9OnnypNavX6+6detaHQmARSiUAIDrdurUKbVs2VKJiYnasGGDatWqZXUkABagUAIA8uTEiRMKDw9XUlKSNmzYoJo1a1odCUABo1ACAPLs+PHjCg8PV2pqqjZs2KDq1atbHQlAAaJQAgDyxe+//67w8HBlZGRow4YNqlatmtWRABQQCiUAIN8cO3ZMLVq0kGma2rBhg6pUqWJ1JAAFwGZ1AACA56hcubJiYmJkmqZatmyp33//3epIAAoAhRIAkK+qVq2qmJgYZWRkqGXLljp+/LjVkQDcYBRKAEC+Cw4O1rp165Samqp7771X//3vf62OBOAGolACAG6ImjVrat26dTp37pxatWqlkydPWh0JwA1CoQQA3DC1atXSunXrsj6qMT4+3upIAG4ACiUA4IaqU6eO1q1bp5MnT6p169ZKSEiwOhKAfEahBADccCEhIVq3bp1+//13tWnTRqdPn7Y6EoB8RKEEABSI+vXra+3atTp69KgiIiJ05swZqyMByCcUSgBAgbnlllsUHR2tgwcPqm3btjp79qzVkQDkAwolAKBANWrUSNHR0dq3b5/atWunpKQkqyMByCMKJQCgwN12222KiorSnj17dP/99+v8+fNWRwKQBxRKAIAlwsLCtHr1au3atUvt27dXcnKy1ZEAXCcKJQDAMnfccYdWrVqlH3/8UR07dlRKSorVkQBcBwolAMBSd911l1asWKGtW7eqc+fOSk1NtToSgFyiUAIALNesWTMtX75cmzdvVteuXZWWlmZ1JAC5QKEEALiF8PBwffXVV1q/fr26d++uCxcuWB0JwDWiUAIA3EarVq305ZdfKjo6Wg8++KDS09OtjgTgGlAoAQBuJSIiQkuWLNGqVavUs2dPZWRkWB0JwN+gUAIA3E67du20aNEiff311+rdu7ecTqfVkQBc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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "\n", + "g = struct_mul.graph.to_networkx()\n", + "labels = nx.get_node_attributes(g, \"class_name\")\n", + "nx.draw(g, labels=labels)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "## output_parser_func parameter: agent self\n", + "def multiply_output_parser(agent):\n", + " return agent.executable.responses\n", + "\n", + "\n", + "executable = ExecutableBranch()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "calc = BaseAgent(\n", + " structure=struct_mul,\n", + " executable_obj=executable,\n", + " output_parser=multiply_output_parser,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------------------Welcome: system--------------------\n" + ] + }, + { + "data": { + "text/markdown": [ + "system: you are asked to perform as a function picker and parameter provider" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "user: Think step by step, understand the following basic math question and provide parameters for function calling." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "assistant: None" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-----------------------------------------------------\n" + ] + } + ], + "source": [ + "result = await calc.execute(context=context)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"system_info\": \"you are asked to perform as a function picker and parameter provider\"}\n", + "{\"instruction\": {\"Notice\": \"\\nyou have 2 step left in current task. if available, integrate previous tool responses. perform reasoning and prepare action plan according to available tools only, apply divide and conquer technique.\\n\", \"Task\": \"Think step by step, understand the following basic math question and provide parameters for function calling.\"}, \"context\": \"{\\\"Question1\\\": \\\"A school is ordering laptops for its students. If each classroom has 25 students and the school wants to provide a laptop for each student in its 8 classrooms, how many laptops in total does the school need to order?\\\", \\\"question2\\\": \\\"A bakery sells cupcakes in boxes of 6. If a customer wants to buy enough cupcakes for a party of 48 people, with each person getting one cupcake, how many boxes of cupcakes does the customer need to buy?\\\"}\"}\n", + "{\"response\": \"To address the given task, we need to solve two basic math problems step by step, integrating the divide and conquer technique and preparing an action plan for function calling. Let's break down each question into smaller, manageable steps and identify the parameters needed for function calling.\\n\\n### Question 1: School Laptops\\n\\n**Task:**\\nCalculate the total number of laptops needed for the students in a school, given that each classroom has 25 students, and there are 8 classrooms in total.\\n\\n**Divide and Conquer Approach:**\\n1. **Divide:** Break down the problem into smaller parts - find the number of students per classroom and the total number of classrooms.\\n2. **Conquer:** Multiply the number of students per classroom by the total number of classrooms to find the total number of laptops needed.\\n3. **Integrate:** Use the result as the final answer.\\n\\n**Parameters for Function Calling:**\\n- Function: `calculate_total_items`\\n- Parameters:\\n - `items_per_group`: 25 (students per classroom)\\n - `number_of_groups`: 8 (classrooms)\\n\\n### Question 2: Cupcake Boxes\\n\\n**Task:**\\nDetermine how many boxes of cupcakes a customer needs to buy for a party of 48 people, with each person getting one cupcake and each box containing 6 cupcakes.\\n\\n**Divide and Conquer Approach:**\\n1. **Divide:** Identify the total number of cupcakes needed (equal to the number of people at the party) and the number of cupcakes per box.\\n2. **Conquer:** Divide the total number of cupcakes needed by the number of cupcakes per box to find the total number of boxes needed. If there's a remainder, round up since you can't buy a fraction of a box.\\n3. **Integrate:** Use the result as the final answer, ensuring any fractional part of a box is considered as an additional whole box to be purchased.\\n\\n**Parameters for Function Calling:**\\n- Function: `calculate_total_boxes_needed`\\n- Parameters:\\n - `total_items_needed`: 48 (cupcakes/people)\\n - `items_per_box`: 6 (cupcakes per box)\\n\\nBy breaking down each problem using the divide and conquer technique, we've identified the specific steps and parameters needed for function calling to solve each mathematical question effectively.\"}\n", + "{\"instruction\": \"\\nyou have 1 step left in current task, if further actions are needed, invoke tools usage. If you are done, present the final result to user without further tool usage\\n\"}\n", + "{\"action_request\": [{\"action\": \"action_multiply\", \"arguments\": \"{\\\"number1\\\": 25, \\\"number2\\\": 8}\"}, {\"action\": \"action_multiply\", \"arguments\": \"{\\\"number1\\\": 48, \\\"number2\\\": 1}\"}]}\n", + "{\"action_response\": {\"function\": \"multiply\", \"arguments\": {\"number1\": 25, \"number2\": 8}, \"output\": 200}}\n", + "{\"action_response\": {\"function\": \"multiply\", \"arguments\": {\"number1\": 48, \"number2\": 1}, \"output\": 48}}\n" + ] + } + ], + "source": [ + "for i in calc.executable.branch.messages[\"content\"]:\n", + " print(i)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
node_idtimestamprolesenderrecipientcontent
08b7afd37c865c7f78480313c9e9750622024_03_21T15_08_27_203139+00_00systemsystemassistant{\"system_info\": \"you are asked to perform as a...
133db336a7392c8c34be074fdd4c65d342024_03_21T15_08_28_094021+00_00useruserassistant{\"instruction\": {\"Notice\": \"\\nyou have 2 step ...
204d9b5cf4d4489b820e05acefb421f0d2024_03_21T15_09_04_307288+00_00assistantassistantuser{\"response\": \"To address the given task, we ne...
3241373b2edfb6f23290bae18f9898ca62024_03_21T15_09_04_310007+00_00useruserassistant{\"instruction\": \"\\nyou have 1 step left in cur...
41feb190bfd2ec81cde767f42eb9872902024_03_21T15_09_07_761085+00_00assistantaction_requestaction{\"action_request\": [{\"action\": \"action_multipl...
5a8816241d62bcdba87da88275dfc6f702024_03_21T15_09_07_763517+00_00assistantaction_responseassistant{\"action_response\": {\"function\": \"multiply\", \"...
6b78ad1f1e1d2aef2abd8d125266550df2024_03_21T15_09_07_764525+00_00assistantaction_responseassistant{\"action_response\": {\"function\": \"multiply\", \"...
\n", + "
" + ], + "text/plain": [ + " node_id timestamp \\\n", + "0 8b7afd37c865c7f78480313c9e975062 2024_03_21T15_08_27_203139+00_00 \n", + "1 33db336a7392c8c34be074fdd4c65d34 2024_03_21T15_08_28_094021+00_00 \n", + "2 04d9b5cf4d4489b820e05acefb421f0d 2024_03_21T15_09_04_307288+00_00 \n", + "3 241373b2edfb6f23290bae18f9898ca6 2024_03_21T15_09_04_310007+00_00 \n", + "4 1feb190bfd2ec81cde767f42eb987290 2024_03_21T15_09_07_761085+00_00 \n", + "5 a8816241d62bcdba87da88275dfc6f70 2024_03_21T15_09_07_763517+00_00 \n", + "6 b78ad1f1e1d2aef2abd8d125266550df 2024_03_21T15_09_07_764525+00_00 \n", + "\n", + " role sender recipient \\\n", + "0 system system assistant \n", + "1 user user assistant \n", + "2 assistant assistant user \n", + "3 user user assistant \n", + "4 assistant action_request action \n", + "5 assistant action_response assistant \n", + "6 assistant action_response assistant \n", + "\n", + " content \n", + "0 {\"system_info\": \"you are asked to perform as a... \n", + "1 {\"instruction\": {\"Notice\": \"\\nyou have 2 step ... \n", + "2 {\"response\": \"To address the given task, we ne... \n", + "3 {\"instruction\": \"\\nyou have 1 step left in cur... \n", + "4 {\"action_request\": [{\"action\": \"action_multipl... \n", + "5 {\"action_response\": {\"function\": \"multiply\", \"... \n", + "6 {\"action_response\": {\"function\": \"multiply\", \"... " + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "calc.executable.branch.messages" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/lion_agent_with_condition.ipynb b/notebooks/lion_agent_with_condition.ipynb new file mode 100644 index 000000000..4fbd3c450 --- /dev/null +++ b/notebooks/lion_agent_with_condition.ipynb @@ -0,0 +1,357 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from lionagi.core.messages import Instruction, System\n", + "from lionagi.core.schema.structure import Structure\n", + "from lionagi.core.agent.base_agent import BaseAgent\n", + "from lionagi.core.branch.executable_branch import ExecutableBranch" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create Graph-based Structure" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "system = System(\n", + " system=\"You are asked to perform as a calculator. Return only a numeric value, i.e. int or float, no text.\"\n", + ")\n", + "\n", + "instruct1 = Instruction(\n", + " instruction={\n", + " \"sum the absolute values\": \"provided with 2 numbers, return the sum of their absolute values. i.e. |x|+|y|\",\n", + " }\n", + ")\n", + "\n", + "instruct2 = Instruction(\n", + " instruction={\n", + " \"diff the absolute values\": \"provided with 2 numbers, return the difference of absolute values. i.e. |x|-|y|\",\n", + " }\n", + ")\n", + "\n", + "instruct3 = Instruction(\n", + " instruction={\n", + " \"if previous response is positive\": \"times 2. i.e. *2\", # case 0\n", + " \"else\": \"plus 2. i.e. +2\", # case 1\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "struct = Structure()\n", + "struct.add_node(system)\n", + "struct.add_node(instruct1)\n", + "struct.add_node(instruct2)\n", + "struct.add_node(instruct3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Relationship Conditions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Conditions help filter the path to be executed.\n", + "It has to be an implemented subclass of Condition class.\n", + "\n", + "There are two functions in Condition class:\n", + "\n", + "```python\n", + "def __init__(self, source_type)\n", + "```\n", + "\n", + "`source_type` can only be \"structure\" or \"executable\"\n", + "\n", + "```python\n", + "@abstractmethod\n", + "def __call__(self, source)\n", + "```\n", + "\n", + "All information in the structure and executable object is available for checking. \n", + "`__call__` is expected to return a bool. If True, the path is selected.\n", + "\n", + "If the `source_type` is \"structure\", the `source` is expected to be the structure object in the agent when checking the condition. \n", + "\n", + "If the `source_type` is \"executable\", the `source` is expected to be the executable object in the agent when checking the condition." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from lionagi.core.schema.condition import Condition\n", + "\n", + "\n", + "class CaseCondition(Condition):\n", + " def __init__(self, case):\n", + " super().__init__(\"executable\")\n", + " self.case = case\n", + "\n", + " def __call__(self, executable):\n", + " case = executable.context[\"case\"]\n", + " return case == self.case\n", + "\n", + "\n", + "cond0 = CaseCondition(case=0)\n", + "cond1 = CaseCondition(case=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Build Relationships with Conditions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the example, if `case` is 0, execute `instruct1`, or if `case` is 1, execute `instruct2`.\n", + "\n", + "Then, execute `instruct3`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "struct.add_relationship(system, instruct1, condition=cond0)\n", + "struct.add_relationship(system, instruct2, condition=cond1)\n", + "struct.add_relationship(instruct1, instruct3)\n", + "struct.add_relationship(instruct2, instruct3)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApQAAAHzCAYAAACe1o1DAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAABf3ElEQVR4nO3dd1zU9+E/8NcdU4aCIEP2VhQ0wxqTOBM1xhljREURcMamJm3z/f5sfm0z+mtSW9M0prWC8QDFvfdM1BhHgsaBLAFlCArK3nB3n98fVOrlAFHG++54PR8PHwnHeby4xAcv31MmSZIEIiIiIqKnJBcdgIiIiIj0GwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1CwslEREREbULCyURERERtQsLJRERERG1i7HoAES6oKpOiayiKtQr1TA1lsPTzhKWZvzjQURE1Bb8iUndVnpBBTb9kINTaYXIKa6G9MjnZADce1tgdIADQoe6w8/RWlRMIiIinSeTJEl6/NOIDEducTU+2JOIsxkPYCSXQaVu+Y/Aw88P97XHp28Ewa23RRcmJSIi0g8slNStbE3IwYf7k6BUS60WyZ8zkstgLJfh4ykDMGuIeycmJCIi0j8slNRt/PNUOlYdv9nu13l/nD/eGe3XAYmIiIgMA3d5U7ewNSGnQ8okAKw6fhPbEnLa/PxRo0Zh1KhRHfK1iYiIdBELJXWJ2NhYyGQyXLp0qcNfOzk5GR999BGysrKa/XxucTU+3J/UoV/zj/uTkFtc3eYMREREhoyFkvRecnIyPv744xbL3Ad7EqF8gvWSbaFUS/hgT2KbMhw/fhzHjx/v0K9PRESkS1goyaClF1TgbMaDpg04kiRB3VDX7tdVqSWczXiA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It0TGxsLa2trvPnmm6KjEFgoqRmbN29GbW0tFixYIDoKERGRFpVKhZiYGMyaNQuWlpai4xBYKOlnJEnC2rVrMWnSJLi4uIiOQ0REpOXbb79Fbm4uz57UISyUpOHHH3/EtWvXsHTpUtFRiIiImqVQKNCvXz8MHTpUdBT6DxZK0rB27Vp4enpi3LhxoqMQERFpKSkpwZ49exAZGQmZTCY6Dv0HCyU1KS0txbZt27B48WLI5fxfg4iIdM+WLVugVCoxb9480VHoEWwN1GTjxo1oaGhARESE6ChERETNUigUmDhxIpycnERHoUewUBKA/27GmTZtGv+QEhGRTrp27RouX77MzTg6iIWSAADnzp1DcnIyN+MQEZHOiomJgYODA15//XXRUehnWCgJQONmHF9fX4wePVp0FCIiIi11dXWIj49HWFgYTExMRMehn2GhJBQVFWHnzp3cjENERDrrwIEDKCoq4jp/HcX2QIiLi4MkSfxDSkREOkuhUOCFF15AYGCg6CjUDBbKbk6SJERFRWHGjBmwt7cXHYeIiEjLnTt3cOzYMW7G0WHGogOQWKdPn8bNmzexbt060VGIiIiatWHDBpiZmSEkJER0FGqBTJIkSXQIEickJASJiYlISkrijQNERKRzJEmCn58fXnrpJcTFxYmOQy3glHc3VlBQgD179mDJkiUsk0REpJPOnj2LzMxMrvPXcSyU3VhMTAyMjIwQFhYmOgoREVGzYmJi4O3tjREjRoiOQq1goeym1Go11q1bh5CQENja2oqOQ0REpKWiogLbt29HREQEj7XTcdyU002dPHkSt27dQnx8vOgoREREzdq+fTtqamowf/580VHoMbgpp5uaPn06MjIycO3aNa6fJCIinfTSSy/B2toaR48eFR2FHoMjlN1Qfn4+9u/fj9WrV7NMEhGRTkpNTcX58+exbds20VGoDbggoRtav349zMzMEBoaKjoKERFRs2JiYtC7d29MnTpVdBRqAxbKbkalUmHdunWYM2cOevXqJToOERGRloaGBsTFxSE0NBRmZmai41AbsFB2M0ePHkVubi6WLFkiOgoREVGzjh49ioKCAl61qEe4KaebmTx5MvLz83H58mXRUYiIiJr1xhtvIDs7Gz/99JPoKNRGHKHsRnJycnD48GEsXbpUdBQiIqJmFRQU4ODBgxyd1DMslN3I119/DUtL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YJmammLIkCEYMmQI/P39ERERgR07duDDDz/E+PHj4ejoiPj4eIwYMQLx8fFwcnLCq6++itSCSgCAkaUNnCNXo+bWT6i5dRk1ty6jKvEkLAeOgf2k37T+xf+zRcFu0m9hZGWr9WmZTA5T4/9OFBoZaU+pt/Y4t0CQLmGhJCKibuH5558HANy9exdAY1GbM2cOYmNjsXLlSuzduxeLFi2CkZERPO0sIQMgAZAZmcDCbygs/IZCktQoPrYGlVePotdLs2Bi2xdA8zf2GNs6N34dy17o4TlY6/MyAJ52lh3/jRIJwDWURERkUE6dOtXs6N3hw4cBNK5BfGjevHkoKSnBkiVLUFlZiblz5wIALM2M4d7bAqqaco3XkMnkMHVo3FktKRsAAHLTxrvG1XWa6yF7eD0LmZkFys5vh6RSauVxMqtv2pBDpO/4fzIRERmUX/3qV6iursYbb7yBfv36ob6+HufPn8e2bdvg6emJiIiIpuc+88wzGDhwIHbs2IH+/fvj2Wefbfrc6AAHXPr6/0JVUwFzj2AYWdtDVVaIissHYOLgDRP7xiOCTB28AZkcZRd3Ql1XDZmRMcw9BsHI0gZ245bhwcG/427su7DsPwJyi15Qlt9HbWYCHJ4bCuCNrn57iDoFCyURERmUVatWYceOHTh8+DCio6NRX18Pd3d3LFu2DL///e9hY2Oj8fywsDD87//+L+bNm6fxeOhQd/wrcBQqrx1FxU+Hoa6rhJGlLSz6D4fNy6GQyRon+YysbNH7tV+i/MIOFB3+EpDUcJz9KYwsbWA5YBSMrHqj7OJOlP2wG1A1wMjKDmZuA/DBe0u76i0h6nQ82JyIiLq1L7/8Er/+9a+RlZUFd3d3jc+15S7vJ8W7vMkQsVASEVG3JUkSBg0aBDs7O5w6dUrr87nF1Xj1izOoe+R4n/YyM5bj5K9Hwq23RYe9JpFonPImIqJup6qqCvv378epU6eQmJiIffv2Nfs8t94W+HjKAKzYndhhX/uTKQNYJsngcISSiIi6naysLHh5ecHGxgbLli3Dn//851af/89T6Vh1/Ga7v+7/jAvAL0f7tvt1iHQNCyUREVEbbE3IwYf7k6BUS0+0ptJILoOxXIZPpgxAyBD3x/8GIj3EQklERNRGucXV+GBPIs5mPICRXNZqsXz4+eG+9vj0jSBOc5NBY6EkIiJ6QukFFdj0Qw5O3SxETlE1Hv1BKgPgbmeB0f4OmPuCO3wdrEXFJOoyLJRERETtUFWnRFZRFeqVapgay+FpZ8kbcKjbYaEkIiIionbhXd5ERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C4slERERETULiyURERERNQuLJRERERE1C7/H+umu241luszAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "\n", + "g = struct.graph.to_networkx()\n", + "labels = nx.get_node_attributes(g, \"class_name\")\n", + "nx.draw(g, labels=labels)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "currently we only support at most one executable path after filtering." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "## output_parser_func parameter: agent self\n", + "def output_parser(agent):\n", + " return agent.executable.responses\n", + "\n", + "\n", + "executable = ExecutableBranch(\n", + " verbose=False\n", + ") # with verbose=False, the intermediate steps will not be printed" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "calc = BaseAgent(\n", + " structure=struct, executable_obj=executable, output_parser=output_parser\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "result = await calc.execute(context={\"x\": -6, \"y\": 0, \"case\": 0})" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
node_idtimestamprolesenderrecipientcontent
0ff7a94ad8acdfded416ecd6584576a102024_03_21T14_20_52_511307+00_00systemsystemassistant{\"system_info\": \"You are asked to perform as a...
17333b6b92f11e9a25e59a9926fcfe7692024_03_21T14_20_52_511371+00_00useruserassistant{\"instruction\": {\"sum the absolute values\": \"p...
2d2b5b19a82800d7212b070f117e71b402024_03_21T14_20_56_014597+00_00assistantassistantuser{\"response\": \"6\"}
329a013ab732602f14bfb291e86a1421e2024_03_21T14_20_52_511430+00_00useruserassistant{\"instruction\": {\"if previous response is posi...
4ad06cee66388d1700dec8deb0d8466282024_03_21T14_20_58_404895+00_00assistantassistantuser{\"response\": \"12\"}
\n", + "
" + ], + "text/plain": [ + " node_id timestamp \\\n", + "0 ff7a94ad8acdfded416ecd6584576a10 2024_03_21T14_20_52_511307+00_00 \n", + "1 7333b6b92f11e9a25e59a9926fcfe769 2024_03_21T14_20_52_511371+00_00 \n", + "2 d2b5b19a82800d7212b070f117e71b40 2024_03_21T14_20_56_014597+00_00 \n", + "3 29a013ab732602f14bfb291e86a1421e 2024_03_21T14_20_52_511430+00_00 \n", + "4 ad06cee66388d1700dec8deb0d846628 2024_03_21T14_20_58_404895+00_00 \n", + "\n", + " role sender recipient \\\n", + "0 system system assistant \n", + "1 user user assistant \n", + "2 assistant assistant user \n", + "3 user user assistant \n", + "4 assistant assistant user \n", + "\n", + " content \n", + "0 {\"system_info\": \"You are asked to perform as a... \n", + "1 {\"instruction\": {\"sum the absolute values\": \"p... \n", + "2 {\"response\": \"6\"} \n", + "3 {\"instruction\": {\"if previous response is posi... \n", + "4 {\"response\": \"12\"} " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "calc.executable.branch.messages" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/lion_agent_with_tool.ipynb b/notebooks/lion_agent_with_tool.ipynb new file mode 100644 index 000000000..8d5f6395f --- /dev/null +++ b/notebooks/lion_agent_with_tool.ipynb @@ -0,0 +1,351 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from lionagi.core.messages import Instruction, System\n", + "from lionagi.core.schema.structure import Structure\n", + "from lionagi.core.agent.base_agent import BaseAgent\n", + "from lionagi.core.branch.executable_branch import ExecutableBranch" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from lionagi.core.tool.tool_manager import func_to_tool" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def multiply(number1: float, number2: float):\n", + " \"\"\"\n", + " Perform multiplication on two numbers.\n", + "\n", + " Args:\n", + " number1: First number to multiply.\n", + " number2: Second number to multiply.\n", + "\n", + " Returns:\n", + " The product of number1 and number2.\n", + "\n", + " \"\"\"\n", + " return number1 * number2\n", + "\n", + "\n", + "tool_m = func_to_tool(multiply)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "sys_mul = System(\n", + " system=\"you are asked to perform as a function picker and parameter provider\"\n", + ")\n", + "instruction = Instruction(\n", + " instruction=\"Think step by step, understand the following basic math question and provide parameters for function calling.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "question1 = \"A school is ordering laptops for its students. If each classroom has 25 students and the school wants to provide a laptop for each student in its 8 classrooms, how many laptops in total does the school need to order?\"\n", + "question2 = \"A bakery sells cupcakes in boxes of 6. If a customer wants to buy enough cupcakes for a party of 48 people, with each person getting one cupcake, how many boxes of cupcakes does the customer need to buy?\"\n", + "\n", + "import json\n", + "\n", + "context = {\"Question1\": question1, \"question2\": question2}\n", + "context = json.dumps(context)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create Graph-based Structure" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "struct_mul = Structure()\n", + "struct_mul.add_node(sys_mul)\n", + "struct_mul.add_node(instruction)\n", + "struct_mul.add_node(tool_m[0])\n", + "struct_mul.add_relationship(sys_mul, instruction)\n", + "struct_mul.add_relationship(instruction, tool_m[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "\n", + "g = struct_mul.graph.to_networkx()\n", + "labels = nx.get_node_attributes(g, \"class_name\")\n", + "nx.draw(g, labels=labels)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "## output_parser_func parameter: agent self\n", + "def multiply_output_parser(agent):\n", + " return agent.executable.responses\n", + "\n", + "\n", + "executable = ExecutableBranch()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "calc = BaseAgent(\n", + " structure=struct_mul,\n", + " executable_obj=executable,\n", + " output_parser=multiply_output_parser,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "------------------Welcome: system--------------------\n" + ] + }, + { + "data": { + "text/markdown": [ + "system: you are asked to perform as a function picker and parameter provider" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "user: Think step by step, understand the following basic math question and provide parameters for function calling." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "result = await calc.execute(context=context)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
node_idtimestamprolesenderrecipientcontent
05fa9518a05bc0ef2823354be2f39d70b2024_03_21T14_56_37_679951+00_00systemsystemassistant{\"system_info\": \"you are asked to perform as a...
193b4abe29a0ce393cbb410b8edc52d162024_03_21T14_56_38_575615+00_00useruserassistant{\"instruction\": \"Think step by step, understan...
2e89628605e74ff9d5290f51242a54b222024_03_21T14_56_42_122397+00_00assistantaction_requestaction{\"action_request\": [{\"action\": \"action_multipl...
3de110f1a9701e14edf36f2468cb25d4c2024_03_21T14_56_42_123636+00_00assistantaction_responseassistant{\"action_response\": {\"function\": \"multiply\", \"...
48d909f901f759ed6f8ea03c29aa1e7572024_03_21T14_56_42_124209+00_00assistantaction_responseassistant{\"action_response\": {\"function\": \"multiply\", \"...
\n", + "
" + ], + "text/plain": [ + " node_id timestamp \\\n", + "0 5fa9518a05bc0ef2823354be2f39d70b 2024_03_21T14_56_37_679951+00_00 \n", + "1 93b4abe29a0ce393cbb410b8edc52d16 2024_03_21T14_56_38_575615+00_00 \n", + "2 e89628605e74ff9d5290f51242a54b22 2024_03_21T14_56_42_122397+00_00 \n", + "3 de110f1a9701e14edf36f2468cb25d4c 2024_03_21T14_56_42_123636+00_00 \n", + "4 8d909f901f759ed6f8ea03c29aa1e757 2024_03_21T14_56_42_124209+00_00 \n", + "\n", + " role sender recipient \\\n", + "0 system system assistant \n", + "1 user user assistant \n", + "2 assistant action_request action \n", + "3 assistant action_response assistant \n", + "4 assistant action_response assistant \n", + "\n", + " content \n", + "0 {\"system_info\": \"you are asked to perform as a... \n", + "1 {\"instruction\": \"Think step by step, understan... \n", + "2 {\"action_request\": [{\"action\": \"action_multipl... \n", + "3 {\"action_response\": {\"function\": \"multiply\", \"... \n", + "4 {\"action_response\": {\"function\": \"multiply\", \"... " + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "calc.executable.branch.messages" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"system_info\": \"you are asked to perform as a function picker and parameter provider\"}\n", + "{\"instruction\": \"Think step by step, understand the following basic math question and provide parameters for function calling.\", \"context\": \"{\\\"Question1\\\": \\\"A school is ordering laptops for its students. If each classroom has 25 students and the school wants to provide a laptop for each student in its 8 classrooms, how many laptops in total does the school need to order?\\\", \\\"question2\\\": \\\"A bakery sells cupcakes in boxes of 6. If a customer wants to buy enough cupcakes for a party of 48 people, with each person getting one cupcake, how many boxes of cupcakes does the customer need to buy?\\\"}\"}\n", + "{\"action_request\": [{\"action\": \"action_multiply\", \"arguments\": \"{\\\"number1\\\": 25, \\\"number2\\\": 8}\"}, {\"action\": \"action_multiply\", \"arguments\": \"{\\\"number1\\\": 48, \\\"number2\\\": 1}\"}]}\n", + "{\"action_response\": {\"function\": \"multiply\", \"arguments\": {\"number1\": 25, \"number2\": 8}, \"output\": 200}}\n", + "{\"action_response\": {\"function\": \"multiply\", \"arguments\": {\"number1\": 48, \"number2\": 1}, \"output\": 48}}\n" + ] + } + ], + "source": [ + "for content in calc.executable.branch.messages[\"content\"]:\n", + " print(content)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/lion_direct.ipynb b/notebooks/lion_direct.ipynb index 6d66bb838..f9a57f0d9 100644 --- a/notebooks/lion_direct.ipynb +++ b/notebooks/lion_direct.ipynb @@ -65,11 +65,11 @@ ], "source": [ "await direct.predict(\n", - " sentence, \n", - " num_sentences=2, \n", - " reason=True, \n", - " confidence_score=True, \n", - " retry_kwargs={\"timeout\": 15}\n", + " sentence,\n", + " num_sentences=2,\n", + " reason=True,\n", + " confidence_score=True,\n", + " retry_kwargs={\"timeout\": 15},\n", ")" ] }, @@ -97,7 +97,7 @@ } ], "source": [ - "choices=[\"funny\", \"catch\", \"apple\", \"deep ocean\"]\n", + "choices = [\"funny\", \"catch\", \"apple\", \"deep ocean\"]\n", "\n", "await direct.select(sentence, choices=choices)" ] @@ -122,12 +122,12 @@ ], "source": [ "await direct.select(\n", - " sentence, \n", - " choices=choices, \n", - " num_choices=2, \n", - " objective='find most weird for the setup', \n", - " reason=True, \n", - " confidence_score=True, \n", + " sentence,\n", + " choices=choices,\n", + " num_choices=2,\n", + " objective=\"find most weird for the setup\",\n", + " reason=True,\n", + " confidence_score=True,\n", " temperature=0.45,\n", ")" ] @@ -179,12 +179,12 @@ ], "source": [ "await direct.score(\n", - " sentence, \n", - " instruction=\"rate weirdness\", \n", - " reason=True, \n", - " score_range=(1,100), \n", - " num_digit=1, \n", - " confidence_score=True\n", + " sentence,\n", + " instruction=\"rate weirdness\",\n", + " reason=True,\n", + " score_range=(1, 100),\n", + " num_digit=1,\n", + " confidence_score=True,\n", ")" ] }, @@ -232,7 +232,7 @@ } ], "source": [ - "await direct.sentiment(sentence, to_type='num')" + "await direct.sentiment(sentence, to_type=\"num\")" ] }, { @@ -252,7 +252,7 @@ } ], "source": [ - "await direct.sentiment(sentence, choices=[\"awesome 😎\", \"hmmm 🤔\", \"mehhh 😭\" ])" + "await direct.sentiment(sentence, choices=[\"awesome 😎\", \"hmmm 🤔\", \"mehhh 😭\"])" ] } ],