feat(persona-chat): practice mode — play the contact
An anchored conversation now has a mode. 'about' is the existing behaviour (a persona talks with the user about the thread). 'as_contact' has the model play the contact so the user can rehearse the conversation: its own prompt (stay in the contact's voice, don't invent facts, coach on 'out of character'), no persona prompt, and only the thread tools — no memory, library, calendar or other-thread access, and no knowledge writes. The shared persona/file-chat loop now refuses tool calls that weren't in the turn's catalog, so the gates hold even when a model names a tool it wasn't offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ac574d4d2
commit
3f1bf42f1c
8 files changed
+284
-29
No files matched your search
@@ -47,6 +47,37 @@ pub struct SmsAnchor {
|
||||
/// Unix seconds of the anchor message.
|
||||
pub timestamp: i64,
|
||||
pub context: String,
|
||||
pub mode: AnchorMode,
|
||||
}
|
||||
|
||||
/// How an anchored conversation relates to its SMS thread.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnchorMode {
|
||||
/// A persona talks with the user about the thread.
|
||||
#[default]
|
||||
About,
|
||||
/// The model plays the contact so the user can practise the
|
||||
/// conversation.
|
||||
AsContact,
|
||||
}
|
||||
|
||||
impl AnchorMode {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
AnchorMode::About => "about",
|
||||
AnchorMode::AsContact => "as_contact",
|
||||
}
|
||||
}
|
||||
|
||||
/// Stored value → mode. NULL and anything unrecognised read as `About`,
|
||||
/// the behaviour rows had before the column existed.
|
||||
pub fn from_column(value: Option<&str>) -> Self {
|
||||
match value {
|
||||
Some("as_contact") => AnchorMode::AsContact,
|
||||
_ => AnchorMode::About,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The column tuple every persona-chat read selects. Shared by the
|
||||
@@ -65,6 +96,7 @@ type PersonaChatColumns = (
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
macro_rules! persona_chat_select {
|
||||
@@ -82,6 +114,7 @@ macro_rules! persona_chat_select {
|
||||
anchor_message_id,
|
||||
anchor_ts,
|
||||
anchor_context,
|
||||
anchor_mode,
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -101,6 +134,7 @@ fn persona_chat_row(
|
||||
anchor_message_id,
|
||||
anchor_ts,
|
||||
anchor_context,
|
||||
anchor_mode,
|
||||
): PersonaChatColumns,
|
||||
) -> PersonaChatRow {
|
||||
// An anchor needs at least the thread and the message; the rest has a
|
||||
@@ -112,6 +146,7 @@ fn persona_chat_row(
|
||||
message_id,
|
||||
timestamp: anchor_ts.unwrap_or(0),
|
||||
context: anchor_context.unwrap_or_default(),
|
||||
mode: AnchorMode::from_column(anchor_mode.as_deref()),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
@@ -545,6 +580,7 @@ impl PersonaDao for SqlitePersonaDao {
|
||||
anchor_message_id.eq(Some(anchor.message_id)),
|
||||
anchor_ts.eq(Some(anchor.timestamp)),
|
||||
anchor_context.eq(Some(&anchor.context)),
|
||||
anchor_mode.eq(Some(anchor.mode.as_str())),
|
||||
))
|
||||
.execute(conn.deref_mut())
|
||||
.map_err(|e| anyhow::anyhow!("Insert error: {}", e))?;
|
||||
@@ -876,6 +912,7 @@ mod tests {
|
||||
message_id: 9001,
|
||||
timestamp: 1_700_000_000,
|
||||
context: "[2023-11-14 22:13] Sam: see you there".to_string(),
|
||||
mode: AnchorMode::AsContact,
|
||||
};
|
||||
let anchored = dao
|
||||
.create_anchored_persona_chat(uid, "journal", 100, &anchor)
|
||||
@@ -895,6 +932,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_mode_reads_null_and_unknown_as_about() {
|
||||
assert_eq!(AnchorMode::from_column(None), AnchorMode::About);
|
||||
assert_eq!(AnchorMode::from_column(Some("weird")), AnchorMode::About);
|
||||
assert_eq!(AnchorMode::from_column(Some("about")), AnchorMode::About);
|
||||
assert_eq!(
|
||||
AnchorMode::from_column(Some(AnchorMode::AsContact.as_str())),
|
||||
AnchorMode::AsContact
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persona_chat_list_is_empty_for_a_user_who_has_never_chatted() {
|
||||
let (mut dao, uid) = dao_with_user("p8");
|
||||
|
||||
Reference in new issue
Block a user