Skip to main content

Relationships

Relationships define how context elements relate to each other. They let you express rules like "this guideline should only fire when this observation is active," or "when these two journeys conflict, this one wins."

Background & Motivation​

Back in the day, our team was building a pizza-sales agent, which had the following guidelines (among others):

offer_pepsi_instead_of_coke = await agent.create_guideline(
condition="The customer wants a coke",
action="Tell them we only have Pepsi",
)

handoff_if_upset = await agent.create_guideline(
condition="The customer is becoming upset",
action="Apologize and tell them you will transfer them to a manager",
tools=[handoff_to_human_manager],
)

This initially worked well, until we encountered the following scenario:

Agent: Do you want anything to drink with your order?

User: A coke please

Agent: I'm sorry, we only have Pepsi. Would you like that instead?

User: Wait, what? I hate Pepsi. Why the hell don't you have coke?

Agent: I'm sorry for this inconvenience. Let me transfer you to a manager. Meanwhile, can I offer you a Pepsi?

User: Are you taking the piss out of me?

The agent's response was definitely not what we wanted, as it can come across as sarcastically hostile. But the poor AI agent was only following the guidelines we had given it, based on the conditions we had set.

Managing instructions is not just a technical challenge—it's a human modeling challenge. We must consider how our instructions relate to each other for an agent who takes them quite literally. How they should interact in different contexts, especially in nuanced situations, is something that only we can decide.

In the case above, we wanted to ensure that when the customer is upset, the Pepsi guideline is excluded. In Parlant, that's expressed simply:

await handoff_if_upset.exclude(offer_pepsi_instead_of_coke)

Relationship Types​

Each relationship is between a source (notated S) and a target (notated T).

RelationshipEffect
ExclusionWhen both S and T are activated, only S should be activated
DependencyWhen S is activated, deactivate it unless T is also activated
EntailmentWhen S is activated, T should always be activated too
DisambiguationWhen S is activated and two or more targets are activated, ask the customer to clarify

Exclusion​

When both S and T are activated, only S should be activated.

await source.exclude(target)

You can also use the prioritize_over alias, which has the same effect but can be more intuitive when you're thinking in terms of one guideline taking precedence over another. They do exactly the same thing:

await source.prioritize_over(target)

Exclusion is the most common relationship. It solves two problems: preventing conflicting instructions from colliding, and controlling the precedence of actions within a conversation.

Observations Excluding Guidelines​

The most common pattern is an observation that, when it fires, disqualifies certain guidelines from the agent's context. This keeps irrelevant or contradictory instructions out:

customer_is_upset = await agent.create_observation(
condition="The customer is becoming upset or frustrated"
)

upsell_premium = await agent.create_guideline(
condition="The customer is browsing products",
action="Suggest premium upgrades",
)

# Don't try to upsell when the customer is upset
await customer_is_upset.exclude(upsell_premium)

Guidelines Excluding Guidelines​

When two guidelines can conflict, you can declare which one takes priority:

# The pizza story from above
await handoff_if_upset.exclude(offer_pepsi_instead_of_coke)

Controlling Precedence​

Sometimes two guidelines are both valid but shouldn't fire at the same time. You can control the order:

complete_transaction = await agent.create_guideline(
condition="The customer wants to make a transaction",
action="Guide them through the process to its completion",
)

offer_savings = await agent.create_guideline(
condition="The customer has less than $1,000 in their account",
action="Offer savings plans",
)

# Don't interrupt the transaction with savings offers—
# once the transaction completes, the savings guideline can fire
await complete_transaction.exclude(offer_savings)

Dependency​

When S is activated, deactivate it unless T is also activated.

await source.depend_on(target)

A dependency ensures that a guideline or observation only fires when a baseline condition also holds. This is the primary way observations gate guidelines—allowing you to scope specific behaviors to the right context.

Observations Gating Guidelines​

The most common pattern: an observation detects a broad conversational state, and specific guidelines only fire within that context:

customer_wants_deals = await agent.create_observation(
condition="Customer expresses interest in special deals or discounts"
)

offer_seasonal_discount = await agent.create_guideline(
condition="There is an active seasonal promotion",
action="Mention the current seasonal discount",
)

offer_loyalty_reward = await agent.create_guideline(
condition="The customer has been a member for over a year",
action="Offer their loyalty reward",
)

# These guidelines only fire when the customer is actually interested in deals
await offer_seasonal_discount.depend_on(customer_wants_deals)
await offer_loyalty_reward.depend_on(customer_wants_deals)

Scoping Edge Cases to a Baseline​

When building flows, you can address specialized scenarios by making them dependent on a baseline guideline:

handle_return = await agent.create_guideline(
condition="The customer wants to return an order",
action="Help them complete the return process",
)

no_order_number = await agent.create_guideline(
condition="The customer isn't able to provide the order number",
action="Load up their last order's items and ask them to confirm",
)

has_order_number = await agent.create_guideline(
condition="The customer specified the exact order number",
action="Load up that order's items and ask them to confirm",
)

# These only apply during a return flow
await no_order_number.depend_on(handle_return)
await has_order_number.depend_on(handle_return)

Entailment​

When S is activated, T should always be activated too.

await source.entail(target)

Entailment solves a timing problem. Parlant evaluates all observations and guidelines before composing the response. So if Guideline A's action will cause the agent to do something that Guideline B's condition depends on, B won't match—because the action hasn't happened yet at evaluation time.

explain_returns = await agent.create_guideline(
condition="The customer asks about returns",
action="Explain the return policy",
)

mention_warranty = await agent.create_guideline(
condition="The agent is explaining the return policy",
action="Also mention the 30-day warranty",
)

# Without entailment, mention_warranty won't fire because at evaluation
# time, the agent hasn't explained the return policy yet.
# Entailment guarantees it fires whenever explain_returns does.
await explain_returns.entail(mention_warranty)

Disambiguation​

When S is activated and two or more targets are activated, ask the customer to clarify which action they want to take.

await source.disambiguate([target_1, target_2, ...])

When the agent can't tell which of several guidelines applies, disambiguation forces it to ask the customer to clarify rather than guessing:

fetch_atm_limits = await agent.create_guideline(
condition="The customer is inquiring about their ATM limits",
action="Fetch the data from their account profile",
)

fetch_credit_card_limits = await agent.create_guideline(
condition="The customer is inquiring about their credit card's limits",
action="Fetch them from the card provider",
)

ambiguous_limits = await agent.create_observation(
condition="The customer is inquiring about limits but it isn't clear which kind",
)

await ambiguous_limits.disambiguate([fetch_atm_limits, fetch_credit_card_limits])

If a customer says "What are my limits?" and both guidelines match, the agent will ask which kind of limits they mean instead of picking one arbitrarily.

github Questions? Reach out!