From 71f362c4c96759bf460d2e1b9d9bbb117f1f36e5 Mon Sep 17 00:00:00 2001 From: Mark Waddle Date: Tue, 5 Nov 2024 15:55:12 -0800 Subject: [PATCH] Adds state_updated_event_after (#216) To the assistant app conversationcontext to simplify my agent and allow others to use it --- .../agents/form_fill_agent/inspector.py | 12 +-------- .../assistant/agents/form_fill_agent/state.py | 4 +-- .../steps/_guided_conversation.py | 3 +-- .../form_fill_agent/steps/fill_form_step.py | 2 +- .../assistant_app/context.py | 26 +++++++++++++++++++ 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/assistants/prospector-assistant/assistant/agents/form_fill_agent/inspector.py b/assistants/prospector-assistant/assistant/agents/form_fill_agent/inspector.py index 19f790d8..e35ce6d5 100644 --- a/assistants/prospector-assistant/assistant/agents/form_fill_agent/inspector.py +++ b/assistants/prospector-assistant/assistant/agents/form_fill_agent/inspector.py @@ -1,10 +1,9 @@ import contextlib import json from pathlib import Path -from typing import AsyncIterator, Callable +from typing import Callable import yaml -from semantic_workbench_api_model.workbench_model import AssistantStateEvent from semantic_workbench_assistant.assistant_app.context import ConversationContext from semantic_workbench_assistant.assistant_app.protocol import ( AssistantConversationInspectorStateDataModel, @@ -53,12 +52,3 @@ def read_state(path: Path) -> dict: return AssistantConversationInspectorStateDataModel( data={"content": f"```yaml\n{yaml.dump(state, sort_keys=False)}\n```"}, ) - - -@contextlib.asynccontextmanager -async def state_change_event_after(context: ConversationContext, state_id: str, set_focus=False) -> AsyncIterator[None]: - """Raise a state change event after the context manager block is executed (optionally set focus as well)""" - yield - if set_focus: - await context.send_conversation_state_event(AssistantStateEvent(state_id=state_id, event="focus", state=None)) - await context.send_conversation_state_event(AssistantStateEvent(state_id=state_id, event="updated", state=None)) diff --git a/assistants/prospector-assistant/assistant/agents/form_fill_agent/state.py b/assistants/prospector-assistant/assistant/agents/form_fill_agent/state.py index e5f445fc..a31dcee6 100644 --- a/assistants/prospector-assistant/assistant/agents/form_fill_agent/state.py +++ b/assistants/prospector-assistant/assistant/agents/form_fill_agent/state.py @@ -8,7 +8,7 @@ from semantic_workbench_assistant.assistant_app.context import ConversationContext, storage_directory_for_context from semantic_workbench_assistant.storage import read_model, write_model -from .inspector import FileStateInspector, state_change_event_after +from .inspector import FileStateInspector class FormField(BaseModel): @@ -55,7 +55,7 @@ async def agent_state(context: ConversationContext) -> AsyncIterator[FormFillAge yield state return - async with state_change_event_after(context, inspector.state_id): + async with context.state_updated_event_after(inspector.state_id): state = read_model(path_for_state(context), FormFillAgentState) or FormFillAgentState() current_state.set(state) yield state diff --git a/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/_guided_conversation.py b/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/_guided_conversation.py index 81884f5d..08dbfbfc 100644 --- a/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/_guided_conversation.py +++ b/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/_guided_conversation.py @@ -17,7 +17,6 @@ from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_workbench_assistant.assistant_app.context import ConversationContext, storage_directory_for_context -from ..inspector import state_change_event_after from .types import GuidedConversationDefinition _state_locks: dict[Path, asyncio.Lock] = defaultdict(asyncio.Lock) @@ -41,7 +40,7 @@ async def engine( given state file. """ - async with _state_locks[state_file_path], state_change_event_after(context, state_id, set_focus=True): + async with _state_locks[state_file_path], context.state_updated_event_after(state_id, focus_event=True): kernel, service_id = _build_kernel_with_service(openai_client, openai_model) state: dict | None = None diff --git a/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/fill_form_step.py b/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/fill_form_step.py index 878a70ad..e79678cb 100644 --- a/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/fill_form_step.py +++ b/assistants/prospector-assistant/assistant/agents/form_fill_agent/steps/fill_form_step.py @@ -128,6 +128,7 @@ def _get_state_file_path(context: ConversationContext) -> Path: "When providing options for a multiple choice field, provide the options in a numbered-list, so the user can refer to them by number.", "When listing anything other than options, like document types, provide them in a bulleted list for improved readability.", "When updating the agenda, the data-collection for each form field must be in a separate step.", + "When asking for data to fill the form, always ask for a single piece of information at a time. Never ask for multiple pieces of information in a single prompt, ex: 'Please provide field Y, and additionally, field X'.", "Terminate conversation if inappropriate content is requested.", ], conversation_flow=""" @@ -137,7 +138,6 @@ def _get_state_file_path(context: ConversationContext) -> Path: 4. When asking for files, suggest types of documents that might contain the data. 5. For each field in the form, check if the data is available in the provided files. 6. If the data is not available in the files, ask the user for the data. -7. When asking for data to fill the form, ask for a single piece of information at a time. 8. When the form is filled out, inform the user that you will now generate a document containing the filled form. """, context="", diff --git a/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/context.py b/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/context.py index 64a106a1..e7ff43c9 100644 --- a/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/context.py +++ b/libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/context.py @@ -129,6 +129,32 @@ async def file_exists(self, filename: str) -> bool: async def delete_file(self, filename: str) -> None: return await self._workbench_client.delete_file(filename) + @asynccontextmanager + async def state_updated_event_after(self, state_id: str, focus_event: bool = False) -> AsyncIterator[None]: + """ + Raise state "updated" event after the context manager block is executed, and optionally, a + state "focus" event. + + Example: + ```python + # notify workbench that state has been updated + async with conversation.state_updated_event_after("my_state_id"): + await do_some_work() + + # notify workbench that state has been updated and set focus + async with conversation.state_updated_event_after("my_state_id", focus_event=True): + await do_some_work() + ``` + """ + yield + if focus_event: + await self.send_conversation_state_event( + workbench_model.AssistantStateEvent(state_id=state_id, event="focus", state=None) + ) + await self.send_conversation_state_event( + workbench_model.AssistantStateEvent(state_id=state_id, event="updated", state=None) + ) + def storage_directory_for_context(context: AssistantContext | ConversationContext, partition: str = "") -> pathlib.Path: match context: