diff --git a/internal/chat/queries/attachments.sql b/internal/chat/queries/attachments.sql new file mode 100644 index 0000000..511b985 --- /dev/null +++ b/internal/chat/queries/attachments.sql @@ -0,0 +1,32 @@ +-- name: InsertAttachment :one +INSERT INTO message_attachments (id, user_id, message_id, filename, mime_type, size_bytes, sha256, storage_path) +VALUES ($1, $2, NULL, $3, $4, $5, $6, $7) +RETURNING *; + +-- name: GetAttachment :one +SELECT * FROM message_attachments WHERE id = $1; + +-- name: ListAttachmentsForMessage :many +SELECT * FROM message_attachments WHERE message_id = $1 ORDER BY created_at ASC; + +-- name: ListOrphanAttachmentsForUser :many +SELECT * FROM message_attachments +WHERE user_id = $1 AND message_id IS NULL AND id = ANY($2::uuid[]); + +-- name: AttachToMessage :exec +UPDATE message_attachments SET message_id = $2 WHERE id = ANY($1::uuid[]); + +-- name: DeleteAttachment :exec +DELETE FROM message_attachments WHERE id = $1; + +-- name: ListExpiredOrphans :many +SELECT id, storage_path FROM message_attachments +WHERE message_id IS NULL AND created_at < NOW() - ($1::interval); + +-- name: ListAttachmentsForSoftDeletedConversations :many +SELECT a.id, a.storage_path +FROM message_attachments a +JOIN messages m ON m.id = a.message_id +JOIN conversations c ON c.id = m.conversation_id +WHERE c.deleted_at IS NOT NULL + AND c.deleted_at < NOW() - ($1::interval); diff --git a/internal/chat/queries/conversations.sql b/internal/chat/queries/conversations.sql new file mode 100644 index 0000000..25b4f02 --- /dev/null +++ b/internal/chat/queries/conversations.sql @@ -0,0 +1,28 @@ +-- name: InsertConversation :one +INSERT INTO conversations (id, user_id, project_id, workspace_id, issue_id, model_id, system_prompt, title) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING *; + +-- name: GetConversation :one +SELECT * FROM conversations WHERE id = $1 AND deleted_at IS NULL; + +-- name: ListConversationsByUser :many +SELECT * FROM conversations +WHERE user_id = $1 AND deleted_at IS NULL + AND ($2::uuid IS NULL OR project_id = $2) + AND ($3::uuid IS NULL OR workspace_id = $3) + AND ($4::uuid IS NULL OR issue_id = $4) + AND ($5::text = '' OR title ILIKE '%' || $5 || '%') +ORDER BY updated_at DESC +LIMIT $6; + +-- name: UpdateConversationTitle :exec +UPDATE conversations SET title = $2, title_generated_at = NOW(), updated_at = NOW() +WHERE id = $1; + +-- name: SetConversationTitleManual :exec +UPDATE conversations SET title = $2, updated_at = NOW() WHERE id = $1; + +-- name: SoftDeleteConversation :exec +UPDATE conversations SET deleted_at = NOW(), updated_at = NOW() +WHERE id = $1 AND deleted_at IS NULL; diff --git a/internal/chat/queries/endpoints.sql b/internal/chat/queries/endpoints.sql new file mode 100644 index 0000000..dc59406 --- /dev/null +++ b/internal/chat/queries/endpoints.sql @@ -0,0 +1,21 @@ +-- name: InsertLLMEndpoint :one +INSERT INTO llm_endpoints (id, provider, display_name, base_url, api_key_encrypted, created_by) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; + +-- name: GetLLMEndpoint :one +SELECT * FROM llm_endpoints WHERE id = $1; + +-- name: ListLLMEndpoints :many +SELECT * FROM llm_endpoints ORDER BY created_at DESC; + +-- name: UpdateLLMEndpoint :exec +UPDATE llm_endpoints +SET display_name = COALESCE($2, display_name), + base_url = COALESCE($3, base_url), + api_key_encrypted = COALESCE($4, api_key_encrypted), + updated_at = NOW() +WHERE id = $1; + +-- name: DeleteLLMEndpoint :exec +DELETE FROM llm_endpoints WHERE id = $1; diff --git a/internal/chat/queries/messages.sql b/internal/chat/queries/messages.sql new file mode 100644 index 0000000..c2ba993 --- /dev/null +++ b/internal/chat/queries/messages.sql @@ -0,0 +1,47 @@ +-- name: InsertMessage :one +INSERT INTO messages (conversation_id, role, content, status) +VALUES ($1, $2, $3, $4) +RETURNING *; + +-- name: GetMessage :one +SELECT * FROM messages WHERE id = $1; + +-- name: ListMessagesByConversation :many +SELECT * FROM messages +WHERE conversation_id = $1 + AND ($2::bigint = 0 OR id < $2) +ORDER BY id DESC +LIMIT $3; + +-- name: ListMessagesAscending :many +SELECT * FROM messages WHERE conversation_id = $1 ORDER BY id ASC; + +-- name: UpdateMessageOK :exec +UPDATE messages +SET content = $2, thinking = $3, + prompt_tokens = $4, completion_tokens = $5, thinking_tokens = $6, + status = 'ok', updated_at = NOW() +WHERE id = $1; + +-- name: UpdateMessageError :exec +UPDATE messages +SET status = 'error', error_message = $2, updated_at = NOW() +WHERE id = $1; + +-- name: UpdateMessageCancelled :exec +UPDATE messages +SET status = 'cancelled', updated_at = NOW() +WHERE id = $1; + +-- name: DeleteMessage :exec +DELETE FROM messages WHERE id = $1; + +-- name: GetFirstUserMessage :one +SELECT * FROM messages +WHERE conversation_id = $1 AND role = 'user' +ORDER BY id ASC LIMIT 1; + +-- name: MarkPendingAsErrorOnStartup :exec +UPDATE messages +SET status = 'error', error_message = 'server restart', updated_at = NOW() +WHERE status = 'pending'; diff --git a/internal/chat/queries/models.sql b/internal/chat/queries/models.sql new file mode 100644 index 0000000..780802a --- /dev/null +++ b/internal/chat/queries/models.sql @@ -0,0 +1,37 @@ +-- name: InsertLLMModel :one +INSERT INTO llm_models ( + id, endpoint_id, model_id, display_name, capabilities, + context_window, max_output_tokens, + prompt_price_per_million_usd, completion_price_per_million_usd, thinking_price_per_million_usd, + is_title_generator, enabled, sort_order +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) +RETURNING *; + +-- name: GetLLMModel :one +SELECT * FROM llm_models WHERE id = $1; + +-- name: GetTitleGeneratorModel :one +SELECT * FROM llm_models WHERE is_title_generator = true AND enabled = true LIMIT 1; + +-- name: ListLLMModels :many +SELECT * FROM llm_models +WHERE ($1::boolean = false) OR enabled = true +ORDER BY sort_order, display_name; + +-- name: UpdateLLMModel :exec +UPDATE llm_models +SET display_name = COALESCE($2, display_name), + capabilities = COALESCE($3, capabilities), + context_window = COALESCE($4, context_window), + max_output_tokens = COALESCE($5, max_output_tokens), + prompt_price_per_million_usd = COALESCE($6, prompt_price_per_million_usd), + completion_price_per_million_usd = COALESCE($7, completion_price_per_million_usd), + thinking_price_per_million_usd = COALESCE($8, thinking_price_per_million_usd), + is_title_generator = COALESCE($9, is_title_generator), + enabled = COALESCE($10, enabled), + sort_order = COALESCE($11, sort_order), + updated_at = NOW() +WHERE id = $1; + +-- name: DeleteLLMModel :exec +DELETE FROM llm_models WHERE id = $1; diff --git a/internal/chat/queries/templates.sql b/internal/chat/queries/templates.sql new file mode 100644 index 0000000..599502f --- /dev/null +++ b/internal/chat/queries/templates.sql @@ -0,0 +1,19 @@ +-- name: InsertPromptTemplate :one +INSERT INTO prompt_templates (id, name, content, scope, owner_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING *; + +-- name: GetPromptTemplate :one +SELECT * FROM prompt_templates WHERE id = $1; + +-- name: ListPromptTemplatesForUser :many +SELECT * FROM prompt_templates +WHERE scope = 'system' OR (scope = 'user' AND owner_id = $1) +ORDER BY scope, name; + +-- name: UpdatePromptTemplate :exec +UPDATE prompt_templates SET name = $2, content = $3, updated_at = NOW() +WHERE id = $1; + +-- name: DeletePromptTemplate :exec +DELETE FROM prompt_templates WHERE id = $1; diff --git a/internal/chat/queries/usage.sql b/internal/chat/queries/usage.sql new file mode 100644 index 0000000..778a6d0 --- /dev/null +++ b/internal/chat/queries/usage.sql @@ -0,0 +1,49 @@ +-- name: InsertUsage :exec +INSERT INTO llm_usage ( + conversation_id, message_id, user_id, endpoint_id, model_id, + prompt_tokens, completion_tokens, thinking_tokens, cost_usd +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9); + +-- name: SummarizeUsageByUser :many +SELECT user_id, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE created_at >= $1 AND created_at < $2 +GROUP BY user_id +ORDER BY cost_usd DESC; + +-- name: SummarizeUsageByModel :many +SELECT model_id, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE created_at >= $1 AND created_at < $2 +GROUP BY model_id +ORDER BY cost_usd DESC; + +-- name: SummarizeUsageByDay :many +SELECT date_trunc('day', created_at) AS day, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE created_at >= $1 AND created_at < $2 +GROUP BY day +ORDER BY day; + +-- name: SummarizeUserDaily :many +SELECT date_trunc('day', created_at) AS day, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE user_id = $1 AND created_at >= $2 AND created_at < $3 +GROUP BY day +ORDER BY day; diff --git a/internal/chat/sqlc/attachments.sql.go b/internal/chat/sqlc/attachments.sql.go new file mode 100644 index 0000000..ba2df1f --- /dev/null +++ b/internal/chat/sqlc/attachments.sql.go @@ -0,0 +1,235 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: attachments.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const attachToMessage = `-- name: AttachToMessage :exec +UPDATE message_attachments SET message_id = $2 WHERE id = ANY($1::uuid[]) +` + +type AttachToMessageParams struct { + Column1 []pgtype.UUID `json:"column_1"` + MessageID *int64 `json:"message_id"` +} + +func (q *Queries) AttachToMessage(ctx context.Context, arg AttachToMessageParams) error { + _, err := q.db.Exec(ctx, attachToMessage, arg.Column1, arg.MessageID) + return err +} + +const deleteAttachment = `-- name: DeleteAttachment :exec +DELETE FROM message_attachments WHERE id = $1 +` + +func (q *Queries) DeleteAttachment(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteAttachment, id) + return err +} + +const getAttachment = `-- name: GetAttachment :one +SELECT id, user_id, message_id, filename, mime_type, size_bytes, sha256, storage_path, created_at FROM message_attachments WHERE id = $1 +` + +func (q *Queries) GetAttachment(ctx context.Context, id pgtype.UUID) (MessageAttachment, error) { + row := q.db.QueryRow(ctx, getAttachment, id) + var i MessageAttachment + err := row.Scan( + &i.ID, + &i.UserID, + &i.MessageID, + &i.Filename, + &i.MimeType, + &i.SizeBytes, + &i.Sha256, + &i.StoragePath, + &i.CreatedAt, + ) + return i, err +} + +const insertAttachment = `-- name: InsertAttachment :one +INSERT INTO message_attachments (id, user_id, message_id, filename, mime_type, size_bytes, sha256, storage_path) +VALUES ($1, $2, NULL, $3, $4, $5, $6, $7) +RETURNING id, user_id, message_id, filename, mime_type, size_bytes, sha256, storage_path, created_at +` + +type InsertAttachmentParams struct { + ID pgtype.UUID `json:"id"` + UserID pgtype.UUID `json:"user_id"` + Filename string `json:"filename"` + MimeType string `json:"mime_type"` + SizeBytes int64 `json:"size_bytes"` + Sha256 string `json:"sha256"` + StoragePath string `json:"storage_path"` +} + +func (q *Queries) InsertAttachment(ctx context.Context, arg InsertAttachmentParams) (MessageAttachment, error) { + row := q.db.QueryRow(ctx, insertAttachment, + arg.ID, + arg.UserID, + arg.Filename, + arg.MimeType, + arg.SizeBytes, + arg.Sha256, + arg.StoragePath, + ) + var i MessageAttachment + err := row.Scan( + &i.ID, + &i.UserID, + &i.MessageID, + &i.Filename, + &i.MimeType, + &i.SizeBytes, + &i.Sha256, + &i.StoragePath, + &i.CreatedAt, + ) + return i, err +} + +const listAttachmentsForMessage = `-- name: ListAttachmentsForMessage :many +SELECT id, user_id, message_id, filename, mime_type, size_bytes, sha256, storage_path, created_at FROM message_attachments WHERE message_id = $1 ORDER BY created_at ASC +` + +func (q *Queries) ListAttachmentsForMessage(ctx context.Context, messageID *int64) ([]MessageAttachment, error) { + rows, err := q.db.Query(ctx, listAttachmentsForMessage, messageID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MessageAttachment + for rows.Next() { + var i MessageAttachment + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.MessageID, + &i.Filename, + &i.MimeType, + &i.SizeBytes, + &i.Sha256, + &i.StoragePath, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAttachmentsForSoftDeletedConversations = `-- name: ListAttachmentsForSoftDeletedConversations :many +SELECT a.id, a.storage_path +FROM message_attachments a +JOIN messages m ON m.id = a.message_id +JOIN conversations c ON c.id = m.conversation_id +WHERE c.deleted_at IS NOT NULL + AND c.deleted_at < NOW() - ($1::interval) +` + +type ListAttachmentsForSoftDeletedConversationsRow struct { + ID pgtype.UUID `json:"id"` + StoragePath string `json:"storage_path"` +} + +func (q *Queries) ListAttachmentsForSoftDeletedConversations(ctx context.Context, dollar_1 pgtype.Interval) ([]ListAttachmentsForSoftDeletedConversationsRow, error) { + rows, err := q.db.Query(ctx, listAttachmentsForSoftDeletedConversations, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAttachmentsForSoftDeletedConversationsRow + for rows.Next() { + var i ListAttachmentsForSoftDeletedConversationsRow + if err := rows.Scan(&i.ID, &i.StoragePath); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listExpiredOrphans = `-- name: ListExpiredOrphans :many +SELECT id, storage_path FROM message_attachments +WHERE message_id IS NULL AND created_at < NOW() - ($1::interval) +` + +type ListExpiredOrphansRow struct { + ID pgtype.UUID `json:"id"` + StoragePath string `json:"storage_path"` +} + +func (q *Queries) ListExpiredOrphans(ctx context.Context, dollar_1 pgtype.Interval) ([]ListExpiredOrphansRow, error) { + rows, err := q.db.Query(ctx, listExpiredOrphans, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListExpiredOrphansRow + for rows.Next() { + var i ListExpiredOrphansRow + if err := rows.Scan(&i.ID, &i.StoragePath); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listOrphanAttachmentsForUser = `-- name: ListOrphanAttachmentsForUser :many +SELECT id, user_id, message_id, filename, mime_type, size_bytes, sha256, storage_path, created_at FROM message_attachments +WHERE user_id = $1 AND message_id IS NULL AND id = ANY($2::uuid[]) +` + +type ListOrphanAttachmentsForUserParams struct { + UserID pgtype.UUID `json:"user_id"` + Column2 []pgtype.UUID `json:"column_2"` +} + +func (q *Queries) ListOrphanAttachmentsForUser(ctx context.Context, arg ListOrphanAttachmentsForUserParams) ([]MessageAttachment, error) { + rows, err := q.db.Query(ctx, listOrphanAttachmentsForUser, arg.UserID, arg.Column2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []MessageAttachment + for rows.Next() { + var i MessageAttachment + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.MessageID, + &i.Filename, + &i.MimeType, + &i.SizeBytes, + &i.Sha256, + &i.StoragePath, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/chat/sqlc/conversations.sql.go b/internal/chat/sqlc/conversations.sql.go new file mode 100644 index 0000000..dcc481c --- /dev/null +++ b/internal/chat/sqlc/conversations.sql.go @@ -0,0 +1,181 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: conversations.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getConversation = `-- name: GetConversation :one +SELECT id, user_id, project_id, workspace_id, issue_id, model_id, system_prompt, title, title_generated_at, created_at, updated_at, deleted_at FROM conversations WHERE id = $1 AND deleted_at IS NULL +` + +func (q *Queries) GetConversation(ctx context.Context, id pgtype.UUID) (Conversation, error) { + row := q.db.QueryRow(ctx, getConversation, id) + var i Conversation + err := row.Scan( + &i.ID, + &i.UserID, + &i.ProjectID, + &i.WorkspaceID, + &i.IssueID, + &i.ModelID, + &i.SystemPrompt, + &i.Title, + &i.TitleGeneratedAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const insertConversation = `-- name: InsertConversation :one +INSERT INTO conversations (id, user_id, project_id, workspace_id, issue_id, model_id, system_prompt, title) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING id, user_id, project_id, workspace_id, issue_id, model_id, system_prompt, title, title_generated_at, created_at, updated_at, deleted_at +` + +type InsertConversationParams struct { + ID pgtype.UUID `json:"id"` + UserID pgtype.UUID `json:"user_id"` + ProjectID pgtype.UUID `json:"project_id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + IssueID pgtype.UUID `json:"issue_id"` + ModelID pgtype.UUID `json:"model_id"` + SystemPrompt string `json:"system_prompt"` + Title *string `json:"title"` +} + +func (q *Queries) InsertConversation(ctx context.Context, arg InsertConversationParams) (Conversation, error) { + row := q.db.QueryRow(ctx, insertConversation, + arg.ID, + arg.UserID, + arg.ProjectID, + arg.WorkspaceID, + arg.IssueID, + arg.ModelID, + arg.SystemPrompt, + arg.Title, + ) + var i Conversation + err := row.Scan( + &i.ID, + &i.UserID, + &i.ProjectID, + &i.WorkspaceID, + &i.IssueID, + &i.ModelID, + &i.SystemPrompt, + &i.Title, + &i.TitleGeneratedAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const listConversationsByUser = `-- name: ListConversationsByUser :many +SELECT id, user_id, project_id, workspace_id, issue_id, model_id, system_prompt, title, title_generated_at, created_at, updated_at, deleted_at FROM conversations +WHERE user_id = $1 AND deleted_at IS NULL + AND ($2::uuid IS NULL OR project_id = $2) + AND ($3::uuid IS NULL OR workspace_id = $3) + AND ($4::uuid IS NULL OR issue_id = $4) + AND ($5::text = '' OR title ILIKE '%' || $5 || '%') +ORDER BY updated_at DESC +LIMIT $6 +` + +type ListConversationsByUserParams struct { + UserID pgtype.UUID `json:"user_id"` + Column2 pgtype.UUID `json:"column_2"` + Column3 pgtype.UUID `json:"column_3"` + Column4 pgtype.UUID `json:"column_4"` + Column5 string `json:"column_5"` + Limit int32 `json:"limit"` +} + +func (q *Queries) ListConversationsByUser(ctx context.Context, arg ListConversationsByUserParams) ([]Conversation, error) { + rows, err := q.db.Query(ctx, listConversationsByUser, + arg.UserID, + arg.Column2, + arg.Column3, + arg.Column4, + arg.Column5, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Conversation + for rows.Next() { + var i Conversation + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.ProjectID, + &i.WorkspaceID, + &i.IssueID, + &i.ModelID, + &i.SystemPrompt, + &i.Title, + &i.TitleGeneratedAt, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setConversationTitleManual = `-- name: SetConversationTitleManual :exec +UPDATE conversations SET title = $2, updated_at = NOW() WHERE id = $1 +` + +type SetConversationTitleManualParams struct { + ID pgtype.UUID `json:"id"` + Title *string `json:"title"` +} + +func (q *Queries) SetConversationTitleManual(ctx context.Context, arg SetConversationTitleManualParams) error { + _, err := q.db.Exec(ctx, setConversationTitleManual, arg.ID, arg.Title) + return err +} + +const softDeleteConversation = `-- name: SoftDeleteConversation :exec +UPDATE conversations SET deleted_at = NOW(), updated_at = NOW() +WHERE id = $1 AND deleted_at IS NULL +` + +func (q *Queries) SoftDeleteConversation(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, softDeleteConversation, id) + return err +} + +const updateConversationTitle = `-- name: UpdateConversationTitle :exec +UPDATE conversations SET title = $2, title_generated_at = NOW(), updated_at = NOW() +WHERE id = $1 +` + +type UpdateConversationTitleParams struct { + ID pgtype.UUID `json:"id"` + Title *string `json:"title"` +} + +func (q *Queries) UpdateConversationTitle(ctx context.Context, arg UpdateConversationTitleParams) error { + _, err := q.db.Exec(ctx, updateConversationTitle, arg.ID, arg.Title) + return err +} diff --git a/internal/chat/sqlc/db.go b/internal/chat/sqlc/db.go new file mode 100644 index 0000000..1ff8a35 --- /dev/null +++ b/internal/chat/sqlc/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/chat/sqlc/endpoints.sql.go b/internal/chat/sqlc/endpoints.sql.go new file mode 100644 index 0000000..3603add --- /dev/null +++ b/internal/chat/sqlc/endpoints.sql.go @@ -0,0 +1,138 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: endpoints.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteLLMEndpoint = `-- name: DeleteLLMEndpoint :exec +DELETE FROM llm_endpoints WHERE id = $1 +` + +func (q *Queries) DeleteLLMEndpoint(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteLLMEndpoint, id) + return err +} + +const getLLMEndpoint = `-- name: GetLLMEndpoint :one +SELECT id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at FROM llm_endpoints WHERE id = $1 +` + +func (q *Queries) GetLLMEndpoint(ctx context.Context, id pgtype.UUID) (LlmEndpoint, error) { + row := q.db.QueryRow(ctx, getLLMEndpoint, id) + var i LlmEndpoint + err := row.Scan( + &i.ID, + &i.Provider, + &i.DisplayName, + &i.BaseUrl, + &i.ApiKeyEncrypted, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const insertLLMEndpoint = `-- name: InsertLLMEndpoint :one +INSERT INTO llm_endpoints (id, provider, display_name, base_url, api_key_encrypted, created_by) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at +` + +type InsertLLMEndpointParams struct { + ID pgtype.UUID `json:"id"` + Provider string `json:"provider"` + DisplayName string `json:"display_name"` + BaseUrl string `json:"base_url"` + ApiKeyEncrypted []byte `json:"api_key_encrypted"` + CreatedBy pgtype.UUID `json:"created_by"` +} + +func (q *Queries) InsertLLMEndpoint(ctx context.Context, arg InsertLLMEndpointParams) (LlmEndpoint, error) { + row := q.db.QueryRow(ctx, insertLLMEndpoint, + arg.ID, + arg.Provider, + arg.DisplayName, + arg.BaseUrl, + arg.ApiKeyEncrypted, + arg.CreatedBy, + ) + var i LlmEndpoint + err := row.Scan( + &i.ID, + &i.Provider, + &i.DisplayName, + &i.BaseUrl, + &i.ApiKeyEncrypted, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listLLMEndpoints = `-- name: ListLLMEndpoints :many +SELECT id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at FROM llm_endpoints ORDER BY created_at DESC +` + +func (q *Queries) ListLLMEndpoints(ctx context.Context) ([]LlmEndpoint, error) { + rows, err := q.db.Query(ctx, listLLMEndpoints) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LlmEndpoint + for rows.Next() { + var i LlmEndpoint + if err := rows.Scan( + &i.ID, + &i.Provider, + &i.DisplayName, + &i.BaseUrl, + &i.ApiKeyEncrypted, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateLLMEndpoint = `-- name: UpdateLLMEndpoint :exec +UPDATE llm_endpoints +SET display_name = COALESCE($2, display_name), + base_url = COALESCE($3, base_url), + api_key_encrypted = COALESCE($4, api_key_encrypted), + updated_at = NOW() +WHERE id = $1 +` + +type UpdateLLMEndpointParams struct { + ID pgtype.UUID `json:"id"` + DisplayName string `json:"display_name"` + BaseUrl string `json:"base_url"` + ApiKeyEncrypted []byte `json:"api_key_encrypted"` +} + +func (q *Queries) UpdateLLMEndpoint(ctx context.Context, arg UpdateLLMEndpointParams) error { + _, err := q.db.Exec(ctx, updateLLMEndpoint, + arg.ID, + arg.DisplayName, + arg.BaseUrl, + arg.ApiKeyEncrypted, + ) + return err +} diff --git a/internal/chat/sqlc/messages.sql.go b/internal/chat/sqlc/messages.sql.go new file mode 100644 index 0000000..93eb100 --- /dev/null +++ b/internal/chat/sqlc/messages.sql.go @@ -0,0 +1,265 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: messages.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteMessage = `-- name: DeleteMessage :exec +DELETE FROM messages WHERE id = $1 +` + +func (q *Queries) DeleteMessage(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, deleteMessage, id) + return err +} + +const getFirstUserMessage = `-- name: GetFirstUserMessage :one +SELECT id, conversation_id, role, content, thinking, tool_calls, status, error_message, prompt_tokens, completion_tokens, thinking_tokens, created_at, updated_at FROM messages +WHERE conversation_id = $1 AND role = 'user' +ORDER BY id ASC LIMIT 1 +` + +func (q *Queries) GetFirstUserMessage(ctx context.Context, conversationID pgtype.UUID) (Message, error) { + row := q.db.QueryRow(ctx, getFirstUserMessage, conversationID) + var i Message + err := row.Scan( + &i.ID, + &i.ConversationID, + &i.Role, + &i.Content, + &i.Thinking, + &i.ToolCalls, + &i.Status, + &i.ErrorMessage, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getMessage = `-- name: GetMessage :one +SELECT id, conversation_id, role, content, thinking, tool_calls, status, error_message, prompt_tokens, completion_tokens, thinking_tokens, created_at, updated_at FROM messages WHERE id = $1 +` + +func (q *Queries) GetMessage(ctx context.Context, id int64) (Message, error) { + row := q.db.QueryRow(ctx, getMessage, id) + var i Message + err := row.Scan( + &i.ID, + &i.ConversationID, + &i.Role, + &i.Content, + &i.Thinking, + &i.ToolCalls, + &i.Status, + &i.ErrorMessage, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const insertMessage = `-- name: InsertMessage :one +INSERT INTO messages (conversation_id, role, content, status) +VALUES ($1, $2, $3, $4) +RETURNING id, conversation_id, role, content, thinking, tool_calls, status, error_message, prompt_tokens, completion_tokens, thinking_tokens, created_at, updated_at +` + +type InsertMessageParams struct { + ConversationID pgtype.UUID `json:"conversation_id"` + Role string `json:"role"` + Content string `json:"content"` + Status string `json:"status"` +} + +func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) (Message, error) { + row := q.db.QueryRow(ctx, insertMessage, + arg.ConversationID, + arg.Role, + arg.Content, + arg.Status, + ) + var i Message + err := row.Scan( + &i.ID, + &i.ConversationID, + &i.Role, + &i.Content, + &i.Thinking, + &i.ToolCalls, + &i.Status, + &i.ErrorMessage, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listMessagesAscending = `-- name: ListMessagesAscending :many +SELECT id, conversation_id, role, content, thinking, tool_calls, status, error_message, prompt_tokens, completion_tokens, thinking_tokens, created_at, updated_at FROM messages WHERE conversation_id = $1 ORDER BY id ASC +` + +func (q *Queries) ListMessagesAscending(ctx context.Context, conversationID pgtype.UUID) ([]Message, error) { + rows, err := q.db.Query(ctx, listMessagesAscending, conversationID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Message + for rows.Next() { + var i Message + if err := rows.Scan( + &i.ID, + &i.ConversationID, + &i.Role, + &i.Content, + &i.Thinking, + &i.ToolCalls, + &i.Status, + &i.ErrorMessage, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listMessagesByConversation = `-- name: ListMessagesByConversation :many +SELECT id, conversation_id, role, content, thinking, tool_calls, status, error_message, prompt_tokens, completion_tokens, thinking_tokens, created_at, updated_at FROM messages +WHERE conversation_id = $1 + AND ($2::bigint = 0 OR id < $2) +ORDER BY id DESC +LIMIT $3 +` + +type ListMessagesByConversationParams struct { + ConversationID pgtype.UUID `json:"conversation_id"` + Column2 int64 `json:"column_2"` + Limit int32 `json:"limit"` +} + +func (q *Queries) ListMessagesByConversation(ctx context.Context, arg ListMessagesByConversationParams) ([]Message, error) { + rows, err := q.db.Query(ctx, listMessagesByConversation, arg.ConversationID, arg.Column2, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Message + for rows.Next() { + var i Message + if err := rows.Scan( + &i.ID, + &i.ConversationID, + &i.Role, + &i.Content, + &i.Thinking, + &i.ToolCalls, + &i.Status, + &i.ErrorMessage, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const markPendingAsErrorOnStartup = `-- name: MarkPendingAsErrorOnStartup :exec +UPDATE messages +SET status = 'error', error_message = 'server restart', updated_at = NOW() +WHERE status = 'pending' +` + +func (q *Queries) MarkPendingAsErrorOnStartup(ctx context.Context) error { + _, err := q.db.Exec(ctx, markPendingAsErrorOnStartup) + return err +} + +const updateMessageCancelled = `-- name: UpdateMessageCancelled :exec +UPDATE messages +SET status = 'cancelled', updated_at = NOW() +WHERE id = $1 +` + +func (q *Queries) UpdateMessageCancelled(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, updateMessageCancelled, id) + return err +} + +const updateMessageError = `-- name: UpdateMessageError :exec +UPDATE messages +SET status = 'error', error_message = $2, updated_at = NOW() +WHERE id = $1 +` + +type UpdateMessageErrorParams struct { + ID int64 `json:"id"` + ErrorMessage *string `json:"error_message"` +} + +func (q *Queries) UpdateMessageError(ctx context.Context, arg UpdateMessageErrorParams) error { + _, err := q.db.Exec(ctx, updateMessageError, arg.ID, arg.ErrorMessage) + return err +} + +const updateMessageOK = `-- name: UpdateMessageOK :exec +UPDATE messages +SET content = $2, thinking = $3, + prompt_tokens = $4, completion_tokens = $5, thinking_tokens = $6, + status = 'ok', updated_at = NOW() +WHERE id = $1 +` + +type UpdateMessageOKParams struct { + ID int64 `json:"id"` + Content string `json:"content"` + Thinking *string `json:"thinking"` + PromptTokens *int32 `json:"prompt_tokens"` + CompletionTokens *int32 `json:"completion_tokens"` + ThinkingTokens *int32 `json:"thinking_tokens"` +} + +func (q *Queries) UpdateMessageOK(ctx context.Context, arg UpdateMessageOKParams) error { + _, err := q.db.Exec(ctx, updateMessageOK, + arg.ID, + arg.Content, + arg.Thinking, + arg.PromptTokens, + arg.CompletionTokens, + arg.ThinkingTokens, + ) + return err +} diff --git a/internal/chat/sqlc/models.go b/internal/chat/sqlc/models.go new file mode 100644 index 0000000..84a1ba3 --- /dev/null +++ b/internal/chat/sqlc/models.go @@ -0,0 +1,232 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package chatsqlc + +import ( + "net/netip" + + "github.com/jackc/pgx/v5/pgtype" +) + +type AuditLog struct { + ID int64 `json:"id"` + UserID pgtype.UUID `json:"user_id"` + Action string `json:"action"` + TargetType *string `json:"target_type"` + TargetID *string `json:"target_id"` + Ip *netip.Addr `json:"ip"` + Metadata []byte `json:"metadata"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type Conversation struct { + ID pgtype.UUID `json:"id"` + UserID pgtype.UUID `json:"user_id"` + ProjectID pgtype.UUID `json:"project_id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + IssueID pgtype.UUID `json:"issue_id"` + ModelID pgtype.UUID `json:"model_id"` + SystemPrompt string `json:"system_prompt"` + Title *string `json:"title"` + TitleGeneratedAt pgtype.Timestamptz `json:"title_generated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + DeletedAt pgtype.Timestamptz `json:"deleted_at"` +} + +type GitCredential struct { + ID pgtype.UUID `json:"id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + Kind string `json:"kind"` + Username *string `json:"username"` + EncryptedSecret []byte `json:"encrypted_secret"` + Fingerprint *string `json:"fingerprint"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type Issue struct { + ID pgtype.UUID `json:"id"` + ProjectID pgtype.UUID `json:"project_id"` + RequirementID pgtype.UUID `json:"requirement_id"` + Number int32 `json:"number"` + Title string `json:"title"` + Description string `json:"description"` + Status string `json:"status"` + AssigneeID pgtype.UUID `json:"assignee_id"` + CreatedBy pgtype.UUID `json:"created_by"` + ClosedAt pgtype.Timestamptz `json:"closed_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + WorkspaceID pgtype.UUID `json:"workspace_id"` +} + +type LlmEndpoint struct { + ID pgtype.UUID `json:"id"` + Provider string `json:"provider"` + DisplayName string `json:"display_name"` + BaseUrl string `json:"base_url"` + ApiKeyEncrypted []byte `json:"api_key_encrypted"` + CreatedBy pgtype.UUID `json:"created_by"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type LlmModel struct { + ID pgtype.UUID `json:"id"` + EndpointID pgtype.UUID `json:"endpoint_id"` + ModelID string `json:"model_id"` + DisplayName string `json:"display_name"` + Capabilities []byte `json:"capabilities"` + ContextWindow int32 `json:"context_window"` + MaxOutputTokens int32 `json:"max_output_tokens"` + PromptPricePerMillionUsd pgtype.Numeric `json:"prompt_price_per_million_usd"` + CompletionPricePerMillionUsd pgtype.Numeric `json:"completion_price_per_million_usd"` + ThinkingPricePerMillionUsd pgtype.Numeric `json:"thinking_price_per_million_usd"` + IsTitleGenerator bool `json:"is_title_generator"` + Enabled bool `json:"enabled"` + SortOrder int32 `json:"sort_order"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type LlmUsage struct { + ID int64 `json:"id"` + ConversationID pgtype.UUID `json:"conversation_id"` + MessageID int64 `json:"message_id"` + UserID pgtype.UUID `json:"user_id"` + EndpointID pgtype.UUID `json:"endpoint_id"` + ModelID pgtype.UUID `json:"model_id"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type Message struct { + ID int64 `json:"id"` + ConversationID pgtype.UUID `json:"conversation_id"` + Role string `json:"role"` + Content string `json:"content"` + Thinking *string `json:"thinking"` + ToolCalls []byte `json:"tool_calls"` + Status string `json:"status"` + ErrorMessage *string `json:"error_message"` + PromptTokens *int32 `json:"prompt_tokens"` + CompletionTokens *int32 `json:"completion_tokens"` + ThinkingTokens *int32 `json:"thinking_tokens"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type MessageAttachment struct { + ID pgtype.UUID `json:"id"` + UserID pgtype.UUID `json:"user_id"` + MessageID *int64 `json:"message_id"` + Filename string `json:"filename"` + MimeType string `json:"mime_type"` + SizeBytes int64 `json:"size_bytes"` + Sha256 string `json:"sha256"` + StoragePath string `json:"storage_path"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type Notification struct { + ID pgtype.UUID `json:"id"` + UserID pgtype.UUID `json:"user_id"` + Topic string `json:"topic"` + Severity string `json:"severity"` + Title string `json:"title"` + Body string `json:"body"` + Link *string `json:"link"` + Metadata []byte `json:"metadata"` + ReadAt pgtype.Timestamptz `json:"read_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type Project struct { + ID pgtype.UUID `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + Visibility string `json:"visibility"` + OwnerID pgtype.UUID `json:"owner_id"` + ArchivedAt pgtype.Timestamptz `json:"archived_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type PromptTemplate struct { + ID pgtype.UUID `json:"id"` + Name string `json:"name"` + Content string `json:"content"` + Scope string `json:"scope"` + OwnerID pgtype.UUID `json:"owner_id"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type Requirement struct { + ID pgtype.UUID `json:"id"` + ProjectID pgtype.UUID `json:"project_id"` + Number int32 `json:"number"` + Title string `json:"title"` + Description string `json:"description"` + Phase string `json:"phase"` + Status string `json:"status"` + OwnerID pgtype.UUID `json:"owner_id"` + ClosedAt pgtype.Timestamptz `json:"closed_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + WorkspaceID pgtype.UUID `json:"workspace_id"` +} + +type User struct { + ID pgtype.UUID `json:"id"` + Email string `json:"email"` + PasswordHash string `json:"password_hash"` + DisplayName string `json:"display_name"` + IsAdmin bool `json:"is_admin"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type UserSession struct { + ID pgtype.UUID `json:"id"` + UserID pgtype.UUID `json:"user_id"` + TokenHash []byte `json:"token_hash"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` + LastSeenAt pgtype.Timestamptz `json:"last_seen_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type Workspace struct { + ID pgtype.UUID `json:"id"` + ProjectID pgtype.UUID `json:"project_id"` + Slug string `json:"slug"` + Name string `json:"name"` + Description string `json:"description"` + GitRemoteUrl string `json:"git_remote_url"` + DefaultBranch string `json:"default_branch"` + MainPath string `json:"main_path"` + SyncStatus string `json:"sync_status"` + LastSyncedAt pgtype.Timestamptz `json:"last_synced_at"` + LastSyncError *string `json:"last_sync_error"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type WorkspaceWorktree struct { + ID pgtype.UUID `json:"id"` + WorkspaceID pgtype.UUID `json:"workspace_id"` + Branch string `json:"branch"` + Path string `json:"path"` + Status string `json:"status"` + ActiveHolder *string `json:"active_holder"` + AcquiredAt pgtype.Timestamptz `json:"acquired_at"` + LastUsedAt pgtype.Timestamptz `json:"last_used_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} diff --git a/internal/chat/sqlc/models.sql.go b/internal/chat/sqlc/models.sql.go new file mode 100644 index 0000000..04c557c --- /dev/null +++ b/internal/chat/sqlc/models.sql.go @@ -0,0 +1,227 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: models.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteLLMModel = `-- name: DeleteLLMModel :exec +DELETE FROM llm_models WHERE id = $1 +` + +func (q *Queries) DeleteLLMModel(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteLLMModel, id) + return err +} + +const getLLMModel = `-- name: GetLLMModel :one +SELECT id, endpoint_id, model_id, display_name, capabilities, context_window, max_output_tokens, prompt_price_per_million_usd, completion_price_per_million_usd, thinking_price_per_million_usd, is_title_generator, enabled, sort_order, created_at, updated_at FROM llm_models WHERE id = $1 +` + +func (q *Queries) GetLLMModel(ctx context.Context, id pgtype.UUID) (LlmModel, error) { + row := q.db.QueryRow(ctx, getLLMModel, id) + var i LlmModel + err := row.Scan( + &i.ID, + &i.EndpointID, + &i.ModelID, + &i.DisplayName, + &i.Capabilities, + &i.ContextWindow, + &i.MaxOutputTokens, + &i.PromptPricePerMillionUsd, + &i.CompletionPricePerMillionUsd, + &i.ThinkingPricePerMillionUsd, + &i.IsTitleGenerator, + &i.Enabled, + &i.SortOrder, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getTitleGeneratorModel = `-- name: GetTitleGeneratorModel :one +SELECT id, endpoint_id, model_id, display_name, capabilities, context_window, max_output_tokens, prompt_price_per_million_usd, completion_price_per_million_usd, thinking_price_per_million_usd, is_title_generator, enabled, sort_order, created_at, updated_at FROM llm_models WHERE is_title_generator = true AND enabled = true LIMIT 1 +` + +func (q *Queries) GetTitleGeneratorModel(ctx context.Context) (LlmModel, error) { + row := q.db.QueryRow(ctx, getTitleGeneratorModel) + var i LlmModel + err := row.Scan( + &i.ID, + &i.EndpointID, + &i.ModelID, + &i.DisplayName, + &i.Capabilities, + &i.ContextWindow, + &i.MaxOutputTokens, + &i.PromptPricePerMillionUsd, + &i.CompletionPricePerMillionUsd, + &i.ThinkingPricePerMillionUsd, + &i.IsTitleGenerator, + &i.Enabled, + &i.SortOrder, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const insertLLMModel = `-- name: InsertLLMModel :one +INSERT INTO llm_models ( + id, endpoint_id, model_id, display_name, capabilities, + context_window, max_output_tokens, + prompt_price_per_million_usd, completion_price_per_million_usd, thinking_price_per_million_usd, + is_title_generator, enabled, sort_order +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) +RETURNING id, endpoint_id, model_id, display_name, capabilities, context_window, max_output_tokens, prompt_price_per_million_usd, completion_price_per_million_usd, thinking_price_per_million_usd, is_title_generator, enabled, sort_order, created_at, updated_at +` + +type InsertLLMModelParams struct { + ID pgtype.UUID `json:"id"` + EndpointID pgtype.UUID `json:"endpoint_id"` + ModelID string `json:"model_id"` + DisplayName string `json:"display_name"` + Capabilities []byte `json:"capabilities"` + ContextWindow int32 `json:"context_window"` + MaxOutputTokens int32 `json:"max_output_tokens"` + PromptPricePerMillionUsd pgtype.Numeric `json:"prompt_price_per_million_usd"` + CompletionPricePerMillionUsd pgtype.Numeric `json:"completion_price_per_million_usd"` + ThinkingPricePerMillionUsd pgtype.Numeric `json:"thinking_price_per_million_usd"` + IsTitleGenerator bool `json:"is_title_generator"` + Enabled bool `json:"enabled"` + SortOrder int32 `json:"sort_order"` +} + +func (q *Queries) InsertLLMModel(ctx context.Context, arg InsertLLMModelParams) (LlmModel, error) { + row := q.db.QueryRow(ctx, insertLLMModel, + arg.ID, + arg.EndpointID, + arg.ModelID, + arg.DisplayName, + arg.Capabilities, + arg.ContextWindow, + arg.MaxOutputTokens, + arg.PromptPricePerMillionUsd, + arg.CompletionPricePerMillionUsd, + arg.ThinkingPricePerMillionUsd, + arg.IsTitleGenerator, + arg.Enabled, + arg.SortOrder, + ) + var i LlmModel + err := row.Scan( + &i.ID, + &i.EndpointID, + &i.ModelID, + &i.DisplayName, + &i.Capabilities, + &i.ContextWindow, + &i.MaxOutputTokens, + &i.PromptPricePerMillionUsd, + &i.CompletionPricePerMillionUsd, + &i.ThinkingPricePerMillionUsd, + &i.IsTitleGenerator, + &i.Enabled, + &i.SortOrder, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listLLMModels = `-- name: ListLLMModels :many +SELECT id, endpoint_id, model_id, display_name, capabilities, context_window, max_output_tokens, prompt_price_per_million_usd, completion_price_per_million_usd, thinking_price_per_million_usd, is_title_generator, enabled, sort_order, created_at, updated_at FROM llm_models +WHERE ($1::boolean = false) OR enabled = true +ORDER BY sort_order, display_name +` + +func (q *Queries) ListLLMModels(ctx context.Context, dollar_1 bool) ([]LlmModel, error) { + rows, err := q.db.Query(ctx, listLLMModels, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LlmModel + for rows.Next() { + var i LlmModel + if err := rows.Scan( + &i.ID, + &i.EndpointID, + &i.ModelID, + &i.DisplayName, + &i.Capabilities, + &i.ContextWindow, + &i.MaxOutputTokens, + &i.PromptPricePerMillionUsd, + &i.CompletionPricePerMillionUsd, + &i.ThinkingPricePerMillionUsd, + &i.IsTitleGenerator, + &i.Enabled, + &i.SortOrder, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateLLMModel = `-- name: UpdateLLMModel :exec +UPDATE llm_models +SET display_name = COALESCE($2, display_name), + capabilities = COALESCE($3, capabilities), + context_window = COALESCE($4, context_window), + max_output_tokens = COALESCE($5, max_output_tokens), + prompt_price_per_million_usd = COALESCE($6, prompt_price_per_million_usd), + completion_price_per_million_usd = COALESCE($7, completion_price_per_million_usd), + thinking_price_per_million_usd = COALESCE($8, thinking_price_per_million_usd), + is_title_generator = COALESCE($9, is_title_generator), + enabled = COALESCE($10, enabled), + sort_order = COALESCE($11, sort_order), + updated_at = NOW() +WHERE id = $1 +` + +type UpdateLLMModelParams struct { + ID pgtype.UUID `json:"id"` + DisplayName string `json:"display_name"` + Capabilities []byte `json:"capabilities"` + ContextWindow int32 `json:"context_window"` + MaxOutputTokens int32 `json:"max_output_tokens"` + PromptPricePerMillionUsd pgtype.Numeric `json:"prompt_price_per_million_usd"` + CompletionPricePerMillionUsd pgtype.Numeric `json:"completion_price_per_million_usd"` + ThinkingPricePerMillionUsd pgtype.Numeric `json:"thinking_price_per_million_usd"` + IsTitleGenerator bool `json:"is_title_generator"` + Enabled bool `json:"enabled"` + SortOrder int32 `json:"sort_order"` +} + +func (q *Queries) UpdateLLMModel(ctx context.Context, arg UpdateLLMModelParams) error { + _, err := q.db.Exec(ctx, updateLLMModel, + arg.ID, + arg.DisplayName, + arg.Capabilities, + arg.ContextWindow, + arg.MaxOutputTokens, + arg.PromptPricePerMillionUsd, + arg.CompletionPricePerMillionUsd, + arg.ThinkingPricePerMillionUsd, + arg.IsTitleGenerator, + arg.Enabled, + arg.SortOrder, + ) + return err +} diff --git a/internal/chat/sqlc/templates.sql.go b/internal/chat/sqlc/templates.sql.go new file mode 100644 index 0000000..e568c4d --- /dev/null +++ b/internal/chat/sqlc/templates.sql.go @@ -0,0 +1,125 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: templates.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deletePromptTemplate = `-- name: DeletePromptTemplate :exec +DELETE FROM prompt_templates WHERE id = $1 +` + +func (q *Queries) DeletePromptTemplate(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deletePromptTemplate, id) + return err +} + +const getPromptTemplate = `-- name: GetPromptTemplate :one +SELECT id, name, content, scope, owner_id, created_at, updated_at FROM prompt_templates WHERE id = $1 +` + +func (q *Queries) GetPromptTemplate(ctx context.Context, id pgtype.UUID) (PromptTemplate, error) { + row := q.db.QueryRow(ctx, getPromptTemplate, id) + var i PromptTemplate + err := row.Scan( + &i.ID, + &i.Name, + &i.Content, + &i.Scope, + &i.OwnerID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const insertPromptTemplate = `-- name: InsertPromptTemplate :one +INSERT INTO prompt_templates (id, name, content, scope, owner_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, name, content, scope, owner_id, created_at, updated_at +` + +type InsertPromptTemplateParams struct { + ID pgtype.UUID `json:"id"` + Name string `json:"name"` + Content string `json:"content"` + Scope string `json:"scope"` + OwnerID pgtype.UUID `json:"owner_id"` +} + +func (q *Queries) InsertPromptTemplate(ctx context.Context, arg InsertPromptTemplateParams) (PromptTemplate, error) { + row := q.db.QueryRow(ctx, insertPromptTemplate, + arg.ID, + arg.Name, + arg.Content, + arg.Scope, + arg.OwnerID, + ) + var i PromptTemplate + err := row.Scan( + &i.ID, + &i.Name, + &i.Content, + &i.Scope, + &i.OwnerID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listPromptTemplatesForUser = `-- name: ListPromptTemplatesForUser :many +SELECT id, name, content, scope, owner_id, created_at, updated_at FROM prompt_templates +WHERE scope = 'system' OR (scope = 'user' AND owner_id = $1) +ORDER BY scope, name +` + +func (q *Queries) ListPromptTemplatesForUser(ctx context.Context, ownerID pgtype.UUID) ([]PromptTemplate, error) { + rows, err := q.db.Query(ctx, listPromptTemplatesForUser, ownerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PromptTemplate + for rows.Next() { + var i PromptTemplate + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Content, + &i.Scope, + &i.OwnerID, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updatePromptTemplate = `-- name: UpdatePromptTemplate :exec +UPDATE prompt_templates SET name = $2, content = $3, updated_at = NOW() +WHERE id = $1 +` + +type UpdatePromptTemplateParams struct { + ID pgtype.UUID `json:"id"` + Name string `json:"name"` + Content string `json:"content"` +} + +func (q *Queries) UpdatePromptTemplate(ctx context.Context, arg UpdatePromptTemplateParams) error { + _, err := q.db.Exec(ctx, updatePromptTemplate, arg.ID, arg.Name, arg.Content) + return err +} diff --git a/internal/chat/sqlc/usage.sql.go b/internal/chat/sqlc/usage.sql.go new file mode 100644 index 0000000..f4aaa09 --- /dev/null +++ b/internal/chat/sqlc/usage.sql.go @@ -0,0 +1,251 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: usage.sql + +package chatsqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const insertUsage = `-- name: InsertUsage :exec +INSERT INTO llm_usage ( + conversation_id, message_id, user_id, endpoint_id, model_id, + prompt_tokens, completion_tokens, thinking_tokens, cost_usd +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) +` + +type InsertUsageParams struct { + ConversationID pgtype.UUID `json:"conversation_id"` + MessageID int64 `json:"message_id"` + UserID pgtype.UUID `json:"user_id"` + EndpointID pgtype.UUID `json:"endpoint_id"` + ModelID pgtype.UUID `json:"model_id"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` +} + +func (q *Queries) InsertUsage(ctx context.Context, arg InsertUsageParams) error { + _, err := q.db.Exec(ctx, insertUsage, + arg.ConversationID, + arg.MessageID, + arg.UserID, + arg.EndpointID, + arg.ModelID, + arg.PromptTokens, + arg.CompletionTokens, + arg.ThinkingTokens, + arg.CostUsd, + ) + return err +} + +const summarizeUsageByDay = `-- name: SummarizeUsageByDay :many +SELECT date_trunc('day', created_at) AS day, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE created_at >= $1 AND created_at < $2 +GROUP BY day +ORDER BY day +` + +type SummarizeUsageByDayParams struct { + CreatedAt pgtype.Timestamptz `json:"created_at"` + CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"` +} + +type SummarizeUsageByDayRow struct { + Day pgtype.Interval `json:"day"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` +} + +func (q *Queries) SummarizeUsageByDay(ctx context.Context, arg SummarizeUsageByDayParams) ([]SummarizeUsageByDayRow, error) { + rows, err := q.db.Query(ctx, summarizeUsageByDay, arg.CreatedAt, arg.CreatedAt_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SummarizeUsageByDayRow + for rows.Next() { + var i SummarizeUsageByDayRow + if err := rows.Scan( + &i.Day, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CostUsd, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const summarizeUsageByModel = `-- name: SummarizeUsageByModel :many +SELECT model_id, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE created_at >= $1 AND created_at < $2 +GROUP BY model_id +ORDER BY cost_usd DESC +` + +type SummarizeUsageByModelParams struct { + CreatedAt pgtype.Timestamptz `json:"created_at"` + CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"` +} + +type SummarizeUsageByModelRow struct { + ModelID pgtype.UUID `json:"model_id"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` +} + +func (q *Queries) SummarizeUsageByModel(ctx context.Context, arg SummarizeUsageByModelParams) ([]SummarizeUsageByModelRow, error) { + rows, err := q.db.Query(ctx, summarizeUsageByModel, arg.CreatedAt, arg.CreatedAt_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SummarizeUsageByModelRow + for rows.Next() { + var i SummarizeUsageByModelRow + if err := rows.Scan( + &i.ModelID, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CostUsd, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const summarizeUsageByUser = `-- name: SummarizeUsageByUser :many +SELECT user_id, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE created_at >= $1 AND created_at < $2 +GROUP BY user_id +ORDER BY cost_usd DESC +` + +type SummarizeUsageByUserParams struct { + CreatedAt pgtype.Timestamptz `json:"created_at"` + CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"` +} + +type SummarizeUsageByUserRow struct { + UserID pgtype.UUID `json:"user_id"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` +} + +func (q *Queries) SummarizeUsageByUser(ctx context.Context, arg SummarizeUsageByUserParams) ([]SummarizeUsageByUserRow, error) { + rows, err := q.db.Query(ctx, summarizeUsageByUser, arg.CreatedAt, arg.CreatedAt_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SummarizeUsageByUserRow + for rows.Next() { + var i SummarizeUsageByUserRow + if err := rows.Scan( + &i.UserID, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CostUsd, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const summarizeUserDaily = `-- name: SummarizeUserDaily :many +SELECT date_trunc('day', created_at) AS day, + SUM(prompt_tokens)::int AS prompt_tokens, + SUM(completion_tokens)::int AS completion_tokens, + SUM(thinking_tokens)::int AS thinking_tokens, + SUM(cost_usd)::numeric AS cost_usd +FROM llm_usage +WHERE user_id = $1 AND created_at >= $2 AND created_at < $3 +GROUP BY day +ORDER BY day +` + +type SummarizeUserDailyParams struct { + UserID pgtype.UUID `json:"user_id"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"` +} + +type SummarizeUserDailyRow struct { + Day pgtype.Interval `json:"day"` + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + ThinkingTokens int32 `json:"thinking_tokens"` + CostUsd pgtype.Numeric `json:"cost_usd"` +} + +func (q *Queries) SummarizeUserDaily(ctx context.Context, arg SummarizeUserDailyParams) ([]SummarizeUserDailyRow, error) { + rows, err := q.db.Query(ctx, summarizeUserDaily, arg.UserID, arg.CreatedAt, arg.CreatedAt_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SummarizeUserDailyRow + for rows.Next() { + var i SummarizeUserDailyRow + if err := rows.Scan( + &i.Day, + &i.PromptTokens, + &i.CompletionTokens, + &i.ThinkingTokens, + &i.CostUsd, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/sqlc.yaml b/sqlc.yaml index f2577b8..27609e5 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -50,3 +50,13 @@ sql: sql_package: pgx/v5 emit_pointers_for_null_types: true emit_json_tags: true + - engine: postgresql + queries: internal/chat/queries + schema: migrations + gen: + go: + package: chatsqlc + out: internal/chat/sqlc + sql_package: pgx/v5 + emit_pointers_for_null_types: true + emit_json_tags: true