Files
emcie-co--parlant/tests/api/test_canned_responses.py
wehub-resource-sync 177604c7d1
Verify and Test / build (3.10) (push) Waiting to run
Lint / build (3.10) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:42:59 +08:00

683 lines
23 KiB
Python

# Copyright 2026 Emcie Co Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import dateutil.parser
from fastapi import status
import httpx
from lagom import Container
from pytest import raises
from parlant.core.agents import AgentStore
from parlant.core.common import ItemNotFoundError
from parlant.core.canned_responses import CannedResponseStore, CannedResponseField
from parlant.core.journeys import JourneyStore
from parlant.core.tags import Tag, TagStore
async def test_that_a_canned_response_can_be_created(
async_client: httpx.AsyncClient,
) -> None:
payload = {
"value": "Your account balance is {{balance}}",
"fields": [
{
"name": "balance",
"description": "Account's balance",
"examples": ["9000"],
}
],
}
response = await async_client.post("/canned_responses", json=payload)
assert response.status_code == status.HTTP_201_CREATED
canned_response = response.json()
assert canned_response["value"] == payload["value"]
assert canned_response["fields"] == payload["fields"]
assert "id" in canned_response
assert "creation_utc" in canned_response
async def test_that_a_canned_response_can_be_created_with_tags(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
tag_1 = await tag_store.create_tag(name="VIP")
tag_2 = await tag_store.create_tag(name="Finance")
payload = {
"value": "Your account balance is {{balance}}",
"fields": [
{
"name": "balance",
"description": "Account's balance",
"examples": ["9000"],
}
],
"tags": [tag_1.id, tag_2.id],
}
response = await async_client.post("/canned_responses", json=payload)
assert response.status_code == status.HTTP_201_CREATED
canned_response_dto = (
(await async_client.get(f"/canned_responses/{response.json()['id']}"))
.raise_for_status()
.json()
)
assert len(canned_response_dto["tags"]) == 2
assert set(canned_response_dto["tags"]) == {tag_1.id, tag_2.id}
async def test_that_a_canned_response_can_be_created_with_signals(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
payload = {
"value": "Your account balance is {{balance}}",
"fields": [
{
"name": "balance",
"description": "Account's balance",
"examples": ["9000"],
}
],
"signals": ["One", "Two", "Three"],
}
response = await async_client.post("/canned_responses", json=payload)
assert response.status_code == status.HTTP_201_CREATED
canned_response_dto = (
(await async_client.get(f"/canned_responses/{response.json()['id']}"))
.raise_for_status()
.json()
)
assert len(canned_response_dto["signals"]) == 3
assert set(canned_response_dto["signals"]) == {"One", "Two", "Three"}
async def test_that_a_canned_response_can_be_read(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
value = "Your account balance is {{balance}}"
fields = [
CannedResponseField(name="balance", description="Account's balance", examples=["9000"])
]
canned_response = await canned_response_store.create_canned_response(value=value, fields=fields)
response = await async_client.get(f"/canned_responses/{canned_response.id}")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == canned_response.id
assert data["value"] == value
assert len(data["fields"]) == 1
canned_response_field = data["fields"][0]
assert canned_response_field["name"] == fields[0].name
assert canned_response_field["description"] == fields[0].description
assert canned_response_field["examples"] == fields[0].examples
assert dateutil.parser.parse(data["creation_utc"]) == canned_response.creation_utc
async def test_that_all_canned_responses_can_be_listed(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
first_value = "Your account balance is {{balance}}"
first_fields = [
CannedResponseField(name="balance", description="Account's balance", examples=["9000"])
]
second_value = "It will take {{day_count}} days to deliver to {{address}}"
second_fields = [
CannedResponseField(
name="day_count", description="Time required for delivery in days", examples=["8"]
),
CannedResponseField(
name="address", description="Customer's address", examples=["Some Address"]
),
]
await canned_response_store.create_canned_response(value=first_value, fields=first_fields)
await canned_response_store.create_canned_response(value=second_value, fields=second_fields)
response = await async_client.get("/canned_responses")
assert response.status_code == status.HTTP_200_OK
canned_responses = response.json()
assert len(canned_responses) >= 2
assert any(f["value"] == first_value for f in canned_responses)
assert any(f["value"] == second_value for f in canned_responses)
async def test_that_relevant_canned_responses_can_be_retrieved_based_on_closest_signals(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
canned_responses = [
await canned_response_store.create_canned_response(value="Red", signals=[]),
await canned_response_store.create_canned_response(value="Green", signals=[]),
await canned_response_store.create_canned_response(value="Blue", signals=[]),
await canned_response_store.create_canned_response(
value="Paneer Cheese", signals=["Colors"]
),
]
closest_canned_response = next(
iter(
await canned_response_store.filter_relevant_canned_responses(
query="Colors",
available_canned_responses=canned_responses,
max_count=1,
)
)
)
assert closest_canned_response.canned_response.value == "Paneer Cheese"
async def test_that_a_canned_response_can_be_updated(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
value = "Your account balance is {{balance}}"
fields = [
CannedResponseField(name="balance", description="Account's balance", examples=["9000"])
]
canned_response = await canned_response_store.create_canned_response(value=value, fields=fields)
update_payload = {
"value": "Updated balance: {{balance}}",
"fields": [
{
"name": "balance",
"description": "Updated account balance",
"examples": ["10000"],
}
],
}
response = await async_client.patch(
f"/canned_responses/{canned_response.id}", json=update_payload
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = response.json()
assert updated_canned_response["value"] == update_payload["value"]
assert updated_canned_response["fields"] == update_payload["fields"]
async def test_that_a_canned_response_can_be_deleted(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
value = "Your account balance is {{balance}}"
fields = [
CannedResponseField(name="balance", description="Account's balance", examples=["9000"])
]
canned_response = await canned_response_store.create_canned_response(value=value, fields=fields)
delete_response = await async_client.delete(f"/canned_responses/{canned_response.id}")
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
with raises(ItemNotFoundError):
await canned_response_store.read_canned_response(canned_response.id)
async def test_that_a_tag_can_be_added_to_a_canned_response(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
tag_store = container[TagStore]
tag = await tag_store.create_tag(name="VIP")
value = "Your account balance is {{balance}}"
fields = [
CannedResponseField(name="balance", description="Account's balance", examples=["9000"])
]
canned_response = await canned_response_store.create_canned_response(value=value, fields=fields)
response = await async_client.patch(
f"/canned_responses/{canned_response.id}", json={"tags": {"add": [tag.id]}}
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = await canned_response_store.read_canned_response(canned_response.id)
assert tag.id in updated_canned_response.tags
async def test_that_a_tag_can_be_removed_from_a_canned_response(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
tag_store = container[TagStore]
tag = await tag_store.create_tag(name="VIP")
value = "Your account balance is {{balance}}"
fields = [
CannedResponseField(name="balance", description="Account's balance", examples=["9000"])
]
canned_response = await canned_response_store.create_canned_response(value=value, fields=fields)
await canned_response_store.upsert_tag(canned_response_id=canned_response.id, tag_id=tag.id)
response = await async_client.patch(
f"/canned_responses/{canned_response.id}", json={"tags": {"remove": [tag.id]}}
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = await canned_response_store.read_canned_response(canned_response.id)
assert tag.id not in updated_canned_response.tags
async def test_that_canned_responses_can_be_filtered_by_tags(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
tag_store = container[TagStore]
tag_vip = await tag_store.create_tag(name="VIP")
tag_finance = await tag_store.create_tag(name="Finance")
tag_greeting = await tag_store.create_tag(name="Greeting")
first_canned_response = await canned_response_store.create_canned_response(
value="Welcome {{username}}!",
fields=[
CannedResponseField(
name="username", description="User's name", examples=["Alice", "Bob"]
)
],
)
await canned_response_store.upsert_tag(first_canned_response.id, tag_greeting.id)
second_canned_response = await canned_response_store.create_canned_response(
value="Your balance is {{balance}}",
fields=[
CannedResponseField(
name="balance", description="Account balance", examples=["5000", "10000"]
)
],
)
await canned_response_store.upsert_tag(second_canned_response.id, tag_finance.id)
third_canned_response = await canned_response_store.create_canned_response(
value="Exclusive VIP offer for {{username}}",
fields=[
CannedResponseField(name="username", description="VIP customer", examples=["Charlie"])
],
)
await canned_response_store.upsert_tag(third_canned_response.id, tag_vip.id)
response = await async_client.get(f"/canned_responses?tags={tag_greeting.id}")
assert response.status_code == status.HTTP_200_OK
canned_responses = response.json()
assert len(canned_responses) == 1
assert canned_responses[0]["value"] == "Welcome {{username}}!"
response = await async_client.get(f"/canned_responses?tags={tag_finance.id}&tags={tag_vip.id}")
assert response.status_code == status.HTTP_200_OK
canned_responses = response.json()
assert len(canned_responses) == 2
values = {f["value"] for f in canned_responses}
assert "Your balance is {{balance}}" in values
assert "Exclusive VIP offer for {{username}}" in values
response = await async_client.get("/canned_responses?tags=non_existent_tag")
assert response.status_code == status.HTTP_200_OK
canned_responses = response.json()
assert len(canned_responses) == 0
async def test_that_agent_tag_can_be_added_to_a_canned_response(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
agent = await container[AgentStore].create_agent("Test Agent")
canrep = await canned_response_store.create_canned_response(
value="Welcome {{username}}!",
fields=[
CannedResponseField(
name="username", description="User's name", examples=["Alice", "Bob"]
)
],
)
agent_tag = Tag.for_agent_id(agent.id).id
update_payload = {"tags": {"add": [agent_tag]}}
response = await async_client.patch(f"/canned_responses/{canrep.id}", json=update_payload)
response.raise_for_status()
updated_canned_response = response.json()
assert updated_canned_response["tags"] == [agent_tag]
async def test_that_agent_tag_can_be_removed_from_a_canned_response(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
canned_response_store = container[CannedResponseStore]
agent = await container[AgentStore].create_agent("Test Agent")
tag1 = await tag_store.create_tag("tag1")
agent_tag = Tag.for_agent_id(agent.id).id
canrep = await canned_response_store.create_canned_response(
value="Welcome {{username}}!",
fields=[
CannedResponseField(
name="username", description="User's name", examples=["Alice", "Bob"]
)
],
tags=[tag1.id, agent_tag],
)
update_payload = {"tags": {"remove": [agent_tag]}}
_ = (
await async_client.patch(f"/canned_responses/{canrep.id}", json=update_payload)
).raise_for_status()
canrep_after_update = (
(await async_client.get(f"/canned_responses/{canrep.id}")).raise_for_status().json()
)
assert agent_tag not in canrep_after_update["tags"]
assert tag1.id in canrep_after_update["tags"]
async def test_that_journey_tags_can_be_added_to_a_canned_response(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
journey_store = container[JourneyStore]
canned_response_store = container[CannedResponseStore]
journey = await journey_store.create_journey(
title="Customer Support Journey",
description="A journey for customer support interactions.",
triggers=[],
)
journey_tag = Tag.for_journey_id(journey.id).id
tag1 = await tag_store.create_tag("tag1")
canrep = await canned_response_store.create_canned_response(
value="Welcome {{username}}!",
fields=[
CannedResponseField(
name="username", description="User's name", examples=["Alice", "Bob"]
)
],
)
update_payload = {"tags": {"add": [tag1.id, journey_tag]}}
response = await async_client.patch(f"/canned_responses/{canrep.id}", json=update_payload)
response.raise_for_status()
updated_canrep = response.json()
assert tag1.id in updated_canrep["tags"]
assert journey_tag in updated_canrep["tags"]
async def test_that_journey_tags_can_be_removed_from_a_canned_response(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
canned_response_store = container[CannedResponseStore]
journey_store = container[JourneyStore]
journey = await journey_store.create_journey(
title="Customer Support Journey",
description="A journey for customer support interactions.",
triggers=[],
)
journey_tag = Tag.for_journey_id(journey.id).id
tag1 = await tag_store.create_tag("tag1")
canrep = await canned_response_store.create_canned_response(
value="Welcome {{username}}!",
fields=[
CannedResponseField(
name="username", description="User's name", examples=["Alice", "Bob"]
)
],
tags=[tag1.id, journey_tag],
)
update_payload = {"tags": {"remove": [journey_tag]}}
_ = (
await async_client.patch(f"/canned_responses/{canrep.id}", json=update_payload)
).raise_for_status()
canrep_after_update = (
(await async_client.get(f"/canned_responses/{canrep.id}")).raise_for_status().json()
)
assert journey_tag not in canrep_after_update["tags"]
assert tag1.id in canrep_after_update["tags"]
async def test_that_a_canned_response_can_be_created_with_metadata(
async_client: httpx.AsyncClient,
) -> None:
payload = {
"value": "Your account balance is {{balance}}",
"fields": [
{
"name": "balance",
"description": "Account's balance",
"examples": ["9000"],
}
],
"metadata": {"priority": "high", "category": "finance"},
}
response = await async_client.post("/canned_responses", json=payload)
assert response.status_code == status.HTTP_201_CREATED
canned_response = response.json()
assert canned_response["value"] == payload["value"]
assert canned_response["fields"] == payload["fields"]
assert canned_response["metadata"] == {"priority": "high", "category": "finance"}
assert "id" in canned_response
assert "creation_utc" in canned_response
async def test_that_canned_response_metadata_can_be_updated(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
canned_response = await canned_response_store.create_canned_response(
value="Your balance is {{balance}}",
fields=[
CannedResponseField(name="balance", description="Account balance", examples=["5000"])
],
metadata={"old_key": "old_value", "category": "finance"},
)
response = await async_client.patch(
f"/canned_responses/{canned_response.id}",
json={
"metadata": {
"set": {
"priority": "high",
"category": "support",
},
"unset": ["old_key"],
}
},
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = response.json()
assert updated_canned_response["id"] == canned_response.id
assert updated_canned_response["metadata"] == {"priority": "high", "category": "support"}
async def test_that_canned_response_metadata_can_be_set_without_unset(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
canned_response = await canned_response_store.create_canned_response(
value="Your balance is {{balance}}",
fields=[
CannedResponseField(name="balance", description="Account balance", examples=["5000"])
],
metadata={"existing_key": "existing_value"},
)
response = await async_client.patch(
f"/canned_responses/{canned_response.id}",
json={
"metadata": {
"set": {
"priority": "high",
"category": "support",
}
}
},
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = response.json()
assert updated_canned_response["id"] == canned_response.id
assert updated_canned_response["metadata"] == {
"existing_key": "existing_value",
"priority": "high",
"category": "support",
}
async def test_that_canned_response_metadata_can_be_unset_without_set(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
canned_response = await canned_response_store.create_canned_response(
value="Your balance is {{balance}}",
fields=[
CannedResponseField(name="balance", description="Account balance", examples=["5000"])
],
metadata={"key1": "value1", "key2": "value2", "key3": "value3"},
)
response = await async_client.patch(
f"/canned_responses/{canned_response.id}",
json={"metadata": {"unset": ["key2", "key3"]}},
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = response.json()
assert updated_canned_response["id"] == canned_response.id
assert updated_canned_response["metadata"] == {"key1": "value1"}
async def test_that_canned_response_metadata_can_handle_empty_operations(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
canned_response = await canned_response_store.create_canned_response(
value="Your balance is {{balance}}",
fields=[
CannedResponseField(name="balance", description="Account balance", examples=["5000"])
],
metadata={"existing": "value"},
)
# Test with empty set and unset
response = await async_client.patch(
f"/canned_responses/{canned_response.id}",
json={"metadata": {"set": {}, "unset": []}},
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = response.json()
assert updated_canned_response["id"] == canned_response.id
assert updated_canned_response["metadata"] == {"existing": "value"}
async def test_that_canned_response_metadata_unset_nonexistent_key_is_ignored(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
canned_response_store = container[CannedResponseStore]
canned_response = await canned_response_store.create_canned_response(
value="Your balance is {{balance}}",
fields=[
CannedResponseField(name="balance", description="Account balance", examples=["5000"])
],
metadata={"key1": "value1"},
)
response = await async_client.patch(
f"/canned_responses/{canned_response.id}",
json={"metadata": {"set": {"key2": "value2"}, "unset": ["nonexistent_key"]}},
)
assert response.status_code == status.HTTP_200_OK
updated_canned_response = response.json()
assert updated_canned_response["id"] == canned_response.id
assert updated_canned_response["metadata"] == {"key1": "value1", "key2": "value2"}