diff --git a/chatflock/base.py b/chatflock/base.py index 90ce01e..4a62c9b 100644 --- a/chatflock/base.py +++ b/chatflock/base.py @@ -209,7 +209,6 @@ def render_new_chat_message(self, chat: "Chat", message: ChatMessage) -> None: class GeneratedChatComposition: participants: Sequence[ChatParticipant] participants_interaction_schema: str - termination_condition: str class ChatCompositionGenerator(abc.ABC): @@ -217,9 +216,9 @@ class ChatCompositionGenerator(abc.ABC): def generate_composition_for_chat( self, chat: "Chat", + goal: str, composition_suggestion: Optional[str] = None, - participants_interaction_schema: Optional[str] = None, - termination_condition: Optional[str] = None, + interaction_schema: Optional[str] = None, ) -> GeneratedChatComposition: raise NotImplementedError() @@ -227,7 +226,6 @@ def generate_composition_for_chat( class Chat: backing_store: ChatDataBackingStore renderer: ChatRenderer - goal: str name: Optional[str] = None max_total_messages: Optional[int] = None hide_messages: bool = False @@ -238,7 +236,6 @@ def __init__( renderer: ChatRenderer, initial_participants: Optional[Sequence[ChatParticipant]] = None, name: Optional[str] = None, - goal: str = "This is a regular chatroom, the goal is to just have a conversation.", max_total_messages: Optional[int] = None, hide_messages: bool = False, ): @@ -247,7 +244,6 @@ def __init__( self.backing_store = backing_store self.renderer = renderer - self.goal = goal self.name = name self.hide_messages = hide_messages self.max_total_messages = max_total_messages diff --git a/chatflock/composition_generators/__init__.py b/chatflock/composition_generators/__init__.py index f3d14cf..21d51ad 100644 --- a/chatflock/composition_generators/__init__.py +++ b/chatflock/composition_generators/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Optional, Union +from typing import List, Literal, Optional from pydantic import BaseModel, Field @@ -6,20 +6,20 @@ class IndividualParticipantToAddSchema(BaseModel): type: Literal["individual"] name: str = Field( - description="Name of the participant to add. Generate a creative name that fits the role and mission. You can " + description="Name of the individual to add. Generate a creative name that fits the role and mission. You can " "use play on words, stereotypes, or any other way you want to be original." ) role: str = Field(description='Role of the participant to add. Title like "CEO" or "CTO", for example.') mission: str = Field( - description="Personal mission of the participant to add. Should be a detailed " "mission statement." + description="Personal mission of the individual to add. Should be a detailed mission statement." ) symbol: str = Field( - description="A unicode symbol of the participant to add (for presentation in chat). This " + description="A unicode symbol of the individual to add (for presentation in chat). This " "needs to reflect the role." ) tools: Optional[List[str]] = Field( - description="List of useful tools that the participant should have access to in order to achieve their goal. " - "Must be one of the available tools given as input. Do not give tools if you think the participant " + description="List of useful tools that an individual should have access to in order to achieve their goal. " + "Must be one of the available tools given as input. Do not give tools if you think the individual " "should not have access to any tools or non of the available tools are useful for the goal." ) @@ -27,43 +27,14 @@ def __str__(self): return f"{self.symbol} {self.name} ({self.role})" -class TeamParticipantToAddSchema(BaseModel): - type: Literal["team"] - name: str = Field(description="Name of the team to add.") - mission: str = Field(description="Mission of the team to add. Should be a detailed mission statement.") - composition_suggestion: str = Field( - description="List of roles of individual participants or names of sub-teams that are suggested to achieve the " - "sub-mission set." +class CreateTeamCompositionForGoalOutputSchema(BaseModel): + team_composition: List[IndividualParticipantToAddSchema] = Field( + description="List of members that make up the team. Must include the fixed members." ) - symbol: str = Field(description="A unicode symbol of the team to add (for presentation in chat).") - - def __str__(self): - return f"{self.symbol} {self.name}" - - -class ManageParticipantsOutputSchema(BaseModel): - participants_to_remove: List[str] = Field(description="List of participants to be removed.") - participants_to_add: List[Union[IndividualParticipantToAddSchema, TeamParticipantToAddSchema]] = Field( - description="List of participants (individuals and teams) to be added. DO NOT include individual participants " - "that are a part of a sub-team. The sub-team will handle the composition based on the suggestion. " - 'For example, if the sub-team "Development Team" is suggested to be added, do not include ' - "individual participants within that team even if they are mentioned. Instead, suggest the team " - "composition within the team definition.", - examples=[ - '[{"type": "individual", "name": "John Doe", "role": "CEO", "mission": "Lead the company.", "symbol": "🤵"},' - '{"type": "team", "name": "Development Team", "mission": "Develop the product.", ' - '"composition_suggestion": "The team should include a Team Leader, Software Engineer, QA Engineer, ' - 'and a Software Architect", "symbol": "🛠️"}]' - ], - ) - updated_speaker_interaction_schema: str = Field( - description="An updated (or new) version of the original interaction schema to better reflect how to achieve " - "the goal." - ) - updated_termination_condition: str = Field( - description="An updated (or new) version of the termination condition to better reflect the achievement of the " - "goal." + interaction_schema: str = Field( + description="A member interaction schema that includes the phases, " + "how members should interact with each other to achieve the goal, etc." ) -__all__ = ["IndividualParticipantToAddSchema", "TeamParticipantToAddSchema", "ManageParticipantsOutputSchema"] +__all__ = ["IndividualParticipantToAddSchema", "CreateTeamCompositionForGoalOutputSchema"] diff --git a/chatflock/composition_generators/langchain.py b/chatflock/composition_generators/langchain.py index e4c9eab..14308a8 100644 --- a/chatflock/composition_generators/langchain.py +++ b/chatflock/composition_generators/langchain.py @@ -6,14 +6,10 @@ from langchain.tools import BaseTool from chatflock.ai_utils import execute_chat_model_messages -from chatflock.backing_stores import InMemoryChatDataBackingStore from chatflock.base import ActiveChatParticipant, Chat, ChatCompositionGenerator, GeneratedChatComposition -from chatflock.composition_generators import ManageParticipantsOutputSchema -from chatflock.conductors import LangChainBasedAIChatConductor +from chatflock.composition_generators import CreateTeamCompositionForGoalOutputSchema from chatflock.parsing_utils import string_output_to_pydantic -from chatflock.participants.internal_group import InternalGroupBasedChatParticipant from chatflock.participants.langchain import LangChainBasedAIChatParticipant -from chatflock.renderers import TerminalChatRenderer from chatflock.structured_string import Section, StructuredString @@ -21,21 +17,21 @@ class LangChainBasedAIChatCompositionGenerator(ChatCompositionGenerator): def __init__( self, chat_model: BaseChatModel, + fixed_team_members: Optional[List[ActiveChatParticipant]] = None, generator_tools: Optional[List[BaseTool]] = None, participant_available_tools: Optional[List[BaseTool]] = None, chat_model_args: Optional[Dict[str, Any]] = None, spinner: Optional[Halo] = None, n_output_parsing_tries: int = 3, - prefer_critics: bool = False, generate_composition_extra_args: Optional[Dict[str, Any]] = None, ): self.chat_model = chat_model self.chat_model_args = chat_model_args or {} + self.fixed_team_members = fixed_team_members or [] self.generator_tools = generator_tools self.participant_available_tools = participant_available_tools self.spinner = spinner self.n_output_parsing_tries = n_output_parsing_tries - self.prefer_critics = prefer_critics self.generate_composition_extra_args = generate_composition_extra_args or {} self.participant_tool_names_to_tools = {tool.name: tool for tool in self.participant_available_tools or []} @@ -43,45 +39,28 @@ def __init__( def generate_composition_for_chat( self, chat: Chat, + goal: str, composition_suggestion: Optional[str] = None, - participants_interaction_schema: Optional[str] = None, - termination_condition: Optional[str] = None, + interaction_schema: Optional[str] = None, ) -> GeneratedChatComposition: if composition_suggestion is None: composition_suggestion = self.generate_composition_extra_args.get("composition_suggestion", None) - if participants_interaction_schema is None: - participants_interaction_schema = self.generate_composition_extra_args.get( - "participants_interaction_schema", None - ) - - if termination_condition is None: - termination_condition = self.generate_composition_extra_args.get("termination_condition", None) - - create_internal_chat = self.generate_composition_extra_args.get("create_internal_chat", None) - if create_internal_chat is None: - - def create_internal_chat(**kwargs: Any) -> Chat: - return Chat( - name=kwargs.get("name", None), - goal=kwargs.get("goal", None), - backing_store=InMemoryChatDataBackingStore(), - renderer=TerminalChatRenderer(), - ) + if interaction_schema is None: + interaction_schema = self.generate_composition_extra_args.get("participants_interaction_schema", None) if self.spinner is not None: - self.spinner.start(text="The Chat Composition Generator is creating a new chat composition...") + self.spinner.start(text="The Chat Composition Generator is creating a team composition for the goal...") # Ask the AI to select the next speaker. messages = [ - SystemMessage(content=self.create_compose_chat_participants_system_prompt(chat=chat)), + SystemMessage(content=self.create_compose_team_system_prompt()), HumanMessage( - content=self.create_compose_chat_participants_first_human_prompt( - chat=chat, + content=self.create_compose_team_first_human_prompt( + goal=goal, participant_available_tools=self.participant_available_tools, composition_suggestion=composition_suggestion, - participants_interaction_schema=participants_interaction_schema, - termination_condition=termination_condition, + participants_interaction_schema=interaction_schema, ) ), ] @@ -91,276 +70,144 @@ def create_internal_chat(**kwargs: Any) -> Chat: output = string_output_to_pydantic( output=result, chat_model=self.chat_model, - output_schema=ManageParticipantsOutputSchema, + output_schema=CreateTeamCompositionForGoalOutputSchema, n_tries=self.n_output_parsing_tries, spinner=self.spinner, hide_message=False, ) - participants_to_add_names = [str(participant) for participant in output.participants_to_add] - participants_to_remove_names = [participant_name for participant_name in output.participants_to_remove] + participants_to_add_names = [str(participant) for participant in output.team_composition] if self.spinner is not None: name = "The Chat Composition Generator" if chat.name is not None: name = f"{name} ({chat.name})" - if len(output.participants_to_remove) == 0 and len(output.participants_to_add) == 0: - self.spinner.succeed(text=f"{name} has decided to keep the current chat composition.") - elif len(output.participants_to_remove) > 0 and len(output.participants_to_add) == 0: - self.spinner.succeed( - text=f"{name} has decided to remove the following participants: " - f'{", ".join(participants_to_remove_names)}' - ) - elif len(output.participants_to_remove) == 0 and len(output.participants_to_add) > 0: - self.spinner.succeed( - text=f"{name} has decided to add the following participants: " - f'{", ".join(participants_to_add_names)}' - ) + if len(output.team_composition) == 0: + self.spinner.succeed(text=f"{name} has decided the existing team composition is satisfactory.") else: self.spinner.succeed( - text=f"{name} has decided to remove the following participants: " - f'{", ".join(participants_to_remove_names)} and add the following participants: ' + text=f"{name} has decided to add the following participants: " f'{", ".join(participants_to_add_names)}' ) - participants: List[ActiveChatParticipant] = [ - p for p in chat.get_active_participants() if p.name not in output.participants_to_remove - ] + participants: Dict[str, ActiveChatParticipant] = {p.name: p for p in self.fixed_team_members} - for participant in output.participants_to_add: - if participant.type == "individual": - participant_tools: List[BaseTool] = [ - self.participant_tool_names_to_tools[tool_name] - for tool_name in participant.tools or [] - if tool_name in self.participant_tool_names_to_tools - ] - chat_participant: ActiveChatParticipant = LangChainBasedAIChatParticipant( - name=participant.name, - role=participant.role, - personal_mission=participant.mission, - tools=participant_tools, - symbol=participant.symbol, - chat_model=self.chat_model, - spinner=self.spinner, - chat_model_args=self.chat_model_args, - ) - else: - chat_participant = InternalGroupBasedChatParticipant( - group_name=participant.name, - mission=participant.mission, - chat=create_internal_chat( - name=participant.name if chat.name is None else f"{chat.name} > {participant.name}", - goal=participant.mission, + for participant in output.team_composition: + if participant.name in participants: + continue + + participant_tools: List[BaseTool] = [ + self.participant_tool_names_to_tools[tool_name] + for tool_name in participant.tools or [] + if tool_name in self.participant_tool_names_to_tools + ] + chat_participant: ActiveChatParticipant = LangChainBasedAIChatParticipant( + name=participant.name, + role=participant.role, + personal_mission=participant.mission, + tools=participant_tools, + symbol=participant.symbol, + chat_model=self.chat_model, + spinner=self.spinner, + chat_model_args=self.chat_model_args, + other_prompt_sections=[ + Section( + name="Chat Main Goal", + text=goal or "No explicit goal provided.", ), - chat_conductor=LangChainBasedAIChatConductor( - chat_model=self.chat_model, - chat_model_args=self.chat_model_args, - spinner=self.spinner, - composition_generator=LangChainBasedAIChatCompositionGenerator( - chat_model=self.chat_model, - chat_model_args=self.chat_model_args, - spinner=self.spinner, - n_output_parsing_tries=self.n_output_parsing_tries, - generate_composition_extra_args=dict( - composition_suggestion=participant.composition_suggestion - ), - ), + Section( + name="Chat Participant Interaction Schema", + text=interaction_schema or "Not provided. Use your best judgement.", ), - spinner=self.spinner, - ) + ], + ) - participants.append(chat_participant) + participants[chat_participant.name] = chat_participant return GeneratedChatComposition( - participants=participants, - participants_interaction_schema=output.updated_speaker_interaction_schema, - termination_condition=output.updated_termination_condition, + participants=list(participants.values()), + participants_interaction_schema=output.interaction_schema, ) - def create_compose_chat_participants_system_prompt(self, chat: "Chat") -> str: - active_participants = chat.get_active_participants() - - adding_participants = [ - "Add participants based on their potential contribution to the goal.", - "You can either add individual participants or entire teams." - 'If you add an individual participant, generate a name, role (succinct title like "Writer", "Developer", ' - "etc.), and personal mission for each new participant such that they can contribute to the goal the best " - "they can, each in their complementary own way.", - "If you add a team, generate a name, and a mission for the team, in the same way.", - "Always try to add or complete a comprehensive composition of participants that have " - "orthogonal and complementary specialties, skills, roles, and missions (whether they are teams or " - "individuals). You may not necessarily have the option to change this composition later, so make sure " - "you summon the right participants.", - ] - - if self.prefer_critics: - adding_participants.append( - "Since most participants you summon will not be the best experts in the world, even though they think " - "they are, they will be to be overseen. For that, most tasks will require at least 2 experts, " - "one doing a task and the other that will act as a critic to that expert; they can loop back and " - "forth and iterate on a better answer. For example, instead of having a Planner only, have a Planner " - "and a Plan Critic participants to have this synergy. You can skip critics for the most trivial tasks." - ) - + def create_compose_team_system_prompt(self) -> str: system_message = StructuredString( sections=[ Section( name="Mission", - text="Evaluate the chat conversation based on the INPUT. " - "Make decisions about adding or removing participants based on their potential contribution " - "towards achieving the goal. Update the interaction schema and the termination condition " - "to reflect changes in participants and the goal.", + text="Create a team capable of accomplishing objectives through effective communication and the " + "use of tools, focusing on the essential elements of the objectives.", ), Section( - name="Process", + name="Goal Simplification", list=[ - "Think about the ideal composition of participants that can contribute to the goal in a " - "step-by-step manner by looking at all the inputs.", - "Assess if the current participants are sufficient for ideally contributing to the goal.", - "If insufficient, summon additional participants (or teams) as needed.", - "If some participants (individuals or teams) are unnecessary, remove them.", - "Update the interaction schema and termination condition to accommodate changes in " - "participants.", + "Distill the main objective into clear, simple, and actionable components.", + "Ensure each component is addressable via verbal or tool-based actions.", ], list_item_prefix=None, ), Section( - name="Participants Composition", + name="Team Assembly", sub_sections=[ Section( - name="Adding Participants", - list=adding_participants, - sub_sections=[ - Section( - name="Team-based Participants", - list=[ - "For very difficult tasks, you may need to summon a team (as a participant) " - "instead of an individual to work together to achieve a sub-goal, similar to " - "actual companies of people.", - "This team will contain a group of internal individual (or even sub-teams) " - "participants. Do not worry about the team's composition at this point.", - "Include a team name, mission, and a composition suggestion for the members of " - "the team (could be individuals or more teams again). Ensure the suggestion " - "contains an indication of whether a participant is an individual or a team. " - 'Format like: "Name: ...\nMission: ...\nComposition Suggestion: ' - 'NAME (individual), NAME (individual), NAME (Team), NAME (Team)..."', - ], - ), - Section( - name="Naming Individual Participants", - list=[ - "Generate a creative name that fits the role and mission.", - "You can use play on words, stereotypes, or any other way you want to be " - "original.", - 'For example: "CEO" -> "Maximilian Power", "CTO" -> "Nova Innovatus"', - ], - ), - Section( - name="Naming Team-based Participants", - list=[ - "In contrast to individual participants, you should name teams based only on " - "their mission and composition. Do not be creative here.", - 'For example: "Development Team", "Marketing Team"', - ], - ), - Section( - name="Giving Tools to Participants", - list=[ - "Only individual participants can be given tools.", - "You must only choose a tool from the AVAILABLE PARTICIPANT TOOLS list.", - "A tools should be given to a participant only if it can help them fulfill " - "their personal mission better.", - ], - ), - Section( - name="Correct Hierarchical Composition", - list=[ - "If you add a team, make sure to add the team as a participant, and not its " - "individual members." - ], - ), - ], - ), - Section( - name="Removing Participants", - list=[ - "Remove participants only if they cannot contribute to the goal or fit into the " - "interaction schema.Ignore past performance. Focus on the participant's potential" - "contribution to the goal and their fit into the interaction schema.", - ], - ), - Section( - name="Order of Participants", - list=[ - "The order of participants is important. It should be the order in which they " - "should be summoned.", - "The first participant will be regarded to as the leader of the group at times, " - "so make sure to choose the right one to put first.", - ], - ), - Section( - name="Orthogonality of Participants", + name="Team Selection", list=[ - "Always strive to have participants with orthogonal skills, roles, and specialties. " - "That includes personal missions, as well.", - "Shared skills and missions is a waste of resources. Aim for maximum coverage of " - "skills, roles, specialities and missions.", + "Select individuals with the necessary conversational abilities and tool proficiency.", + "Allocate roles and tasks that correspond to the objective's components.", + "Invent role-based monikers for team members.", + "Include all the fixed team members in the new team.", ], ), Section( - name="Composition Suggestion", - text="If provided by the user, take into account the " - "composition suggestion when deciding how to add/remove.", + name="Skill Diversity", + list=["Assemble a skill set that spans all aspects of the objective."], ), ], ), Section( - name="Updating The Speaker Interaction Schema", + name="Interaction Outline", list=[ - "Update the interaction schema to accommodate changes in participants.", - "The interaction schema should provide guidelines for a chat manager on how to coordinate the " - "participants to achieve the goal. Like an algorithm for choosing the next speaker in the " - "conversation.", - "The goal of the chat (if provided) must be included in the interaction schema. The whole " - "purpose of the interaction schema is to help achieve the goal.", - "It should be very clear how the process goes and when it should end.", - "The interaction schema should be simple, concise, and very focused on the goal. Formalities " - "should be avoided, unless they are necessary for achieving the goal.", - "If the chat goal has some output (like an answer), make sure to have the last step be the " - "presentation of the final answer by one of the participants as a final message to the chat.", + "Design a blueprint for team interactions, dialogue flow, and tool application for each " + "component of the objective.", + "Incorporate stages, interaction patterns, contingency plans, and success metrics.", ], ), Section( - name="Updating The Termination Condition", + name="Tool Distribution", + list=["Match tools from the provided inventory to team members to facilitate their tasks."], + ), + Section( + name="Input Requirements", list=[ - "Update the termination condition to accommodate changes in participants.", - "The termination condition should be a simple, concise, and very focused on the goal.", - "The chat should terminate when the goal is achieved, or when it is clear that the goal " - "cannot be achieved.", + "Required: Objective or task description.", + "Optional: Proposals for interaction outline, team structure, and tool inventory.", ], ), Section( - name="Input", + name="Output Details", list=[ - "Goal for the conversation", - "Previous messages from the conversation", - "Current speaker interaction schema (Optional)", - "Current termination condition (Optional)", - "Composition suggestion (Optional)", - "Available participant tools (Optional)", + "Recommend a team with designated roles, tasks, and tools.", + "Provide an interaction outline to guide the achievement of the objective.", ], ), Section( - name="Output", - text="The output can be compressed, as it will not be used by a human, but by an AI. It should " - "include:", - list=[ - "Participants to Remove: List of participants to be removed (if any).", - "Participants to Add: List of participants to be added, with their name, role, team (if " - "applicable), and personal (or team) mission, and tools (if applicable).", - "Updated Interaction Schema: An updated version of the original interaction schema.", - "Updated Termination Condition: An updated version of the original termination condition.", + name="Output Structure", + sub_sections=[ + Section( + name="Objective Breakdown", + text="Itemize the main objective into practical tasks for communication and tool use.", + ), + Section( + name="Team Roster", + text="Enumerate team members with identifiers, roles, and tasks. " + "Format: - emojii (role): mission (tools)", + ), + Section( + name="Interaction Outline", + text="Offer a systematic outline with phases, detailing team interactions, discussion " + "points, tool usage, steps for each task, and criteria for concluding efforts, " + "including objective fulfillment and user-directed stoppage.", + ), ], ), ] @@ -368,50 +215,36 @@ def create_compose_chat_participants_system_prompt(self, chat: "Chat") -> str: return str(system_message) - def create_compose_chat_participants_first_human_prompt( + def create_compose_team_first_human_prompt( self, - chat: Chat, + goal: str, participant_available_tools: Optional[List[BaseTool]] = None, composition_suggestion: Optional[str] = None, participants_interaction_schema: Optional[str] = None, - termination_condition: Optional[str] = None, ) -> str: - messages = chat.get_messages() - messages_list = [f"- {message.sender_name}: {message.content}" for message in messages] - available_tools_list = list({f'{x.name}: "{x.description}"' for x in participant_available_tools or []}) - active_participants = chat.get_active_participants() - prompt = StructuredString( sections=[ - Section(name="Chat Goal", text=chat.goal or "No explicit chat goal provided."), - Section( - name="Currently Active Participants", list=[str(participant) for participant in active_participants] - ), + Section(name="Goal", text=goal or "No explicit goal provided."), Section( - name="Current Speaker Interaction Schema", + name="Suggested Interaction Schema", text=participants_interaction_schema or "Not provided. Use your best judgement.", ), Section( - name="Current Termination Condition", - text=termination_condition or "Not provided. Use your best judgement.", + name="Suggested Composition", + text=composition_suggestion + or "Not provided. Use the goal and other inputs to come up with a good composition.", ), Section( - name="Composition Suggestion", - text=composition_suggestion - or "Not provided. Use the goal and other inputs to come up with a " "good composition.", + name="Fixed Team Members", + list=[f"{participant.detailed_str()}" for participant in self.fixed_team_members], ), Section( - name="Available Participant Tools", + name="Tool Inventory", text="No tools available." if len(available_tools_list) == 0 else None, list=available_tools_list if len(available_tools_list) > 0 else [], ), - Section( - name="Chat Messages", - text="No messages yet." if len(messages_list) == 0 else None, - list=messages_list if len(messages_list) > 0 else [], - ), ] ) diff --git a/chatflock/conductors/langchain.py b/chatflock/conductors/langchain.py index fe8bf29..88469a4 100644 --- a/chatflock/conductors/langchain.py +++ b/chatflock/conductors/langchain.py @@ -12,16 +12,12 @@ class LangChainBasedAIChatConductor(ChatConductor): - default_termination_condition: str = f"""Terminate the chat on the following conditions: - - When the goal of the chat has been achieved - - If one of the participants asks you to terminate it or has finished their sentence with "TERMINATE".""" - def __init__( self, chat_model: BaseChatModel, + goal: str = "No explicit goal provided.", composition_generator: Optional[ChatCompositionGenerator] = None, - participants_interaction_schema: Optional[str] = None, - termination_condition: Optional[str] = None, + interaction_schema: Optional[str] = None, retriever: Optional[BaseRetriever] = None, spinner: Optional[Halo] = None, tools: Optional[List[BaseTool]] = None, @@ -29,11 +25,11 @@ def __init__( ): self.chat_model = chat_model self.chat_model_args = chat_model_args or {} + self.goal = goal self.tools = tools self.retriever = retriever self.composition_generator = composition_generator - self.participants_interaction_schema = participants_interaction_schema - self.termination_condition = termination_condition or self.default_termination_condition + self.interaction_schema = interaction_schema self.spinner = spinner self.composition_initialized = False @@ -51,7 +47,7 @@ def create_next_speaker_system_prompt(self, chat: "Chat") -> str: Section( name="Mission", text="Select the next speaker in the conversation based on the previous messages in the " - "conversation and an optional SPEAKER INTERACTION SCHEMA. If it seems to you that the chat " + "conversation and an optional INTERACTION SCHEMA. If it seems to you that the chat " "should end instead of selecting a next speaker, terminate it.", ), Section(name="Rules", list=["You can only select one of the participants in the group chat."]), @@ -59,18 +55,18 @@ def create_next_speaker_system_prompt(self, chat: "Chat") -> str: name="Process", list=[ "Look at the last message in the conversation and determine who should speak next based on the " - "SPEAKER INTERACTION SCHEMA, if provided.", - "If based on TERMINATION CONDITION you determine that the chat should end, you should return the " - "string TERMINATE instead of a participant name.", - "If there is only one participant either choose them or terminate the chat, " - "based on the termination condition.", + "INTERACTION SCHEMA, if provided.", + "If you determine that the chat should end, you should return the " + "string TERMINATE instead of a participant name. For example, when the goal has been achieved, " + ", it is impossible to reach, or if the user asks to terminate the chat.", ], ), Section( name="Input", list=[ "Chat goal", - "Currently active participants in the conversation" "Speaker interaction schema", + "Currently active participants in the conversation", + "Speaker interaction schema", "Previous messages from the conversation", ], ), @@ -97,7 +93,7 @@ def create_next_speaker_system_prompt(self, chat: "Chat") -> str: return str(system_message) - def create_next_speaker_first_human_prompt(self, chat: "Chat") -> str: + def create_next_speaker_first_human_prompt(self, chat: "Chat", goal: str) -> str: messages = chat.get_messages() messages_list = [f"- {message.sender_name}: {message.content}" for message in messages] @@ -105,15 +101,14 @@ def create_next_speaker_first_human_prompt(self, chat: "Chat") -> str: prompt = StructuredString( sections=[ - Section(name="Chat Goal", text=chat.goal or "No explicit chat goal provided."), + Section(name="Goal", text=goal or "No explicit goal provided."), Section( name="Currently Active Participants", list=[f"{str(participant)}" for participant in participants] ), Section( - name="Speaker Interaction Schema", - text=self.participants_interaction_schema or "Not provided. Use your best judgement.", + name="Interaction Schema", + text=self.interaction_schema or "Not provided. Use your best judgement.", ), - Section(name="Termination Condition", text=self.termination_condition), Section( name="Chat Messages", text="No messages yet." if len(messages_list) == 0 else None, @@ -130,9 +125,9 @@ def prepare_chat(self, chat: "Chat", **kwargs: Any) -> None: composition_suggestion = kwargs.get("composition_suggestion", None) new_composition = self.composition_generator.generate_composition_for_chat( chat=chat, + goal=self.goal, composition_suggestion=composition_suggestion, - participants_interaction_schema=self.participants_interaction_schema, - termination_condition=self.termination_condition, + interaction_schema=self.interaction_schema, ) # Sync participants with the new composition. @@ -149,8 +144,7 @@ def prepare_chat(self, chat: "Chat", **kwargs: Any) -> None: if participant.name not in new_participants_names: chat.remove_participant(participant) - self.participants_interaction_schema = new_composition.participants_interaction_schema - self.termination_condition = new_composition.termination_condition + self.interaction_schema = new_composition.participants_interaction_schema self.composition_initialized = True @@ -170,7 +164,7 @@ def select_next_speaker(self, chat: Chat) -> Optional[ActiveChatParticipant]: # Ask the AI to select the next speaker. messages = [ SystemMessage(content=self.create_next_speaker_system_prompt(chat=chat)), - HumanMessage(content=self.create_next_speaker_first_human_prompt(chat=chat)), + HumanMessage(content=self.create_next_speaker_first_human_prompt(chat=chat, goal=self.goal)), ] result = self.execute_messages(messages=messages) diff --git a/chatflock/parsing_utils.py b/chatflock/parsing_utils.py index b02b5bc..8937bb3 100644 --- a/chatflock/parsing_utils.py +++ b/chatflock/parsing_utils.py @@ -71,7 +71,6 @@ def chat_messages_to_pydantic( pass parser_chat = Chat( - goal="Convert the chat contents to a valid and logical JSON.", backing_store=InMemoryChatDataBackingStore(messages=list(chat_messages)), renderer=NoChatRenderer(), initial_participants=[text_to_json_ai, json_parser], diff --git a/chatflock/participants/__init__.py b/chatflock/participants/__init__.py index 7f19f7f..6cc7fc8 100644 --- a/chatflock/participants/__init__.py +++ b/chatflock/participants/__init__.py @@ -1,11 +1,9 @@ -from .internal_group import InternalGroupBasedChatParticipant from .langchain import LangChainBasedAIChatParticipant from .output_parser import JSONOutputParserChatParticipant from .spr import SPRWriterChatParticipant from .user import UserChatParticipant __all__ = [ - "InternalGroupBasedChatParticipant", "LangChainBasedAIChatParticipant", "JSONOutputParserChatParticipant", "UserChatParticipant", diff --git a/chatflock/participants/internal_group.py b/chatflock/participants/internal_group.py deleted file mode 100644 index cbbe35b..0000000 --- a/chatflock/participants/internal_group.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import Any, Optional - -from halo import Halo - -from chatflock.base import ActiveChatParticipant, Chat, ChatConductor -from chatflock.structured_string import Section, StructuredString -from chatflock.use_cases.request_response import get_response - - -class InternalGroupBasedChatParticipant(ActiveChatParticipant): - inner_chat_conductor: ChatConductor - inner_chat: Chat - mission: str - spinner: Optional[Halo] = None - clear_inner_chat_before_responding: bool = False - - def __init__( - self, - group_name: str, - chat: Chat, - mission: str, - chat_conductor: ChatConductor, - clear_inner_chat_before_responding: bool = False, - spinner: Optional[Halo] = None, - **kwargs: Any, - ) -> None: - self.inner_chat = chat - self.inner_chat_conductor = chat_conductor - self.clear_inner_chat_before_responding = clear_inner_chat_before_responding - self.mission = mission - self.spinner = spinner - - # Make sure the inner chat is aligned - self.inner_chat.name = group_name - self.inner_chat.goal = self.mission - - super().__init__(name=group_name, **kwargs) - - # Make sure the chat & conductor are initialized, as it may be a dynamic chat with - # no participants yet. - self.inner_chat_conductor.prepare_chat(chat=self.inner_chat) - - def respond_to_chat(self, chat: "Chat") -> str: - if self.clear_inner_chat_before_responding: - self.inner_chat.clear_messages() - - prev_spinner_text = None - if self.spinner is not None: - prev_spinner_text = self.spinner.text - self.spinner.stop_and_persist(symbol="👥", text=f"{self.name}'s group started a discussion.") - self.spinner.start(text=f"{self.name}'s group is discussing...") - - messages = chat.get_messages() - conversation_str = "\n".join([f"- {message.sender_name}: {message.content}" for message in messages]) - - leader = self.inner_chat.get_active_participants()[0] - - request_for_group, _ = get_response( - query="Please translate the request for yourself in the external conversation into a collaboration " - "request for your internal group. This is the external conversation:" - f"\n```{conversation_str}```\n\nThe group should understand exactly what to discuss, what to " - "decide on, and how to respond back based on this. ", - answerer=leader, - ) - - group_response = self.inner_chat_conductor.initiate_dialog( - chat=self.inner_chat, initial_message=request_for_group - ) - - if self.spinner is not None: - self.spinner.succeed(text=f"{self.name}'s group discussion was concluded.") - if prev_spinner_text is not None: - self.spinner.start(text=prev_spinner_text) - - messages = self.inner_chat.get_messages() - group_response_conversation_str = "\n".join( - [f"- {message.sender_name}: {message.content}" for message in messages] - ) - - leader_response_back, _ = get_response( - query=str( - StructuredString( - sections=[ - Section(name="External Conversation", text=conversation_str), - Section(name="Internal Group Conversation", text=group_response_conversation_str), - Section( - name="Task", - text="You are a part of the EXTERNAL CONVERSATION and need to respond back. " - "You and your group have collaborated on a response back for the EXTERNAL CONVERSATION. " - "Please transform the INTERNAL GROUP CONVERSATION into a proper," - "in-context response back (in your name) for the EXTERNAL CONVERSATION; it should be " - "mainly based on the conclusion of the internal conversation. Your response" - "will be sent to the EXTERNAL CONVERSATION verbatim.", - ), - ] - ) - ), - answerer=leader, - ) - - return leader_response_back - - def __str__(self) -> str: - active_participants = self.inner_chat.get_active_participants() - - if len(active_participants) > 0: - names = [str(p) for p in active_participants] - - return f'{self.name} (Includes: {", ".join(names)})' - - return self.name - - def detailed_str(self, level: int = 0) -> str: - prefix = " " * level - - participants = self.inner_chat.get_active_participants() - members_str = "\n\n".join([p.detailed_str(level=level + 1) for p in participants]) - return ( - f'{prefix}- Name: {self.name}\n{prefix} Symbol: {self.symbol}\n{prefix} Mission: "{self.mission}"' - f"\n{members_str}" - ) diff --git a/chatflock/participants/langchain.py b/chatflock/participants/langchain.py index 2c5f36a..938406e 100644 --- a/chatflock/participants/langchain.py +++ b/chatflock/participants/langchain.py @@ -101,7 +101,6 @@ def create_system_message(self, chat: "Chat", relevant_docs: Sequence[Document]) name="Chat", sub_sections=[ Section(name="Name", text=chat.name or "No name provided. Just a general chat."), - Section(name="Goal", text=chat.goal or "No explicit chat goal provided."), Section( name="Participants", text="\n".join( diff --git a/examples/automatic_chat_simple_composition.py b/examples/automatic_chat_simple_composition.py index 22ae280..8ccafec 100644 --- a/examples/automatic_chat_simple_composition.py +++ b/examples/automatic_chat_simple_composition.py @@ -19,6 +19,9 @@ def automatic_simple_chat_composition(model: str = "gpt-4-1106-preview", tempera chat_conductor = LangChainBasedAIChatConductor( chat_model=chat_model, spinner=spinner, + # Set up a proper goal so the composition generator can use it to generate the composition that will best fit + goal="Come up with a plan for the user to invest their money. The goal is to maximize wealth over the " + "long-term, while minimizing risk.", # Pass in a composition generator to the conductor composition_generator=LangChainBasedAIChatCompositionGenerator( chat_model=chat_model, @@ -28,13 +31,10 @@ def automatic_simple_chat_composition(model: str = "gpt-4-1106-preview", tempera chat = Chat( backing_store=InMemoryChatDataBackingStore(), renderer=TerminalChatRenderer(), - # Set up a proper goal so the composition generator can use it to generate the composition that will best fit - goal="Come up with a plan for the user to invest their money. The goal is to maximize wealth over the " - "long-term, while minimizing risk.", initial_participants=[user], ) - # Not necessary in practice since initiation is done automatically when calling `initiate_chat_with_result`. + # Not necessary in practice since initiation is done automatically when calling `initiate_dialogue`. # However, this is needed to eagerly generate the composition. Default is lazy. chat_conductor.prepare_chat(chat=chat) diff --git a/examples/automatic_hierarchical_chat_composition.py b/examples/automatic_hierarchical_chat_composition.py index a1b651b..89d6022 100644 --- a/examples/automatic_hierarchical_chat_composition.py +++ b/examples/automatic_hierarchical_chat_composition.py @@ -29,37 +29,32 @@ def create_default_backing_store() -> ChatDataBackingStore: else: return InMemoryChatDataBackingStore() - def create_chat(**kwargs: Any) -> Chat: - return Chat(backing_store=create_default_backing_store(), renderer=TerminalChatRenderer(), **kwargs) - spinner = Halo(spinner="dots") user = UserChatParticipant(name="User") chat_conductor = LangChainBasedAIChatConductor( chat_model=chat_model, spinner=spinner, + # Set up a proper goal so the composition generator can use it to generate the composition that will best fit + goal="The goal is to create the best website for the user.", # Pass in a composition generator to the conductor composition_generator=LangChainBasedAIChatCompositionGenerator( + fixed_team_members=[user], chat_model=chat_model, spinner=spinner, - generate_composition_extra_args=dict(create_internal_chat=create_chat), participant_available_tools=[CodeExecutionTool(executor=LocalCodeExecutor(), spinner=spinner)], ), ) - chat = create_chat( - # Set up a proper goal so the composition generator can use it to generate the composition that will best fit - goal="The goal is to create the best website for the user.", - initial_participants=[user], - ) + chat = Chat(backing_store=create_default_backing_store(), renderer=TerminalChatRenderer()) # It's not necessary in practice to manually call `initialize_chat` since initiation is done automatically - # when calling `initiate_chat_with_result`. However, this is needed to eagerly generate the composition. + # when calling `initiate_dialogue`. However, this is needed to eagerly generate the composition. # Default is lazy and will happen when the chat is initiated. chat_conductor.prepare_chat( chat=chat, # Only relevant when passing in a composition generator - composition_suggestion="DevCompany: Includes a CEO, Product Team, Marketing Team, and a Development " - "Department. The Development Department includes a Director, QA Team and Development " - "Team.", + # composition_suggestion="DevCompany: Includes a CEO, Product Team, Marketing Team, and a Development " + # "Department. The Development Department includes a Director, QA Team and Development " + # "Team.", ) print(f"\nGenerated composition:\n=================\n{chat.active_participants_str}\n=================\n\n") diff --git a/examples/automatic_internal_group_participant.py b/examples/automatic_internal_group_participant.py deleted file mode 100644 index 52af24c..0000000 --- a/examples/automatic_internal_group_participant.py +++ /dev/null @@ -1,55 +0,0 @@ -from dotenv import load_dotenv -from halo import Halo - -from chatflock.backing_stores import InMemoryChatDataBackingStore -from chatflock.base import Chat -from chatflock.composition_generators.langchain import LangChainBasedAIChatCompositionGenerator -from chatflock.conductors import LangChainBasedAIChatConductor -from chatflock.participants.internal_group import InternalGroupBasedChatParticipant -from chatflock.participants.user import UserChatParticipant -from chatflock.renderers import TerminalChatRenderer -from examples.common import create_chat_model - - -def automatic_internal_group_participant(model: str = "gpt-4-1106-preview", temperature: float = 0.0) -> None: - chat_model = create_chat_model(model=model, temperature=temperature) - - spinner = Halo(spinner="dots") - comedy_team = InternalGroupBasedChatParticipant( - group_name="Financial Team", - mission="Ensure the user's financial strategy maximizes wealth over the long term without too much risk.", - chat=Chat(backing_store=InMemoryChatDataBackingStore(), renderer=TerminalChatRenderer()), - chat_conductor=LangChainBasedAIChatConductor( - chat_model=chat_model, - spinner=spinner, - composition_generator=LangChainBasedAIChatCompositionGenerator(chat_model=chat_model, spinner=spinner), - ), - spinner=spinner, - ) - user = UserChatParticipant(name="User") - participants = [user, comedy_team] - - chat = Chat( - backing_store=InMemoryChatDataBackingStore(), renderer=TerminalChatRenderer(), initial_participants=participants - ) - - chat_conductor = LangChainBasedAIChatConductor( - participants_interaction_schema="The user should take the lead and go back and forth with the financial team," - " collaborating on the financial strategy. The user should be the one to " - "initiate the chat.", - chat_model=chat_model, - spinner=spinner, - ) - - # Not necessary in practice since initiation is done automatically when calling `initiate_chat_with_result`. - # However, this is needed to eagerly generate the composition. Default is lazy. - chat_conductor.prepare_chat(chat=chat) - print(f"\nGenerated composition:\n=================\n{chat.active_participants_str}\n=================\n\n") - - # You can also pass in a composition suggestion here. - result = chat_conductor.initiate_dialog(chat=chat) - print(result) - - -if __name__ == "__main__": - load_dotenv() diff --git a/examples/manual_internal_group_participant.py b/examples/manual_internal_group_participant.py deleted file mode 100644 index 303f07a..0000000 --- a/examples/manual_internal_group_participant.py +++ /dev/null @@ -1,68 +0,0 @@ -import typer -from dotenv import load_dotenv -from halo import Halo - -from chatflock.backing_stores import InMemoryChatDataBackingStore -from chatflock.base import Chat -from chatflock.conductors import LangChainBasedAIChatConductor, RoundRobinChatConductor -from chatflock.participants.internal_group import InternalGroupBasedChatParticipant -from chatflock.participants.langchain import LangChainBasedAIChatParticipant -from chatflock.participants.user import UserChatParticipant -from chatflock.renderers import TerminalChatRenderer -from examples.common import create_chat_model - - -def manual_internal_group_participant(model: str = "gpt-4-1106-preview", temperature: float = 0.0) -> None: - chat_model = create_chat_model(model=model, temperature=temperature) - - spinner = Halo(spinner="dots") - comedy_team = InternalGroupBasedChatParticipant( - group_name="Comedy Team", - mission="Collaborate on funny humour-filled responses based on the original request for the user", - chat=Chat( - backing_store=InMemoryChatDataBackingStore(), - renderer=TerminalChatRenderer(), - initial_participants=[ - LangChainBasedAIChatParticipant( - name="Bob", - role="Chief Comedian", - personal_mission="Take questions from the user and collaborate with " - "Tom to come up with a succinct funny (yet realistic) " - "response. Short responses are preferred.", - chat_model=chat_model, - spinner=spinner, - ), - LangChainBasedAIChatParticipant( - name="Tom", - role="Junior Comedian", - personal_mission="Collaborate with Bob to come up with a succinct " - "funny (yet realistic) response to the user. Short responses are preferred", - chat_model=chat_model, - spinner=spinner, - ), - ], - ), - chat_conductor=LangChainBasedAIChatConductor( - chat_model=chat_model, - spinner=spinner, - termination_condition="Once a humour-filled, succinct response is collaborated upon and agreed upon, " - "terminate the conversation.", - ), - spinner=spinner, - ) - user = UserChatParticipant(name="User") - participants = [user, comedy_team] - - chat = Chat( - backing_store=InMemoryChatDataBackingStore(), renderer=TerminalChatRenderer(), initial_participants=participants - ) - - chat_conductor = RoundRobinChatConductor() - - chat_conductor.initiate_dialog(chat=chat) - - -if __name__ == "__main__": - load_dotenv() - - typer.run(manual_internal_group_participant) diff --git a/examples/three_way_ai_conductor.py b/examples/three_way_ai_conductor.py index f993a9a..64180cd 100644 --- a/examples/three_way_ai_conductor.py +++ b/examples/three_way_ai_conductor.py @@ -45,15 +45,14 @@ def three_way_ai_conductor(model: str = "gpt-4-1106-preview", temperature: float chat_conductor = LangChainBasedAIChatConductor( chat_model=chat_model, spinner=spinner, + goal="Serve the user as best as possible.", # This tells the conductor how to select the next speaker - participants_interaction_schema="The User is a customer at a Cafe called 'Coffee Time'. " + interaction_schema="The User is a customer at a Cafe called 'Coffee Time'. " "The bartender should go first and greet the customer. " "When the user asks for food and orders something, the bartender should ask the cook to cook the food. " "There might be some conversation between the cook and bartender. " "The cook should then give the food to the bartender and the bartender should give the food to the user. " "The user should then eat the food and give feedback to the bartender. The cook should not talk to the user.", - # This tells the conductor when to stop the chat - termination_condition="When the user finds the food satisfactory.", ) chat_conductor.initiate_dialog(chat=chat)