chat history added to context and patching text content

This commit is contained in:
Per Stark
2025-03-16 21:24:24 +01:00
parent bbc1cbc302
commit bc5c4b32b6
13 changed files with 141 additions and 166 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -35,3 +35,28 @@ impl Message {
}
}
}
impl fmt::Display for MessageRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MessageRole::User => write!(f, "User"),
MessageRole::AI => write!(f, "AI"),
MessageRole::System => write!(f, "System"),
}
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.role, self.content)
}
}
// helper function to format a vector of messages
pub fn format_history(history: &[Message]) -> String {
history
.iter()
.map(|msg| format!("{}", msg))
.collect::<Vec<String>>()
.join("\n")
}

View File

@@ -1,6 +1,7 @@
use surrealdb::opt::PatchOp;
use uuid::Uuid;
use crate::stored_object;
use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
use super::file_info::FileInfo;
@@ -35,4 +36,24 @@ impl TextContent {
user_id,
}
}
pub async fn patch(
id: &str,
instructions: &str,
category: &str,
text: &str,
db: &SurrealDbClient,
) -> Result<(), AppError> {
let now = Utc::now();
let _res: Option<Self> = db
.update((Self::table_name(), id))
.patch(PatchOp::replace("/instructions", instructions))
.patch(PatchOp::replace("/category", category))
.patch(PatchOp::replace("/text", text))
.patch(PatchOp::replace("/updated_at", now))
.await?;
Ok(())
}
}

View File

@@ -5,6 +5,7 @@ use async_openai::{
CreateChatCompletionRequest, CreateChatCompletionRequestArgs, CreateChatCompletionResponse,
ResponseFormat, ResponseFormatJsonSchema,
},
MessageFiles,
};
use serde::Deserialize;
use serde_json::{json, Value};
@@ -12,7 +13,13 @@ use tracing::debug;
use common::{
error::AppError,
storage::{db::SurrealDbClient, types::knowledge_entity::KnowledgeEntity},
storage::{
db::SurrealDbClient,
types::{
knowledge_entity::KnowledgeEntity,
message::{format_history, Message},
},
},
};
use crate::retrieve_entities;
@@ -109,6 +116,31 @@ pub fn create_user_message(entities_json: &Value, query: &str) -> String {
)
}
pub fn create_user_message_with_history(
entities_json: &Value,
history: &[Message],
query: &str,
) -> String {
format!(
r#"
Chat history:
==================
{}
Context Information:
==================
{}
User Question:
==================
{}
"#,
format_history(history),
entities_json,
query
)
}
pub fn create_chat_request(
user_message: String,
) -> Result<CreateChatCompletionRequest, OpenAIError> {

View File

@@ -1,139 +0,0 @@
// use axum::{
// http::StatusCode,
// response::{Html, IntoResponse, Response},
// };
// use common::error::AppError;
// use minijinja::context;
// use minijinja_autoreload::AutoReloader;
// use std::sync::Arc;
// pub type TemplateResult<T> = Result<T, HtmlError>;
// // Helper trait for converting to HtmlError with templates
// pub trait IntoHtmlError {
// fn with_template(self, templates: Arc<AutoReloader>) -> HtmlError;
// }
// // // Implement for AppError
// impl IntoHtmlError for AppError {
// fn with_template(self, templates: Arc<AutoReloader>) -> HtmlError {
// HtmlError::new(self, templates)
// }
// }
// // // Implement for minijinja::Error directly
// impl IntoHtmlError for minijinja::Error {
// fn with_template(self, templates: Arc<AutoReloader>) -> HtmlError {
// HtmlError::from_template_error(self, templates)
// }
// }
// pub enum HtmlError {
// ServerError(Arc<AutoReloader>),
// NotFound(Arc<AutoReloader>),
// Unauthorized(Arc<AutoReloader>),
// BadRequest(String, Arc<AutoReloader>),
// Template(String, Arc<AutoReloader>),
// }
// impl HtmlError {
// pub fn new(error: AppError, templates: Arc<AutoReloader>) -> Self {
// match error {
// AppError::NotFound(_msg) => HtmlError::NotFound(templates),
// AppError::Auth(_msg) => HtmlError::Unauthorized(templates),
// AppError::Validation(msg) => HtmlError::BadRequest(msg, templates),
// _ => {
// tracing::error!("Internal error: {:?}", error);
// HtmlError::ServerError(templates)
// }
// }
// }
// pub fn from_template_error(error: minijinja::Error, templates: Arc<AutoReloader>) -> Self {
// tracing::error!("Template error: {:?}", error);
// HtmlError::Template(error.to_string(), templates)
// }
// }
// impl IntoResponse for HtmlError {
// fn into_response(self) -> Response {
// let (status, context, templates) = match self {
// HtmlError::ServerError(templates) | HtmlError::Template(_, templates) => (
// StatusCode::INTERNAL_SERVER_ERROR,
// context! {
// status_code => 500,
// title => "Internal Server Error",
// error => "Internal Server Error",
// description => "Something went wrong on our end."
// },
// templates,
// ),
// HtmlError::NotFound(templates) => (
// StatusCode::NOT_FOUND,
// context! {
// status_code => 404,
// title => "Page Not Found",
// error => "Not Found",
// description => "The page you're looking for doesn't exist or was removed."
// },
// templates,
// ),
// HtmlError::Unauthorized(templates) => (
// StatusCode::UNAUTHORIZED,
// context! {
// status_code => 401,
// title => "Unauthorized",
// error => "Access Denied",
// description => "You need to be logged in to access this page."
// },
// templates,
// ),
// HtmlError::BadRequest(msg, templates) => (
// StatusCode::BAD_REQUEST,
// context! {
// status_code => 400,
// title => "Bad Request",
// error => "Bad Request",
// description => msg
// },
// templates,
// ),
// };
// let html = match templates.acquire_env() {
// Ok(env) => match env.get_template("errors/error.html") {
// Ok(tmpl) => match tmpl.render(context) {
// Ok(output) => output,
// Err(e) => {
// tracing::error!("Template render error: {:?}", e);
// Self::fallback_html()
// }
// },
// Err(e) => {
// tracing::error!("Template get error: {:?}", e);
// Self::fallback_html()
// }
// },
// Err(e) => {
// tracing::error!("Environment acquire error: {:?}", e);
// Self::fallback_html()
// }
// };
// (status, Html(html)).into_response()
// }
// }
// impl HtmlError {
// fn fallback_html() -> String {
// r#"
// <html>
// <body>
// <div class="container mx-auto p-4">
// <h1 class="text-4xl text-error">Error</h1>
// <p class="mt-4">Sorry, something went wrong displaying this page.</p>
// </div>
// </body>
// </html>
// "#
// .to_string()
// }
// }

View File

@@ -1,4 +1,3 @@
pub mod error;
pub mod html_state;
mod middleware_analytics;
mod middleware_auth;

View File

@@ -12,7 +12,8 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use composite_retrieval::{
answer_retrieval::{
create_chat_request, create_user_message, format_entities_json, LLMResponseFormat,
create_chat_request, create_user_message_with_history, format_entities_json,
LLMResponseFormat,
},
retrieve_entities,
};
@@ -30,6 +31,7 @@ use tracing::{error, info};
use common::storage::{
db::SurrealDbClient,
types::{
conversation::Conversation,
message::{Message, MessageRole},
user::User,
},
@@ -50,7 +52,10 @@ async fn get_message_and_user(
db: &SurrealDbClient,
current_user: Option<User>,
message_id: &str,
) -> Result<(Message, User), Sse<Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>>>> {
) -> Result<
(Message, User, Conversation, Vec<Message>),
Sse<Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>>>,
> {
// Check authentication
let user = match current_user {
Some(user) => user,
@@ -77,7 +82,23 @@ async fn get_message_and_user(
}
};
Ok((message, user))
// Get conversation history
let (conversation, mut history) =
match Conversation::get_complete_conversation(&message.conversation_id, &user.id, db).await
{
Err(e) => {
error!("Database error retrieving message {}: {:?}", message_id, e);
return Err(Sse::new(create_error_stream(
"Failed to retrieve message: database error",
)));
}
Ok((conversation, history)) => (conversation, history),
};
// Remove the last message, its the same as the message
history.pop();
Ok((message, user, conversation, history))
}
#[derive(Deserialize)]
@@ -91,9 +112,11 @@ pub async fn get_response_stream(
Query(params): Query<QueryParams>,
) -> Sse<Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>>> {
// 1. Authentication and initial data validation
let (user_message, user) =
let (user_message, user, _conversation, history) =
match get_message_and_user(&state.db, auth.current_user, &params.message_id).await {
Ok((user_message, user)) => (user_message, user),
Ok((user_message, user, conversation, history)) => {
(user_message, user, conversation, history)
}
Err(error_stream) => return error_stream,
};
@@ -114,7 +137,8 @@ pub async fn get_response_stream(
// 3. Create the OpenAI request
let entities_json = format_entities_json(&entities);
let formatted_user_message = create_user_message(&entities_json, &user_message.content);
let formatted_user_message =
create_user_message_with_history(&entities_json, &history, &user_message.content);
let request = match create_chat_request(formatted_user_message) {
Ok(req) => req,
Err(..) => {

View File

@@ -1,8 +1,9 @@
use axum::{
extract::{Path, State},
response::IntoResponse,
Form,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use common::storage::types::{text_content::TextContent, user::User};
@@ -33,12 +34,6 @@ pub async fn show_content_page(
))
}
#[derive(Serialize)]
pub struct TextContentEditModal {
pub user: User,
pub text_content: TextContent,
}
pub async fn show_text_content_edit_form(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
@@ -46,20 +41,40 @@ pub async fn show_text_content_edit_form(
) -> Result<impl IntoResponse, HtmlError> {
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
#[derive(Serialize)]
pub struct TextContentEditModal {
pub user: User,
pub text_content: TextContent,
}
Ok(TemplateResponse::new_template(
"content/edit_text_content_modal.html",
TextContentEditModal { user, text_content },
))
}
#[derive(Deserialize)]
pub struct PatchTextContentParams {
instructions: String,
category: String,
text: String,
}
pub async fn patch_text_content(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
Path(id): Path<String>,
Form(form): Form<PatchTextContentParams>,
) -> Result<impl IntoResponse, HtmlError> {
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
// ADD FUNCTION TO PATCH CONTENT
TextContent::patch(
&id,
&form.instructions,
&form.category,
&form.text,
&state.db,
)
.await?;
let text_contents = User::get_text_contents(&user.id, &state.db).await?;

View File

@@ -261,7 +261,6 @@ impl From<surrealdb::Error> for HtmlError {
}
}
// Now implement IntoResponse for HtmlError
impl IntoResponse for HtmlError {
fn into_response(self) -> Response {
match self {

View File

@@ -11,23 +11,21 @@ hx-swap="outerHTML"
<div class="form-control">
<label class="floating-label">
<span class="label-text">Instructions</span>
<input type="text" name="name" value="{{ text_content.instructions}}" class="w-full input input-bordered">
<input type="text" name="instructions" value="{{ text_content.instructions}}" class="w-full input input-bordered">
</label>
</div>
<div class="form-control ">
<label class="floating-label">
<span class="label-text">Category</span>
<input type="text" name="name" value="{{ text_content.category}}" class="w-full input input-bordered">
<input type="text" name="category" value="{{ text_content.category}}" class="w-full input input-bordered">
</label>
</div>
<input type="text" name="id" value="{{ text_content.id }}" class="hidden">
<div class="form-control">
<label class="floating-label">
<span class="label-text">Text</span>
<textarea name="description" class="textarea textarea-bordered h-32 w-full">{{ text_content.text}}</textarea>
<textarea name="text" class="textarea textarea-bordered h-32 w-full">{{ text_content.text}}</textarea>
</label>
</div>
{% endblock %}

View File

@@ -1,19 +1,20 @@
\[\] fix patch_text_conent
\[\] archive ingressed webpage
\[x\] chat styling overhaul
\[\] configs primarily get envs
\[\] filtering on categories
\[x\] link to ingressed urls or archives
\[\] three js graph explorer
\[\] three js vector explorer
\[x\] add user_id to ingress objects
\[x\] admin controls re registration
\[x\] chat functionality
\[x\] chat history
\[x\] chat styling overhaul
\[x\] fix patch_text_content
\[x\] gdpr
\[x\] html ingression
\[x\] hx-redirect
\[x\] ios shortcut generation
\[x\] job queue
\[x\] link to ingressed urls or archives
\[x\] macro for pagedata?
\[x\] on updates of knowledgeentity create new embeddings
\[x\] redirects