Add conversation pkg
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -90,8 +90,8 @@ const (
|
|||||||
// appears in the OpenAI Chat Completions messages array.
|
// appears in the OpenAI Chat Completions messages array.
|
||||||
type Message struct {
|
type Message struct {
|
||||||
// Content is the model-visible message JSON, without inference
|
// Content is the model-visible message JSON, without inference
|
||||||
// metadata.
|
// metadata. The schema's check keeps it valid JSON.
|
||||||
Content string `json:"content"`
|
Content jsontext.Value `json:"content"`
|
||||||
// ConversationId identifies the conversation that owns this message
|
// ConversationId identifies the conversation that owns this message
|
||||||
// row.
|
// row.
|
||||||
ConversationId string `json:"conversation_id"`
|
ConversationId string `json:"conversation_id"`
|
||||||
|
|||||||
+354
-12
@@ -310,7 +310,8 @@ RETURNING content, conversation_id, created_at, id, json(metadata), role, sequen
|
|||||||
// InsertMessageParams holds arguments for InsertMessage.
|
// InsertMessageParams holds arguments for InsertMessage.
|
||||||
type InsertMessageParams struct {
|
type InsertMessageParams struct {
|
||||||
// Content is the model-visible message JSON saved in the new row.
|
// 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 identifies the conversation that owns the new row.
|
||||||
ConversationId string
|
ConversationId string
|
||||||
// Id is the new row's UUID. Nil generates a version 4 UUID.
|
// 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,
|
ctx,
|
||||||
insertMessage,
|
insertMessage,
|
||||||
id.String(),
|
id.String(),
|
||||||
arg.Content,
|
string(arg.Content),
|
||||||
arg.ConversationId,
|
arg.ConversationId,
|
||||||
metadataArg,
|
metadataArg,
|
||||||
arg.Role,
|
arg.Role,
|
||||||
@@ -349,13 +350,14 @@ func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (*
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
m Message
|
m Message
|
||||||
|
content []byte
|
||||||
metadata []byte
|
metadata []byte
|
||||||
createdAt string
|
createdAt string
|
||||||
updatedAt string
|
updatedAt string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if err := row.Scan(
|
if err := row.Scan(
|
||||||
&m.Content,
|
&content,
|
||||||
&m.ConversationId,
|
&m.ConversationId,
|
||||||
&createdAt,
|
&createdAt,
|
||||||
&m.Id,
|
&m.Id,
|
||||||
@@ -366,6 +368,7 @@ func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (*
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, fmt.Errorf("InsertMessage: %w", err)
|
return nil, fmt.Errorf("InsertMessage: %w", err)
|
||||||
}
|
}
|
||||||
|
m.Content = jsontext.Value(content)
|
||||||
m.Metadata = jsontext.Value(metadata)
|
m.Metadata = jsontext.Value(metadata)
|
||||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||||
return nil, fmt.Errorf("InsertMessage: parse created_at: %w", err)
|
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
|
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 = `
|
const selectMessagesConversationId = `
|
||||||
SELECT content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
SELECT content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
||||||
FROM messages
|
FROM messages
|
||||||
@@ -396,12 +451,13 @@ func (q *Queries) SelectMessagesConversationId(ctx context.Context, conversation
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var (
|
var (
|
||||||
m Message
|
m Message
|
||||||
|
content []byte
|
||||||
metadata []byte
|
metadata []byte
|
||||||
createdAt string
|
createdAt string
|
||||||
updatedAt string
|
updatedAt string
|
||||||
)
|
)
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&m.Content,
|
&content,
|
||||||
&m.ConversationId,
|
&m.ConversationId,
|
||||||
&createdAt,
|
&createdAt,
|
||||||
&m.Id,
|
&m.Id,
|
||||||
@@ -412,6 +468,7 @@ func (q *Queries) SelectMessagesConversationId(ctx context.Context, conversation
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, fmt.Errorf("SelectMessagesConversationId: %w", err)
|
return nil, fmt.Errorf("SelectMessagesConversationId: %w", err)
|
||||||
}
|
}
|
||||||
|
m.Content = jsontext.Value(content)
|
||||||
m.Metadata = jsontext.Value(metadata)
|
m.Metadata = jsontext.Value(metadata)
|
||||||
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
if m.CreatedAt, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||||
return nil, fmt.Errorf("SelectMessagesConversationId: parse created_at: %w", err)
|
return nil, fmt.Errorf("SelectMessagesConversationId: parse created_at: %w", err)
|
||||||
@@ -431,26 +488,54 @@ const updateMessageContent = `
|
|||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET content = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
SET content = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
|
RETURNING content, conversation_id, created_at, id, json(metadata), role, sequence, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
// UpdateMessageContentParams holds arguments for UpdateMessageContent.
|
// UpdateMessageContentParams holds arguments for UpdateMessageContent.
|
||||||
type UpdateMessageContentParams struct {
|
type UpdateMessageContentParams struct {
|
||||||
Content string
|
Content jsontext.Value
|
||||||
Id string
|
Id string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMessageContent sets the content JSON for a message.
|
// UpdateMessageContent sets the content JSON for a message and returns
|
||||||
func (q *Queries) UpdateMessageContent(ctx context.Context, arg UpdateMessageContentParams) error {
|
// the updated row.
|
||||||
_, err := q.db.ExecContext(
|
func (q *Queries) UpdateMessageContent(ctx context.Context, arg UpdateMessageContentParams) (*Message, error) {
|
||||||
|
row := q.db.QueryRowContext(
|
||||||
ctx,
|
ctx,
|
||||||
updateMessageContent,
|
updateMessageContent,
|
||||||
arg.Content,
|
string(arg.Content),
|
||||||
arg.Id,
|
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 = `
|
const deleteMessageId = `
|
||||||
@@ -467,6 +552,205 @@ func (q *Queries) DeleteMessageId(ctx context.Context, id string) error {
|
|||||||
return nil
|
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 = `
|
const selectMaxSequence = `
|
||||||
SELECT COALESCE(MAX(sequence), 0)
|
SELECT COALESCE(MAX(sequence), 0)
|
||||||
FROM messages
|
FROM messages
|
||||||
@@ -691,3 +975,61 @@ func (q *Queries) UpdateExecutionStatus(ctx context.Context, arg UpdateExecution
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
+156
-18
@@ -8,6 +8,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
"uuid"
|
"uuid"
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
@@ -33,6 +34,9 @@ func TestQueries(t *testing.T) {
|
|||||||
t.Run("Messaging", func(t *testing.T) {
|
t.Run("Messaging", func(t *testing.T) {
|
||||||
testMessaging(t, ctx, db)
|
testMessaging(t, ctx, db)
|
||||||
})
|
})
|
||||||
|
t.Run("SequenceShift", func(t *testing.T) {
|
||||||
|
testSequenceShift(t, ctx, db)
|
||||||
|
})
|
||||||
t.Run("ConversationTimestamp", func(t *testing.T) {
|
t.Run("ConversationTimestamp", func(t *testing.T) {
|
||||||
testConversationTimestamp(t, ctx, db)
|
testConversationTimestamp(t, ctx, db)
|
||||||
})
|
})
|
||||||
@@ -160,7 +164,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: seq,
|
Sequence: seq,
|
||||||
Role: MessageRoleSystem,
|
Role: MessageRoleSystem,
|
||||||
Content: `{"role":"system","content":"You are helpful"}`,
|
Content: jsontext.Value(`{"role":"system","content":"You are helpful"}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -175,7 +179,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: seq,
|
Sequence: seq,
|
||||||
Role: MessageRoleUser,
|
Role: MessageRoleUser,
|
||||||
Content: `{"role":"user","content":"Hello"}`,
|
Content: jsontext.Value(`{"role":"user","content":"Hello"}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -192,7 +196,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
Sequence: seq,
|
Sequence: seq,
|
||||||
Role: MessageRoleAssistant,
|
Role: MessageRoleAssistant,
|
||||||
Metadata: jsontext.Value(metadata),
|
Metadata: jsontext.Value(metadata),
|
||||||
Content: `{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"NYC\"}"}}]}`,
|
Content: jsontext.Value(`{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"NYC\"}"}}]}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -207,7 +211,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: seq,
|
Sequence: seq,
|
||||||
Role: MessageRoleTool,
|
Role: MessageRoleTool,
|
||||||
Content: `{"role":"tool","content":"{\"temperature\":72}","tool_call_id":"call_abc"}`,
|
Content: jsontext.Value(`{"role":"tool","content":"{\"temperature\":72}","tool_call_id":"call_abc"}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -238,17 +242,17 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
t.Errorf("message %d metadata = %v, want nil", i, message.Metadata)
|
t.Errorf("message %d metadata = %v, want nil", i, message.Metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
|
if _, err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
|
||||||
Id: messages[2].Id,
|
Id: messages[2].Id,
|
||||||
Content: `{"role":"assistant","content":"edited"}`,
|
Content: jsontext.Value(`{"role":"assistant","content":"edited"}`),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("UpdateMessageContent: %v", err)
|
t.Fatalf("UpdateMessageContent: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test UpdateMessageContent.
|
// Test UpdateMessageContent.
|
||||||
if err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
|
if _, err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
|
||||||
Id: toolMsg.Id,
|
Id: toolMsg.Id,
|
||||||
Content: `{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}`,
|
Content: jsontext.Value(`{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}`),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("UpdateMessageContent: %v", err)
|
t.Fatalf("UpdateMessageContent: %v", err)
|
||||||
}
|
}
|
||||||
@@ -260,7 +264,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
if updated[2].Metadata == nil || string(updated[2].Metadata) != metadata {
|
if updated[2].Metadata == nil || string(updated[2].Metadata) != metadata {
|
||||||
t.Errorf("edited assistant metadata = %v", updated[2].Metadata)
|
t.Errorf("edited assistant metadata = %v", updated[2].Metadata)
|
||||||
}
|
}
|
||||||
if updated[3].Content != `{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}` {
|
if string(updated[3].Content) != `{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}` {
|
||||||
t.Errorf("updated content mismatch: %s", updated[3].Content)
|
t.Errorf("updated content mismatch: %s", updated[3].Content)
|
||||||
}
|
}
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
@@ -283,7 +287,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
inserted, err := qw.InsertMessage(ctx, InsertMessageParams{
|
inserted, err := qw.InsertMessage(ctx, InsertMessageParams{
|
||||||
Content: `{"role":"assistant","content":"answer"}`,
|
Content: jsontext.Value(`{"role":"assistant","content":"answer"}`),
|
||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Metadata: tt.metadata,
|
Metadata: tt.metadata,
|
||||||
Role: MessageRoleAssistant,
|
Role: MessageRoleAssistant,
|
||||||
@@ -361,7 +365,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
inserted, err := qw.InsertMessage(ctx, InsertMessageParams{
|
inserted, err := qw.InsertMessage(ctx, InsertMessageParams{
|
||||||
Content: `{"role":"assistant","content":"answer"}`,
|
Content: jsontext.Value(`{"role":"assistant","content":"answer"}`),
|
||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Id: tt.id,
|
Id: tt.id,
|
||||||
Role: MessageRoleAssistant,
|
Role: MessageRoleAssistant,
|
||||||
@@ -397,7 +401,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err = New(tx).InsertMessage(ctx, InsertMessageParams{
|
_, err = New(tx).InsertMessage(ctx, InsertMessageParams{
|
||||||
Content: `{"role":"assistant","content":"rolled back"}`,
|
Content: jsontext.Value(`{"role":"assistant","content":"rolled back"}`),
|
||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Metadata: jsontext.Value(metadata),
|
Metadata: jsontext.Value(metadata),
|
||||||
Role: MessageRoleAssistant,
|
Role: MessageRoleAssistant,
|
||||||
@@ -416,6 +420,140 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSequenceShift(t *testing.T, ctx context.Context, db *SQLite) {
|
||||||
|
var (
|
||||||
|
qr = New(db.Reader)
|
||||||
|
qw = New(db.Writer)
|
||||||
|
)
|
||||||
|
|
||||||
|
u, err := qw.InsertUser(ctx, InsertUserParams{
|
||||||
|
Name: "sequence-shift",
|
||||||
|
Password: "test",
|
||||||
|
Role: UserRoleOwner,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InsertUser: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := qw.InsertConversation(ctx, InsertConversationParams{
|
||||||
|
UserId: u.Id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InsertConversation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
insert := func(text string) {
|
||||||
|
t.Helper()
|
||||||
|
seq, err := qw.NextSequence(ctx, c.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NextSequence: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := qw.InsertMessage(ctx, InsertMessageParams{
|
||||||
|
ConversationId: c.Id,
|
||||||
|
Sequence: seq,
|
||||||
|
Role: MessageRoleUser,
|
||||||
|
Content: jsontext.Value(`{"role":"user","content":"` + text + `"}`),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, text := range []string{"one", "two", "three", "four"} {
|
||||||
|
insert(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updated_at has millisecond precision, so sleep to put the
|
||||||
|
// shift in a later millisecond than the inserts.
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
before, err := qr.SelectMessagesConversationId(ctx, c.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectMessagesConversationId: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting "two" and shifting closes the gap: "three" moves to
|
||||||
|
// 2 and "four" to 3, while "one" stays put.
|
||||||
|
if err := qw.DeleteMessageId(ctx, before[1].Id); err != nil {
|
||||||
|
t.Fatalf("DeleteMessageId: %v", err)
|
||||||
|
}
|
||||||
|
if err := qw.UpdateMessagesSequence(ctx, c.Id, before[1].Sequence); err != nil {
|
||||||
|
t.Fatalf("UpdateMessagesSequence: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
moved, err := qr.SelectMessagesConversationId(ctx, c.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectMessagesConversationId: %v", err)
|
||||||
|
}
|
||||||
|
if len(moved) != 3 {
|
||||||
|
t.Fatalf("expected 3 messages, got %d", len(moved))
|
||||||
|
}
|
||||||
|
if moved[0].Sequence != 1 {
|
||||||
|
t.Errorf("unshifted row sequence = %d, want 1", moved[0].Sequence)
|
||||||
|
}
|
||||||
|
if !moved[0].UpdatedAt.Equal(before[0].UpdatedAt) {
|
||||||
|
t.Errorf("unshifted row updated_at = %s, want %s",
|
||||||
|
moved[0].UpdatedAt, before[0].UpdatedAt)
|
||||||
|
}
|
||||||
|
for i, wantSeq := range []int{2, 3} {
|
||||||
|
row, prev := moved[i+1], before[i+2]
|
||||||
|
if row.Id != prev.Id {
|
||||||
|
t.Fatalf("row %d id = %s, want %s", i+1, row.Id, prev.Id)
|
||||||
|
}
|
||||||
|
if row.Sequence != wantSeq {
|
||||||
|
t.Errorf("row %d sequence = %d, want %d",
|
||||||
|
i+1, row.Sequence, wantSeq)
|
||||||
|
}
|
||||||
|
if !row.UpdatedAt.After(prev.UpdatedAt) {
|
||||||
|
t.Errorf("row %d updated_at = %s, want later than %s",
|
||||||
|
i+1, row.UpdatedAt, prev.UpdatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free sequence 2 by incrementing rows 2 and 3, inside a
|
||||||
|
// transaction as the doc requires.
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
tx, err := db.Writer.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BeginTx: %v", err)
|
||||||
|
}
|
||||||
|
if err := New(tx).UpdateMessagesSequenceIncrement(ctx, c.Id, 2); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
t.Fatalf("UpdateMessagesSequenceIncrement: %v", err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
t.Fatalf("Commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
after, err := qr.SelectMessagesConversationId(ctx, c.Id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectMessagesConversationId: %v", err)
|
||||||
|
}
|
||||||
|
if len(after) != 3 {
|
||||||
|
t.Fatalf("expected 3 messages, got %d", len(after))
|
||||||
|
}
|
||||||
|
if after[0].Sequence != 1 {
|
||||||
|
t.Errorf("unshifted row sequence = %d, want 1", after[0].Sequence)
|
||||||
|
}
|
||||||
|
if !after[0].UpdatedAt.Equal(before[0].UpdatedAt) {
|
||||||
|
t.Errorf("unshifted row updated_at = %s, want %s",
|
||||||
|
after[0].UpdatedAt, before[0].UpdatedAt)
|
||||||
|
}
|
||||||
|
for i, wantSeq := range []int{3, 4} {
|
||||||
|
row, prev := after[i+1], moved[i+1]
|
||||||
|
if row.Id != prev.Id {
|
||||||
|
t.Fatalf("row %d id = %s, want %s", i+1, row.Id, prev.Id)
|
||||||
|
}
|
||||||
|
if row.Sequence != wantSeq {
|
||||||
|
t.Errorf("row %d sequence = %d, want %d",
|
||||||
|
i+1, row.Sequence, wantSeq)
|
||||||
|
}
|
||||||
|
if !row.UpdatedAt.After(prev.UpdatedAt) {
|
||||||
|
t.Errorf("row %d updated_at = %s, want later than %s",
|
||||||
|
i+1, row.UpdatedAt, prev.UpdatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLite) {
|
func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLite) {
|
||||||
var (
|
var (
|
||||||
qr = New(database.Reader)
|
qr = New(database.Reader)
|
||||||
@@ -448,7 +586,7 @@ func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLi
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: 0,
|
Sequence: 0,
|
||||||
Role: MessageRoleUser,
|
Role: MessageRoleUser,
|
||||||
Content: `{"role":"user","content":"Hello"}`,
|
Content: jsontext.Value(`{"role":"user","content":"Hello"}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -469,9 +607,9 @@ func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLi
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
t.Fatalf("set conversation timestamp before update: %v", err)
|
t.Fatalf("set conversation timestamp before update: %v", err)
|
||||||
}
|
}
|
||||||
if err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
|
if _, err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
|
||||||
Id: msg.Id,
|
Id: msg.Id,
|
||||||
Content: `{"role":"user","content":"Updated"}`,
|
Content: jsontext.Value(`{"role":"user","content":"Updated"}`),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("UpdateMessageContent: %v", err)
|
t.Fatalf("UpdateMessageContent: %v", err)
|
||||||
}
|
}
|
||||||
@@ -529,7 +667,7 @@ func testDelete(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: 1,
|
Sequence: 1,
|
||||||
Role: MessageRoleUser,
|
Role: MessageRoleUser,
|
||||||
Content: `{"role":"user","content":"Hello"}`,
|
Content: jsontext.Value(`{"role":"user","content":"Hello"}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -585,7 +723,7 @@ func testExecutions(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: 1,
|
Sequence: 1,
|
||||||
Role: MessageRoleAssistant,
|
Role: MessageRoleAssistant,
|
||||||
Content: `{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"host_run","arguments":"{\"command\":\"rm -rf /\"}"}}]}`,
|
Content: jsontext.Value(`{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"host_run","arguments":"{\"command\":\"rm -rf /\"}"}}]}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage: %v", err)
|
t.Fatalf("InsertMessage: %v", err)
|
||||||
@@ -721,7 +859,7 @@ func testExecutions(t *testing.T, ctx context.Context, db *SQLite) {
|
|||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Sequence: 2,
|
Sequence: 2,
|
||||||
Role: MessageRoleAssistant,
|
Role: MessageRoleAssistant,
|
||||||
Content: `{"role":"assistant","content":"second"}`,
|
Content: jsontext.Value(`{"role":"assistant","content":"second"}`),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("InsertMessage second: %v", err)
|
t.Fatalf("InsertMessage second: %v", err)
|
||||||
|
|||||||
@@ -53,15 +53,16 @@ CREATE TABLE IF NOT EXISTS conversations (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_conv_user ON conversations(user_id);
|
CREATE INDEX IF NOT EXISTS idx_conv_user ON conversations(user_id);
|
||||||
|
|
||||||
-- Messages are ordered entries within a conversation. The content column
|
-- Messages are ordered entries within a conversation. The content column
|
||||||
-- stores the wire-format JSON for the message — the exact JSON object
|
-- stores the wire-format JSON for the message — the exact JSON object that
|
||||||
-- that appears in the OpenAI Chat Completions messages array. The role
|
-- appears in the OpenAI Chat Completions messages array; a CHECK constraint
|
||||||
-- column is extracted for querying without JSON parsing. Nullable metadata
|
-- keeps it valid JSON. The role column is extracted for querying without
|
||||||
-- stores inference JSONB separately from model-visible content. The sequence
|
-- JSON parsing. Nullable metadata stores inference JSONB separately from
|
||||||
-- column defines ordering within the conversation. Deleting a message
|
-- model-visible content. The sequence column defines ordering within the
|
||||||
-- cascades to nothing further (all message data is in the row).
|
-- conversation. Deleting a message cascades to nothing further (all message
|
||||||
|
-- data is in the row).
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL CHECK (json_valid(content)),
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
metadata BLOB CHECK (metadata IS NULL OR json_valid(metadata, 8)),
|
metadata BLOB CHECK (metadata IS NULL OR json_valid(metadata, 8)),
|
||||||
|
|||||||
+2
-2
@@ -352,7 +352,7 @@ func (j *Job) createConversation(
|
|||||||
return nil, fmt.Errorf("marshal initial message: %w", err)
|
return nil, fmt.Errorf("marshal initial message: %w", err)
|
||||||
}
|
}
|
||||||
if _, err := q.InsertMessage(ctx, db.InsertMessageParams{
|
if _, err := q.InsertMessage(ctx, db.InsertMessageParams{
|
||||||
Content: string(content),
|
Content: content,
|
||||||
ConversationId: c.Id,
|
ConversationId: c.Id,
|
||||||
Role: db.MessageRole(message.Role),
|
Role: db.MessageRole(message.Role),
|
||||||
Sequence: sequence,
|
Sequence: sequence,
|
||||||
@@ -411,7 +411,7 @@ func (j *Job) insertMessage(
|
|||||||
return nil, fmt.Errorf("get next message sequence: %w", err)
|
return nil, fmt.Errorf("get next message sequence: %w", err)
|
||||||
}
|
}
|
||||||
m, err := q.InsertMessage(ctx, db.InsertMessageParams{
|
m, err := q.InsertMessage(ctx, db.InsertMessageParams{
|
||||||
Content: string(content),
|
Content: content,
|
||||||
ConversationId: cid,
|
ConversationId: cid,
|
||||||
Metadata: metadataJSON,
|
Metadata: metadataJSON,
|
||||||
Role: db.MessageRole(message.Role),
|
Role: db.MessageRole(message.Role),
|
||||||
|
|||||||
@@ -685,7 +685,7 @@ func TestCallTool(t *testing.T) {
|
|||||||
t.Errorf("tool metadata = %v, want nil", last.Metadata)
|
t.Errorf("tool metadata = %v, want nil", last.Metadata)
|
||||||
}
|
}
|
||||||
if last.Role != db.MessageRoleTool ||
|
if last.Role != db.MessageRoleTool ||
|
||||||
last.Content != string(want) {
|
string(last.Content) != string(want) {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"last stored message = role %q, %s; "+
|
"last stored message = role %q, %s; "+
|
||||||
"want %q, %s",
|
"want %q, %s",
|
||||||
|
|||||||
@@ -548,6 +548,37 @@ func (m *Message) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateToolCall replaces the call identified by callId, keeping its
|
||||||
|
// position in ToolCalls. The replacement must have the same ID: the
|
||||||
|
// corresponding tool response carries it in Message.ToolCallID.
|
||||||
|
func (m *Message) UpdateToolCall(call ToolCall) error {
|
||||||
|
if err := call.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid tool call: %w", err)
|
||||||
|
}
|
||||||
|
for i := range m.ToolCalls {
|
||||||
|
if m.ToolCalls[i].ID == call.ID {
|
||||||
|
m.ToolCalls[i] = call
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("not found: call %q", call.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToolCall appends call to the message's ToolCalls. The call must be
|
||||||
|
// valid and its ID must not already appear in the message.
|
||||||
|
func (m *Message) AddToolCall(call ToolCall) error {
|
||||||
|
if err := call.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid tool call: %w", err)
|
||||||
|
}
|
||||||
|
for _, existing := range m.ToolCalls {
|
||||||
|
if existing.ID == call.ID {
|
||||||
|
return fmt.Errorf("duplicate call id %q", call.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.ToolCalls = append(m.ToolCalls, call)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalJSON decodes a message while normalizing a string-valued content
|
// UnmarshalJSON decodes a message while normalizing a string-valued content
|
||||||
// field to one text ContentPart. An array-valued content field is decoded in
|
// field to one text ContentPart. An array-valued content field is decoded in
|
||||||
// order. If reasoning_content is absent, reasoning is copied into
|
// order. If reasoning_content is absent, reasoning is copied into
|
||||||
|
|||||||
Reference in New Issue
Block a user