Add conversation pkg
This commit is contained in:
+354
-12
@@ -310,7 +310,8 @@ RETURNING content, conversation_id, created_at, id, json(metadata), role, sequen
|
||||
// InsertMessageParams holds arguments for InsertMessage.
|
||||
type InsertMessageParams struct {
|
||||
// Content is the model-visible message JSON saved in the new row.
|
||||
Content string
|
||||
// Malformed JSON causes insertion to fail.
|
||||
Content jsontext.Value
|
||||
// ConversationId identifies the conversation that owns the new row.
|
||||
ConversationId string
|
||||
// Id is the new row's UUID. Nil generates a version 4 UUID.
|
||||
@@ -340,7 +341,7 @@ func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (*
|
||||
ctx,
|
||||
insertMessage,
|
||||
id.String(),
|
||||
arg.Content,
|
||||
string(arg.Content),
|
||||
arg.ConversationId,
|
||||
metadataArg,
|
||||
arg.Role,
|
||||
@@ -349,13 +350,14 @@ func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (*
|
||||
|
||||
var (
|
||||
m Message
|
||||
content []byte
|
||||
metadata []byte
|
||||
createdAt string
|
||||
updatedAt string
|
||||
err error
|
||||
)
|
||||
if err := row.Scan(
|
||||
&m.Content,
|
||||
&content,
|
||||
&m.ConversationId,
|
||||
&createdAt,
|
||||
&m.Id,
|
||||
@@ -366,6 +368,7 @@ func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (*
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("InsertMessage: %w", err)
|
||||
}
|
||||
m.Content = jsontext.Value(content)
|
||||
m.Metadata = jsontext.Value(metadata)
|
||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||
return nil, fmt.Errorf("InsertMessage: parse created_at: %w", err)
|
||||
@@ -376,6 +379,58 @@ func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (*
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
const selectConversationMessageId = `
|
||||
SELECT content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
||||
FROM messages
|
||||
WHERE conversation_id = ? AND id = ?
|
||||
`
|
||||
|
||||
// SelectConversationMessageId returns a conversation's message by ID.
|
||||
func (q *Queries) SelectConversationMessageId(
|
||||
ctx context.Context,
|
||||
conversationId string,
|
||||
id string,
|
||||
) (*Message, error) {
|
||||
row := q.db.QueryRowContext(
|
||||
ctx, selectConversationMessageId, conversationId, id,
|
||||
)
|
||||
|
||||
var (
|
||||
m Message
|
||||
content []byte
|
||||
metadata []byte
|
||||
createdAt string
|
||||
updatedAt string
|
||||
)
|
||||
err := row.Scan(
|
||||
&content,
|
||||
&m.ConversationId,
|
||||
&createdAt,
|
||||
&m.Id,
|
||||
&metadata,
|
||||
&m.Role,
|
||||
&m.Sequence,
|
||||
&updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SelectConversationMessageId: %w", err)
|
||||
}
|
||||
m.Content = jsontext.Value(content)
|
||||
m.Metadata = jsontext.Value(metadata)
|
||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"SelectConversationMessageId: parse created_at: %w", err,
|
||||
)
|
||||
}
|
||||
if m.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"SelectConversationMessageId: parse updated_at: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
const selectMessagesConversationId = `
|
||||
SELECT content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
||||
FROM messages
|
||||
@@ -396,12 +451,13 @@ func (q *Queries) SelectMessagesConversationId(ctx context.Context, conversation
|
||||
for rows.Next() {
|
||||
var (
|
||||
m Message
|
||||
content []byte
|
||||
metadata []byte
|
||||
createdAt string
|
||||
updatedAt string
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&m.Content,
|
||||
&content,
|
||||
&m.ConversationId,
|
||||
&createdAt,
|
||||
&m.Id,
|
||||
@@ -412,6 +468,7 @@ func (q *Queries) SelectMessagesConversationId(ctx context.Context, conversation
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("SelectMessagesConversationId: %w", err)
|
||||
}
|
||||
m.Content = jsontext.Value(content)
|
||||
m.Metadata = jsontext.Value(metadata)
|
||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||
return nil, fmt.Errorf("SelectMessagesConversationId: parse created_at: %w", err)
|
||||
@@ -431,26 +488,54 @@ const updateMessageContent = `
|
||||
UPDATE messages
|
||||
SET content = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||
WHERE id = ?
|
||||
RETURNING content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
||||
`
|
||||
|
||||
// UpdateMessageContentParams holds arguments for UpdateMessageContent.
|
||||
type UpdateMessageContentParams struct {
|
||||
Content string
|
||||
Content jsontext.Value
|
||||
Id string
|
||||
}
|
||||
|
||||
// UpdateMessageContent sets the content JSON for a message.
|
||||
func (q *Queries) UpdateMessageContent(ctx context.Context, arg UpdateMessageContentParams) error {
|
||||
_, err := q.db.ExecContext(
|
||||
// UpdateMessageContent sets the content JSON for a message and returns
|
||||
// the updated row.
|
||||
func (q *Queries) UpdateMessageContent(ctx context.Context, arg UpdateMessageContentParams) (*Message, error) {
|
||||
row := q.db.QueryRowContext(
|
||||
ctx,
|
||||
updateMessageContent,
|
||||
arg.Content,
|
||||
string(arg.Content),
|
||||
arg.Id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateMessageContent: %w", err)
|
||||
|
||||
var (
|
||||
m Message
|
||||
content []byte
|
||||
metadata []byte
|
||||
createdAt string
|
||||
updatedAt string
|
||||
err error
|
||||
)
|
||||
if err := row.Scan(
|
||||
&content,
|
||||
&m.ConversationId,
|
||||
&createdAt,
|
||||
&m.Id,
|
||||
&metadata,
|
||||
&m.Role,
|
||||
&m.Sequence,
|
||||
&updatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("UpdateMessageContent: %w", err)
|
||||
}
|
||||
return nil
|
||||
m.Content = jsontext.Value(content)
|
||||
m.Metadata = jsontext.Value(metadata)
|
||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||
return nil, fmt.Errorf("UpdateMessageContent: parse created_at: %w", err)
|
||||
}
|
||||
if m.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt); err != nil {
|
||||
return nil, fmt.Errorf("UpdateMessageContent: parse updated_at: %w", err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
const deleteMessageId = `
|
||||
@@ -467,6 +552,205 @@ func (q *Queries) DeleteMessageId(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const updateMessagesSequence = `
|
||||
UPDATE messages
|
||||
SET sequence = sequence - 1, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||
WHERE conversation_id = ? AND sequence > ?
|
||||
`
|
||||
|
||||
// UpdateMessagesSequence decrements the sequence of every message that
|
||||
// follows the given sequence in the conversation. Every moved row gets a
|
||||
// new updated_at because its position changed.
|
||||
func (q *Queries) UpdateMessagesSequence(
|
||||
ctx context.Context,
|
||||
conversationId string,
|
||||
sequence int,
|
||||
) error {
|
||||
_, err := q.db.ExecContext(
|
||||
ctx,
|
||||
updateMessagesSequence,
|
||||
conversationId,
|
||||
sequence,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateMessagesSequence: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The shift takes two statements because SQLite checks
|
||||
// UNIQUE(conversation_id, sequence) row by row, not at the end of the
|
||||
// statement. A single "sequence = sequence + 1" fails if it visits row n
|
||||
// while row n+1 still exists. Step one negates the shifted rows to values
|
||||
// no real row uses (sequences start at 1); step two flips them back.
|
||||
const updateMessagesSequenceNegate = `
|
||||
UPDATE messages
|
||||
SET sequence = -(sequence + 1)
|
||||
WHERE conversation_id = ? AND sequence >= ?
|
||||
`
|
||||
|
||||
const updateMessagesSequenceRestore = `
|
||||
UPDATE messages
|
||||
SET sequence = -sequence, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||
WHERE conversation_id = ? AND sequence < 0
|
||||
`
|
||||
|
||||
// UpdateMessagesSequenceIncrement increments the sequence of every message
|
||||
// at or after the given sequence in the conversation, freeing that sequence
|
||||
// for an insert. Every moved row gets a new updated_at because its position
|
||||
// changed.
|
||||
// It runs two statements; call it inside a transaction so a failure
|
||||
// between them cannot leave negative sequences behind.
|
||||
func (q *Queries) UpdateMessagesSequenceIncrement(
|
||||
ctx context.Context,
|
||||
conversationId string,
|
||||
sequence int,
|
||||
) error {
|
||||
if _, err := q.db.ExecContext(
|
||||
ctx,
|
||||
updateMessagesSequenceNegate,
|
||||
conversationId,
|
||||
sequence,
|
||||
); err != nil {
|
||||
return fmt.Errorf("UpdateMessagesSequenceIncrement: %w", err)
|
||||
}
|
||||
if _, err := q.db.ExecContext(
|
||||
ctx,
|
||||
updateMessagesSequenceRestore,
|
||||
conversationId,
|
||||
); err != nil {
|
||||
return fmt.Errorf("UpdateMessagesSequenceIncrement: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectMessageSequenceToolResult returns the sequence where a new tool
|
||||
// result belongs: right after the assistant's existing results. The caller
|
||||
// frees that sequence with UpdateMessagesSequenceIncrement, then inserts.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// 2 assistant (calls A, B, C)
|
||||
// 3 tool (A)
|
||||
// 4 tool (B)
|
||||
// 5 user
|
||||
//
|
||||
// A result for C belongs at 5; the user message moves to 6. Without the
|
||||
// user message, 5 would be the end of the conversation, and the result
|
||||
// would still go at 5.
|
||||
const selectMessageSequenceToolResult = `
|
||||
SELECT COALESCE(
|
||||
-- The first message after the assistant that is not a tool result
|
||||
-- (row 5 above) marks the end of the assistant's results. The new
|
||||
-- result takes its place. If there is no such message, MIN returns
|
||||
-- NULL and COALESCE uses the fallback below.
|
||||
MIN(sequence),
|
||||
-- Fallback: the results run to the end of the conversation, so
|
||||
-- append. This is the same calculation as NextSequence.
|
||||
(
|
||||
SELECT COALESCE(MAX(sequence), 0) + 1
|
||||
FROM messages
|
||||
WHERE conversation_id = ?
|
||||
)
|
||||
)
|
||||
-- MIN always returns one row, even when nothing matches, so the
|
||||
-- caller never gets sql.ErrNoRows.
|
||||
FROM messages
|
||||
WHERE conversation_id = ?
|
||||
AND sequence > ?
|
||||
AND role != 'tool'
|
||||
`
|
||||
|
||||
// SelectMessageSequenceToolResult returns the sequence at which to insert
|
||||
// a new tool result for the assistant message at assistantSequence.
|
||||
// See selectMessageSequenceToolResult.
|
||||
func (q *Queries) SelectMessageSequenceToolResult(
|
||||
ctx context.Context,
|
||||
conversationId string,
|
||||
assistantSequence int,
|
||||
) (int, error) {
|
||||
var sequence int
|
||||
// The subquery's placeholder comes first in the query text.
|
||||
err := q.db.QueryRowContext(
|
||||
ctx,
|
||||
selectMessageSequenceToolResult,
|
||||
conversationId,
|
||||
conversationId,
|
||||
assistantSequence,
|
||||
).Scan(&sequence)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("SelectMessageSequenceToolResult: %w", err)
|
||||
}
|
||||
return sequence, nil
|
||||
}
|
||||
|
||||
// selectConversationToolResult returns the tool result a call has
|
||||
// among its assistant's results: after the assistant, before the
|
||||
// sequence from selectMessageSequenceToolResult. The search stops there
|
||||
// because tool call IDs are only unique within one assistant message;
|
||||
// another assistant's results may reuse the ID.
|
||||
const selectConversationToolResult = `
|
||||
SELECT content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
||||
FROM messages
|
||||
WHERE conversation_id = ? AND role = 'tool'
|
||||
AND sequence > ? AND sequence < ?
|
||||
AND json_extract(content, '$.tool_call_id') = ?
|
||||
`
|
||||
|
||||
// SelectConversationToolResult returns toolCallId's tool result with a
|
||||
// sequence strictly between after and before. It returns
|
||||
// sql.ErrNoRows if the call has no result in that range.
|
||||
func (q *Queries) SelectConversationToolResult(
|
||||
ctx context.Context,
|
||||
conversationId string,
|
||||
after int,
|
||||
before int,
|
||||
toolCallId string,
|
||||
) (*Message, error) {
|
||||
row := q.db.QueryRowContext(
|
||||
ctx,
|
||||
selectConversationToolResult,
|
||||
conversationId,
|
||||
after,
|
||||
before,
|
||||
toolCallId,
|
||||
)
|
||||
|
||||
var (
|
||||
m Message
|
||||
content []byte
|
||||
metadata []byte
|
||||
createdAt string
|
||||
updatedAt string
|
||||
)
|
||||
err := row.Scan(
|
||||
&content,
|
||||
&m.ConversationId,
|
||||
&createdAt,
|
||||
&m.Id,
|
||||
&metadata,
|
||||
&m.Role,
|
||||
&m.Sequence,
|
||||
&updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SelectConversationToolResult: %w", err)
|
||||
}
|
||||
m.Content = jsontext.Value(content)
|
||||
m.Metadata = jsontext.Value(metadata)
|
||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"SelectConversationToolResult: parse created_at: %w", err,
|
||||
)
|
||||
}
|
||||
if m.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"SelectConversationToolResult: parse updated_at: %w", err,
|
||||
)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
const selectMaxSequence = `
|
||||
SELECT COALESCE(MAX(sequence), 0)
|
||||
FROM messages
|
||||
@@ -691,3 +975,61 @@ func (q *Queries) UpdateExecutionStatus(ctx context.Context, arg UpdateExecution
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const deleteExecutionMessageIdToolCallId = `
|
||||
DELETE FROM executions
|
||||
WHERE message_id = ? AND tool_call_id = ?
|
||||
`
|
||||
|
||||
// DeleteExecutionMessageIdToolCallId deletes the execution record for one
|
||||
// tool call within a message. It is a no-op if no record exists.
|
||||
func (q *Queries) DeleteExecutionMessageIdToolCallId(
|
||||
ctx context.Context,
|
||||
messageId string,
|
||||
toolCallId string,
|
||||
) error {
|
||||
_, err := q.db.ExecContext(
|
||||
ctx,
|
||||
deleteExecutionMessageIdToolCallId,
|
||||
messageId,
|
||||
toolCallId,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"DeleteExecutionMessageIdToolCallId: %w", err,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertExecutionCompleted marks a call's execution completed, creating
|
||||
// the row if the call never had one. An existing row is completed
|
||||
// regardless of its status, so a pending or denied call is treated as
|
||||
// answered and Inference will not wait on or run it.
|
||||
const insertExecutionCompleted = `
|
||||
INSERT INTO executions (id, message_id, status, tool_call_id)
|
||||
VALUES (?, ?, 'completed', ?)
|
||||
ON CONFLICT (message_id, tool_call_id) DO UPDATE
|
||||
SET status = 'completed',
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||
`
|
||||
|
||||
// InsertExecutionCompleted creates or completes the execution for one tool
|
||||
// call within a message. See insertExecutionCompleted.
|
||||
func (q *Queries) InsertExecutionCompleted(
|
||||
ctx context.Context,
|
||||
messageId string,
|
||||
toolCallId string,
|
||||
) error {
|
||||
_, err := q.db.ExecContext(
|
||||
ctx,
|
||||
insertExecutionCompleted,
|
||||
uuid.New().String(),
|
||||
messageId,
|
||||
toolCallId,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("InsertExecutionCompleted: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user