Changeset 0.23.0 (#153)
This commit is contained in:
@@ -183,20 +183,40 @@ func (s *ChannelStore) SetCursor(ctx context.Context, channelID, userID, leafID
|
||||
}
|
||||
|
||||
func (s *ChannelStore) SetModel(ctx context.Context, cm *models.ChannelModel) error {
|
||||
// For persona entries, use the persona-aware path
|
||||
if cm.PersonaID != nil && *cm.PersonaID != "" {
|
||||
return s.SetPersonaModel(ctx, cm)
|
||||
}
|
||||
// Raw model upsert (no persona) — matches idx_channel_models_raw partial index
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO channel_models (channel_id, model_id, provider_config_id, display_name, system_prompt, settings, is_default)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (channel_id, model_id) DO UPDATE SET
|
||||
provider_config_id = $3, display_name = $4, system_prompt = $5, settings = $6, is_default = $7`,
|
||||
ON CONFLICT (channel_id, model_id, provider_config_id) WHERE persona_id IS NULL DO UPDATE SET
|
||||
display_name = $4, system_prompt = $5, settings = $6, is_default = $7`,
|
||||
cm.ChannelID, cm.ModelID, cm.ProviderConfigID, cm.DisplayName, cm.SystemPrompt, "{}", cm.IsDefault)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetPersonaModel inserts or updates a persona's channel model roster entry.
|
||||
// Uses the idx_channel_models_persona partial index (one per persona per channel).
|
||||
func (s *ChannelStore) SetPersonaModel(ctx context.Context, cm *models.ChannelModel) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO channel_models (channel_id, model_id, provider_config_id, persona_id, display_name, system_prompt, settings, is_default)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (channel_id, persona_id) WHERE persona_id IS NOT NULL DO UPDATE SET
|
||||
model_id = $2, provider_config_id = $3, display_name = $5, system_prompt = $6, settings = $7, is_default = $8`,
|
||||
cm.ChannelID, cm.ModelID, cm.ProviderConfigID, cm.PersonaID, cm.DisplayName, cm.SystemPrompt, "{}", cm.IsDefault)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]models.ChannelModel, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, channel_id, model_id, COALESCE(provider_config_id::text, ''),
|
||||
COALESCE(display_name, ''), COALESCE(system_prompt, ''), is_default
|
||||
FROM channel_models WHERE channel_id = $1`, channelID)
|
||||
SELECT cm.id, cm.channel_id, cm.model_id, COALESCE(cm.provider_config_id::text, ''),
|
||||
COALESCE(cm.persona_id::text, ''), COALESCE(p.handle, ''), COALESCE(cm.display_name, ''),
|
||||
COALESCE(cm.system_prompt, ''), cm.is_default
|
||||
FROM channel_models cm
|
||||
LEFT JOIN personas p ON p.id = cm.persona_id
|
||||
WHERE cm.channel_id = $1`, channelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,10 +225,14 @@ func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]model
|
||||
var result []models.ChannelModel
|
||||
for rows.Next() {
|
||||
var cm models.ChannelModel
|
||||
var personaID string
|
||||
if err := rows.Scan(&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID,
|
||||
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault); err != nil {
|
||||
&personaID, &cm.Handle, &cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if personaID != "" {
|
||||
cm.PersonaID = &personaID
|
||||
}
|
||||
result = append(result, cm)
|
||||
}
|
||||
return result, rows.Err()
|
||||
@@ -216,15 +240,22 @@ func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]model
|
||||
|
||||
func (s *ChannelStore) GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error) {
|
||||
var cm models.ChannelModel
|
||||
var personaID string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, channel_id, model_id, COALESCE(provider_config_id::text, ''),
|
||||
COALESCE(display_name, ''), COALESCE(system_prompt, ''), is_default
|
||||
FROM channel_models WHERE id = $1`, id).Scan(
|
||||
SELECT cm.id, cm.channel_id, cm.model_id, COALESCE(cm.provider_config_id::text, ''),
|
||||
COALESCE(cm.persona_id::text, ''), COALESCE(p.handle, ''), COALESCE(cm.display_name, ''),
|
||||
COALESCE(cm.system_prompt, ''), cm.is_default
|
||||
FROM channel_models cm
|
||||
LEFT JOIN personas p ON p.id = cm.persona_id
|
||||
WHERE cm.id = $1`, id).Scan(
|
||||
&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID,
|
||||
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault)
|
||||
&personaID, &cm.Handle, &cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if personaID != "" {
|
||||
cm.PersonaID = &personaID
|
||||
}
|
||||
return &cm, nil
|
||||
}
|
||||
|
||||
@@ -287,3 +318,99 @@ func (s *ChannelStore) ResolveWorkspaceID(ctx context.Context, channelID string)
|
||||
}
|
||||
return NullableString(wsID), nil
|
||||
}
|
||||
|
||||
// ── Channel Participants (ICD §3.7) ──────────
|
||||
|
||||
func (s *ChannelStore) AddParticipant(ctx context.Context, p *models.ChannelParticipant) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO channel_participants (channel_id, participant_type, participant_id, role, display_name, avatar_url)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
p.ChannelID, p.ParticipantType, p.ParticipantID, p.Role, p.DisplayName, p.AvatarURL)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListParticipants(ctx context.Context, channelID string) ([]models.ChannelParticipant, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, channel_id, participant_type, participant_id, role,
|
||||
display_name, avatar_url, joined_at
|
||||
FROM channel_participants WHERE channel_id = $1 ORDER BY joined_at`, channelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.ChannelParticipant
|
||||
for rows.Next() {
|
||||
var p models.ChannelParticipant
|
||||
if err := rows.Scan(&p.ID, &p.ChannelID, &p.ParticipantType, &p.ParticipantID,
|
||||
&p.Role, &p.DisplayName, &p.AvatarURL, &p.JoinedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetParticipantByID(ctx context.Context, id string) (*models.ChannelParticipant, error) {
|
||||
var p models.ChannelParticipant
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, channel_id, participant_type, participant_id, role,
|
||||
display_name, avatar_url, joined_at
|
||||
FROM channel_participants WHERE id = $1`, id).Scan(
|
||||
&p.ID, &p.ChannelID, &p.ParticipantType, &p.ParticipantID,
|
||||
&p.Role, &p.DisplayName, &p.AvatarURL, &p.JoinedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UpdateParticipantRole(ctx context.Context, id, role string) error {
|
||||
_, err := DB.ExecContext(ctx, `UPDATE channel_participants SET role = $1 WHERE id = $2`, role, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) RemoveParticipant(ctx context.Context, id string) error {
|
||||
res, err := DB.ExecContext(ctx, `DELETE FROM channel_participants WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) IsParticipant(ctx context.Context, channelID, pType, pID string) (bool, error) {
|
||||
var exists bool
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT EXISTS(SELECT 1 FROM channel_participants
|
||||
WHERE channel_id = $1 AND participant_type = $2 AND participant_id = $3)`,
|
||||
channelID, pType, pID).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetParticipantRole(ctx context.Context, channelID, pType, pID string) (string, error) {
|
||||
var role string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT role FROM channel_participants
|
||||
WHERE channel_id = $1 AND participant_type = $2 AND participant_id = $3`,
|
||||
channelID, pType, pID).Scan(&role)
|
||||
return role, err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) CountParticipantsByRole(ctx context.Context, channelID, role string) (int, error) {
|
||||
var count int
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM channel_participants WHERE channel_id = $1 AND role = $2`,
|
||||
channelID, role).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) DeleteModelByPersona(ctx context.Context, channelID, personaID string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM channel_models WHERE channel_id = $1 AND persona_id = $2`,
|
||||
channelID, personaID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,20 +13,24 @@ type PersonaStore struct{}
|
||||
|
||||
func NewPersonaStore() *PersonaStore { return &PersonaStore{} }
|
||||
|
||||
const personaCols = `id, name, description, icon, avatar, base_model_id, provider_config_id,
|
||||
const personaCols = `id, name, handle, description, icon, avatar, base_model_id, provider_config_id,
|
||||
system_prompt, temperature, max_tokens, thinking_budget, top_p,
|
||||
scope, owner_id, created_by, is_active, is_shared, memory_enabled, memory_extraction_prompt,
|
||||
created_at, updated_at`
|
||||
|
||||
func (s *PersonaStore) Create(ctx context.Context, p *models.Persona) error {
|
||||
// Auto-generate handle from name if not set
|
||||
if p.Handle == "" {
|
||||
p.Handle = models.HandleFromName(p.Name)
|
||||
}
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO personas (name, description, icon, avatar, base_model_id, provider_config_id,
|
||||
INSERT INTO personas (name, handle, description, icon, avatar, base_model_id, provider_config_id,
|
||||
system_prompt, temperature, max_tokens, thinking_budget, top_p,
|
||||
scope, owner_id, created_by, is_active, is_shared,
|
||||
memory_enabled, memory_extraction_prompt)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
p.Name, p.Description, p.Icon, p.Avatar, p.BaseModelID,
|
||||
p.Name, p.Handle, p.Description, p.Icon, p.Avatar, p.BaseModelID,
|
||||
models.NullString(p.ProviderConfigID),
|
||||
p.SystemPrompt, models.NullFloat(p.Temperature), models.NullInt(p.MaxTokens),
|
||||
models.NullInt(p.ThinkingBudget), models.NullFloat(p.TopP),
|
||||
@@ -53,6 +57,9 @@ func (s *PersonaStore) Update(ctx context.Context, id string, patch models.Perso
|
||||
if patch.Name != nil {
|
||||
b.Set("name", *patch.Name)
|
||||
}
|
||||
if patch.Handle != nil {
|
||||
b.Set("handle", *patch.Handle)
|
||||
}
|
||||
if patch.Description != nil {
|
||||
b.Set("description", *patch.Description)
|
||||
}
|
||||
@@ -281,10 +288,11 @@ func (s *PersonaStore) UserCanAccess(ctx context.Context, userID, personaID stri
|
||||
func scanPersona(row *sql.Row) (*models.Persona, error) {
|
||||
var p models.Persona
|
||||
var providerConfigID, ownerID sql.NullString
|
||||
var handle sql.NullString
|
||||
var temp, topP sql.NullFloat64
|
||||
var maxTokens, thinkingBudget sql.NullInt64
|
||||
err := row.Scan(
|
||||
&p.ID, &p.Name, &p.Description, &p.Icon, &p.Avatar,
|
||||
&p.ID, &p.Name, &handle, &p.Description, &p.Icon, &p.Avatar,
|
||||
&p.BaseModelID, &providerConfigID,
|
||||
&p.SystemPrompt, &temp, &maxTokens, &thinkingBudget, &topP,
|
||||
&p.Scope, &ownerID, &p.CreatedBy, &p.IsActive, &p.IsShared,
|
||||
@@ -294,6 +302,9 @@ func scanPersona(row *sql.Row) (*models.Persona, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if handle.Valid {
|
||||
p.Handle = handle.String
|
||||
}
|
||||
p.ProviderConfigID = NullableStringPtr(providerConfigID)
|
||||
p.OwnerID = NullableStringPtr(ownerID)
|
||||
p.Temperature = NullableFloat64Ptr(temp)
|
||||
@@ -308,10 +319,11 @@ func scanPersonas(rows *sql.Rows) ([]models.Persona, error) {
|
||||
for rows.Next() {
|
||||
var p models.Persona
|
||||
var providerConfigID, ownerID sql.NullString
|
||||
var handle sql.NullString
|
||||
var temp, topP sql.NullFloat64
|
||||
var maxTokens, thinkingBudget sql.NullInt64
|
||||
err := rows.Scan(
|
||||
&p.ID, &p.Name, &p.Description, &p.Icon, &p.Avatar,
|
||||
&p.ID, &p.Name, &handle, &p.Description, &p.Icon, &p.Avatar,
|
||||
&p.BaseModelID, &providerConfigID,
|
||||
&p.SystemPrompt, &temp, &maxTokens, &thinkingBudget, &topP,
|
||||
&p.Scope, &ownerID, &p.CreatedBy, &p.IsActive, &p.IsShared,
|
||||
@@ -321,6 +333,9 @@ func scanPersonas(rows *sql.Rows) ([]models.Persona, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if handle.Valid {
|
||||
p.Handle = handle.String
|
||||
}
|
||||
p.ProviderConfigID = NullableStringPtr(providerConfigID)
|
||||
p.OwnerID = NullableStringPtr(ownerID)
|
||||
p.Temperature = NullableFloat64Ptr(temp)
|
||||
|
||||
@@ -14,7 +14,7 @@ func NewUserModelSettingsStore() *UserModelSettingsStore { return &UserModelSett
|
||||
|
||||
func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, user_id, model_id, COALESCE(hidden, false), preferred_temperature, preferred_max_tokens,
|
||||
SELECT id, user_id, model_id, provider_config_id, COALESCE(hidden, false), preferred_temperature, preferred_max_tokens,
|
||||
COALESCE(sort_order, 0), created_at, updated_at
|
||||
FROM user_model_settings WHERE user_id = $1 ORDER BY sort_order, model_id`, userID)
|
||||
if err != nil {
|
||||
@@ -27,7 +27,7 @@ func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string)
|
||||
var s models.UserModelSetting
|
||||
var prefTemp, prefMaxTokens interface{}
|
||||
err := rows.Scan(
|
||||
&s.ID, &s.UserID, &s.ModelID, &s.Hidden,
|
||||
&s.ID, &s.UserID, &s.ModelID, &s.ProviderConfigID, &s.Hidden,
|
||||
&prefTemp, &prefMaxTokens,
|
||||
&s.SortOrder, &s.CreatedAt, &s.UpdatedAt,
|
||||
)
|
||||
@@ -46,10 +46,10 @@ func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string)
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// GetHiddenModelIDs returns a map of model_id → true for all hidden models.
|
||||
// GetHiddenModelIDs returns a map of provider_config_id:model_id → true for all hidden models.
|
||||
func (s *UserModelSettingsStore) GetHiddenModelIDs(ctx context.Context, userID string) (map[string]bool, error) {
|
||||
rows, err := DB.QueryContext(ctx,
|
||||
"SELECT model_id FROM user_model_settings WHERE user_id = $1 AND hidden = true",
|
||||
"SELECT model_id, COALESCE(provider_config_id::text, '') FROM user_model_settings WHERE user_id = $1 AND hidden = true",
|
||||
userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -58,17 +58,17 @@ func (s *UserModelSettingsStore) GetHiddenModelIDs(ctx context.Context, userID s
|
||||
|
||||
result := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
var modelID string
|
||||
if err := rows.Scan(&modelID); err != nil {
|
||||
var modelID, provCfgID string
|
||||
if err := rows.Scan(&modelID, &provCfgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[modelID] = true
|
||||
result[models.CompositeModelKey(provCfgID, modelID)] = true
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// Set upserts a single user model setting.
|
||||
func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string, patch models.UserModelSettingPatch) error {
|
||||
func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string, providerConfigID *string, patch models.UserModelSettingPatch) error {
|
||||
b := NewUpdate("user_model_settings")
|
||||
if patch.Hidden != nil {
|
||||
b.Set("hidden", *patch.Hidden)
|
||||
@@ -88,17 +88,16 @@ func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string
|
||||
}
|
||||
|
||||
// Use upsert: insert if not exists, update if exists
|
||||
// Build a custom upsert since the update builder doesn't handle INSERT ON CONFLICT
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO user_model_settings (user_id, model_id, hidden, preferred_temperature, preferred_max_tokens, sort_order)
|
||||
VALUES ($1, $2, COALESCE($3, false), $4, $5, COALESCE($6, 0))
|
||||
ON CONFLICT (user_id, model_id)
|
||||
INSERT INTO user_model_settings (user_id, model_id, provider_config_id, hidden, preferred_temperature, preferred_max_tokens, sort_order)
|
||||
VALUES ($1, $2, $3, COALESCE($4, false), $5, $6, COALESCE($7, 0))
|
||||
ON CONFLICT (user_id, model_id, provider_config_id)
|
||||
DO UPDATE SET
|
||||
hidden = COALESCE($3, user_model_settings.hidden),
|
||||
preferred_temperature = COALESCE($4, user_model_settings.preferred_temperature),
|
||||
preferred_max_tokens = COALESCE($5, user_model_settings.preferred_max_tokens),
|
||||
sort_order = COALESCE($6, user_model_settings.sort_order)`,
|
||||
userID, modelID,
|
||||
hidden = COALESCE($4, user_model_settings.hidden),
|
||||
preferred_temperature = COALESCE($5, user_model_settings.preferred_temperature),
|
||||
preferred_max_tokens = COALESCE($6, user_model_settings.preferred_max_tokens),
|
||||
sort_order = COALESCE($7, user_model_settings.sort_order)`,
|
||||
userID, modelID, providerConfigID,
|
||||
patchBoolOrNil(patch.Hidden),
|
||||
patchFloat64OrNil(patch.PreferredTemperature),
|
||||
patchIntOrNil(patch.PreferredMaxTokens),
|
||||
@@ -107,30 +106,20 @@ func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string
|
||||
return err
|
||||
}
|
||||
|
||||
// BulkSetHidden sets the hidden state for multiple models at once.
|
||||
func (s *UserModelSettingsStore) BulkSetHidden(ctx context.Context, userID string, modelIDs []string, hidden bool) error {
|
||||
if len(modelIDs) == 0 {
|
||||
// BulkSetHidden sets the hidden state for multiple model+provider pairs at once.
|
||||
func (s *UserModelSettingsStore) BulkSetHidden(ctx context.Context, userID string, entries []models.HiddenEntry, hidden bool) error {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build parameterized IN clause
|
||||
placeholders := make([]string, len(modelIDs))
|
||||
args := make([]interface{}, 0, len(modelIDs)+2)
|
||||
args = append(args, userID, hidden)
|
||||
for i, id := range modelIDs {
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+3)
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
// Upsert each: some might not have rows yet
|
||||
for _, modelID := range modelIDs {
|
||||
for _, entry := range entries {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO user_model_settings (user_id, model_id, hidden)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, model_id) DO UPDATE SET hidden = $3`,
|
||||
userID, modelID, hidden)
|
||||
INSERT INTO user_model_settings (user_id, model_id, provider_config_id, hidden)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, model_id, provider_config_id) DO UPDATE SET hidden = $4`,
|
||||
userID, entry.ModelID, entry.ProviderConfigID, hidden)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set hidden for %s: %w", modelID, err)
|
||||
return fmt.Errorf("set hidden for %s:%s: %w", entry.ProviderConfigID, entry.ModelID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user