From 2f45c41534c46bd5b575fd80c56ba080caa94660 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 22 Oct 2024 22:54:20 +0000 Subject: [PATCH 1/3] document agent changes for new flow. removal of guided conversation agent --- .../agents/document/guided_conversation.py | 145 +++++++++ .../assistant/agents/document_agent.py | 292 ++++++++++++++---- .../agents/guided_conversation/config.py | 82 ++--- .../agents/guided_conversation_agent.py | 286 ----------------- .../prospector-assistant/assistant/chat.py | 40 ++- 5 files changed, 435 insertions(+), 410 deletions(-) create mode 100644 assistants/prospector-assistant/assistant/agents/document/guided_conversation.py delete mode 100644 assistants/prospector-assistant/assistant/agents/guided_conversation_agent.py diff --git a/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py new file mode 100644 index 00000000..8c98b854 --- /dev/null +++ b/assistants/prospector-assistant/assistant/agents/document/guided_conversation.py @@ -0,0 +1,145 @@ +import json +import logging +from pathlib import Path + +from assistant.agents.guided_conversation.config import GuidedConversationAgentConfigModel +from guided_conversation.guided_conversation_agent import GuidedConversation +from openai import AsyncOpenAI +from semantic_kernel import Kernel +from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion +from semantic_workbench_assistant.assistant_app import ( + ConversationContext, + storage_directory_for_context, +) + +from ...config import AssistantConfigModel + +logger = logging.getLogger(__name__) + + +# +# region Agent +# + + +class GuidedConversationAgent: + """ + An agent for managing artifacts. + """ + + @staticmethod + def get_state( + conversation_context: ConversationContext, + ) -> dict | None: + """ + Get the state of the guided conversation agent. + """ + return _read_guided_conversation_state(conversation_context) + + @staticmethod + async def step_conversation( + config: AssistantConfigModel, + openai_client: AsyncOpenAI, + agent_config: GuidedConversationAgentConfigModel, + conversation_context: ConversationContext, + last_user_message: str | None, + ) -> tuple[str | None, bool]: + """ + Step the conversation to the next turn. + """ + + rules = agent_config.rules + conversation_flow = agent_config.conversation_flow + context = agent_config.context + resource_constraint = agent_config.resource_constraint + artifact = agent_config.get_artifact_model() + + kernel = Kernel() + service_id = "gc_main" + + chat_service = OpenAIChatCompletion( + service_id=service_id, + async_client=openai_client, + ai_model_id=config.request_config.openai_model, + ) + kernel.add_service(chat_service) + + guided_conversation_agent: GuidedConversation + + state = _read_guided_conversation_state(conversation_context) + if state: + guided_conversation_agent = GuidedConversation.from_json( + json_data=state, + kernel=kernel, + artifact=artifact, # type: ignore + conversation_flow=conversation_flow, + context=context, + rules=rules, + resource_constraint=resource_constraint, + service_id=service_id, + ) + else: + guided_conversation_agent = GuidedConversation( + kernel=kernel, + artifact=artifact, # type: ignore + conversation_flow=conversation_flow, + context=context, + rules=rules, + resource_constraint=resource_constraint, + service_id=service_id, + ) + + # Step the conversation to start the conversation with the agent + # or message + result = await guided_conversation_agent.step_conversation(last_user_message) + + # 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 + + # endregion + + +# +# region Helpers +# + + +def _get_guided_conversation_storage_path(context: ConversationContext, filename: str | None = None) -> Path: + """ + Get the path to the directory for storing guided conversation files. + """ + path = storage_directory_for_context(context) / "guided-conversation" + if filename: + path /= filename + return path + + +def _write_guided_conversation_state(context: ConversationContext, state: dict) -> None: + """ + Write the state of the guided conversation agent to a file. + """ + json_data = json.dumps(state) + path = _get_guided_conversation_storage_path(context) + if not path.exists(): + path.mkdir(parents=True) + path = path / "state.json" + path.write_text(json_data) + + +def _read_guided_conversation_state(context: ConversationContext) -> dict | None: + """ + Read the state of the guided conversation agent from a file. + """ + path = _get_guided_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 + + +# endregion diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py index 6f414de5..b59ef8a2 100644 --- a/assistants/prospector-assistant/assistant/agents/document_agent.py +++ b/assistants/prospector-assistant/assistant/agents/document_agent.py @@ -1,5 +1,5 @@ import logging -from os import path +from enum import Enum from typing import Any, Callable import deepmerge @@ -9,6 +9,7 @@ ChatCompletionMessageParam, ChatCompletionSystemMessageParam, ) +from pydantic import BaseModel from semantic_workbench_api_model.workbench_model import ( ConversationMessage, ConversationParticipant, @@ -17,16 +18,29 @@ ) from semantic_workbench_assistant.assistant_app import ( ConversationContext, - storage_directory_for_context, ) from ..config import AssistantConfigModel +from .document.guided_conversation import GuidedConversationAgent, GuidedConversationAgentConfigModel logger = logging.getLogger(__name__) + # # region Agent # +class RoutineMode(Enum): + UNDEFINED = 1 + E2E_DRAFT_OUTLINE = 2 # change name later + + +class Routine(BaseModel): + mode: RoutineMode = RoutineMode.UNDEFINED + step: Callable | None = None + + +class State(BaseModel): + routine: Routine = Routine() class DocumentAgent: @@ -34,9 +48,11 @@ class DocumentAgent: An agent for working on document content: creation, editing, translation, etc. """ + state: State = State() + def __init__(self, attachments_extension: AttachmentsExtension) -> None: self.attachments_extension = attachments_extension - self._commands = [self.draft_outline] + self._commands = [self.set_draft_outline_mode] # self.draft_outline] @property def commands(self) -> list[Callable]: @@ -59,85 +75,214 @@ async def receive_command( if command.__name__ == msg_command_name: logger.info(f"Found command {message.command_name}") command_found = True - await command(config, context, message, metadata) # TO DO, handle commands with args + command(config, context, message, metadata) # does not handle command with args or async commands break if not command_found: logger.warning(f"Could not find command {message.command_name}") - async def draft_outline( + def respond_to_conversation( self, config: AssistantConfigModel, context: ConversationContext, message: ConversationMessage, metadata: dict[str, Any] = {}, ) -> 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 attachments related info - attachment_messages = await self.attachments_extension.get_completion_messages_for_attachments( - context, config=config.agents_config.attachment_agent - ) - - # 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() - - # create chat completion messages - chat_completion_messages: list[ChatCompletionMessageParam] = [] - chat_completion_messages.append(_main_system_message()) - 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)) - - # make completion call to openai - async with openai_client.create_client(config.service_config) as client: + # check state mode + match self.state.routine.mode: + case RoutineMode.UNDEFINED: + logger.info("Document Agent has no routine mode set. Returning.") + return + case RoutineMode.E2E_DRAFT_OUTLINE: + return self._run_e2e_draft_outline() + + @classmethod + def set_draft_outline_mode( + cls, + config: AssistantConfigModel, + context: ConversationContext, + message: ConversationMessage, + metadata: dict[str, Any] = {}, + ) -> None: + if cls.state.routine.mode is RoutineMode.UNDEFINED: + cls.state.routine.mode = RoutineMode.E2E_DRAFT_OUTLINE + else: + logger.info( + f"Document Agent in the middle of routine: {cls.state.routine.mode}. Cannot change routine modes." + ) + + def _run_e2e_draft_outline(self) -> None: + logger.info("In _run_e2e_draft_outline") + return + + async def _gc_respond_to_conversation( + self, + config: AssistantConfigModel, + gc_config: GuidedConversationAgentConfigModel, + context: ConversationContext, + metadata: dict[str, Any] = {}, + ) -> None: + method_metadata_key = "document_agent_gc_response" + is_conversation_over = False + last_user_message = None + + while not is_conversation_over: try: - completion_args = { - "messages": chat_completion_messages, - "model": config.request_config.openai_model, - "response_format": {"type": "text"}, - } - completion = await client.chat.completions.create(**completion_args) - content = completion.choices[0].message.content - _on_success_metadata_update(metadata, method_metadata_key, config, chat_completion_messages, completion) + response_message, is_conversation_over = 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=last_user_message, + ) + 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 + + if not is_conversation_over: + # add the completion to the metadata for debugging + deepmerge.always_merger.merge( + metadata, + { + "debug": { + f"{method_metadata_key}": {"response": agent_message}, + } + }, + ) + else: + break except Exception as e: - logger.exception(f"exception occurred calling openai chat completion: {e}") - content = ( - "An error occurred while calling the OpenAI API. Is it configured correctly?" - "View the debug inspector for more information." + logger.exception(f"exception occurred processing guided conversation: {e}") + agent_message = "An error occurred while processing the guided conversation." + deepmerge.always_merger.merge( + metadata, + { + "debug": { + f"{method_metadata_key}": { + "error": str(e), + }, + } + }, ) - _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) + # send the response to the conversation + await context.send_messages( + NewConversationMessage( + content=agent_message, + message_type=MessageType.chat, + metadata=metadata, + ) + ) - # send the response to the conversation - message_type = MessageType.chat - if message.message_type == MessageType.command: - message_type = MessageType.command_response + # async def draft_outline( + # self, + # config: AssistantConfigModel, + # context: ConversationContext, + # message: ConversationMessage, + # metadata: dict[str, Any] = {}, + # ) -> tuple[str, dict[str, Any]]: + # method_metadata_key = "draft_outline" - await context.send_messages( - NewConversationMessage( - content=content, - message_type=message_type, - metadata=metadata, - ) - ) + +# +# # 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 attachments related info +# attachment_messages = await self.attachments_extension.get_completion_messages_for_attachments( +# context, config=config.agents_config.attachment_agent +# ) +# +# # 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() +# +# # create chat completion messages +# chat_completion_messages: list[ChatCompletionMessageParam] = [] +# chat_completion_messages.append(_main_system_message()) +# 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)) +# +# # make completion call to openai +# async with openai_client.create_client(config.service_config) as client: +# try: +# completion_args = { +# "messages": chat_completion_messages, +# "model": config.request_config.openai_model, +# "response_format": {"type": "text"}, +# } +# completion = await client.chat.completions.create(**completion_args) +# 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 = ( +# "An error occurred while calling the OpenAI API. Is it configured correctly?" +# "View the debug inspector for more information." +# ) +# _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) +# +# # send the response to the conversation only if from a command. Otherwise return info to caller. +# message_type = MessageType.chat +# if message.message_type == MessageType.command: +# message_type = MessageType.command +# +# await context.send_messages( +# NewConversationMessage( +# content=content, +# message_type=message_type, +# metadata=metadata, +# ) +# ) +# +# return content, metadata # endregion + +# +# region Inspector +# + + +# class DocumentAgentConversationInspectorStateProvider: +# display_name = "Guided Conversation" +# description = "State of the guided conversation feature within the conversation." +# +# def __init__( +# self, +# config_provider: BaseModelAssistantConfig["AssistantConfigModel"], +# ) -> None: +# self.config_provider = config_provider +# +# async def get(self, context: ConversationContext) -> AssistantConversationInspectorStateDataModel: +# """ +# Get the state for the conversation. +# """ +# +# state = _read_guided_conversation_state(context) +# +# return AssistantConversationInspectorStateDataModel(data=state or {"content": "No state available."}) +# +# +## endregion + + # # region Message Helpers # @@ -258,3 +403,28 @@ 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 = GuidedConversationAgentConfigModel() + + +# endregion + + +##### FROM NOTEBOOK +# await document_skill.draft_outline(context=unused, openai_client=async_client, model=model) +# +# decision, user_feedback = await document_skill.get_user_feedback( +# context=unused, openai_client=async_client, model=model, outline=True +# ) +# +# while decision == "[ITERATE]": +# await document_skill.draft_outline( +# context=unused, openai_client=async_client, model=model, user_feedback=user_feedback +# ) +# decision, user_feedback = await document_skill.get_user_feedback( +# context=unused, openai_client=async_client, model=model, outline=True +# ) diff --git a/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py b/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py index d76dedab..f920d7de 100644 --- a/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py +++ b/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py @@ -1,59 +1,58 @@ import json -from typing import Annotated, Any, Dict, List, Type, get_type_hints +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 pydantic_core import PydanticUndefinedType from semantic_workbench_assistant.config import UISchema from ... import helpers -from . import draft_grant_proposal_config_defaults as config_defaults +from . import config_defaults as config_defaults + +if TYPE_CHECKING: + pass + # # region Helpers # +# take a full json schema and return a pydantic model, including support for +# nested objects and typed arrays -def determine_type(type_str: str) -> Type: - type_mapping = {"str": str, "int": int, "float": float, "bool": bool, "list": List[Any], "dict": Dict[str, Any]} - return type_mapping.get(type_str, Any) +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(json_data: str) -> Type[BaseModel]: - data = json.loads(json_data) - def create_fields(data: Dict[str, Any]) -> Dict[str, 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 key, value in data.items(): - if value["type"] == "dict": - nested_model = create_pydantic_model_from_json(json.dumps(value["value"])) - fields[key] = (nested_model, Field(description=value["description"])) + 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: - fields[key] = ( - determine_type(value["type"]), - Field(default=value["value"], description=value["description"]), - ) + python_type = json_type_to_python_type(prop_type) + fields[prop_name] = (python_type, Field(..., description=description)) return fields - fields = create_fields(data) - return create_model("DynamicModel", **fields) - - -def pydantic_model_to_json(model: BaseModel) -> Dict[str, Any]: - def get_type_str(py_type: Any) -> str: - type_mapping = {str: "str", int: "int", float: "float", bool: "bool", list: "list", dict: "dict"} - return type_mapping.get(py_type, "any") - - json_dict = {} - for field_name, field in model.model_fields.items(): - field_type = get_type_hints(model)[field_name] - default_value = field.default if not isinstance(field.default, PydanticUndefinedType) else "" - json_dict[field_name] = { - "value": default_value, - "type": get_type_str(field_type), - "description": field.description or "", - } - return json_dict + properties = schema.get("properties", {}) + fields = parse_properties(properties) + return create_model(model_name, **fields) # endregion @@ -77,13 +76,13 @@ class GuidedConversationAgentConfigModel(BaseModel): title="Artifact", description="The artifact that the agent will manage.", ), - UISchema(widget="textarea"), - ] = json.dumps(pydantic_model_to_json(config_defaults.ArtifactModel), indent=2) # type: ignore + UISchema(widget="baseModelEditor"), + ] = json.dumps(config_defaults.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"}}), + UISchema(schema={"items": {"ui:widget": "textarea", "ui:options": {"rows": 2}}}), ] = config_defaults.rules conversation_flow: Annotated[ @@ -92,7 +91,7 @@ class GuidedConversationAgentConfigModel(BaseModel): title="Conversation Flow", description="A loose natural language description of the steps of the conversation", ), - UISchema(widget="textarea", placeholder="[optional]"), + UISchema(widget="textarea", schema={"ui:options": {"rows": 10}}, placeholder="[optional]"), ] = config_defaults.conversation_flow.strip() context: Annotated[ @@ -141,7 +140,8 @@ class ResourceConstraint(ResourceConstraint): ] = ResourceConstraint() def get_artifact_model(self) -> Type[BaseModel]: - return create_pydantic_model_from_json(self.artifact) + schema = json.loads(self.artifact) + return create_pydantic_model_from_json_schema(schema) # endregion diff --git a/assistants/prospector-assistant/assistant/agents/guided_conversation_agent.py b/assistants/prospector-assistant/assistant/agents/guided_conversation_agent.py deleted file mode 100644 index 54611224..00000000 --- a/assistants/prospector-assistant/assistant/agents/guided_conversation_agent.py +++ /dev/null @@ -1,286 +0,0 @@ -import json -import logging -from pathlib import Path -from typing import TYPE_CHECKING, Any - -import deepmerge -import openai_client -from assistant.agents.guided_conversation.config import GuidedConversationAgentConfigModel -from guided_conversation.guided_conversation_agent import GuidedConversation -from openai import AsyncOpenAI -from openai.types.chat import ChatCompletionMessageParam -from semantic_kernel import Kernel -from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion -from semantic_workbench_api_model.workbench_model import ( - AssistantStateEvent, - MessageType, - NewConversationMessage, - ParticipantRole, -) -from semantic_workbench_assistant.assistant_app import ( - AssistantConversationInspectorStateDataModel, - BaseModelAssistantConfig, - ConversationContext, - storage_directory_for_context, -) - -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from ..config import AssistantConfigModel, RequestConfig - - -# -# region Agent -# - - -class GuidedConversationAgent: - """ - An agent for managing artifacts. - """ - - def __init__( - self, - config_provider: BaseModelAssistantConfig["AssistantConfigModel"], - ) -> None: - self.config_provider = config_provider - - @staticmethod - def get_state( - conversation_context: ConversationContext, - ) -> dict | None: - """ - Get the state of the guided conversation agent. - """ - return _read_guided_conversation_state(conversation_context) - - @staticmethod - async def step_conversation( - conversation_context: ConversationContext, - openai_client: AsyncOpenAI, - request_config: "RequestConfig", - agent_config: GuidedConversationAgentConfigModel, - additional_messages: list[ChatCompletionMessageParam] | None = None, - ) -> str | None: - """ - Step the conversation to the next turn. - """ - - rules = agent_config.rules - conversation_flow = agent_config.conversation_flow - context = agent_config.context - resource_constraint = agent_config.resource_constraint - artifact = agent_config.get_artifact_model() - - kernel = Kernel() - service_id = "gc_main" - - chat_service = OpenAIChatCompletion( - service_id=service_id, - async_client=openai_client, - ai_model_id=request_config.openai_model, - ) - kernel.add_service(chat_service) - - guided_conversation_agent: GuidedConversation - - state = _read_guided_conversation_state(conversation_context) - if state: - guided_conversation_agent = GuidedConversation.from_json( - json_data=state, - kernel=kernel, - artifact=artifact, # type: ignore - conversation_flow=conversation_flow, - context=context, - rules=rules, - resource_constraint=resource_constraint, - service_id=service_id, - ) - else: - guided_conversation_agent = GuidedConversation( - kernel=kernel, - artifact=artifact, # type: ignore - conversation_flow=conversation_flow, - context=context, - rules=rules, - resource_constraint=resource_constraint, - service_id=service_id, - ) - - # Get the latest message from the user - messages_response = await conversation_context.get_messages(limit=1, participant_role=ParticipantRole.user) - last_user_message = messages_response.messages[0].content if messages_response.messages else None - - # Step the conversation to start the conversation with the agent - result = await guided_conversation_agent.step_conversation(last_user_message) - - # Save the state of the guided conversation agent - _write_guided_conversation_state(conversation_context, guided_conversation_agent.to_json()) - - return result.ai_message - - # endregion - - # - # region Response - # - - # demonstrates how to respond to a conversation message using the guided conversation library - async def respond_to_conversation( - self, - context: ConversationContext, - metadata: dict[str, Any] = {}, - additional_messages: list[ChatCompletionMessageParam] | None = None, - ) -> None: - """ - Respond to a conversation message. - - This method uses the guided conversation agent to respond to a conversation message. The guided conversation - agent is designed to guide the conversation towards a specific goal as specified in its definition. - """ - - # define the metadata key for any metadata created within this method - method_metadata_key = "respond_to_conversation" - - # get the assistant configuration - assistant_config = await self.config_provider.get(context.assistant) - - # initialize variables for the response content - content: str | None = None - - try: - content = await self.step_conversation( - conversation_context=context, - openai_client=openai_client.create_client(assistant_config.service_config), - request_config=assistant_config.request_config, - agent_config=assistant_config.agents_config.guided_conversation_agent, - additional_messages=additional_messages, - ) - # add the completion to the metadata for debugging - deepmerge.always_merger.merge( - metadata, - { - "debug": { - f"{method_metadata_key}": {"response": content}, - } - }, - ) - except Exception as e: - logger.exception(f"exception occurred processing guided conversation: {e}") - content = "An error occurred while processing the guided conversation." - deepmerge.always_merger.merge( - metadata, - { - "debug": { - f"{method_metadata_key}": { - "error": str(e), - }, - } - }, - ) - - # add the state to the metadata for debugging - state = self.get_state(context) - deepmerge.always_merger.merge( - metadata, - { - "debug": { - f"{method_metadata_key}": { - "state": state, - }, - } - }, - ) - - # send the response to the conversation - await context.send_messages( - NewConversationMessage( - content=content or "[no response from assistant]", - message_type=MessageType.chat if content else MessageType.note, - metadata=metadata, - ) - ) - - await context.send_conversation_state_event( - AssistantStateEvent( - state_id="guided_conversation", - event="updated", - state=None, - ) - ) - - -# endregion - - -# -# region Inspector -# - - -class GuidedConversationConversationInspectorStateProvider: - display_name = "Guided Conversation" - description = "State of the guided conversation feature within the conversation." - - def __init__( - self, - config_provider: BaseModelAssistantConfig["AssistantConfigModel"], - ) -> None: - self.config_provider = config_provider - - async def get(self, context: ConversationContext) -> AssistantConversationInspectorStateDataModel: - """ - Get the state for the conversation. - """ - - state = _read_guided_conversation_state(context) - - return AssistantConversationInspectorStateDataModel(data=state or {"content": "No state available."}) - - -# endregion - - -# -# region Helpers -# - - -def _get_guided_conversation_storage_path(context: ConversationContext, filename: str | None = None) -> Path: - """ - Get the path to the directory for storing guided conversation files. - """ - path = storage_directory_for_context(context) / "guided-conversation" - if filename: - path /= filename - return path - - -def _write_guided_conversation_state(context: ConversationContext, state: dict) -> None: - """ - Write the state of the guided conversation agent to a file. - """ - json_data = json.dumps(state) - path = _get_guided_conversation_storage_path(context) - if not path.exists(): - path.mkdir(parents=True) - path = path / "state.json" - path.write_text(json_data) - - -def _read_guided_conversation_state(context: ConversationContext) -> dict | None: - """ - Read the state of the guided conversation agent from a file. - """ - path = _get_guided_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 - - -# endregion diff --git a/assistants/prospector-assistant/assistant/chat.py b/assistants/prospector-assistant/assistant/chat.py index f9e45554..8bd3a8df 100644 --- a/assistants/prospector-assistant/assistant/chat.py +++ b/assistants/prospector-assistant/assistant/chat.py @@ -33,10 +33,6 @@ from .agents.artifact_agent import Artifact, ArtifactAgent, ArtifactConversationInspectorStateProvider from .agents.document_agent import DocumentAgent -from .agents.guided_conversation_agent import ( - GuidedConversationAgent, - GuidedConversationConversationInspectorStateProvider, -) from .agents.skills_agent import SkillsAgent, SkillsAgentConversationInspectorStateProvider from .config import AssistantConfigModel @@ -76,7 +72,6 @@ async def content_evaluator_factory(context: ConversationContext) -> ContentSafe content_interceptor=content_safety, inspector_state_providers={ "artifacts": ArtifactConversationInspectorStateProvider(assistant_config), - "guided_conversation": GuidedConversationConversationInspectorStateProvider(assistant_config), "skills_agent": SkillsAgentConversationInspectorStateProvider(assistant_config), }, ) @@ -106,6 +101,7 @@ async def content_evaluator_factory(context: ConversationContext) -> ContentSafe # - @assistant.events.conversation.message.on_created (event triggered when a new message of any type is created) # - @assistant.events.conversation.message.chat.on_created (event triggered when a new chat message is created) # +doc_agent_running = False @assistant.events.conversation.message.command.on_created @@ -119,7 +115,10 @@ async def on_command_message_created( # We assume Document Agent is available and future logic would determine which agent # the command is intended for. Assumption made in order to make doc agent available asap. - # if config.agents_config.document_agent.enabled: + # config.agents_config.document_agent.enabled = True # To do... tie into config. + global doc_agent_running + doc_agent_running = True + doc_agent = DocumentAgent(attachments_extension) await doc_agent.receive_command(config, context, message, metadata) @@ -155,9 +154,10 @@ async def on_message_created( # NOTE: we're experimenting with agents, if they are enabled, use them to respond to the conversation # - # Guided conversation agent response - if config.agents_config.guided_conversation_agent.enabled: - return await guided_conversation_agent_respond_to_conversation(context, config, metadata) + # if config.agents_config.document_agent.enabled: # To do... tie into config. + global doc_agent_running + if doc_agent_running: + return document_agent_respond_to_conversation(config, context, message, metadata) # Skills agent response if config.agents_config.skills_agent.enabled: @@ -198,22 +198,18 @@ async def on_conversation_created(context: ConversationContext) -> None: # -async def guided_conversation_agent_respond_to_conversation( - context: ConversationContext, config: AssistantConfigModel, metadata: dict[str, Any] = {} +def document_agent_respond_to_conversation( + config: AssistantConfigModel, + context: ConversationContext, + message: ConversationMessage, + metadata: dict[str, Any] = {}, ) -> None: """ - Respond to a conversation message using the guided conversation agent. + Respond to a conversation message using the document agent. """ - # create the guided conversation agent instance - guided_conversation_agent = GuidedConversationAgent(config_provider=assistant_config) - - # add the attachment agent messages to the completion messages - attachment_messages = await attachments_extension.get_completion_messages_for_attachments( - context, config=config.agents_config.attachment_agent - ) - additional_messages: list[ChatCompletionMessageParam] = list(attachment_messages) - - return await guided_conversation_agent.respond_to_conversation(context, metadata, additional_messages) + # create the document agent instance + document_agent = DocumentAgent(attachments_extension) + return document_agent.respond_to_conversation(config, context, message, metadata) async def skills_agent_respond_to_conversation( From 650d73c3b881d501466a051c22da81992c02b9d0 Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 22 Oct 2024 23:10:10 +0000 Subject: [PATCH 2/3] remove rest of gc agent material --- .../agents/guided_conversation/config.py | 147 ----------------- .../guided_conversation/config_defaults.py | 67 -------- .../draft_grant_proposal_config_defaults.py | 155 ------------------ 3 files changed, 369 deletions(-) delete mode 100644 assistants/prospector-assistant/assistant/agents/guided_conversation/config.py delete mode 100644 assistants/prospector-assistant/assistant/agents/guided_conversation/config_defaults.py delete mode 100644 assistants/prospector-assistant/assistant/agents/guided_conversation/draft_grant_proposal_config_defaults.py diff --git a/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py b/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py deleted file mode 100644 index f920d7de..00000000 --- a/assistants/prospector-assistant/assistant/agents/guided_conversation/config.py +++ /dev/null @@ -1,147 +0,0 @@ -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 - -if TYPE_CHECKING: - pass - - -# -# 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 GuidedConversationAgentConfigModel(BaseModel): - 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(config_defaults.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}}}), - ] = config_defaults.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]"), - ] = config_defaults.conversation_flow.strip() - - context: Annotated[ - str, - Field( - title="Context", - description="General background context for the conversation.", - ), - UISchema(widget="textarea", placeholder="[optional]"), - ] = config_defaults.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.' - ), - ), - ] = config_defaults.resource_constraint.mode - - unit: Annotated[ - ResourceConstraintUnit, - Field( - title="Resource Unit", - description="The unit for the resource constraint.", - ), - ] = config_defaults.resource_constraint.unit - - quantity: Annotated[ - float, - Field( - title="Resource Quantity", - description="The quantity for the resource constraint. If <=0, the resource constraint is disabled.", - ), - ] = config_defaults.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/guided_conversation/config_defaults.py b/assistants/prospector-assistant/assistant/agents/guided_conversation/config_defaults.py deleted file mode 100644 index ad172ecd..00000000 --- a/assistants/prospector-assistant/assistant/agents/guided_conversation/config_defaults.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import TYPE_CHECKING - -from guided_conversation.utils.resources import ResourceConstraint, ResourceConstraintMode, ResourceConstraintUnit -from pydantic import BaseModel, Field - -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): - student_poem: str = Field(description="The acrostic poem written by the student.") - initial_feedback: str = Field(description="Feedback on the student's final revised poem.") - final_feedback: str = Field(description="Feedback on how the student was able to improve their poem.") - inappropriate_behavior: list[str] = Field( - description="""List any inappropriate behavior the student attempted while chatting with you. \ -It is ok to leave this field Unanswered if there was none.""" - ) - - -# Rules - These are the do's and don'ts that the agent should follow during the conversation. -rules = [ - "DO NOT write the poem for the student.", - "Terminate the conversation immediately if the students asks for harmful or inappropriate content.", -] - -# Conversation Flow (optional) - This defines in natural language the steps of the conversation. -conversation_flow = """1. Start by explaining interactively what an acrostic poem is. -2. Then give the following instructions for how to go ahead and write one: - 1. Choose a word or phrase that will be the subject of your acrostic poem. - 2. Write the letters of your chosen word or phrase vertically down the page. - 3. Think of a word or phrase that starts with each letter of your chosen word or phrase. - 4. Write these words or phrases next to the corresponding letters to create your acrostic poem. -3. Then give the following example of a poem where the word or phrase is HAPPY: - Having fun with friends all day, - Awesome games that we all play. - Pizza parties on the weekend, - Puppies we bend down to tend, - Yelling yay when we win the game -4. Finally have the student write their own acrostic poem using the word or phrase of their choice. Encourage them to be creative and have fun with it. -After they write it, you should review it and give them feedback on what they did well and what they could improve on. -Have them revise their poem based on your feedback and then review it again. -""" - -# 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 1 on 1 a 4th grade student who is chatting with you in the computer lab at school while being supervised by their teacher.""" - - -# 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=10, - unit=ResourceConstraintUnit.TURNS, - mode=ResourceConstraintMode.EXACT, -) - -__all__ = [ - "ArtifactModel", - "rules", - "conversation_flow", - "context", - "resource_constraint", -] diff --git a/assistants/prospector-assistant/assistant/agents/guided_conversation/draft_grant_proposal_config_defaults.py b/assistants/prospector-assistant/assistant/agents/guided_conversation/draft_grant_proposal_config_defaults.py deleted file mode 100644 index e891472f..00000000 --- a/assistants/prospector-assistant/assistant/agents/guided_conversation/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", -] From da13de8f53dd9d413b923011f4a14b54768cf28a Mon Sep 17 00:00:00 2001 From: Mollie Munoz Date: Tue, 22 Oct 2024 23:16:51 +0000 Subject: [PATCH 3/3] Fix GC config reference for prospector --- assistants/prospector-assistant/assistant/config.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/assistants/prospector-assistant/assistant/config.py b/assistants/prospector-assistant/assistant/config.py index b7987f02..e754649c 100644 --- a/assistants/prospector-assistant/assistant/config.py +++ b/assistants/prospector-assistant/assistant/config.py @@ -8,7 +8,6 @@ from . import helpers from .agents.artifact_agent import ArtifactAgentConfigModel -from .agents.guided_conversation.config import GuidedConversationAgentConfigModel # The semantic workbench app uses react-jsonschema-form for rendering # dynamic configuration forms based on the configuration model and UI schema @@ -42,15 +41,6 @@ class AgentsConfigModel(BaseModel): ), ] = AttachmentsConfigModel() - guided_conversation_agent: Annotated[ - GuidedConversationAgentConfigModel, - Field( - title="Guided Conversation Agent Configuration", - description="Configuration for the guided conversation agent.", - ), - UISchema(widget="hidden"), # Hide the guided conversation agent configuration for now, until we can remove it - ] = GuidedConversationAgentConfigModel() - class HighTokenUsageWarning(BaseModel): enabled: Annotated[