From 1e437e38cb0214650dadaad73feffb1b49d17cc3 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 12 Nov 2024 23:49:35 +0000 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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/8] 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/8] 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) From 8831e5b73d80ae1561f18114af8714107c882a3c Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 19 Nov 2024 20:42:21 +0000 Subject: [PATCH 7/8] Update config for outline feedback with explicit logic instructions on greeting --- .../draft_grant_proposal_config_defaults.py | 155 ------------------ .../gc_draft_outline_feedback_config.py | 10 +- 2 files changed, 6 insertions(+), 159 deletions(-) delete mode 100644 assistants/prospector-assistant/assistant/agents/document/draft_grant_proposal_config_defaults.py diff --git a/assistants/prospector-assistant/assistant/agents/document/draft_grant_proposal_config_defaults.py b/assistants/prospector-assistant/assistant/agents/document/draft_grant_proposal_config_defaults.py deleted file mode 100644 index e891472f..00000000 --- a/assistants/prospector-assistant/assistant/agents/document/draft_grant_proposal_config_defaults.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import List - -from guided_conversation.utils.base_model_llm import BaseModelLLM -from guided_conversation.utils.resources import ResourceConstraint, ResourceConstraintMode, ResourceConstraintUnit -from pydantic import BaseModel, Field - -# Introduction -# This configuration defines a guided conversation for assisting users in drafting a comprehensive grant proposal. -# The goal is to gather all necessary information systematically, validating and categorizing it as required, -# without actually drafting the proposal. The assistant's task is to ensure all required sections of the grant proposal -# are filled with accurate and relevant information provided by the user. - - -# Define the Grant Proposal Artifact -class DocumentDetail(BaseModel): - section: str = Field(description="Section of the document.") - content: str = Field(description="Content extracted from the document.") - - -class BudgetItem(BaseModel): - category: str = Field(description="Category of the budget item.") - amount: float = Field(description="Amount allocated for this item.") - - -class TeamMember(BaseModel): - name: str = Field(description="Name of the team member.") - role: str = Field(description="Role of the team member in the project.") - - -class Milestone(BaseModel): - description: str = Field(description="Description of the milestone.") - date: str = Field(description="Date of the milestone.") - - -class ArtifactModel(BaseModelLLM): - # Grant Source Documents - grant_source_document_list: List[str] = Field(description="List of provided source documents.") - grant_requirements: List[DocumentDetail] = Field( - description="Detailed requirements extracted from the source documents." - ) - key_criteria: List[DocumentDetail] = Field(description="Important criteria and evaluation points for the grant.") - - # User Documents - user_document_list: List[str] = Field(description="List of provided user documents.") - extracted_details: List[DocumentDetail] = Field( - description="Extracted information categorized by the types of details needed." - ) - - # Project Information - project_title: str = Field(description="Title of the project.") - project_summary: str = Field(description="A brief summary of the project.") - project_objectives: List[DocumentDetail] = Field(description="Key objectives of the project.", default=[]) - project_methods: List[DocumentDetail] = Field(description="Methods and approaches to be used.", default=[]) - - # Budget - total_budget: float = Field(description="Total amount requested.") - budget_breakdown: List[BudgetItem] = Field(description="Detailed budget breakdown.") - - # Team - team_members: List[TeamMember] = Field(description="List of team members and their roles.") - - # Timeline - start_date: str = Field(description="Proposed start date.") - end_date: str = Field(description="Proposed end date.") - milestones: List[Milestone] = Field(description="Key milestones and their dates.") - - # Additional Information - additional_info: str = Field(description="Additional information from the user.") - - # Missing Information - missing_info: str = Field(description="Information that is still missing.") - - # Final Details - final_details: str = Field(description="Final details to complete the proposal.") - - -# Define the rules for the conversation -rules = [ - "Always ask clarifying questions if the information provided is ambiguous.", - "Do not make assumptions about the user's responses.", - "Ensure all required fields are filled before proceeding to the next step.", - "Politely remind the user to provide missing information.", - "Review all provided documents before requesting additional information.", - "Do not share user's documents with others.", - "Provide concise progress updates at key milestones or checkpoints, summarizing what has been collected and what " - "is still needed.", - "Limit responses to just what is needed to drive the next request from the user.", - "Ensure that the data entered into the artifact matches the information provided by the user without modification.", - "Ensure that all dates, amounts, and other data follow a consistent format as specified in the grant requirements.", - "If the user indicates that they will provide a response later, set a reminder or follow-up at the end of the " - "conversation.", - "Gracefully handle any errors or invalid inputs by asking the user to correct or rephrase their responses.", - "Prioritize critical information that is essential to the grant application before collecting additional details.", - "Only proceed to the next section once the current section is fully completed.", - "Provide a set of standardized responses or suggestions based on common grant proposal templates.", - "Confirm all entered data with the user before finalizing the artifact.", - "Ensure the assistant does not attempt to draft the proposal; focus solely on gathering and validating information.", -] - -# Define the conversation flow -conversation_flow = """ -1. Initial Greetings: Start with a friendly greeting and an overview of the process. -2. Request Grant Source Documents: - 1. Ask the user to provide any documents from the grant source. - 2. Extract and confirm necessary details from the provided documents. -3. Request User Documents: - 1. Ask the user to provide any of their own documents, notes, transcripts, etc. that might contain relevant information. - 2. Categorize the extracted details from the user documents. -4. Gather Project Information: - 1. Ask for the title of the project. - 2. Request a brief summary of the project. - 3. Collect key objectives of the project. - 4. Gather information on the methods and approaches to be used. -5. Collect Budget Details: - 1. Ask for the total amount requested for the project. - 2. Gather a detailed budget breakdown. -6. Collect Team Information: - 1. Ask for the names and roles of the project team members. -7. Determine Project Timeline: - 1. Ask for the proposed start date. - 2. Ask for the proposed end date. - 3. Collect key milestones and their dates. -8. Review and Suggest Additional Information: - 1. Review the provided documents. - 2. Suggest any additional information that might be needed. -9. Request Missing Information: - 1. Inform the user of what is still missing. - 2. Offer the opportunity to upload more documents or provide direct answers. -10. Finalize and Confirm: - 1. Walk through the remaining items one-by-one until all required information is gathered. - 2. Confirm all entered data with the user. -""" - -# Provide context for the guided conversation -context = """ -You are an AI assistant helping the user draft a comprehensive grant proposal. The goal is to gather all necessary -information systematically, validating and categorizing it as required, without actually drafting the proposal. -Your task is to ensure all required sections of the grant proposal are filled with accurate and relevant -information provided by the user. -""" - -# Define the resource constraint -resource_constraint = ResourceConstraint( - quantity=50, # Number of turns - unit=ResourceConstraintUnit.TURNS, - mode=ResourceConstraintMode.EXACT, -) - -__all__ = [ - "ArtifactModel", - "rules", - "conversation_flow", - "context", - "resource_constraint", -] 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 70128c60..917edbab 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 @@ -58,11 +58,13 @@ class ArtifactModel(BaseModel): ] # Conversation Flow (optional) - This defines in natural language the steps of the conversation. -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 +conversation_flow = """ +1. If there is no prior conversation history to reference, use the conversation_status to determine if the user is initiating a new conversation (user_initiated) or returning to an existing conversation (user_returned). +2. Only greet the user if the user is initiating a new conversation.If the user is NOT initiating a new conversation, you should respond as if you are in the middle of a conversation. In this scenario, do not say "hello", or "welcome back" or any type of formalized greeting. +3. 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: +4. Answer any questions about the outline or the drafting process the user inquires about. +5. 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. From d52c2a18b9cc4d28739812bd46be9dc24d5ec4a2 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 19 Nov 2024 20:44:49 +0000 Subject: [PATCH 8/8] line edit --- .../agents/document/gc_draft_outline_feedback_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 917edbab..5e37a39b 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 @@ -60,7 +60,7 @@ class ArtifactModel(BaseModel): # Conversation Flow (optional) - This defines in natural language the steps of the conversation. conversation_flow = """ 1. If there is no prior conversation history to reference, use the conversation_status to determine if the user is initiating a new conversation (user_initiated) or returning to an existing conversation (user_returned). -2. Only greet the user if the user is initiating a new conversation.If the user is NOT initiating a new conversation, you should respond as if you are in the middle of a conversation. In this scenario, do not say "hello", or "welcome back" or any type of formalized greeting. +2. Only greet the user if the user is initiating a new conversation. If the user is NOT initiating a new conversation, you should respond as if you are in the middle of a conversation. In this scenario, do not say "hello", or "welcome back" or any type of formalized greeting. 3. 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. 4. Answer any questions about the outline or the drafting process the user inquires about.