Build image / build-and-push (push) Successful in 6s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
96 lines
3.5 KiB
Go
96 lines
3.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
|
)
|
|
|
|
// agentMessageColumns lists agent_messages columns in scan order.
|
|
const agentMessageColumns = `id, conversation_id, role, body, change_set_id, created_at`
|
|
|
|
// EnsureConversation returns the (garden, user) conversation id, creating it on
|
|
// first use. One thread per person per garden: two people editing a shared
|
|
// garden hold separate dialogues, and merging them would put words in someone's
|
|
// mouth.
|
|
func (d *DB) EnsureConversation(ctx context.Context, gardenID, userID int64) (int64, error) {
|
|
var id int64
|
|
err := d.sql.QueryRowContext(ctx,
|
|
`SELECT id FROM agent_conversations WHERE garden_id = ? AND user_id = ?`,
|
|
gardenID, userID).Scan(&id)
|
|
if err == nil {
|
|
return id, nil
|
|
}
|
|
if err := d.sql.QueryRowContext(ctx,
|
|
`INSERT INTO agent_conversations (garden_id, user_id) VALUES (?, ?)
|
|
ON CONFLICT (garden_id, user_id) DO UPDATE SET updated_at = updated_at
|
|
RETURNING id`,
|
|
gardenID, userID).Scan(&id); err != nil {
|
|
return 0, fmt.Errorf("store: ensure conversation: %w", err)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// AppendAgentMessage records one side of an exchange.
|
|
func (d *DB) AppendAgentMessage(ctx context.Context, m *domain.AgentMessage) (*domain.AgentMessage, error) {
|
|
var out domain.AgentMessage
|
|
if err := d.sql.QueryRowContext(ctx,
|
|
`INSERT INTO agent_messages (conversation_id, role, body, change_set_id)
|
|
VALUES (?, ?, ?, ?)
|
|
RETURNING `+agentMessageColumns,
|
|
m.ConversationID, m.Role, m.Body, m.ChangeSetID,
|
|
).Scan(&out.ID, &out.ConversationID, &out.Role, &out.Body, &out.ChangeSetID, &out.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("store: append agent message: %w", err)
|
|
}
|
|
// Touch the conversation so a "most recent thread" read is possible later.
|
|
if _, err := d.sql.ExecContext(ctx,
|
|
`UPDATE agent_conversations SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?`,
|
|
m.ConversationID); err != nil {
|
|
return nil, fmt.Errorf("store: touch conversation: %w", err)
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// ListAgentMessages returns the last `limit` messages of a conversation in
|
|
// chronological order.
|
|
//
|
|
// Reading the TAIL rather than the head is the point: an old opening exchange is
|
|
// the least useful context for "now do the same to the north bed", and an
|
|
// uncapped read would grow without bound over a season.
|
|
func (d *DB) ListAgentMessages(ctx context.Context, conversationID int64, limit int) ([]domain.AgentMessage, error) {
|
|
rows, err := d.sql.QueryContext(ctx,
|
|
`SELECT `+agentMessageColumns+` FROM (
|
|
SELECT `+agentMessageColumns+` FROM agent_messages
|
|
WHERE conversation_id = ? ORDER BY id DESC LIMIT ?
|
|
) ORDER BY id`,
|
|
conversationID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store: list agent messages: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
msgs := []domain.AgentMessage{}
|
|
for rows.Next() {
|
|
var m domain.AgentMessage
|
|
if err := rows.Scan(&m.ID, &m.ConversationID, &m.Role, &m.Body, &m.ChangeSetID, &m.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("store: scan agent message: %w", err)
|
|
}
|
|
msgs = append(msgs, m)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("store: iterate agent messages: %w", err)
|
|
}
|
|
return msgs, nil
|
|
}
|
|
|
|
// DeleteConversation clears a thread (the messages cascade).
|
|
func (d *DB) DeleteConversation(ctx context.Context, gardenID, userID int64) error {
|
|
if _, err := d.sql.ExecContext(ctx,
|
|
`DELETE FROM agent_conversations WHERE garden_id = ? AND user_id = ?`,
|
|
gardenID, userID); err != nil {
|
|
return fmt.Errorf("store: delete conversation: %w", err)
|
|
}
|
|
return nil
|
|
}
|