use std::{
fmt::Display,
path::PathBuf,
sync::{Arc, Mutex, RwLock},
};
use anyhow::Result;
use futures_util::Future;
use kalosm_language_model::ChatMarkers;
use kalosm_language_model::Session;
use kalosm_language_model::{GenerationParameters, Model, ModelExt, SyncModel, SyncModelExt};
use kalosm_sample::{ArcParser, CreateParserState, ParserExt, SendCreateParserState};
use kalosm_streams::text_stream::ChannelTextStream;
use llm_samplers::types::Sampler;
use tokio::sync::{mpsc::unbounded_channel, oneshot};
type ResponseConstraintGenerator =
Arc<Mutex<Box<dyn FnMut(&[ChatHistoryItem]) -> ArcParser<()> + Send + Sync>>>;
const DEFAULT_SYSTEM_PROMPT: &str = "Always assist with care, respect, and truth. Respond with utmost utility yet securely. Avoid harmful, unethical, prejudiced, or negative content. Ensure replies promote fairness and positivity.";
pub fn prompt_input(prompt: impl Display) -> Result<String> {
use std::io::Write;
print!("{}", prompt);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
input.pop();
Ok(input)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MessageType {
SystemPrompt,
UserMessage,
ModelAnswer,
}
#[derive(Clone, Debug)]
pub struct ChatHistoryItem {
ty: MessageType,
contents: String,
}
impl ChatHistoryItem {
pub fn new(ty: MessageType, contents: impl Into<String>) -> Self {
Self {
ty,
contents: contents.into(),
}
}
pub fn ty(&self) -> MessageType {
self.ty
}
pub fn contents(&self) -> &str {
&self.contents
}
}
struct ChatSession<Model: SyncModel> {
logits_scratch: Vec<f32>,
system_prompt_marker: String,
end_system_prompt_marker: String,
user_marker: String,
end_user_marker: String,
assistant_marker: String,
end_assistant_marker: String,
history: Arc<RwLock<Vec<ChatHistoryItem>>>,
session: Model::Session,
unfed_text: String,
bot_constraints: Option<ResponseConstraintGenerator>,
sampler: Arc<Mutex<dyn Sampler + Send + Sync>>,
}
impl<Model: SyncModel> ChatSession<Model> {
#[allow(clippy::too_many_arguments)]
fn new(
model: &mut Model,
system_prompt_marker: String,
end_system_prompt_marker: String,
user_marker: String,
end_user_marker: String,
assistant_marker: String,
end_assistant_marker: String,
system_prompt: Option<String>,
bot_constraints: Option<ResponseConstraintGenerator>,
sampler: Arc<Mutex<dyn Sampler + Send + Sync>>,
session: Option<Model::Session>,
initial_history: Vec<ChatHistoryItem>,
shared_history: Arc<RwLock<Vec<ChatHistoryItem>>>,
) -> Self {
let feed_initial_messages = session.is_none();
let session = session.unwrap_or_else(|| model.new_session().unwrap());
let unfed_text = String::new();
shared_history.write().unwrap().clear();
let mut myself = Self {
logits_scratch: Vec::new(),
system_prompt_marker,
end_system_prompt_marker,
user_marker,
end_user_marker,
assistant_marker,
end_assistant_marker,
session,
unfed_text,
history: shared_history,
bot_constraints,
sampler,
};
if feed_initial_messages {
if initial_history
.first()
.filter(|item| item.ty() != MessageType::SystemPrompt)
.is_none()
{
let system_prompt = system_prompt.unwrap_or(DEFAULT_SYSTEM_PROMPT.into());
myself.add_system_message(system_prompt);
}
for item in initial_history {
match item.ty() {
MessageType::SystemPrompt => {
myself.add_system_message(item.contents);
}
MessageType::UserMessage => {
myself.add_user_message(item.contents);
}
MessageType::ModelAnswer => {
myself.add_bot_message(item.contents);
}
}
}
}
myself
}
fn add_message(
&mut self,
message: String,
model: &mut Model,
stream: tokio::sync::mpsc::UnboundedSender<String>,
) -> Result<()> {
self.add_user_message(message);
let mut bot_response = String::new();
self.unfed_text += &self.assistant_marker;
let prompt = std::mem::take(&mut self.unfed_text);
let bot_constraints = &self.bot_constraints;
let mut on_token = |tok: String| {
let tok = tok
.strip_suffix(&self.end_assistant_marker)
.unwrap_or(&tok)
.to_string();
bot_response += &tok;
stream.send(tok)?;
Ok(())
};
match bot_constraints {
Some(constraints) => {
let mut constraints = constraints.lock().unwrap();
let constraints = constraints(&self.history.read().unwrap());
let state = constraints.create_parser_state();
model.generate_structured(
&mut self.session,
&prompt,
constraints,
state,
self.sampler.clone(),
on_token,
Some(4),
)?;
let end_assistant_token = model
.tokenizer()
.token_to_id(&self.end_assistant_marker)
.unwrap();
if self.session.tokens().last() != Some(&end_assistant_token) {
model.feed_tokens(
&mut self.session,
&[end_assistant_token],
&mut self.logits_scratch,
)?;
}
}
None => {
model.stream_text_with_sampler(
&mut self.session,
&prompt,
None,
Some(&self.end_assistant_marker),
self.sampler.clone(),
|tok| {
on_token(tok)?;
Ok(kalosm_language_model::ModelFeedback::Continue)
},
)?;
}
}
Ok(())
}
fn add_system_message(&mut self, message: String) {
self.unfed_text += &self.system_prompt_marker;
self.unfed_text += &message;
self.unfed_text += &self.end_system_prompt_marker;
let mut history = self.history.write().unwrap();
if !history.is_empty() {
tracing::error!("System prompt should be the first message in the history. System prompt was added to the end of the history: {history:?}");
}
history.push(ChatHistoryItem {
ty: MessageType::SystemPrompt,
contents: message,
});
}
fn add_user_message(&mut self, message: String) {
self.unfed_text += &self.user_marker;
self.unfed_text += &message;
self.unfed_text += &self.end_user_marker;
self.history.write().unwrap().push(ChatHistoryItem {
ty: MessageType::UserMessage,
contents: message,
});
}
fn add_bot_message(&mut self, message: String) {
self.unfed_text += &self.assistant_marker;
self.unfed_text += &message;
self.unfed_text += &self.end_assistant_marker;
self.history.write().unwrap().push(ChatHistoryItem {
ty: MessageType::ModelAnswer,
contents: message,
});
}
}
pub struct ChatBuilder<M: Model> {
model: M,
chat_markers: ChatMarkers,
session: Option<<M::SyncModel as kalosm_language_model::SyncModel>::Session>,
system_prompt: Option<String>,
sampler: Arc<Mutex<dyn Sampler + Send + Sync>>,
bot_constraints: Option<ResponseConstraintGenerator>,
initial_history: Vec<ChatHistoryItem>,
}
impl<M: Model> ChatBuilder<M> {
fn new(model: M) -> ChatBuilder<M> {
let chat_markers = model.chat_markers().expect("Model does not support chat");
ChatBuilder {
model,
chat_markers,
session: None,
system_prompt: None,
sampler: Arc::new(Mutex::new(GenerationParameters::default().sampler())),
bot_constraints: None,
initial_history: Vec::new(),
}
}
}
impl<M: Model> ChatBuilder<M> {
pub fn with_system_prompt(mut self, system_prompt: impl ToString) -> Self {
self.system_prompt = Some(system_prompt.to_string());
self
}
pub fn with_sampler(mut self, sampler: impl Sampler + 'static) -> Self {
self.sampler = Arc::new(Mutex::new(sampler));
self
}
#[deprecated(note = "renamed to `with_constraints`")]
pub fn constrain_response<Parser: SendCreateParserState + 'static>(
self,
bot_constraints: impl FnMut(&[ChatHistoryItem]) -> Parser + Send + Sync + 'static,
) -> ChatBuilder<M> {
Self::with_constraints(self, bot_constraints)
}
pub fn with_constraints<Parser: SendCreateParserState + 'static>(
self,
mut bot_constraints: impl FnMut(&[ChatHistoryItem]) -> Parser + Send + Sync + 'static,
) -> ChatBuilder<M> {
ChatBuilder {
model: self.model,
chat_markers: self.chat_markers,
session: self.session,
system_prompt: self.system_prompt,
sampler: self.sampler,
bot_constraints: Some(Arc::new(Mutex::new(Box::new(
move |history: &[ChatHistoryItem]| {
bot_constraints(history).map_output(|_| ()).boxed()
},
)
as Box<dyn FnMut(&[ChatHistoryItem]) -> ArcParser + Send + Sync>))),
initial_history: self.initial_history,
}
}
pub fn with_session(
mut self,
session: <M::SyncModel as kalosm_language_model::SyncModel>::Session,
) -> Self {
self.session = Some(session);
self
}
pub fn with_try_session_path(self, path: impl AsRef<std::path::Path>) -> Self {
let session = <M::SyncModel as kalosm_language_model::SyncModel>::Session::load_from(path);
if let Ok(session) = session {
self.with_session(session)
} else {
self
}
}
pub fn with_initial_history(mut self, initial_history: Vec<ChatHistoryItem>) -> Self {
self.initial_history = initial_history;
self
}
pub fn build(self) -> Chat
where
<M::SyncModel as SyncModel>::Session: Send,
{
let Self {
model,
chat_markers,
system_prompt,
sampler,
bot_constraints,
session,
initial_history,
} = self;
let system_prompt_marker = chat_markers.system_prompt_marker.to_string();
let end_system_prompt_marker = chat_markers.end_system_prompt_marker.to_string();
let user_marker = chat_markers.user_marker.to_string();
let end_user_marker = chat_markers.end_user_marker.to_string();
let assistant_marker = chat_markers.assistant_marker.to_string();
let end_assistant_marker = chat_markers.end_assistant_marker.to_string();
let (sender_tx, mut sender_rx) = unbounded_channel();
let shared_history = Arc::new(RwLock::new(Vec::new()));
{
let shared_history = shared_history.clone();
tokio::spawn(async move {
let (tx, rx) = oneshot::channel();
{
model
.run_sync(move |model| {
Box::pin(async move {
let _ = tx.send(ChatSession::new(
model,
system_prompt_marker,
end_system_prompt_marker,
user_marker,
end_user_marker,
assistant_marker,
end_assistant_marker,
system_prompt,
bot_constraints,
sampler,
session,
initial_history,
shared_history,
));
})
})
.unwrap();
}
let Ok(session) = rx.await else {
tracing::error!("Error loading session");
return;
};
let chat_session = Arc::new(Mutex::new(session));
while let Some(message) = sender_rx.recv().await {
match message {
Message::AddMessage {
message,
response_tx,
} => {
let chat_session = chat_session.clone();
model
.run_sync(move |model| {
Box::pin(async move {
let mut chat_session = chat_session.lock().unwrap();
if let Err(err) =
chat_session.add_message(message, model, response_tx)
{
tracing::error!("Error adding message: {}", err);
}
})
})
.unwrap();
}
Message::SaveSession { path, resolve } => {
let chat_session = chat_session.lock().unwrap();
resolve.send(chat_session.session.save_to(path)).unwrap();
}
}
}
});
}
Chat {
sender: sender_tx,
shared_history,
}
}
}
enum Message {
AddMessage {
message: String,
response_tx: tokio::sync::mpsc::UnboundedSender<String>,
},
SaveSession {
path: PathBuf,
resolve: tokio::sync::oneshot::Sender<Result<()>>,
},
}
pub struct Chat {
sender: tokio::sync::mpsc::UnboundedSender<Message>,
shared_history: Arc<RwLock<Vec<ChatHistoryItem>>>,
}
impl Chat {
pub fn builder<M: Model>(model: M) -> ChatBuilder<M>
where
<M::SyncModel as SyncModel>::Session: Send,
{
ChatBuilder::new(model)
}
pub fn new<M: Model>(model: M) -> Chat
where
<M::SyncModel as SyncModel>::Session: Send,
{
Self::builder(model).build()
}
pub fn add_message(&mut self, message: impl ToString) -> ChannelTextStream {
let (tx, rx) = unbounded_channel();
let message = message.to_string();
let message = message.trim().to_string();
let _ = self.sender.send(Message::AddMessage {
message,
response_tx: tx,
});
ChannelTextStream::from(rx)
}
pub fn save_session(
&mut self,
path: impl AsRef<std::path::Path>,
) -> impl Future<Output = Result<()>> {
let (tx, rx) = oneshot::channel();
let result = self.sender.send(Message::SaveSession {
path: path.as_ref().to_path_buf(),
resolve: tx,
});
async move {
result.map_err(|_| anyhow::anyhow!("Model stopped"))?;
rx.await.map_err(|_| anyhow::anyhow!("Model stopped"))?
}
}
pub fn history(&self) -> Vec<ChatHistoryItem> {
self.shared_history.read().unwrap().clone()
}
}