From d21c84e52637380b0d9014b7eb18d07ae71b7449 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Wed, 30 Oct 2024 21:21:00 +0000 Subject: [PATCH 1/5] Fix extra line that got missed --- .../assistant/agents/document_agent.py | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 3e29f99c..0c5cb836 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -295,10 +295,7 @@ def _set_mode_draft_outline( context: ConversationContext, message: ConversationMessage, metadata: dict[str, Any] = {}, - ) -> bool: - # Retrieve Document Agent conversation state - state = _get_state(context) - + ) -> None: # Pre-requisites if self._state is None: logger.error("Document Agent state is None. Returning.") @@ -860,59 +857,6 @@ def _format_message(message: ConversationMessage, participants: list[Conversatio # endregion -# -# region Helpers -# -def _get_state(context: ConversationContext) -> State: - state_dict = _read_document_agent_conversation_state(context) - if state_dict is not None: - state = State(**state_dict) - else: - logger.info("Document Agent: no state found. Creating new state.") - state = State() - return state - - -def _set_state(context: ConversationContext, state: State) -> None: - _write_document_agent_conversation_state(context, state.model_dump()) - - -def _get_document_agent_conversation_storage_path(context: ConversationContext, filename: str | None = None) -> Path: - """ - Get the path to the directory for storing files. - """ - path = storage_directory_for_context(context) / "document_agent" - if filename: - path /= filename - return path - - -def _write_document_agent_conversation_state(context: ConversationContext, state: dict) -> None: - """ - Write the state to a file. - """ - json_data = json.dumps(state) - path = _get_document_agent_conversation_storage_path(context) - if not path.exists(): - path.mkdir(parents=True) - path = path / "state.json" - path.write_text(json_data) - - -def _read_document_agent_conversation_state(context: ConversationContext) -> dict | None: - """ - Read the state from a file. - """ - path = _get_document_agent_conversation_storage_path(context, "state.json") - if path.exists(): - try: - json_data = path.read_text() - return json.loads(json_data) - except Exception: - pass - return None - - ##### FROM NOTEBOOK # await document_skill.draft_outline(context=unused, openai_client=async_client, model=model) # From ccfe3ddc81117a6442e9daf2d149e0381819840d Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Fri, 1 Nov 2024 20:49:04 +0000 Subject: [PATCH 2/5] ready for merge testing --- .../document/gc_attachment_check_config.py | 191 ++++++++++++++++++ .../gc_draft_outline_feedback_config.py | 190 +++++++++++++++++ .../agents/document/guided_conversation.py | 54 ++++- .../assistant/agents/document/status.py | 9 + .../assistant/agents/document_agent.py | 158 ++++++++++++--- .../attachments/_attachments.py | 27 +++ 6 files changed, 599 insertions(+), 30 deletions(-) create mode 100644 assistants/prospector-assistant/assistant/agents/document/gc_attachment_check_config.py create mode 100644 assistants/prospector-assistant/assistant/agents/document/gc_draft_outline_feedback_config.py create mode 100644 assistants/prospector-assistant/assistant/agents/document/status.py diff --git a/assistants/prospector-assistant/assistant/agents/document/gc_attachment_check_config.py b/assistants/prospector-assistant/assistant/agents/document/gc_attachment_check_config.py new file mode 100644 index 00000000..3083d740 --- /dev/null +++ b/assistants/prospector-assistant/assistant/agents/document/gc_attachment_check_config.py @@ -0,0 +1,191 @@ +import json +from typing import TYPE_CHECKING, Annotated, Any, Dict, List, Type + +from guided_conversation.utils.resources import ResourceConstraint, ResourceConstraintMode, ResourceConstraintUnit +from pydantic import BaseModel, Field, create_model +from semantic_workbench_assistant.config import UISchema + +from ... import helpers +from . import config_defaults as config_defaults +from .config import GuidedConversationAgentConfigModel + +if TYPE_CHECKING: + pass + + +# Artifact - The artifact is like a form that the agent must complete throughout the conversation. +# 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.") + + +# Rules - These are the do's and don'ts that the agent should follow during the conversation. +rules = [ + "Terminate the conversation immediately if the user asks for harmful or inappropriate content.", + "Set the conversation_status to user_completed once you have provided a final_response.", +] + +# Conversation Flow (optional) - This defines in natural language the steps of the conversation. +conversation_flow = """1. Start by asking if the user has all their documents attached to the conversation that they +would like to use in drafting their outline. If any filenames are available, list those to the user to demonstrate you +know what they have already attached. If no filenames are available, let the user know no documents have been attached." +2. If the user attaches files, be sure to let them know all the filenames you are aware of that have been attached. +3.You want to reach the point that the user confirms all the docs they want attached have been attached. Once you interpret +the user's response as a confirmation, then go ahead and provide a final response. +4. Your final response should share the list of attachments being used and how they will be used. In this scenario they will be used +to construct a draft outline, which you will be requesting user feedback on. With this final response, the conversation_status must be +marked as user_completed. +""" + +# 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 = """The purpose of gathering these attachments is for the future user need to draft an outline. The purpose of this conversation +is to make sure the user is aware of what documents they have uploaded as attachments and if they need to upload anymore before +the user proceeds to drafting the outline.""" + + +# Resource Constraints (optional) - This defines the constraints on the conversation such as time or turns. +# It can also help with pacing the conversation, +# For example, here we have set an exact time limit of 10 turns which the agent will try to fill. +resource_constraint = ResourceConstraint( + quantity=5, + unit=ResourceConstraintUnit.TURNS, + mode=ResourceConstraintMode.MAXIMUM, +) + + +# +# region Helpers +# + +# take a full json schema and return a pydantic model, including support for +# nested objects and typed arrays + + +def json_type_to_python_type(json_type: str) -> Type: + # Mapping JSON types to Python types + type_mapping = {"integer": int, "string": str, "number": float, "boolean": bool, "object": dict, "array": list} + return type_mapping.get(json_type, Any) + + +def create_pydantic_model_from_json_schema(schema: Dict[str, Any], model_name="DynamicModel") -> Type[BaseModel]: + # Nested function to parse properties from the schema + def parse_properties(properties: Dict[str, Any]) -> Dict[str, Any]: + fields = {} + for prop_name, prop_attrs in properties.items(): + prop_type = prop_attrs.get("type") + description = prop_attrs.get("description", None) + + if prop_type == "object": + nested_model = create_pydantic_model_from_json_schema(prop_attrs, model_name=prop_name.capitalize()) + fields[prop_name] = (nested_model, Field(..., description=description)) + elif prop_type == "array": + items = prop_attrs.get("items", {}) + if items.get("type") == "object": + nested_model = create_pydantic_model_from_json_schema(items) + fields[prop_name] = (List[nested_model], Field(..., description=description)) + else: + nested_type = json_type_to_python_type(items.get("type")) + fields[prop_name] = (List[nested_type], Field(..., description=description)) + else: + python_type = json_type_to_python_type(prop_type) + fields[prop_name] = (python_type, Field(..., description=description)) + return fields + + properties = schema.get("properties", {}) + fields = parse_properties(properties) + return create_model(model_name, **fields) + + +# endregion + + +# +# region Models +# + + +class GCAttachmentCheckConfigModel(GuidedConversationAgentConfigModel): + enabled: Annotated[ + bool, + Field(description=helpers.load_text_include("guided_conversation_agent_enabled.md")), + UISchema(enable_markdown_in_description=True), + ] = False + + artifact: Annotated[ + str, + Field( + title="Artifact", + description="The artifact that the agent will manage.", + ), + UISchema(widget="baseModelEditor"), + ] = json.dumps(ArtifactModel.model_json_schema(), indent=2) + + rules: Annotated[ + list[str], + Field(title="Rules", description="Do's and don'ts that the agent should attempt to follow"), + UISchema(schema={"items": {"ui:widget": "textarea", "ui:options": {"rows": 2}}}), + ] = rules + + conversation_flow: Annotated[ + str, + Field( + title="Conversation Flow", + description="A loose natural language description of the steps of the conversation", + ), + UISchema(widget="textarea", schema={"ui:options": {"rows": 10}}, placeholder="[optional]"), + ] = conversation_flow.strip() + + context: Annotated[ + str, + Field( + title="Context", + description="General background context for the conversation.", + ), + UISchema(widget="textarea", placeholder="[optional]"), + ] = context.strip() + + class ResourceConstraint(ResourceConstraint): + mode: Annotated[ + ResourceConstraintMode, + Field( + title="Resource Mode", + description=( + 'If "exact", the agents will try to pace the conversation to use exactly the resource quantity. If' + ' "maximum", the agents will try to pace the conversation to use at most the resource quantity.' + ), + ), + ] = resource_constraint.mode + + unit: Annotated[ + ResourceConstraintUnit, + Field( + title="Resource Unit", + description="The unit for the resource constraint.", + ), + ] = resource_constraint.unit + + quantity: Annotated[ + float, + Field( + title="Resource Quantity", + description="The quantity for the resource constraint. If <=0, the resource constraint is disabled.", + ), + ] = resource_constraint.quantity + + resource_constraint: Annotated[ + ResourceConstraint, + Field( + title="Resource Constraint", + ), + UISchema(schema={"quantity": {"ui:widget": "updown"}}), + ] = ResourceConstraint() + + def get_artifact_model(self) -> Type[BaseModel]: + schema = json.loads(self.artifact) + return create_pydantic_model_from_json_schema(schema) + + +# endregion 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 new file mode 100644 index 00000000..6b68d8c0 --- /dev/null +++ b/assistants/prospector-assistant/assistant/agents/document/gc_draft_outline_feedback_config.py @@ -0,0 +1,190 @@ +import json +from typing import TYPE_CHECKING, Annotated, Any, Dict, List, Type + +from guided_conversation.utils.resources import ResourceConstraint, ResourceConstraintMode, ResourceConstraintUnit +from pydantic import BaseModel, Field, create_model +from semantic_workbench_assistant.config import UISchema + +from ... import helpers +from . import config_defaults as config_defaults +from .config import GuidedConversationAgentConfigModel + +if TYPE_CHECKING: + pass + + +# Artifact - The artifact is like a form that the agent must complete throughout the conversation. +# 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.") + + +# Rules - These are the do's and don'ts that the agent should follow during the conversation. +rules = [ + "Terminate the conversation immediately if the user asks for harmful or inappropriate content.", + "If the conversation is complete due to the user wanting the outline updated, set the conversation_status to update_outline.", + "If the conversation is complete due to the user ready to move on to drafting the paper, set the conversation_state to user_completed.", +] + +# 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, consider the conversation complete. +In this scenario, your final message should 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, consider the +conversation complete. In this scenario, your final message should 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.""" + + +# Resource Constraints (optional) - This defines the constraints on the conversation such as time or turns. +# It can also help with pacing the conversation, +# For example, here we have set an exact time limit of 10 turns which the agent will try to fill. +resource_constraint = ResourceConstraint( + quantity=5, + unit=ResourceConstraintUnit.TURNS, + mode=ResourceConstraintMode.MAXIMUM, +) + + +# +# region Helpers +# + +# take a full json schema and return a pydantic model, including support for +# nested objects and typed arrays + + +def json_type_to_python_type(json_type: str) -> Type: + # Mapping JSON types to Python types + type_mapping = {"integer": int, "string": str, "number": float, "boolean": bool, "object": dict, "array": list} + return type_mapping.get(json_type, Any) + + +def create_pydantic_model_from_json_schema(schema: Dict[str, Any], model_name="DynamicModel") -> Type[BaseModel]: + # Nested function to parse properties from the schema + def parse_properties(properties: Dict[str, Any]) -> Dict[str, Any]: + fields = {} + for prop_name, prop_attrs in properties.items(): + prop_type = prop_attrs.get("type") + description = prop_attrs.get("description", None) + + if prop_type == "object": + nested_model = create_pydantic_model_from_json_schema(prop_attrs, model_name=prop_name.capitalize()) + fields[prop_name] = (nested_model, Field(..., description=description)) + elif prop_type == "array": + items = prop_attrs.get("items", {}) + if items.get("type") == "object": + nested_model = create_pydantic_model_from_json_schema(items) + fields[prop_name] = (List[nested_model], Field(..., description=description)) + else: + nested_type = json_type_to_python_type(items.get("type")) + fields[prop_name] = (List[nested_type], Field(..., description=description)) + else: + python_type = json_type_to_python_type(prop_type) + fields[prop_name] = (python_type, Field(..., description=description)) + return fields + + properties = schema.get("properties", {}) + fields = parse_properties(properties) + return create_model(model_name, **fields) + + +# endregion + + +# +# region Models +# + + +class GCDraftOutlineFeedbackConfigModel(GuidedConversationAgentConfigModel): + enabled: Annotated[ + bool, + Field(description=helpers.load_text_include("guided_conversation_agent_enabled.md")), + UISchema(enable_markdown_in_description=True), + ] = False + + artifact: Annotated[ + str, + Field( + title="Artifact", + description="The artifact that the agent will manage.", + ), + UISchema(widget="baseModelEditor"), + ] = json.dumps(ArtifactModel.model_json_schema(), indent=2) + + rules: Annotated[ + list[str], + Field(title="Rules", description="Do's and don'ts that the agent should attempt to follow"), + UISchema(schema={"items": {"ui:widget": "textarea", "ui:options": {"rows": 2}}}), + ] = rules + + conversation_flow: Annotated[ + str, + Field( + title="Conversation Flow", + description="A loose natural language description of the steps of the conversation", + ), + UISchema(widget="textarea", schema={"ui:options": {"rows": 10}}, placeholder="[optional]"), + ] = conversation_flow.strip() + + context: Annotated[ + str, + Field( + title="Context", + description="General background context for the conversation.", + ), + UISchema(widget="textarea", placeholder="[optional]"), + ] = context.strip() + + class ResourceConstraint(ResourceConstraint): + mode: Annotated[ + ResourceConstraintMode, + Field( + title="Resource Mode", + description=( + 'If "exact", the agents will try to pace the conversation to use exactly the resource quantity. If' + ' "maximum", the agents will try to pace the conversation to use at most the resource quantity.' + ), + ), + ] = resource_constraint.mode + + unit: Annotated[ + ResourceConstraintUnit, + Field( + title="Resource Unit", + description="The unit for the resource constraint.", + ), + ] = resource_constraint.unit + + quantity: Annotated[ + float, + Field( + title="Resource Quantity", + description="The quantity for the resource constraint. If <=0, the resource constraint is disabled.", + ), + ] = resource_constraint.quantity + + resource_constraint: Annotated[ + ResourceConstraint, + Field( + title="Resource Constraint", + ), + UISchema(schema={"quantity": {"ui:widget": "updown"}}), + ] = ResourceConstraint() + + def get_artifact_model(self) -> Type[BaseModel]: + schema = json.loads(self.artifact) + return create_pydantic_model_from_json_schema(schema) + + +# endregion diff --git a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py index 1dc7a84e..dd5ad040 100644 --- a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py +++ b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py @@ -13,6 +13,7 @@ from ...config import AssistantConfigModel from .config import GuidedConversationAgentConfigModel +from .status import Status logger = logging.getLogger(__name__) @@ -43,7 +44,7 @@ async def step_conversation( agent_config: GuidedConversationAgentConfigModel, conversation_context: ConversationContext, last_user_message: str | None, - ) -> tuple[str | None, bool]: + ) -> tuple[str, Status]: """ Step the conversation to the next turn. """ @@ -54,6 +55,8 @@ async def step_conversation( resource_constraint = agent_config.resource_constraint artifact = agent_config.get_artifact_model() + # plug in attachments + kernel = Kernel() service_id = "gc_main" @@ -96,7 +99,45 @@ async def step_conversation( # Save the state of the guided conversation agent _write_guided_conversation_state(conversation_context, guided_conversation_agent.to_json()) - return result.ai_message, result.is_conversation_over + # convert information in artifact for Document Agent + # conversation_status: # this should relate to result.is_conversation_over + # final_response: # replace result.ai_message with final_response if "user_completed" + + final_response: str = "" + conversation_status: str | None = None + response: str = "" + + # to_json is actually to dict, not to json. + gc_dict = guided_conversation_agent.to_json() + artifact_item = gc_dict.get("artifact") + if artifact_item is not None: + artifact_item = artifact_item.get("artifact") + if artifact_item is not None: + final_response = artifact_item.get("final_response") + conversation_status = artifact_item.get("conversation_status") + + # 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 result.is_conversation_over is True: + _delete_guided_conversation_state(conversation_context) + if conversation_status == "user_completed": + status = Status.USER_COMPLETED + response = final_response + elif conversation_status == "update_outline": + status = Status.UPDATE_OUTLINE + response = final_response + else: + status = Status.USER_EXIT_EARLY + response = final_response + else: + if result.ai_message is not None: + response = result.ai_message + else: + response = "" + status = Status.NOT_COMPLETED + + return response, status # endregion @@ -142,4 +183,13 @@ def _read_guided_conversation_state(context: ConversationContext) -> dict | None return None +def _delete_guided_conversation_state(context: ConversationContext) -> None: + """ + Delete the file containing state of the guided conversation agent. + """ + path = _get_guided_conversation_storage_path(context, "state.json") + if path.exists(): + path.unlink() + + # endregion diff --git a/assistants/prospector-assistant/assistant/agents/document/status.py b/assistants/prospector-assistant/assistant/agents/document/status.py new file mode 100644 index 00000000..31a0b66e --- /dev/null +++ b/assistants/prospector-assistant/assistant/agents/document/status.py @@ -0,0 +1,9 @@ +from enum import StrEnum + + +class Status(StrEnum): + UNDEFINED = "undefined" + NOT_COMPLETED = "not_completed" + UPDATE_OUTLINE = "update_outline" + USER_COMPLETED = "user_completed" + USER_EXIT_EARLY = "user_exit_early" diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 0c5cb836..6bd49037 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -23,7 +23,10 @@ from ..config import AssistantConfigModel from .document.config import GuidedConversationAgentConfigModel +from .document.gc_attachment_check_config import GCAttachmentCheckConfigModel +from .document.gc_draft_outline_feedback_config import GCDraftOutlineFeedbackConfigModel from .document.guided_conversation import GuidedConversationAgent +from .document.status import Status logger = logging.getLogger(__name__) @@ -33,13 +36,6 @@ # -class Status(StrEnum): - UNDEFINED = "undefined" - NOT_COMPLETED = "not_completed" - USER_COMPLETED = "user_completed" - USER_EXIT_EARLY = "user_exit_early" - - class StepName(StrEnum): UNDEFINED = "undefined" DO_GC_ATTACHMENT_CHECK = "step_gc_attachment_check" @@ -570,8 +566,39 @@ async def _step_gc_get_outline_feedback( message: ConversationMessage, metadata: dict[str, Any] = {}, ) -> Status: - # pretend completed - return Status.USER_COMPLETED + # Pre-requisites + if self._state is None: + logger.error("Document Agent state is None. Returning.") + return Status.UNDEFINED + + step = self._state.mode.get_step() + step_name = step.get_name() + step_status = step.get_status() + + # Pre-requisites + step_called = StepName.DO_GC_GET_OUTLINE_FEEDBACK + if step_name is not step_called or step_status is not Status.NOT_COMPLETED: + logger.error( + "Document Agent state step: %s, step called: %s, state step completion status: %s. Resetting Mode.", + step_name, + step_called, + step_status, + ) + self._state.mode.reset() + self._write_state(context) + return self._state.mode.get_status() + + # Run + logger.info("Document Agent running step: %s", step_name) + status = await self._gc_outline_feedback(config, gc_do_feedback_config, context, message, metadata) + + if status is Status.UPDATE_OUTLINE: + status = await self._draft_outline(config, context, message, metadata) + + step.set_status(status) + self._state.mode.set_step(step) + self._write_state(context) + return step.get_status() async def _step_final_outline( self, @@ -598,38 +625,38 @@ async def _gc_respond_to_conversation( metadata: dict[str, Any] = {}, ) -> Status: method_metadata_key = "document_agent_gc_response" - is_conversation_over = False + + # get attachment filenames for context + filenames = await self._attachments_extension.get_attachment_filenames( + context, config=config.agents_config.attachment_agent + ) + + filenames_str = ", ".join(filenames) + filenames_str = "Filenames already attached: " + filenames_str + gc_config.context = gc_config.context + "\n\n" + filenames_str try: - response_message, is_conversation_over = await GuidedConversationAgent.step_conversation( + response_message, conversation_status = await GuidedConversationAgent.step_conversation( config=config, openai_client=openai_client.create_client(config.service_config), agent_config=gc_config, conversation_context=context, last_user_message=message.content, ) - if is_conversation_over: # need to get info from gc on if user-ended early or actually completed - return Status.USER_COMPLETED # Do not send the hard-coded response message from gc - - if response_message is None: - # need to double check this^^ None logic, when it would occur in GC. Make "" for now. - agent_message = "" - else: - agent_message = response_message # add the completion to the metadata for debugging deepmerge.always_merger.merge( metadata, { "debug": { - f"{method_metadata_key}": {"response": agent_message}, + f"{method_metadata_key}": {"response": response_message}, } }, ) except Exception as e: logger.exception(f"exception occurred processing guided conversation: {e}") - agent_message = "An error occurred while processing the guided conversation." + response_message = "An error occurred while processing the guided conversation." deepmerge.always_merger.merge( metadata, { @@ -643,7 +670,7 @@ async def _gc_respond_to_conversation( await context.send_messages( NewConversationMessage( - content=agent_message, + content=response_message, message_type=MessageType.chat, metadata=metadata, ) @@ -652,7 +679,7 @@ async def _gc_respond_to_conversation( # Need to add a good way to stop mode if an exception occurs. # Also need to update the gc state turn count to 0 (and any thing else that needs to be reset) once conversation is over... or exception occurs?) - return Status.NOT_COMPLETED + return conversation_status async def _draft_outline( self, @@ -676,8 +703,9 @@ async def _draft_outline( # get outline related info outline: str | None = None - if path.exists(storage_directory_for_context(context) / "outline.txt"): - outline = (storage_directory_for_context(context) / "outline.txt").read_text() + # path = _get_document_agent_conversation_storage_path(context) + if path.exists(storage_directory_for_context(context) / "document_agent/outline.txt"): + outline = (storage_directory_for_context(context) / "document_agent/outline.txt").read_text() # create chat completion messages chat_completion_messages: list[ChatCompletionMessageParam] = [] @@ -710,7 +738,7 @@ async def _draft_outline( _on_error_metadata_update(metadata, method_metadata_key, config, chat_completion_messages, e) # store only latest version for now (will keep all versions later as need arises) - (storage_directory_for_context(context) / "outline.txt").write_text(content) + (storage_directory_for_context(context) / "document_agent/outline.txt").write_text(content) # send the response to the conversation only if from a command. Otherwise return info to caller. message_type = MessageType.chat @@ -727,6 +755,80 @@ async def _draft_outline( return Status.USER_COMPLETED + async def _gc_outline_feedback( + self, + config: AssistantConfigModel, + gc_config: GuidedConversationAgentConfigModel, + context: ConversationContext, + message: ConversationMessage, + metadata: dict[str, Any] = {}, + ) -> Status: + method_metadata_key = "document_agent_gc_response" + + # get attachment filenames for context + filenames = await self._attachments_extension.get_attachment_filenames( + context, config=config.agents_config.attachment_agent + ) + + filenames_str = ", ".join(filenames) + filenames_str = "Filenames already attached: " + filenames_str + gc_config.context = gc_config.context + "\n\n" + filenames_str + + # get current outline related info + current_outline: str | None = None + if path.exists(storage_directory_for_context(context) / "document_agent/outline.txt"): + current_outline = (storage_directory_for_context(context) / "document_agent/outline.txt").read_text() + + if current_outline is not None: + outline_str = "Current outline under review: " + current_outline + gc_config.context = gc_config.context + "\n\n" + outline_str + + try: + response_message, conversation_status = await GuidedConversationAgent.step_conversation( + config=config, + openai_client=openai_client.create_client(config.service_config), + agent_config=gc_config, + conversation_context=context, + last_user_message=None, # fix this. Should be None on the first call, but keep the user message after. + ) + + # add the completion to the metadata for debugging + deepmerge.always_merger.merge( + metadata, + { + "debug": { + f"{method_metadata_key}": {"response": response_message}, + } + }, + ) + + except Exception as e: + logger.exception(f"exception occurred processing guided conversation: {e}") + response_message = "An error occurred while processing the guided conversation." + deepmerge.always_merger.merge( + metadata, + { + "debug": { + f"{method_metadata_key}": { + "error": str(e), + }, + } + }, + ) + + await context.send_messages( + NewConversationMessage( + content=response_message, + message_type=MessageType.chat, + metadata=metadata, + ) + ) + + # Need to add a good way to stop mode if an exception occurs. + # Also need to update the gc state turn count to 0 (and any thing else that needs to be reset) once conversation is over... or exception occurs?) + + return conversation_status + # endregion @@ -851,8 +953,8 @@ def _format_message(message: ConversationMessage, participants: list[Conversatio # region GC agent config temp # # pull in GC config with its defaults, and then make changes locally here for now. -gc_config = GuidedConversationAgentConfigModel() - +gc_config = GCAttachmentCheckConfigModel() +gc_do_feedback_config = GCDraftOutlineFeedbackConfigModel() # endregion diff --git a/libraries/python/assistant-extensions/assistant_extensions/attachments/_attachments.py b/libraries/python/assistant-extensions/assistant_extensions/attachments/_attachments.py index 1d33cbeb..f5ef7904 100644 --- a/libraries/python/assistant-extensions/assistant_extensions/attachments/_attachments.py +++ b/libraries/python/assistant-extensions/assistant_extensions/attachments/_attachments.py @@ -197,6 +197,33 @@ async def get_completion_messages_for_attachments( return messages + async def get_attachment_filenames( + self, + context: ConversationContext, + config: AttachmentsConfigModel, + include_filenames: list[str] | None = None, + exclude_filenames: list[str] = [], + ) -> list[str]: + if not config.include_in_response_generation: + return [] + + # get attachments, filtered by include_filenames and exclude_filenames + attachments = await _get_attachments( + context, + error_handler=self._error_handler, + include_filenames=include_filenames, + exclude_filenames=exclude_filenames, + ) + + if not attachments: + return [] + + filenames: list[str] = [] + for attachment in attachments: + filenames.append(attachment.filename) + + return filenames + async def _get_attachments( context: ConversationContext, From 1c92411e2b56a5fc604afde33a009ff3f295e2c1 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Fri, 1 Nov 2024 21:59:00 +0000 Subject: [PATCH 3/5] Adding initiated status to give control for when a last user message should be used at the start of a gc or not. --- .../assistant/agents/document/status.py | 1 + .../assistant/agents/document_agent.py | 119 ++++++++++-------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/document/status.py b/assistants/prospector-assistant/assistant/agents/document/status.py index 31a0b66e..1e5f7ddd 100644 --- a/assistants/prospector-assistant/assistant/agents/document/status.py +++ b/assistants/prospector-assistant/assistant/agents/document/status.py @@ -3,6 +3,7 @@ class Status(StrEnum): UNDEFINED = "undefined" + INITIATED = "initiated" NOT_COMPLETED = "not_completed" UPDATE_OUTLINE = "update_outline" USER_COMPLETED = "user_completed" diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 8dc971ae..bd8d2489 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -94,7 +94,7 @@ def get_status(self) -> Status: class Mode(BaseModel): name: ModeName = ModeName.UNDEFINED status: Status = Status.UNDEFINED - current_step: Step = Step() + step: Step = Step() step_order: list[StepName] = [] def _error_check(self) -> None: @@ -137,13 +137,15 @@ def get_status(self) -> Status: def is_running(self) -> bool: if self.status is Status.NOT_COMPLETED: return True + if self.status is Status.INITIATED: + return True return False # UNDEFINED, USER_EXIT_EARLY, USER_COMPLETED def set_step(self, step: Step) -> None: - self.current_step = step + self.step = step def get_step(self) -> Step: - return self.current_step + return self.step def set_step_order(self, steps: list[StepName]) -> None: self.step_order = steps @@ -156,17 +158,17 @@ def get_next_step(self) -> Step | None: if len(steps) == 0: return None - current_step = self.get_step() - current_step_name = current_step.get_name() - if current_step_name is steps[-1]: + step = self.get_step() + step_name = step.get_name() + if step_name is steps[-1]: return None # on final step for index, step in enumerate(steps[:-1]): - if step is current_step_name: + if step is step_name: next_step_name = steps[index + 1] break - return Step(name=next_step_name, status=Status.NOT_COMPLETED) + return Step(name=next_step_name, status=Status.INITIATED) class State(BaseModel): @@ -305,7 +307,7 @@ def _set_mode_draft_outline( return # Run - self._state.mode = Mode(name=ModeName.DRAFT_OUTLINE, status=Status.NOT_COMPLETED) + self._state.mode = Mode(name=ModeName.DRAFT_OUTLINE, status=Status.INITIATED) self._write_state(context) async def respond_to_conversation( @@ -379,17 +381,18 @@ async def _run_mode( step_name = step.get_name() step_status = step.get_status() - if step_name is StepName.UNDEFINED: + # This Status.INITIATED will occur when the mode is setting up the first step on its first run + if step_status is Status.INITIATED: logger.info("Document Agent mode (%s) at beginning.", mode_name) - first_step_name = mode.get_step_order()[0] - self._state.mode.set_step(Step(name=first_step_name, status=Status.NOT_COMPLETED)) + self._state.mode.get_step().set_status(Status.NOT_COMPLETED) self._write_state(context) step = self._state.mode.get_step() step_name = step.get_name() step_status = step.get_status() - while step_status is Status.NOT_COMPLETED: + # This Status.INITIATED will occur when a new step is setup upon a prior step's Status.USER_COMPLETED. + while step_status is Status.INITIATED or step_status is Status.NOT_COMPLETED: step_method = self._get_step_method(step) if step_method: logger.info("Document Agent in step: %s", step_name) @@ -456,7 +459,9 @@ async def _mode_draft_outline( mode_name = mode.get_name() mode_status = mode.get_status() - if mode_name is not ModeName.DRAFT_OUTLINE or mode_status is not Status.NOT_COMPLETED: + if mode_name is not ModeName.DRAFT_OUTLINE or ( + mode_status is not Status.NOT_COMPLETED and mode_status is not Status.INITIATED + ): logger.error( "Document Agent state mode: %s, mode called: %s, state mode completion status: %s. Resetting Mode.", mode_name, @@ -467,16 +472,20 @@ async def _mode_draft_outline( self._write_state(context) return self._state.mode.get_status() - # Setup - self._state.mode.set_step_order( - [ - StepName.DO_GC_ATTACHMENT_CHECK, - StepName.DO_DRAFT_OUTLINE, - StepName.DO_GC_GET_OUTLINE_FEEDBACK, - StepName.DO_FINAL_OUTLINE, - ], - ) - self._write_state(context) + # Setup on first run. + 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_FINAL_OUTLINE, + ], + ) + first_step_name = self._state.mode.get_step_order()[0] + self._state.mode.set_step(Step(name=first_step_name, status=Status.INITIATED)) + self._state.mode.set_status(Status.NOT_COMPLETED) + self._write_state(context) self._step_name_to_method: dict[StepName, Callable] = { StepName.DO_GC_ATTACHMENT_CHECK: self._step_gc_attachment_check, @@ -506,7 +515,9 @@ async def _step_gc_attachment_check( # Pre-requisites step_called = StepName.DO_GC_ATTACHMENT_CHECK - if step_name is not step_called or step_status is not Status.NOT_COMPLETED: + if step_name is not step_called or ( + step_status is not Status.NOT_COMPLETED and step_status is not Status.INITIATED + ): logger.error( "Document Agent state step: %s, step called: %s, state step completion status: %s. Resetting Mode.", step_name, @@ -519,7 +530,7 @@ async def _step_gc_attachment_check( # Run logger.info("Document Agent running step: %s", step_name) - status = await self._gc_respond_to_conversation(config, gc_config, context, message, metadata) + status = await self._gc_attachment_check(config, context, message, metadata) step.set_status(status) self._state.mode.set_step(step) self._write_state(context) @@ -542,7 +553,9 @@ async def _step_draft_outline( step_status = step.get_status() step_called = StepName.DO_DRAFT_OUTLINE - if step_name is not step_called or step_status is not Status.NOT_COMPLETED: + if step_name is not step_called or ( + step_status is not Status.NOT_COMPLETED and step_status is not Status.INITIATED + ): logger.error( "Document Agent state step: %s, step called: %s, state step completion status: %s. Resetting Mode.", step_name, @@ -579,7 +592,9 @@ async def _step_gc_get_outline_feedback( # Pre-requisites step_called = StepName.DO_GC_GET_OUTLINE_FEEDBACK - if step_name is not step_called or step_status is not Status.NOT_COMPLETED: + if step_name is not step_called or ( + step_status is not Status.NOT_COMPLETED and step_status is not Status.INITIATED + ): logger.error( "Document Agent state step: %s, step called: %s, state step completion status: %s. Resetting Mode.", step_name, @@ -591,9 +606,20 @@ async def _step_gc_get_outline_feedback( return self._state.mode.get_status() # Run + # Because the last user message will be ending a prior step, and not be related to this step. + user_message: ConversationMessage | None + if step_status is Status.INITIATED: + user_message = None + else: + user_message = message + logger.info("Document Agent running step: %s", step_name) - status = await self._gc_outline_feedback(config, gc_do_feedback_config, context, message, metadata) + status = await self._gc_outline_feedback(config, context, user_message, metadata) + # should this be status or a different return as part of a tuple... like next step request? + # this way the control would still be higher (and non recursive) as the branching logic will return instead of call internally the next step. + # but controlling of what gets called next will be determined by each step at its completion. it seems like there should still be + # some level of oversight by the mode layer in case we end up in an endless loop of steps saying the should each be called next...(still a problem.) if status is Status.UPDATE_OUTLINE: status = await self._draft_outline(config, context, message, metadata) @@ -618,16 +644,16 @@ async def _step_final_outline( # region language model methods # - async def _gc_respond_to_conversation( + async def _gc_attachment_check( self, config: AssistantConfigModel, - gc_config: GuidedConversationAgentConfigModel, context: ConversationContext, message: ConversationMessage, metadata: dict[str, Any] = {}, ) -> Status: method_metadata_key = "document_agent_gc_response" + gc_convo_config: GuidedConversationAgentConfigModel = GCAttachmentCheckConfigModel() # get attachment filenames for context filenames = await self._attachments_extension.get_attachment_filenames( context, config=config.agents_config.attachment_agent @@ -635,13 +661,13 @@ async def _gc_respond_to_conversation( filenames_str = ", ".join(filenames) filenames_str = "Filenames already attached: " + filenames_str - gc_config.context = gc_config.context + "\n\n" + filenames_str + gc_convo_config.context = gc_convo_config.context + "\n\n" + filenames_str try: response_message, conversation_status = await GuidedConversationAgent.step_conversation( config=config, openai_client=openai_client.create_client(config.service_config), - agent_config=gc_config, + agent_config=gc_convo_config, conversation_context=context, last_user_message=message.content, ) @@ -760,13 +786,13 @@ async def _draft_outline( async def _gc_outline_feedback( self, config: AssistantConfigModel, - gc_config: GuidedConversationAgentConfigModel, context: ConversationContext, - message: ConversationMessage, + message: ConversationMessage | None, metadata: dict[str, Any] = {}, ) -> Status: method_metadata_key = "document_agent_gc_response" + gc_do_feedback_config: GuidedConversationAgentConfigModel = GCDraftOutlineFeedbackConfigModel() # get attachment filenames for context filenames = await self._attachments_extension.get_attachment_filenames( context, config=config.agents_config.attachment_agent @@ -774,7 +800,7 @@ async def _gc_outline_feedback( filenames_str = ", ".join(filenames) filenames_str = "Filenames already attached: " + filenames_str - gc_config.context = gc_config.context + "\n\n" + filenames_str + gc_do_feedback_config.context = gc_do_feedback_config.context + "\n\n" + filenames_str # get current outline related info current_outline: str | None = None @@ -783,15 +809,20 @@ async def _gc_outline_feedback( if current_outline is not None: outline_str = "Current outline under review: " + current_outline - gc_config.context = gc_config.context + "\n\n" + outline_str + gc_do_feedback_config.context = gc_do_feedback_config.context + "\n\n" + outline_str + + if message is not None: + user_message = message.content + else: + user_message = None try: response_message, conversation_status = await GuidedConversationAgent.step_conversation( config=config, openai_client=openai_client.create_client(config.service_config), - agent_config=gc_config, + agent_config=gc_do_feedback_config, conversation_context=context, - last_user_message=None, # fix this. Should be None on the first call, but keep the user message after. + last_user_message=user_message, ) # add the completion to the metadata for debugging @@ -951,16 +982,6 @@ def _format_message(message: ConversationMessage, participants: list[Conversatio # endregion -# -# region GC agent config temp -# -# pull in GC config with its defaults, and then make changes locally here for now. -gc_config = GCAttachmentCheckConfigModel() -gc_do_feedback_config = GCDraftOutlineFeedbackConfigModel() - -# endregion - - ##### FROM NOTEBOOK # await document_skill.draft_outline(context=unused, openai_client=async_client, model=model) # From 1d269ae5d5fb829646966d59e36e255a04d5cab7 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 5 Nov 2024 21:03:10 +0000 Subject: [PATCH 4/5] check status initiated --- .../assistant/agents/document_agent.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index bd8d2489..149e603a 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -529,6 +529,9 @@ async def _step_gc_attachment_check( return self._state.mode.get_status() # Run + if step_status is Status.INITIATED: + self._state.mode.get_step().set_status(Status.NOT_COMPLETED) + logger.info("Document Agent running step: %s", step_name) status = await self._gc_attachment_check(config, context, message, metadata) step.set_status(status) @@ -567,6 +570,9 @@ async def _step_draft_outline( return self._state.mode.get_status() # Run + if step_status is Status.INITIATED: + self._state.mode.get_step().set_status(Status.NOT_COMPLETED) + logger.info("Document Agent running step: %s", step_name) status = await self._draft_outline(config, context, message, metadata) step.set_status(status) @@ -606,6 +612,9 @@ async def _step_gc_get_outline_feedback( return self._state.mode.get_status() # Run + if step_status is Status.INITIATED: + self._state.mode.get_step().set_status(Status.NOT_COMPLETED) + # Because the last user message will be ending a prior step, and not be related to this step. user_message: ConversationMessage | None if step_status is Status.INITIATED: From 273bc9258b56d804bb6e275a749cfc4252208b9b Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Wed, 6 Nov 2024 01:03:50 +0000 Subject: [PATCH 5/5] Changes to support correct output from gc on outline feedback to new draft. Fixes for correct state updates. Does not address outline feedback loop issue --- .../gc_draft_outline_feedback_config.py | 11 ++--- .../agents/document/guided_conversation.py | 31 +++++++----- .../assistant/agents/document_agent.py | 49 ++++++------------- 3 files changed, 39 insertions(+), 52 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 6b68d8c0..cc75ac86 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 @@ -19,22 +19,21 @@ 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.") # Rules - These are the do's and don'ts that the agent should follow during the conversation. rules = [ "Terminate the conversation immediately if the user asks for harmful or inappropriate content.", - "If the conversation is complete due to the user wanting the outline updated, set the conversation_status to update_outline.", - "If the conversation is complete due to the user ready to move on to drafting the paper, set the conversation_state to user_completed.", ] # 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, consider the conversation complete. -In this scenario, your final message should 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, consider the -conversation complete. In this scenario, your final message should inform the user that you will start drafting the beginning of the +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 should 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 should inform the user that you will start drafting the beginning of the document based on this outline. """ diff --git a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py index dd5ad040..51f1bee9 100644 --- a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py +++ b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py @@ -105,6 +105,7 @@ async def step_conversation( final_response: str = "" conversation_status: str | None = None + user_decision: str = "" response: str = "" # to_json is actually to dict, not to json. @@ -115,27 +116,33 @@ async def step_conversation( if artifact_item is not None: final_response = artifact_item.get("final_response") conversation_status = artifact_item.get("conversation_status") + user_decision = artifact_item.get("user_decision") # 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 result.is_conversation_over is True: - _delete_guided_conversation_state(conversation_context) - if conversation_status == "user_completed": - status = Status.USER_COMPLETED - response = final_response - elif conversation_status == "update_outline": - status = Status.UPDATE_OUTLINE - response = final_response - else: - status = Status.USER_EXIT_EARLY - response = final_response - else: + if conversation_status == "Unanswered": if result.ai_message is not None: response = result.ai_message else: response = "" status = Status.NOT_COMPLETED + elif conversation_status == "user_completed": + _delete_guided_conversation_state(conversation_context) + response = final_response + if user_decision is None: + status = Status.USER_COMPLETED + else: + if user_decision == "update_outline": # this code is becoming highly coupled fyi to the gc configs + status = Status.UPDATE_OUTLINE + elif user_decision == "draft_paper": + status = Status.USER_COMPLETED + else: + logger.error("unknown user decision") + else: + _delete_guided_conversation_state(conversation_context) + status = Status.USER_EXIT_EARLY + response = final_response return response, status diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 149e603a..039eda2f 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -381,36 +381,16 @@ async def _run_mode( step_name = step.get_name() step_status = step.get_status() - # This Status.INITIATED will occur when the mode is setting up the first step on its first run - if step_status is Status.INITIATED: - logger.info("Document Agent mode (%s) at beginning.", mode_name) - self._state.mode.get_step().set_status(Status.NOT_COMPLETED) - self._write_state(context) - - step = self._state.mode.get_step() - step_name = step.get_name() - step_status = step.get_status() - - # This Status.INITIATED will occur when a new step is setup upon a prior step's Status.USER_COMPLETED. while step_status is Status.INITIATED or step_status is Status.NOT_COMPLETED: step_method = self._get_step_method(step) if step_method: logger.info("Document Agent in step: %s", step_name) step_status = await step_method(config, context, message, metadata) - match step_status: - case Status.UNDEFINED: - logger.error( - "Calling corresponding step method for %s resulted in status %s. Resetting mode %s.", - step_name, - step_status, - mode_name, - ) - self._state.mode.reset() - break # problem - + match step_status: # resulting status of step_method() case Status.NOT_COMPLETED: self._state.mode.get_step().set_status(step_status) + self._state.mode.set_status(step_status) break # ok - get more user input case Status.USER_COMPLETED: @@ -418,8 +398,9 @@ async def _run_mode( if next_step is not None: step = next_step step_name = next_step.get_name() - step_status = next_step.get_status() + step_status = next_step.get_status() # new step is Status.INITIATED self._state.mode.set_step(next_step) + self._write_state(context) continue # ok - don't need user input yet else: self._state.mode.get_step().set_status(step_status) @@ -430,6 +411,16 @@ async def _run_mode( self._state.mode.get_step().set_status(step_status) self._state.mode.set_status(step_status) break # ok - done early :) + + case _: # UNDEFINED, INITIATED + logger.error( + "Document Agent: Calling corresponding step method for %s resulted in status %s. Resetting mode %s.", + step_name, + step_status, + mode_name, + ) + self._state.mode.reset() + break # problem else: logger.error( "Document Agent failed to find a corresponding step method for %s. Resetting mode %s.", @@ -482,9 +473,9 @@ async def _mode_draft_outline( StepName.DO_FINAL_OUTLINE, ], ) + logger.info("Document Agent mode (%s) at beginning.", mode_name) first_step_name = self._state.mode.get_step_order()[0] self._state.mode.set_step(Step(name=first_step_name, status=Status.INITIATED)) - self._state.mode.set_status(Status.NOT_COMPLETED) self._write_state(context) self._step_name_to_method: dict[StepName, Callable] = { @@ -529,9 +520,6 @@ async def _step_gc_attachment_check( return self._state.mode.get_status() # Run - if step_status is Status.INITIATED: - self._state.mode.get_step().set_status(Status.NOT_COMPLETED) - logger.info("Document Agent running step: %s", step_name) status = await self._gc_attachment_check(config, context, message, metadata) step.set_status(status) @@ -570,9 +558,6 @@ async def _step_draft_outline( return self._state.mode.get_status() # Run - if step_status is Status.INITIATED: - self._state.mode.get_step().set_status(Status.NOT_COMPLETED) - logger.info("Document Agent running step: %s", step_name) status = await self._draft_outline(config, context, message, metadata) step.set_status(status) @@ -612,10 +597,6 @@ async def _step_gc_get_outline_feedback( return self._state.mode.get_status() # Run - if step_status is Status.INITIATED: - self._state.mode.get_step().set_status(Status.NOT_COMPLETED) - - # Because the last user message will be ending a prior step, and not be related to this step. user_message: ConversationMessage | None if step_status is Status.INITIATED: user_message = None