From 1e437e38cb0214650dadaad73feffb1b49d17cc3 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 12 Nov 2024 23:49:35 +0000 Subject: [PATCH 1/6] Changes to gc outline feedback config -- update instructions for turns and for not providing outline itself. Update logic in doc agent to track turns of step and pass info to gc artifact --- .../gc_draft_outline_feedback_config.py | 73 ++++++++--- .../assistant/agents/document_agent.py | 117 +++++++++++++++--- 2 files changed, 155 insertions(+), 35 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document/gc_draft_outline_feedback_config.py b/assistants/prospector-assistant/assistant/agents/document/gc_draft_outline_feedback_config.py index 7034a47f..70128c60 100644 --- a/assistants/prospector-assistant/assistant/agents/document/gc_draft_outline_feedback_config.py +++ b/assistants/prospector-assistant/assistant/agents/document/gc_draft_outline_feedback_config.py @@ -17,40 +17,73 @@ # It can also be thought of as a working memory for the agent. # We allow any valid Pydantic BaseModel class to be used. class ArtifactModel(BaseModel): - final_response: str = Field(description="The final response from the agent to the user.") - conversation_status: str = Field(description="The status of the conversation.") - user_decision: str = Field(description="The decision of the user on what should happen next.") + final_response: str = Field( + description="The final response from the agent to the user. You will update this field." + ) + conversation_status: str = Field( + description="The status of the conversation. May be user_initiated, user_returned, or " + "user_completed. You are only allowed to update this field to user_completed, otherwise you will not update it." + ) + user_decision: str = Field( + description="The decision of the user on what should happen next. May be update_outline or " + "draft_paper. You will update this field." + ) filenames: str = Field( - description="Names of the available files currently uploaded as attachments. Information from the content of these files was used to help draft the outline under review." + description="Names of the available files currently uploaded as attachments. Information " + "from the content of these files was used to help draft the outline under review. You " + "CANNOT change this field." + ) + current_outline: str = Field( + description="The most up-to-date version of the outline under review. You CANNOT change this field." ) - current_outline: str = Field(description="The most up-to-date version of the outline under review.") # Rules - These are the do's and don'ts that the agent should follow during the conversation. rules = [ - "Do NOT rewrite or update the outline, even if the user asks you to." - "You are ONLY allowed to help the user decide on any changes to the outline or answer questions about writing an outline." + "Do NOT rewrite or update the outline, even if the user asks you to.", + "Do NOT show the outline, unless the user asks you to.", + ( + "You are ONLY allowed to help the user decide on any changes to the outline or answer questions " + "about writing an outline." + ), + ( + "You are only allowed to update conversation_status to user_completed. All other values for that field" + " will be preset." + ), + ( + "If the conversation_status is marked as user_completed, the final_response cannot be left as " + "Unanswered. The final_response must be set based on the conversation flow instructions." + ), "Terminate the conversation immediately if the user asks for harmful or inappropriate content.", - "If the conversation_status is marked as user_complete, the final_response cannot be left as 'Unanswered'. The final_response must be set based on the conversation flow instructions.", ] # Conversation Flow (optional) - This defines in natural language the steps of the conversation. -conversation_flow = """1. Start by asking the user to review the drafted outline. -2. Answer any questions about the outline or the drafting process the user might want to explore. -3. At any time, if the user asks for a change to the outline or updates the attachment file list, the conversation_status must be -marked as user_completed. The user_decision must be marked as update_outline. The final_response must inform the user that a new outline is being generated based off the new info or request. -4. At any time, if the user is good with the outline in its current form and ready to move on to drafting a paper from it, the conversation_status must be -marked as user_completed. The user_decision must be marked as draft_paper. The final_response must inform the user that you will start drafting the beginning of the -document based on this outline. +conversation_flow = """1. Start by asking the user to review the outline. The outline will have +already been provided to the user. You do not provide the outline yourself unless the user +specifically asks for it from you. +2. Answer any questions about the outline or the drafting process the user inquires about. +3. Use the following logic to fill in the artifact fields: +a. At any time, if the user asks for a change to the outline, the conversation_status must be +marked as user_completed. The user_decision must be marked as update_outline. The final_response +must inform the user that a new outline is being generated based off the request. +b. At any time, if the user has provided new attachments (detected via the filenames in the artifact), +the conversation_status must be marked as user_completed. The user_decision must be marked as +update_outline. The final_response must inform the user that a new outline is being generated based +on the addition of new attachments. +c. At any time, if the user is good with the outline in its current form and ready to move on to +drafting a paper from it, the conversation_status must be marked as user_completed. The +user_decision must be marked as draft_paper. The final_response must inform the user that you will +start drafting the beginning of the document based on this outline. """ # Context (optional) - This is any additional information or the circumstances the agent is in that it should be aware of. # It can also include the high level goal of the conversation if needed. -context = """You are working with a user on drafting an outline. The current drafted outline is provided, along with any filenames -that were used to help draft the outline. You do not have access to the content within the filenames that were used to help draft the outline. - Your purpose here is to help the user decide on any changes to the outline they might want or answer questions about it. This may be the first time - the user is asking for you help, or the nth time. Please use the conversation history provided to determine if you should be give an initial greeting - to the user or continuing the draft outline process.""" +context = """You are working with a user on drafting an outline. The current drafted outline is +provided, along with any filenames that were used to help draft the outline. You do not have access +to the content within the filenames that were used to help draft the outline. Your purpose here is +to help the user decide on any changes to the outline they might want or answer questions about it. +This may be the first time the user is asking for you help (conversation_status is user_initiated), +or the nth time (conversation_status is user_returned).""" # Resource Constraints (optional) - This defines the constraints on the conversation such as time or turns. diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 7a06b68e..4d446367 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -88,7 +88,7 @@ class Mode(BaseModel): name: ModeName = ModeName.UNDEFINED status: Status = Status.UNDEFINED step: Step = Step() - step_order: list[StepName] = [] + step_order: list[dict[str, StepName | int]] = [{}] def _error_check(self) -> None: # name and status should either both be UNDEFINED or both be defined. Always. @@ -140,10 +140,10 @@ def set_step(self, step: Step) -> None: def get_step(self) -> Step: return self.step - def set_step_order(self, steps: list[StepName]) -> None: + def set_step_order(self, steps: list[dict[str, StepName | int]]) -> None: self.step_order = steps - def get_step_order(self) -> list[StepName]: + def get_step_order(self) -> list[dict[str, StepName | int]]: return self.step_order def get_next_step(self) -> Step | None: @@ -158,8 +158,12 @@ def get_next_step(self) -> Step | None: for index, step in enumerate(steps[:-1]): if step is step_name: - next_step_name = steps[index + 1] - break + next_step_name = steps[index + 1].get("step_name") + if isinstance(next_step_name, StepName): + break + else: + next_step_name = StepName.UNDEFINED + break return Step(name=next_step_name, status=Status.INITIATED) @@ -404,6 +408,39 @@ async def _run_mode( logger.info("Document Agent in step: %s", step_name) step_status, next_step_name = await step_method(config, context, message, metadata) + # Update run_count of step (This will need to be simplified--moved into its own function) + step_list = self._state.mode.get_step_order() + for step in step_list: + list_step_name = step.get("step_name") + if isinstance(list_step_name, StepName): + if list_step_name is step_name: + # This is bad... "run_count" and "step_name" dependent on implementation in a different function. Need to cleanup. + step_run_count = step.get("run_count") + if isinstance(step_run_count, int): + step_run_count += 1 + step["run_count"] = step_run_count + self._state.mode.set_step_order(step_list) + break # done + else: + logger.error( + "Document Agent - step %s in step order does run_count not of type int.", step_name + ) + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status() # problem + else: + # End of list + if step is step_list[-1]: + logger.error("Document Agent - step %s not found in step order.", step_name) + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status() # problem + else: + logger.error("Document Agent - step_name of wrong type") + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status() # problem + match step_status: # resulting status of step_method() case Status.NOT_COMPLETED: self._state.mode.get_step().set_status(step_status) @@ -492,14 +529,18 @@ async def _mode_draft_outline( if mode_status is Status.INITIATED: self._state.mode.set_step_order( [ - StepName.DO_GC_ATTACHMENT_CHECK, - StepName.DO_DRAFT_OUTLINE, - StepName.DO_GC_GET_OUTLINE_FEEDBACK, - StepName.DO_FINISH, + {"step_name": StepName.DO_GC_ATTACHMENT_CHECK, "run_count": 0}, + {"step_name": StepName.DO_DRAFT_OUTLINE, "run_count": 0}, + {"step_name": StepName.DO_GC_GET_OUTLINE_FEEDBACK, "run_count": 0}, + {"step_name": StepName.DO_FINISH, "run_count": 0}, ], ) logger.info("Document Agent mode (%s) at beginning.", mode_name) - first_step_name = self._state.mode.get_step_order()[0] + first_step_name = self._state.mode.get_step_order()[0].get("step_name") + if not isinstance(first_step_name, StepName): + logger.error("Document Agent: StepName could not be found in Mode's step order.") + self._state.mode.reset() + return self._state.mode.get_status() self._state.mode.set_step(Step(name=first_step_name, status=Status.INITIATED)) self._write_state(context) @@ -546,14 +587,18 @@ async def _mode_draft_paper( if mode_status is Status.INITIATED: self._state.mode.set_step_order( [ - StepName.DO_GC_ATTACHMENT_CHECK, - StepName.DO_DRAFT_OUTLINE, - StepName.DO_GC_GET_OUTLINE_FEEDBACK, - StepName.DP_DRAFT_CONTENT, + {"step_name": StepName.DO_GC_ATTACHMENT_CHECK, "run_count": 0}, + {"step_name": StepName.DO_DRAFT_OUTLINE, "run_count": 0}, + {"step_name": StepName.DO_GC_GET_OUTLINE_FEEDBACK, "run_count": 0}, + {"step_name": StepName.DP_DRAFT_CONTENT, "run_count": 0}, ], ) logger.info("Document Agent mode (%s) at beginning.", mode_name) - first_step_name = self._state.mode.get_step_order()[0] + first_step_name = self._state.mode.get_step_order()[0].get("step_name") + if not isinstance(first_step_name, StepName): + logger.error("Document Agent: StepName could not be found in Mode's step order.") + self._state.mode.reset() + return self._state.mode.get_status() self._state.mode.set_step(Step(name=first_step_name, status=Status.INITIATED)) self._write_state(context) @@ -909,6 +954,12 @@ async def _gc_outline_feedback( ) -> tuple[Status, StepName | None]: method_metadata_key = "document_agent_gc_outline_feedback" + # Pre-requisites + if self._state is None: + logger.error("Document Agent state is None. Returning.") + return Status.UNDEFINED, StepName.UNDEFINED + + # Run if message is not None: user_message = message.content else: @@ -924,6 +975,40 @@ async def _gc_outline_feedback( ) # update artifact + # This step info approach is not cool. Rewriting code. Need to refactor. + step_name = self._state.mode.get_step().get_name() + step_list = self._state.mode.get_step_order() + for step in step_list: + list_step_name = step.get("step_name") + if isinstance(list_step_name, StepName): + if list_step_name is step_name: + # This is bad... "run_count" and "step_name" dependent on implementation in a different function. Need to cleanup. + step_run_count = step.get("run_count") + else: + # End of list + if step is step_list[-1]: + logger.error("Document Agent - step %s not found in step order.", step_name) + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status(), StepName.UNDEFINED # problem + else: + logger.error("Document Agent - step_name of wrong type") + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status(), StepName.UNDEFINED # problem + + if not isinstance(step_run_count, int): + logger.error("Document Agent - step %s in step order does run_count not of type int.", step_name) + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status(), StepName.UNDEFINED # problem + else: + match step_run_count: + case 0: + conversation_status_str = "user_initiated" + case _: + conversation_status_str = "user_returned" + filenames = await self._attachments_extension.get_attachment_filenames( context, config=config.agents_config.attachment_agent ) @@ -935,8 +1020,10 @@ async def _gc_outline_feedback( artifact_dict = guided_conversation.get_artifact_dict() if artifact_dict is not None: + artifact_dict["conversation_status"] = conversation_status_str artifact_dict["filenames"] = filenames_str artifact_dict["current_outline"] = outline_str + guided_conversation.set_artifact_dict(artifact_dict) else: logger.error("artifact_dict unavailable.") From 3b5a4834cab1c95a0a356fd21bd1db62ed4fb744 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Wed, 13 Nov 2024 20:10:38 +0000 Subject: [PATCH 2/6] Fixes in run --- .../agents/document/guided_conversation.py | 6 ++++- .../assistant/agents/document_agent.py | 27 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py index 8430d4ad..e7e97038 100644 --- a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py +++ b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py @@ -154,7 +154,11 @@ async def step_conversation( # should be returning str and Status for Document Agent to consume. Update doc agent logic accordingly. status: Status = Status.UNDEFINED if conversation_status is not None: - if conversation_status == "Unanswered": + if ( + conversation_status == "Unanswered" + or conversation_status == "user_initiated" # highly coupled to config ... + or conversation_status == "user_returned" + ): if result.ai_message is not None: response = result.ai_message else: diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 4d446367..2717af5a 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -157,15 +157,25 @@ def get_next_step(self) -> Step | None: return None # on final step for index, step in enumerate(steps[:-1]): - if step is step_name: - next_step_name = steps[index + 1].get("step_name") - if isinstance(next_step_name, StepName): - break - else: - next_step_name = StepName.UNDEFINED - break + current_step_name = step.get("step_name") + if isinstance(current_step_name, StepName): + if current_step_name is step_name: + next_step_name = steps[index + 1].get("step_name") + if isinstance(next_step_name, StepName): + status = Status.INITIATED + break + else: + logger.error("step_name not found in step of step_list") + next_step_name = StepName.UNDEFINED + status = Status.UNDEFINED + break + else: + logger.error("step_name not found in step of step_list") + next_step_name = StepName.UNDEFINED + status = Status.UNDEFINED + break - return Step(name=next_step_name, status=Status.INITIATED) + return Step(name=next_step_name, status=status) class State(BaseModel): @@ -984,6 +994,7 @@ async def _gc_outline_feedback( if list_step_name is step_name: # This is bad... "run_count" and "step_name" dependent on implementation in a different function. Need to cleanup. step_run_count = step.get("run_count") + break else: # End of list if step is step_list[-1]: From 5d66ff1063c30680eb689de66ed4c528461a7d55 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Wed, 13 Nov 2024 20:39:17 +0000 Subject: [PATCH 3/6] Makes default execution path for mode_draft_outline in gc helper code (decoupling to occur later). Address final fixes. --- .../agents/document/guided_conversation.py | 3 ++- .../assistant/agents/document_agent.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py index e7e97038..a638eac8 100644 --- a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py +++ b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py @@ -176,7 +176,8 @@ async def step_conversation( elif user_decision == "draft_paper": status = Status.USER_COMPLETED next_step_name = ( - StepName.DP_DRAFT_CONTENT + StepName.DO_FINISH # temp for mode_draft_outline. + # StepName.DP_DRAFT_CONTENT ) # problem if in draft outline mode... that is supposed to go to DO_FINISH. # coupling is now a problem. and Need to fix the two locations for setting the branching/flow. else: diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 2717af5a..967cca78 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -151,10 +151,18 @@ def get_next_step(self) -> Step | None: if len(steps) == 0: return None + # logic below is repetitive... needs clean up step = self.get_step() step_name = step.get_name() - if step_name is steps[-1]: - return None # on final step + current_step_name = steps[-1].get("step_name") + if isinstance(current_step_name, StepName): + if current_step_name is step_name: + return None # on final step + else: + logger.error("step_name is not StepName instance") + next_step_name = StepName.UNDEFINED + status = Status.UNDEFINED + return Step(name=next_step_name, status=status) for index, step in enumerate(steps[:-1]): current_step_name = step.get("step_name") @@ -170,7 +178,7 @@ def get_next_step(self) -> Step | None: status = Status.UNDEFINED break else: - logger.error("step_name not found in step of step_list") + logger.error("step_name is not StepName instance") next_step_name = StepName.UNDEFINED status = Status.UNDEFINED break From d28a532f2230e348ca51c6fded4cfd5cfcb1f6af Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Thu, 14 Nov 2024 00:24:50 +0000 Subject: [PATCH 4/6] Changes to allow mode selection for assistant + routing to correct mode upon new conversation or message --- .../assistant/agents/document_agent.py | 113 ++++++++++-------- .../prospector-assistant/assistant/chat.py | 79 ++++++------ .../prospector-assistant/assistant/config.py | 10 +- 3 files changed, 108 insertions(+), 94 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 967cca78..c8c2a700 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -283,6 +283,7 @@ def _get_step_method(self, step: Step | None) -> Callable | None: return None return self._step_name_to_method.get(step.name) + # Not currently used async def receive_command( self, config: AssistantConfigModel, @@ -311,7 +312,7 @@ def _set_mode_draft_outline( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> None: # Pre-requisites @@ -328,11 +329,12 @@ def _set_mode_draft_outline( self._state.mode = Mode(name=ModeName.DRAFT_OUTLINE, status=Status.INITIATED) self._write_state(context) + # Not currently used def _set_mode_draft_paper( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> None: # Pre-requisites @@ -349,11 +351,11 @@ def _set_mode_draft_paper( self._state.mode = Mode(name=ModeName.DRAFT_PAPER, status=Status.INITIATED) self._write_state(context) - async def respond_to_conversation( + async def create_document( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> bool: self._state = self._read_state(context) @@ -364,11 +366,17 @@ async def respond_to_conversation( return False mode = self._state.mode + current_mode_name = mode.get_name() + correct_mode_name = ModeName.DRAFT_OUTLINE # Will update if not mode.is_running(): + self._set_mode_draft_outline( + config, context, message, metadata + ) # Will update this mode as implementation expands to full document. + elif current_mode_name is not correct_mode_name: logger.warning( - "Document Agent must be running in a mode to respond. Current mode: %s and status: %s", - mode.get_name(), - mode.get_status(), + "Document Agent not in correct mode. Returning. Current mode: %s Correct mode: %s", + current_mode_name, + correct_mode_name, ) return mode.is_running() @@ -404,7 +412,7 @@ async def _run_mode( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> Status: # Pre-requisites @@ -518,7 +526,7 @@ async def _mode_draft_outline( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> Status: # Pre-requisites @@ -576,7 +584,7 @@ async def _mode_draft_paper( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> Status: # Pre-requisites @@ -634,7 +642,7 @@ async def _step_gc_attachment_check( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: next_step = None @@ -675,7 +683,7 @@ async def _step_draft_outline( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: next_step = None @@ -715,7 +723,7 @@ async def _step_gc_get_outline_feedback( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: next_step_name = None @@ -763,7 +771,7 @@ async def _step_finish( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: # pretend completed @@ -773,7 +781,7 @@ async def _step_draft_content( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: next_step = None @@ -819,7 +827,7 @@ async def _gc_attachment_check( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: method_metadata_key = "document_agent_gc_attachment_check" @@ -848,8 +856,12 @@ async def _gc_attachment_check( # run guided conversation step try: + if message is None: + user_message = None + else: + user_message = message.content response_message, conversation_status, next_step_name = await guided_conversation.step_conversation( - last_user_message=message.content, + last_user_message=user_message, ) # add the completion to the metadata for debugging @@ -893,16 +905,18 @@ async def _draft_outline( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: method_metadata_key = "draft_outline" - # get conversation related info - conversation = await context.get_messages(before=message.id) - if message.message_type == MessageType.chat: - conversation.messages.append(message) - participants_list = await context.get_participants(include_inactive=True) + # get conversation related info -- for now, if no message, assuming no prior conversation + conversation = None + if message is not None: + conversation = await context.get_messages(before=message.id) + if message.message_type == MessageType.chat: + conversation.messages.append(message) + participants_list = await context.get_participants(include_inactive=True) # get attachments related info attachment_messages = await self._attachments_extension.get_completion_messages_for_attachments( @@ -918,9 +932,10 @@ async def _draft_outline( # create chat completion messages chat_completion_messages: list[ChatCompletionMessageParam] = [] chat_completion_messages.append(_draft_outline_main_system_message()) - chat_completion_messages.append( - _chat_history_system_message(conversation.messages, participants_list.participants) - ) + if conversation is not None: + chat_completion_messages.append( + _chat_history_system_message(conversation.messages, participants_list.participants) + ) chat_completion_messages.extend(attachment_messages) if outline is not None: chat_completion_messages.append(_outline_system_message(outline)) @@ -948,9 +963,9 @@ async def _draft_outline( # store only latest version for now (will keep all versions later as need arises) (storage_directory_for_context(context) / "document_agent/outline.txt").write_text(message_content) - # send the response to the conversation only if from a command. Otherwise return info to caller. + # send a command response to the conversation only if from a command. Otherwise return a normal chat message. message_type = MessageType.chat - if message.message_type == MessageType.command: + if message is not None and message.message_type == MessageType.command: message_type = MessageType.command await context.send_messages( @@ -978,11 +993,6 @@ async def _gc_outline_feedback( return Status.UNDEFINED, StepName.UNDEFINED # Run - if message is not None: - user_message = message.content - else: - user_message = None - gc_outline_feedback_config: GuidedConversationConfigModel = GCDraftOutlineFeedbackConfigModel() guided_conversation = GuidedConversation( @@ -1049,6 +1059,10 @@ async def _gc_outline_feedback( # run guided conversation step try: + if message is None: + user_message = None + else: + user_message = message.content response_message, conversation_status, next_step_name = await guided_conversation.step_conversation( last_user_message=user_message, ) @@ -1094,16 +1108,18 @@ async def _draft_content( self, config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> tuple[Status, StepName | None]: method_metadata_key = "draft_content" - # get conversation related info - conversation = await context.get_messages(before=message.id) - if message.message_type == MessageType.chat: - conversation.messages.append(message) - participants_list = await context.get_participants(include_inactive=True) + # get conversation related info -- for now, if no message, assuming no prior conversation + conversation = None + if message is not None: + conversation = await context.get_messages(before=message.id) + if message.message_type == MessageType.chat: + conversation.messages.append(message) + participants_list = await context.get_participants(include_inactive=True) # get attachments related info attachment_messages = await self._attachments_extension.get_completion_messages_for_attachments( @@ -1113,9 +1129,10 @@ async def _draft_content( # create chat completion messages chat_completion_messages: list[ChatCompletionMessageParam] = [] chat_completion_messages.append(_draft_content_main_system_message()) - chat_completion_messages.append( - _chat_history_system_message(conversation.messages, participants_list.participants) - ) + if conversation is not None: + chat_completion_messages.append( + _chat_history_system_message(conversation.messages, participants_list.participants) + ) chat_completion_messages.extend(attachment_messages) # get outline related info @@ -1139,12 +1156,12 @@ async def _draft_content( "response_format": {"type": "text"}, } completion = await client.chat.completions.create(**completion_args) - content = completion.choices[0].message.content + message_content = completion.choices[0].message.content _on_success_metadata_update(metadata, method_metadata_key, config, chat_completion_messages, completion) except Exception as e: logger.exception(f"exception occurred calling openai chat completion: {e}") - content = ( + message_content = ( "An error occurred while calling the OpenAI API. Is it configured correctly?" "View the debug inspector for more information." ) @@ -1152,16 +1169,16 @@ async def _draft_content( if content is not None: # store only latest version for now (will keep all versions later as need arises) - (storage_directory_for_context(context) / "document_agent/content.txt").write_text(content) + (storage_directory_for_context(context) / "document_agent/content.txt").write_text(message_content) - # send the response to the conversation only if from a command. Otherwise return info to caller. + # send a command response to the conversation only if from a command. Otherwise return a normal chat message. message_type = MessageType.chat - if message.message_type == MessageType.command: + if message is not None and message.message_type == MessageType.command: message_type = MessageType.command await context.send_messages( NewConversationMessage( - content=content, + content=message_content, message_type=message_type, metadata=metadata, ) diff --git a/assistants/prospector-assistant/assistant/chat.py b/assistants/prospector-assistant/assistant/chat.py index 2e90cd1e..2e142195 100644 --- a/assistants/prospector-assistant/assistant/chat.py +++ b/assistants/prospector-assistant/assistant/chat.py @@ -114,22 +114,11 @@ async def on_message_created( await legacy.provide_guidance_if_necessary(context) -is_doc_agent_running = False - - @assistant.events.conversation.message.command.on_created async def on_command_message_created( context: ConversationContext, event: ConversationEvent, message: ConversationMessage ) -> None: - config = await assistant_config.get(context.assistant) - metadata: dict[str, Any] = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}} - - # config.agents_config.document_agent.enabled = True # To do... tie into config. - global is_doc_agent_running - is_doc_agent_running = True - - doc_agent = DocumentAgent(attachments_extension) - await doc_agent.receive_command(config, context, message, metadata) + pass @assistant.events.conversation.message.chat.on_created @@ -151,24 +140,16 @@ async def on_chat_message_created( # update the participant status to indicate the assistant is thinking async with send_error_message_on_exception(context), context.set_status("thinking..."): - config = await assistant_config.get(context.assistant) - - metadata: dict[str, Any] = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}} - # # NOTE: we're experimenting with agents, if they are enabled, use them to respond to the conversation # + config = await assistant_config.get(context.assistant) + metadata: dict[str, Any] = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}} - # if config.agents_config.document_agent.enabled: # To do... tie into config. - global is_doc_agent_running - if is_doc_agent_running: - is_doc_agent_running = await document_agent_respond_to_conversation(config, context, message, metadata) - return - - await form_fill_execute(context, message) - - # # Prospector assistant response - # await respond_to_conversation(context, config, message, metadata) + if config.guided_workflow == "Form Completion": + await form_fill_execute(context, message) + else: # "Document Creation" + await create_document_execute(config, context, message, metadata) background_tasks: set[asyncio.Task] = set() @@ -179,32 +160,41 @@ async def on_conversation_created(context: ConversationContext) -> None: """ Handle the event triggered when the assistant is added to a conversation. """ - assistant_sent_messages = await context.get_messages(participant_ids=[context.assistant.id], limit=1) welcome_sent_before = len(assistant_sent_messages.messages) > 0 if welcome_sent_before: return - task = asyncio.create_task(welcome_message(context)) + # + # NOTE: we're experimenting with agents, if they are enabled, use them to respond to the conversation + # + config = await assistant_config.get(context.assistant) + metadata: dict[str, Any] = {"debug": {}} + + if config.guided_workflow == "Form Completion": + task = asyncio.create_task(welcome_message_form_fill(context)) + else: # "Document Creation" + task = asyncio.create_task(welcome_message_create_document(config, context, message=None, metadata=metadata)) + background_tasks.add(task) task.add_done_callback(background_tasks.remove) - # send a welcome message to the conversation - # welcome_message = config.welcome_message - # await context.send_messages( - # NewConversationMessage( - # content=welcome_message, - # message_type=MessageType.chat, - # metadata={"generated_content": False}, - # ) - # ) - -async def welcome_message(context: ConversationContext) -> None: +async def welcome_message_form_fill(context: ConversationContext) -> None: async with send_error_message_on_exception(context), context.set_status("thinking..."): await form_fill_execute(context, None) +async def welcome_message_create_document( + config: AssistantConfigModel, + context: ConversationContext, + message: ConversationMessage | None, + metadata: dict[str, Any], +) -> None: + async with send_error_message_on_exception(context), context.set_status("thinking..."): + await create_document_execute(config, context, message, metadata) + + @asynccontextmanager async def send_error_message_on_exception(context: ConversationContext): try: @@ -271,23 +261,22 @@ async def get(filename: str) -> str: # -# region Response +# region document agent extension helpers # -async def document_agent_respond_to_conversation( +async def create_document_execute( config: AssistantConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, -) -> bool: +) -> None: """ Respond to a conversation message using the document agent. """ # create the document agent instance document_agent = DocumentAgent(attachments_extension) - is_doc_agent_running = await document_agent.respond_to_conversation(config, context, message, metadata) - return is_doc_agent_running + await document_agent.create_document(config, context, message, metadata) # demonstrates how to respond to a conversation message using the OpenAI API. diff --git a/assistants/prospector-assistant/assistant/config.py b/assistants/prospector-assistant/assistant/config.py index 354cca29..f751eb78 100644 --- a/assistants/prospector-assistant/assistant/config.py +++ b/assistants/prospector-assistant/assistant/config.py @@ -1,4 +1,4 @@ -from typing import Annotated +from typing import Annotated, Literal import openai_client from assistant_extensions.attachments import AttachmentsConfigModel @@ -117,6 +117,14 @@ class RequestConfig(BaseModel): # the workbench app builds dynamic forms based on the configuration model and UI schema class AssistantConfigModel(BaseModel): + guided_workflow: Annotated[ + Literal["Form Completion", "Document Creation"], + Field( + title="Guided Workflow", + description="The workflow extension to guide this conversation.", + ), + ] = "Form Completion" + enable_debug_output: Annotated[ bool, Field( From f53a6f71ed7400a6bc3fdc33cde2a144c3b861cf Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Thu, 14 Nov 2024 00:58:01 +0000 Subject: [PATCH 5/6] small cleanup in chat. fix mode read in doc agent --- .../assistant/agents/document_agent.py | 1 + .../prospector-assistant/assistant/chat.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index c8c2a700..7ee486be 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -381,6 +381,7 @@ async def create_document( return mode.is_running() # Run + mode = self._state.mode logger.info("Document Agent in mode %s", mode.get_name()) mode_method = self._get_mode_method(mode) if mode_method: diff --git a/assistants/prospector-assistant/assistant/chat.py b/assistants/prospector-assistant/assistant/chat.py index 2e142195..a8734579 100644 --- a/assistants/prospector-assistant/assistant/chat.py +++ b/assistants/prospector-assistant/assistant/chat.py @@ -114,13 +114,6 @@ async def on_message_created( await legacy.provide_guidance_if_necessary(context) -@assistant.events.conversation.message.command.on_created -async def on_command_message_created( - context: ConversationContext, event: ConversationEvent, message: ConversationMessage -) -> None: - pass - - @assistant.events.conversation.message.chat.on_created async def on_chat_message_created( context: ConversationContext, event: ConversationEvent, message: ConversationMessage @@ -146,10 +139,13 @@ async def on_chat_message_created( config = await assistant_config.get(context.assistant) metadata: dict[str, Any] = {"debug": {"content_safety": event.data.get(content_safety.metadata_key, {})}} - if config.guided_workflow == "Form Completion": - await form_fill_execute(context, message) - else: # "Document Creation" - await create_document_execute(config, context, message, metadata) + match config.guided_workflow: + case "Form Completion": + await form_fill_execute(context, message) + case "Document Creation": + await create_document_execute(config, context, message, metadata) + case _: + logger.error("Guided workflow unknown or not supported.") background_tasks: set[asyncio.Task] = set() From 945d0c1121b4292b59f480789ff48d2b6d806960 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Thu, 14 Nov 2024 19:05:47 +0000 Subject: [PATCH 6/6] updates welcome message to match statement. tested. --- assistants/prospector-assistant/assistant/chat.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/assistants/prospector-assistant/assistant/chat.py b/assistants/prospector-assistant/assistant/chat.py index a8734579..c0226407 100644 --- a/assistants/prospector-assistant/assistant/chat.py +++ b/assistants/prospector-assistant/assistant/chat.py @@ -167,10 +167,15 @@ async def on_conversation_created(context: ConversationContext) -> None: config = await assistant_config.get(context.assistant) metadata: dict[str, Any] = {"debug": {}} - if config.guided_workflow == "Form Completion": - task = asyncio.create_task(welcome_message_form_fill(context)) - else: # "Document Creation" - task = asyncio.create_task(welcome_message_create_document(config, context, message=None, metadata=metadata)) + match config.guided_workflow: + case "Form Completion": + task = asyncio.create_task(welcome_message_form_fill(context)) + case "Document Creation": + task = asyncio.create_task( + welcome_message_create_document(config, context, message=None, metadata=metadata) + ) + case _: + logger.error("Guided workflow unknown or not supported.") background_tasks.add(task) task.add_done_callback(background_tasks.remove)