add reasoning block

This commit is contained in:
duanfuxiang
2025-02-18 11:02:24 +08:00
parent dc520535fc
commit d15681b0d5
12 changed files with 285 additions and 169 deletions

View File

@@ -1,162 +1,164 @@
import { SerializedEditorState } from 'lexical'
import { App } from 'obsidian'
import { editorStateToPlainText } from '../../../components/chat-view/chat-input/utils/editor-state-to-plain-text'
import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat'
import { ContentPart } from '../../../types/llm/request'
import { Mentionable, SerializedMentionable } from '../../../types/mentionable'
import { Mentionable } from '../../../types/mentionable'
import { deserializeMentionable, serializeMentionable } from '../../../utils/mentionable'
import { DBManager } from '../../database-manager'
import { InsertMessage } from '../../schema'
import { InsertMessage, SelectConversation, SelectMessage } from '../../schema'
import { ConversationRepository } from './conversation-repository'
export class ConversationManager {
private app: App
private repository: ConversationRepository
private dbManager: DBManager
private app: App
private repository: ConversationRepository
private dbManager: DBManager
constructor(app: App, dbManager: DBManager) {
this.app = app
this.dbManager = dbManager
const db = dbManager.getPgClient()
if (!db) throw new Error('Database not initialized')
this.repository = new ConversationRepository(app, db)
}
constructor(app: App, dbManager: DBManager) {
this.app = app
this.dbManager = dbManager
const db = dbManager.getPgClient()
if (!db) throw new Error('Database not initialized')
this.repository = new ConversationRepository(app, db)
}
async createConversation(id: string, title = 'New chat'): Promise<void> {
const conversation = {
id,
title,
createdAt: new Date(),
updatedAt: new Date(),
}
await this.repository.create(conversation)
await this.dbManager.save()
}
async createConversation(id: string, title = 'New chat'): Promise<void> {
const conversation = {
id,
title,
createdAt: new Date(),
updatedAt: new Date(),
}
await this.repository.create(conversation)
await this.dbManager.save()
}
async saveConversation(id: string, messages: ChatMessage[]): Promise<void> {
const conversation = await this.repository.findById(id)
if (!conversation) {
let title = 'New chat'
if (messages.length > 0 && messages[0].role === 'user') {
const query = editorStateToPlainText(messages[0].content)
if (query.length > 20) {
title = `${query.slice(0, 20)}...`
} else {
title = query
}
}
await this.createConversation(id, title)
}
async saveConversation(id: string, messages: ChatMessage[]): Promise<void> {
const conversation = await this.repository.findById(id)
if (!conversation) {
let title = 'New chat'
if (messages.length > 0 && messages[0].role === 'user') {
const query = editorStateToPlainText(messages[0].content)
if (query.length > 20) {
title = `${query.slice(0, 20)}...`
} else {
title = query
}
}
await this.createConversation(id, title)
}
// Delete existing messages
await this.repository.deleteAllMessagesFromConversation(id)
// Delete existing messages
await this.repository.deleteAllMessagesFromConversation(id)
// Insert new messages
for (const message of messages) {
const insertMessage = this.serializeMessage(message, id)
await this.repository.createMessage(insertMessage)
}
// Insert new messages
for (const message of messages) {
const insertMessage = this.serializeMessage(message, id)
await this.repository.createMessage(insertMessage)
}
// Update conversation timestamp
await this.repository.update(id, { updatedAt: new Date() })
await this.dbManager.save()
}
// Update conversation timestamp
await this.repository.update(id, { updatedAt: new Date() })
await this.dbManager.save()
}
async findConversation(id: string): Promise<ChatMessage[] | null> {
const conversation = await this.repository.findById(id)
if (!conversation) {
return null
}
async findConversation(id: string): Promise<ChatMessage[] | null> {
const conversation = await this.repository.findById(id)
if (!conversation) {
return null
}
const messages = await this.repository.findMessagesByConversationId(id)
return messages.map(msg => this.deserializeMessage(msg))
}
const messages = await this.repository.findMessagesByConversationId(id)
return messages.map(msg => this.deserializeMessage(msg))
}
async deleteConversation(id: string): Promise<void> {
await this.repository.delete(id)
await this.dbManager.save()
}
async deleteConversation(id: string): Promise<void> {
await this.repository.delete(id)
await this.dbManager.save()
}
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
const db = this.dbManager.getPgClient()
db?.live.query('SELECT * FROM conversations ORDER BY updated_at', [], (results) => {
callback(results.rows.map(conv => ({
id: conv.id,
title: conv.title,
schemaVersion: 2,
createdAt: conv.createdAt instanceof Date ? conv.createdAt.getTime() : conv.createdAt,
updatedAt: conv.updatedAt instanceof Date ? conv.updatedAt.getTime() : conv.updatedAt,
})))
})
}
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
const db = this.dbManager.getPgClient()
db?.live.query('SELECT * FROM conversations ORDER BY updated_at DESC', [], (results: { rows: Array<SelectConversation> }) => {
callback(results.rows.map(conv => ({
schemaVersion: 2,
id: conv.id,
title: conv.title,
createdAt: conv.created_at instanceof Date ? conv.created_at.getTime() : conv.created_at,
updatedAt: conv.updated_at instanceof Date ? conv.updated_at.getTime() : conv.updated_at,
})))
})
}
async updateConversationTitle(id: string, title: string): Promise<void> {
await this.repository.update(id, { title })
await this.dbManager.save()
}
async updateConversationTitle(id: string, title: string): Promise<void> {
await this.repository.update(id, { title })
await this.dbManager.save()
}
private serializeMessage(message: ChatMessage, conversationId: string): InsertMessage {
const base = {
id: message.id,
conversationId,
role: message.role,
createdAt: new Date(),
}
// convert ChatMessage to InsertMessage
private serializeMessage(message: ChatMessage, conversationId: string): InsertMessage {
const base = {
id: message.id,
conversationId: conversationId,
role: message.role,
createdAt: new Date(),
}
if (message.role === 'user') {
const userMessage: ChatUserMessage = message
return {
...base,
content: userMessage.content ? JSON.stringify(userMessage.content) : null,
promptContent: userMessage.promptContent
? typeof userMessage.promptContent === 'string'
? userMessage.promptContent
: JSON.stringify(userMessage.promptContent)
: null,
mentionables: JSON.stringify(userMessage.mentionables.map(serializeMentionable)),
similaritySearchResults: userMessage.similaritySearchResults
? JSON.stringify(userMessage.similaritySearchResults)
: null,
}
} else {
const assistantMessage: ChatAssistantMessage = message
return {
...base,
content: assistantMessage.content,
metadata: assistantMessage.metadata ? JSON.stringify(assistantMessage.metadata) : null,
}
}
}
if (message.role === 'user') {
const userMessage: ChatUserMessage = message
return {
...base,
content: userMessage.content ? JSON.stringify(userMessage.content) : null,
promptContent: userMessage.promptContent
? typeof userMessage.promptContent === 'string'
? userMessage.promptContent
: JSON.stringify(userMessage.promptContent)
: null,
mentionables: JSON.stringify(userMessage.mentionables.map(serializeMentionable)),
similaritySearchResults: userMessage.similaritySearchResults
? JSON.stringify(userMessage.similaritySearchResults)
: null,
}
} else {
const assistantMessage: ChatAssistantMessage = message
return {
...base,
content: assistantMessage.content,
reasoningContent: assistantMessage.reasoningContent,
metadata: assistantMessage.metadata ? JSON.stringify(assistantMessage.metadata) : null,
}
}
}
private deserializeMessage(message: InsertMessage): ChatMessage {
if (message.role === 'user') {
return {
id: message.id,
role: 'user',
content: message.content ? JSON.parse(message.content) as SerializedEditorState : null,
promptContent: message.promptContent
? message.promptContent.startsWith('{')
? JSON.parse(message.promptContent) as ContentPart[]
: message.promptContent
: null,
mentionables: message.mentionables
? (JSON.parse(message.mentionables) as SerializedMentionable[])
.map(m => deserializeMentionable(m, this.app))
.filter((m: Mentionable | null): m is Mentionable => m !== null)
: [],
similaritySearchResults: message.similaritySearchResults
? JSON.parse(message.similaritySearchResults)
: undefined,
}
} else {
return {
id: message.id,
role: 'assistant',
content: message.content || '',
metadata: message.metadata ? JSON.parse(message.metadata) : undefined,
}
}
}
// convert SelectMessage to ChatMessage
private deserializeMessage(message: SelectMessage): ChatMessage {
if (message.role === 'user') {
return {
id: message.id,
role: 'user',
content: message.content ? JSON.parse(message.content) : null,
promptContent: message.prompt_content
? message.prompt_content.startsWith('{')
? JSON.parse(message.prompt_content)
: message.prompt_content
: null,
mentionables: message.mentionables
? JSON.parse(message.mentionables)
.map(m => deserializeMentionable(m, this.app))
.filter((m: Mentionable | null): m is Mentionable => m !== null)
: [],
similaritySearchResults: message.similarity_search_results
? JSON.parse(message.similarity_search_results)
: undefined,
}
} else {
return {
id: message.id,
role: 'assistant',
content: message.content || '',
reasoningContent: message.reasoning_content || '',
metadata: message.metadata ? JSON.parse(message.metadata) : undefined,
}
}
}
}

View File

@@ -8,10 +8,6 @@ import {
SelectMessage,
} from '../../schema'
type QueryResult<T> = {
rows: T[]
}
export class ConversationRepository {
private app: App
private db: PGliteInterface
@@ -32,31 +28,32 @@ export class ConversationRepository {
conversation.createdAt || new Date(),
conversation.updatedAt || new Date()
]
) as QueryResult<SelectConversation>
)
return result.rows[0]
}
async createMessage(message: InsertMessage): Promise<SelectMessage> {
const result = await this.db.query<SelectMessage>(
const result = await this.db.query<SelectMessage>(
`INSERT INTO messages (
id, conversation_id, role, content,
id, conversation_id, role, content, reasoning_content,
prompt_content, metadata, mentionables,
similarity_search_results, created_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`,
[
message.id,
message.conversationId,
message.role,
message.content,
message.reasoningContent,
message.promptContent,
message.metadata,
message.mentionables,
message.similaritySearchResults,
message.createdAt || new Date()
]
) as QueryResult<SelectMessage>
)
return result.rows[0]
}
@@ -64,30 +61,30 @@ export class ConversationRepository {
const result = await this.db.query<SelectConversation>(
`SELECT * FROM conversations WHERE id = $1 LIMIT 1`,
[id]
) as QueryResult<SelectConversation>
)
return result.rows[0]
}
async findMessagesByConversationId(conversationId: string): Promise<SelectMessage[]> {
async findMessagesByConversationId(conversationId: string): Promise<SelectMessage[]> {
const result = await this.db.query<SelectMessage>(
`SELECT * FROM messages
WHERE conversation_id = $1
ORDER BY created_at`,
[conversationId]
) as QueryResult<SelectMessage>
)
return result.rows
}
async findAll(): Promise<SelectConversation[]> {
const result = await this.db.query<SelectConversation>(
`SELECT * FROM conversations ORDER BY updated_at DESC`
) as QueryResult<SelectConversation>
`SELECT * FROM conversations ORDER BY created_at DESC`
)
return result.rows
}
async update(id: string, data: Partial<InsertConversation>): Promise<SelectConversation> {
const setClauses: string[] = []
const values: any[] = []
const values: (string | Date)[] = []
let paramIndex = 1
if (data.title !== undefined) {
@@ -110,7 +107,7 @@ export class ConversationRepository {
WHERE id = $${paramIndex}
RETURNING *`,
values
) as QueryResult<SelectConversation>
)
return result.rows[0]
}
@@ -118,7 +115,7 @@ export class ConversationRepository {
const result = await this.db.query<SelectConversation>(
`DELETE FROM conversations WHERE id = $1 RETURNING *`,
[id]
) as QueryResult<SelectConversation>
)
return result.rows.length > 0
}

View File

@@ -125,6 +125,7 @@ export type Message = {
conversationId: string // uuid
role: 'user' | 'assistant'
content: string | null
reasoningContent?: string | null
promptContent?: string | null
metadata?: string | null
mentionables?: string | null
@@ -139,13 +140,19 @@ export type InsertConversation = {
updatedAt?: Date
}
export type SelectConversation = Conversation
export type SelectConversation = {
id: string // uuid
title: string
created_at: Date
updated_at: Date
}
export type InsertMessage = {
id: string
conversationId: string
role: 'user' | 'assistant'
content: string | null
reasoningContent?: string | null
promptContent?: string | null
metadata?: string | null
mentionables?: string | null
@@ -153,4 +160,15 @@ export type InsertMessage = {
createdAt?: Date
}
export type SelectMessage = Message
export type SelectMessage = {
id: string // uuid
conversation_id: string // uuid
role: 'user' | 'assistant'
content: string | null
reasoning_content?: string | null
prompt_content?: string | null
metadata?: string | null
mentionables?: string | null
similarity_search_results?: string | null
created_at: Date
}

View File

@@ -102,11 +102,23 @@ export const migrations: Record<string, SqlMigration> = {
"updated_at" timestamp DEFAULT now() NOT NULL
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages'
AND column_name = 'reasoning_content'
) THEN
ALTER TABLE "messages" ADD COLUMN "reasoning_content" text;
END IF;
END $$;
CREATE TABLE IF NOT EXISTS "messages" (
"id" uuid PRIMARY KEY NOT NULL,
"conversation_id" uuid NOT NULL REFERENCES "conversations"("id") ON DELETE CASCADE,
"role" text NOT NULL,
"content" text,
"reasoning_content" text,
"prompt_content" text,
"metadata" text,
"mentionables" text,