This notebook explores one of our core modular components or plugins, the Artifact.
The artifact is a form, or a type of working memory for the agent. We implement it using a Pydantic BaseModel. As the conversation creator, you can define an arbitrary BaseModel that includes the fields you want the agent to fill out during the conversation.
Let's setup an artifact where the goal is to collect information about a customer's issue with a service.
from typing import Literal
from pydantic import BaseModel, Field, conlist
class Issue(BaseModel):
incident_type: Literal["Service Outage", "Degradation", "Billing", "Security", "Data Loss", "Other"] = Field(
description="A high level type describing the incident."
)
description: str = Field(description="A detailed description of what is going wrong.")
affected_services: conlist(str, min_length=0) = Field(description="The services affected by the incident.")
class OutageArtifact(BaseModel):
name: str = Field(description="How to address the customer.")
company: str = Field(description="The company the customer works for.")
role: str = Field(description="The role of the customer.")
email: str = Field(description="The best email to contact the customer.", pattern=r"^/^.+@.+$/$")
phone: str = Field(description="The best phone number to contact the customer.", pattern=r"^\d{3}-\d{3}-\d{4}$")
incident_start: int = Field(
description="About how many hours ago the incident started.",
)
incident_end: int = Field(
description="About how many hours ago the incident ended. If the incident is ongoing, set this to 0.",
)
issues: conlist(Issue, min_length=1) = Field(description="The issues the customer is experiencing.")
additional_comments: conlist(str, min_length=0) = Field("Any additional comments the customer has.")
Let's initialize the artifact as a standalone module.
It requires a Kernel and LLM Service, alongside a Conversation object.
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from guided_conversation.plugins.artifact import Artifact
from guided_conversation.utils.conversation_helpers import Conversation
kernel = Kernel()
service_id = "artifact_chat_completion"
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name="gpt-4o-2024-05-13",
api_version="2024-05-01-preview",
)
kernel.add_service(chat_service)
# Initialize the artifact
artifact = Artifact(kernel, service_id, OutageArtifact, max_artifact_field_retries=2)
conversation = Conversation()
To power the Artifact's ability to automatically fix issues, we provide the conversation history as additional context.
from semantic_kernel.contents import ChatMessageContent, AuthorRole
from guided_conversation.utils.conversation_helpers import ConversationMessageType
conversation.add_messages(
ChatMessageContent(role=AuthorRole.ASSISTANT,
content="Hello! I'm here to help you with your issue. Can you tell me your name, company, and role?")
)
conversation.add_messages(
ChatMessageContent(role=AuthorRole.USER,
content="Yes my name is Jane Doe, I work at Contoso, and I'm a database uhh administrator.")
)
result = await artifact.update_artifact(
field_name="name",
field_value="Jane Doe",
conversation=conversation,
)
conversation.add_messages(result.messages)
result = await artifact.update_artifact(
field_name="company",
field_value="Contoso",
conversation=conversation,
)
conversation.add_messages(result.messages)
result = await artifact.update_artifact(
field_name="role",
field_value="Database Administrator",
conversation=conversation,
)
conversation.add_messages(result.messages)
Let's see how the artifact was updated with these valid updates and the resulting conversation messages that were generated.
The Artifact creates messages whenever a field is updated for use in downstream agents like the main GuidedConversation.
print(f"Conversation up to this point:\n{conversation.get_repr_for_prompt()}\n")
print(f"Current state of the artifact:\n{artifact.get_artifact_for_prompt()}")
Conversation up to this point: Assistant: Hello! I'm here to help you with your issue. Can you tell me your name, company, and role? None: Yes my name is Jane Doe, I work at Contoso, and I'm a database uhh administrator. Assistant updated name to Jane Doe Assistant updated company to Contoso Assistant updated role to Database Administrator Current state of the artifact: {'name': 'Jane Doe', 'company': 'Contoso', 'role': 'Database Administrator', 'email': 'Unanswered', 'phone': 'Unanswered', 'incident_start': 'Unanswered', 'incident_end': 'Unanswered', 'issues': 'Unanswered', 'additional_comments': 'Unanswered'}
Next we test an invalid update on a field with a regex. The agent should not update the artifact and instead resume the conversation because the provided email is incomplete.
conversation.add_messages(ChatMessageContent(role=AuthorRole.ASSISTANT, content="What is the best email to contact you at?"))
conversation.add_messages(ChatMessageContent(role=AuthorRole.USER, content="my email is jdoe"))
result = await artifact.update_artifact(
field_name="email",
field_value="jdoe",
conversation=conversation,
)
conversation.add_messages(result.messages)
Error updating field email: 1 validation error for Artifact email String should match pattern '^/^.+@.+$/$|Unanswered' [type=string_pattern_mismatch, input_value='jdoe', input_type=str] For further information visit https://errors.pydantic.dev/2.8/v/string_pattern_mismatch. Retrying...
If the agent returned success, but did make an update (as shown by not generating a conversation message indicating such), then we implicitly assume the agent has resumed the conversation.
print(f"Conversation up to this point:\n{conversation.get_repr_for_prompt()}")
Conversation up to this point: Assistant: Hello! I'm here to help you with your issue. Can you tell me your name, company, and role? None: Yes my name is Jane Doe, I work at Contoso, and I'm a database uhh administrator. Assistant updated name to Jane Doe Assistant updated company to Contoso Assistant updated role to Database Administrator Assistant: What is the best email to contact you at? None: my email is jdoe
Now let's see what happens if we keep trying to update that failed field.
result = await artifact.update_artifact(
field_name="email",
field_value="jdoe",
conversation=conversation,
)
# And again
result = await artifact.update_artifact(
field_name="email",
field_value="jdoe",
conversation=conversation,
)
Error updating field email: 1 validation error for Artifact email String should match pattern '^/^.+@.+$/$|Unanswered' [type=string_pattern_mismatch, input_value='jdoe', input_type=str] For further information visit https://errors.pydantic.dev/2.8/v/string_pattern_mismatch. Retrying... Updating field email has failed too many times. Skipping.
If we look at the current state of the artifact, we should see that the email has been removed since it has now failed 3 times which is greater than the max_artifact_field_retries parameter we set when we instantiated the artifact.
artifact.get_artifact_for_prompt()
{'name': 'Jane Doe', 'company': 'Contoso', 'role': 'Database Administrator', 'phone': 'Unanswered', 'incident_start': 'Unanswered', 'incident_end': 'Unanswered', 'issues': 'Unanswered', 'additional_comments': 'Unanswered'}
Now let's move on to trying to update a more complex field: the issues field.
conversation.add_messages(
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Can you tell me about the issues you're experiencing?")
)
conversation.add_messages(
ChatMessageContent(role=AuthorRole.USER, content="""The latency of accessing our database service has increased by 200\% in the last 24 hours,
even on a fresh instance. Additionally, we're seeing a lot of timeouts when trying to access the management portal.""")
)
result = await artifact.update_artifact(
field_name="issues",
field_value=[
{
"incident_type": "Degradation",
"description": """The latency of accessing the customer's database service has increased by 200% in the \
last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal.""",
"affected_services": ["Database Service", "Database Management Portal"],
}
],
conversation=conversation,
)
conversation.add_messages(result.messages)
print(f"Conversation up to this point:\n{conversation.get_repr_for_prompt()}\n")
print(f"Current state of the artifact:\n{artifact.get_artifact_for_prompt()}")
Conversation up to this point: Assistant: Hello! I'm here to help you with your issue. Can you tell me your name, company, and role? None: Yes my name is Jane Doe, I work at Contoso, and I'm a database uhh administrator. Assistant updated name to Jane Doe Assistant updated company to Contoso Assistant updated role to Database Administrator Assistant: What is the best email to contact you at? None: my email is jdoe Assistant: Can you tell me about the issues you're experiencing? None: The latency of accessing our database service has increased by 200\% in the last 24 hours, even on a fresh instance. Additionally, we're seeing a lot of timeouts when trying to access the management portal. Assistant updated issues to [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal.", 'affected_services': ['Database Service', 'Database Management Portal']}] Current state of the artifact: {'name': 'Jane Doe', 'company': 'Contoso', 'role': 'Database Administrator', 'phone': 'Unanswered', 'incident_start': 'Unanswered', 'incident_end': 'Unanswered', 'issues': [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal.", 'affected_services': ['Database Service', 'Database Management Portal']}], 'additional_comments': 'Unanswered'}
To add another affected service, we can need to update the issues field with the new value again. The obvious con of this approach is that the model generating the field_value has to regenerate the entire field_value. However, the pro is that keeps the available tools simple for the model.
conversation.add_messages(
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Is there anything else you'd like to add about the issues you're experiencing?")
)
conversation.add_messages(
ChatMessageContent(role=AuthorRole.USER, content="Yes another thing that is effected is access to billing information is very slow.")
)
result = await artifact.update_artifact(
field_name="issues",
field_value=[
{
"incident_type": "Degradation",
"description": """The latency of accessing the customer's database service has increased by 200% in the \
last 24 hours, even on a fresh instance. They also report timeouts when trying to access the \
management portal and slowdowns in the access to billing information.""",
"affected_services": ["Database Service", "Database Management Portal", "Billing portal"],
},
],
conversation=conversation,
)
conversation.add_messages(result.messages)
print(f"Conversation up to this point:\n{conversation.get_repr_for_prompt()}\n")
print(f"Current state of the artifact:\n{artifact.get_artifact_for_prompt()}")
Conversation up to this point: Assistant: Hello! I'm here to help you with your issue. Can you tell me your name, company, and role? None: Yes my name is Jane Doe, I work at Contoso, and I'm a database uhh administrator. Assistant updated name to Jane Doe Assistant updated company to Contoso Assistant updated role to Database Administrator Assistant: What is the best email to contact you at? None: my email is jdoe Assistant: Can you tell me about the issues you're experiencing? None: The latency of accessing our database service has increased by 200\% in the last 24 hours, even on a fresh instance. Additionally, we're seeing a lot of timeouts when trying to access the management portal. Assistant updated issues to [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal.", 'affected_services': ['Database Service', 'Database Management Portal']}] Assistant: Is there anything else you'd like to add about the issues you're experiencing? None: Yes another thing that is effected is access to billing information is very slow. Assistant updated issues to [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal and slowdowns in the access to billing information.", 'affected_services': ['Database Service', 'Database Management Portal', 'Billing portal']}] Current state of the artifact: {'name': 'Jane Doe', 'company': 'Contoso', 'role': 'Database Administrator', 'phone': 'Unanswered', 'incident_start': 'Unanswered', 'incident_end': 'Unanswered', 'issues': [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal and slowdowns in the access to billing information.", 'affected_services': ['Database Service', 'Database Management Portal', 'Billing portal']}], 'additional_comments': 'Unanswered'}
Now let's see what happens if we try to update a field that is not in the artifact.
result = await artifact.update_artifact(
field_name="not_a_field",
field_value="some value",
conversation=conversation,
)
# We should see that the update was immediately unsuccessful, but the conversation and artifact should remain unchanged.
print(f"Was the update successful? {result.update_successful}")
print(f"Conversation up to this point:\n{conversation.get_repr_for_prompt()}\n")
print(f"Current state of the artifact:\n{artifact.get_artifact_for_prompt()}")
Was the update successful? False Conversation up to this point: Assistant: Hello! I'm here to help you with your issue. Can you tell me your name, company, and role? None: Yes my name is Jane Doe, I work at Contoso, and I'm a database uhh administrator. Assistant updated name to Jane Doe Assistant updated company to Contoso Assistant updated role to Database Administrator Assistant: What is the best email to contact you at? None: my email is jdoe Assistant: Can you tell me about the issues you're experiencing? None: The latency of accessing our database service has increased by 200\% in the last 24 hours, even on a fresh instance. Additionally, we're seeing a lot of timeouts when trying to access the management portal. Assistant updated issues to [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal.", 'affected_services': ['Database Service', 'Database Management Portal']}] Assistant: Is there anything else you'd like to add about the issues you're experiencing? None: Yes another thing that is effected is access to billing information is very slow. Assistant updated issues to [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal and slowdowns in the access to billing information.", 'affected_services': ['Database Service', 'Database Management Portal', 'Billing portal']}] Current state of the artifact: {'name': 'Jane Doe', 'company': 'Contoso', 'role': 'Database Administrator', 'phone': 'Unanswered', 'incident_start': 'Unanswered', 'incident_end': 'Unanswered', 'issues': [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal and slowdowns in the access to billing information.", 'affected_services': ['Database Service', 'Database Management Portal', 'Billing portal']}], 'additional_comments': 'Unanswered'}
Finally, let's see what happens if we try to update a field with the incorrect type, but the correct information was provided in the conversation. We should see the agent correctly updated the field correctly as an integer.
conversation.add_messages(
ChatMessageContent(role=AuthorRole.ASSISTANT, content="How many hours ago did the incident start?")
)
conversation.add_messages(
ChatMessageContent(role=AuthorRole.USER, content="about 3 hours ago")
)
result = await artifact.update_artifact(
field_name="incident_start",
field_value="3 hours",
conversation=conversation,
)
print(f"Current state of the artifact:\n{artifact.get_artifact_for_prompt()}")
Error updating field incident_start: 2 validation errors for Artifact incident_start.int Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='3 hours', input_type=str] For further information visit https://errors.pydantic.dev/2.8/v/int_parsing incident_start.literal['Unanswered'] Input should be 'Unanswered' [type=literal_error, input_value='3 hours', input_type=str] For further information visit https://errors.pydantic.dev/2.8/v/literal_error. Retrying... Agent failed to fix field incident_start. Retrying...
Current state of the artifact: {'name': 'Jane Doe', 'company': 'Contoso', 'role': 'Database Administrator', 'phone': 'Unanswered', 'incident_start': 3, 'incident_end': 'Unanswered', 'issues': [{'incident_type': 'Degradation', 'description': "The latency of accessing the customer's database service has increased by 200% in the last 24 hours, even on a fresh instance. They also report timeouts when trying to access the management portal and slowdowns in the access to billing information.", 'affected_services': ['Database Service', 'Database Management Portal', 'Billing portal']}], 'additional_comments': 'Unanswered'}