Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.
The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.
Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.
Future changes go through PRs against this repo — no more bin-only
deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
514
internal/service/persona.go
Normal file
514
internal/service/persona.go
Normal file
@@ -0,0 +1,514 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/gosec/gsc-ops-api/pkg/types"
|
||||
)
|
||||
|
||||
// PersonaService handles persona operations against gsc_persona database
|
||||
type PersonaService struct {
|
||||
pool *pgxpool.Pool
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// NewPersonaService creates a new persona service
|
||||
func NewPersonaService(pool *pgxpool.Pool, logger zerolog.Logger) *PersonaService {
|
||||
return &PersonaService{
|
||||
pool: pool,
|
||||
logger: logger.With().Str("service", "persona").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// ListPersonas lists personas for a tenant with optional status filter
|
||||
func (s *PersonaService) ListPersonas(ctx context.Context, tenantID uuid.UUID, params types.ListParams) ([]types.PersonaSummary, int64, error) {
|
||||
params = types.DefaultListParams(params)
|
||||
|
||||
countQuery := `SELECT COUNT(*) FROM persona.personas WHERE tenant_id = $1`
|
||||
listQuery := `SELECT id, tenant_id, name, archetype, status, is_default, created_at, updated_at
|
||||
FROM persona.personas WHERE tenant_id = $1`
|
||||
|
||||
args := []interface{}{tenantID}
|
||||
argIdx := 2
|
||||
|
||||
if params.Status != "" {
|
||||
countQuery += fmt.Sprintf(" AND status = $%d", argIdx)
|
||||
listQuery += fmt.Sprintf(" AND status = $%d", argIdx)
|
||||
args = append(args, params.Status)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
|
||||
return nil, 0, fmt.Errorf("count query failed: %w", err)
|
||||
}
|
||||
|
||||
listQuery += fmt.Sprintf(" ORDER BY created_at DESC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
||||
args = append(args, params.Limit, params.Offset)
|
||||
|
||||
rows, err := s.pool.Query(ctx, listQuery, args...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("list query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
personas := make([]types.PersonaSummary, 0)
|
||||
for rows.Next() {
|
||||
var p types.PersonaSummary
|
||||
if err := rows.Scan(&p.ID, &p.TenantID, &p.Name, &p.Archetype, &p.Status, &p.IsDefault, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
||||
return nil, 0, fmt.Errorf("scan failed: %w", err)
|
||||
}
|
||||
personas = append(personas, p)
|
||||
}
|
||||
|
||||
return personas, total, nil
|
||||
}
|
||||
|
||||
// GetPersona gets a full persona configuration by ID and tenant
|
||||
func (s *PersonaService) GetPersona(ctx context.Context, id, tenantID uuid.UUID) (*types.PersonaConfig, error) {
|
||||
var p types.PersonaConfig
|
||||
var positiveRules, negativeRules, guardrailsConfig []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, name, archetype, voice_tone, mbti,
|
||||
openness, conscientiousness, extraversion, agreeableness, neuroticism,
|
||||
positive_rules, negative_rules, backstory, world_building,
|
||||
guardrails_config, topical_rails, status,
|
||||
default_model, temperature, max_tokens_per_turn,
|
||||
moral_care, moral_fairness, moral_rights,
|
||||
moral_loyalty, moral_authority, moral_sanctity,
|
||||
is_default, created_at, updated_at
|
||||
FROM persona.personas
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`, id, tenantID).Scan(
|
||||
&p.ID, &p.TenantID, &p.Name, &p.Archetype, &p.VoiceTone, &p.MBTI,
|
||||
&p.Openness, &p.Conscientiousness, &p.Extraversion, &p.Agreeableness, &p.Neuroticism,
|
||||
&positiveRules, &negativeRules, &p.Backstory, &p.WorldBuilding,
|
||||
&guardrailsConfig, &p.TopicalRails, &p.Status,
|
||||
&p.DefaultModel, &p.Temperature, &p.MaxTokensPerTurn,
|
||||
&p.MoralCare, &p.MoralFairness, &p.MoralRights,
|
||||
&p.MoralLoyalty, &p.MoralAuthority, &p.MoralSanctity,
|
||||
&p.IsDefault, &p.CreatedAt, &p.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("not found: %w", err)
|
||||
}
|
||||
|
||||
p.PositiveRules = json.RawMessage(positiveRules)
|
||||
p.NegativeRules = json.RawMessage(negativeRules)
|
||||
p.GuardrailsConfig = json.RawMessage(guardrailsConfig)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// CreatePersona creates a new persona
|
||||
func (s *PersonaService) CreatePersona(ctx context.Context, req *types.PersonaCreate) (*types.PersonaConfig, error) {
|
||||
positiveRules := req.PositiveRules
|
||||
if len(positiveRules) == 0 {
|
||||
positiveRules = json.RawMessage(`[]`)
|
||||
}
|
||||
negativeRules := req.NegativeRules
|
||||
if len(negativeRules) == 0 {
|
||||
negativeRules = json.RawMessage(`[]`)
|
||||
}
|
||||
guardrailsConfig := req.GuardrailsConfig
|
||||
if len(guardrailsConfig) == 0 {
|
||||
guardrailsConfig = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
var p types.PersonaConfig
|
||||
var prOut, nrOut, gcOut []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
INSERT INTO persona.personas (
|
||||
tenant_id, name, archetype, voice_tone, mbti,
|
||||
openness, conscientiousness, extraversion, agreeableness, neuroticism,
|
||||
positive_rules, negative_rules, backstory, world_building,
|
||||
guardrails_config, topical_rails,
|
||||
default_model, temperature, max_tokens_per_turn,
|
||||
moral_care, moral_fairness, moral_rights,
|
||||
moral_loyalty, moral_authority, moral_sanctity
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
|
||||
RETURNING id, tenant_id, name, archetype, voice_tone, mbti,
|
||||
openness, conscientiousness, extraversion, agreeableness, neuroticism,
|
||||
positive_rules, negative_rules, backstory, world_building,
|
||||
guardrails_config, topical_rails, status,
|
||||
default_model, temperature, max_tokens_per_turn,
|
||||
moral_care, moral_fairness, moral_rights,
|
||||
moral_loyalty, moral_authority, moral_sanctity,
|
||||
is_default, created_at, updated_at`,
|
||||
req.TenantID, req.Name, req.Archetype, req.VoiceTone, req.MBTI,
|
||||
req.Openness, req.Conscientiousness, req.Extraversion, req.Agreeableness, req.Neuroticism,
|
||||
positiveRules, negativeRules, req.Backstory, req.WorldBuilding,
|
||||
guardrailsConfig, req.TopicalRails,
|
||||
req.DefaultModel, req.Temperature, req.MaxTokensPerTurn,
|
||||
req.MoralCare, req.MoralFairness, req.MoralRights,
|
||||
req.MoralLoyalty, req.MoralAuthority, req.MoralSanctity,
|
||||
).Scan(
|
||||
&p.ID, &p.TenantID, &p.Name, &p.Archetype, &p.VoiceTone, &p.MBTI,
|
||||
&p.Openness, &p.Conscientiousness, &p.Extraversion, &p.Agreeableness, &p.Neuroticism,
|
||||
&prOut, &nrOut, &p.Backstory, &p.WorldBuilding,
|
||||
&gcOut, &p.TopicalRails, &p.Status,
|
||||
&p.DefaultModel, &p.Temperature, &p.MaxTokensPerTurn,
|
||||
&p.MoralCare, &p.MoralFairness, &p.MoralRights,
|
||||
&p.MoralLoyalty, &p.MoralAuthority, &p.MoralSanctity,
|
||||
&p.IsDefault, &p.CreatedAt, &p.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert failed: %w", err)
|
||||
}
|
||||
|
||||
p.PositiveRules = json.RawMessage(prOut)
|
||||
p.NegativeRules = json.RawMessage(nrOut)
|
||||
p.GuardrailsConfig = json.RawMessage(gcOut)
|
||||
s.logger.Info().Str("id", p.ID.String()).Str("name", p.Name).Msg("Created persona")
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// UpdatePersona updates an existing persona
|
||||
func (s *PersonaService) UpdatePersona(ctx context.Context, id, tenantID uuid.UUID, req *types.PersonaUpdate) (*types.PersonaConfig, error) {
|
||||
setClauses := []string{}
|
||||
args := []interface{}{}
|
||||
argIdx := 1
|
||||
|
||||
addField := func(clause string, val interface{}) {
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", clause, argIdx))
|
||||
args = append(args, val)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
addField("name", *req.Name)
|
||||
}
|
||||
if req.Archetype != nil {
|
||||
addField("archetype", *req.Archetype)
|
||||
}
|
||||
if req.VoiceTone != nil {
|
||||
addField("voice_tone", *req.VoiceTone)
|
||||
}
|
||||
if req.MBTI != nil {
|
||||
addField("mbti", *req.MBTI)
|
||||
}
|
||||
if req.Openness != nil {
|
||||
addField("openness", *req.Openness)
|
||||
}
|
||||
if req.Conscientiousness != nil {
|
||||
addField("conscientiousness", *req.Conscientiousness)
|
||||
}
|
||||
if req.Extraversion != nil {
|
||||
addField("extraversion", *req.Extraversion)
|
||||
}
|
||||
if req.Agreeableness != nil {
|
||||
addField("agreeableness", *req.Agreeableness)
|
||||
}
|
||||
if req.Neuroticism != nil {
|
||||
addField("neuroticism", *req.Neuroticism)
|
||||
}
|
||||
if len(req.PositiveRules) > 0 {
|
||||
addField("positive_rules", req.PositiveRules)
|
||||
}
|
||||
if len(req.NegativeRules) > 0 {
|
||||
addField("negative_rules", req.NegativeRules)
|
||||
}
|
||||
if req.Backstory != nil {
|
||||
addField("backstory", *req.Backstory)
|
||||
}
|
||||
if req.WorldBuilding != nil {
|
||||
addField("world_building", *req.WorldBuilding)
|
||||
}
|
||||
if len(req.GuardrailsConfig) > 0 {
|
||||
addField("guardrails_config", req.GuardrailsConfig)
|
||||
}
|
||||
if req.TopicalRails != nil {
|
||||
addField("topical_rails", *req.TopicalRails)
|
||||
}
|
||||
if req.Status != nil {
|
||||
addField("status", *req.Status)
|
||||
}
|
||||
if req.DefaultModel != nil {
|
||||
addField("default_model", *req.DefaultModel)
|
||||
}
|
||||
if req.Temperature != nil {
|
||||
addField("temperature", *req.Temperature)
|
||||
}
|
||||
if req.MaxTokensPerTurn != nil {
|
||||
addField("max_tokens_per_turn", *req.MaxTokensPerTurn)
|
||||
}
|
||||
if req.MoralCare != nil {
|
||||
addField("moral_care", *req.MoralCare)
|
||||
}
|
||||
if req.MoralFairness != nil {
|
||||
addField("moral_fairness", *req.MoralFairness)
|
||||
}
|
||||
if req.MoralRights != nil {
|
||||
addField("moral_rights", *req.MoralRights)
|
||||
}
|
||||
if req.MoralLoyalty != nil {
|
||||
addField("moral_loyalty", *req.MoralLoyalty)
|
||||
}
|
||||
if req.MoralAuthority != nil {
|
||||
addField("moral_authority", *req.MoralAuthority)
|
||||
}
|
||||
if req.MoralSanctity != nil {
|
||||
addField("moral_sanctity", *req.MoralSanctity)
|
||||
}
|
||||
if req.IsDefault != nil {
|
||||
addField("is_default", *req.IsDefault)
|
||||
}
|
||||
|
||||
if len(setClauses) == 0 {
|
||||
return s.GetPersona(ctx, id, tenantID)
|
||||
}
|
||||
|
||||
setClauses = append(setClauses, "updated_at = NOW()")
|
||||
|
||||
query := fmt.Sprintf("UPDATE persona.personas SET %s WHERE id = $%d AND tenant_id = $%d",
|
||||
joinClauses(setClauses), argIdx, argIdx+1)
|
||||
args = append(args, id, tenantID)
|
||||
query += ` RETURNING id, tenant_id, name, archetype, voice_tone, mbti,
|
||||
openness, conscientiousness, extraversion, agreeableness, neuroticism,
|
||||
positive_rules, negative_rules, backstory, world_building,
|
||||
guardrails_config, topical_rails, status,
|
||||
default_model, temperature, max_tokens_per_turn,
|
||||
moral_care, moral_fairness, moral_rights,
|
||||
moral_loyalty, moral_authority, moral_sanctity,
|
||||
is_default, created_at, updated_at`
|
||||
|
||||
var p types.PersonaConfig
|
||||
var positiveRules, negativeRules, guardrailsConfig []byte
|
||||
err := s.pool.QueryRow(ctx, query, args...).Scan(
|
||||
&p.ID, &p.TenantID, &p.Name, &p.Archetype, &p.VoiceTone, &p.MBTI,
|
||||
&p.Openness, &p.Conscientiousness, &p.Extraversion, &p.Agreeableness, &p.Neuroticism,
|
||||
&positiveRules, &negativeRules, &p.Backstory, &p.WorldBuilding,
|
||||
&guardrailsConfig, &p.TopicalRails, &p.Status,
|
||||
&p.DefaultModel, &p.Temperature, &p.MaxTokensPerTurn,
|
||||
&p.MoralCare, &p.MoralFairness, &p.MoralRights,
|
||||
&p.MoralLoyalty, &p.MoralAuthority, &p.MoralSanctity,
|
||||
&p.IsDefault, &p.CreatedAt, &p.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update failed: %w", err)
|
||||
}
|
||||
p.PositiveRules = json.RawMessage(positiveRules)
|
||||
p.NegativeRules = json.RawMessage(negativeRules)
|
||||
p.GuardrailsConfig = json.RawMessage(guardrailsConfig)
|
||||
s.logger.Info().Str("id", id.String()).Msg("Updated persona")
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// DeletePersona deletes a persona
|
||||
func (s *PersonaService) DeletePersona(ctx context.Context, id, tenantID uuid.UUID) error {
|
||||
tag, err := s.pool.Exec(ctx, `DELETE FROM persona.personas WHERE id = $1 AND tenant_id = $2`, id, tenantID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete failed: %w", err)
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("persona not found")
|
||||
}
|
||||
s.logger.Info().Str("id", id.String()).Msg("Deleted persona")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSelfModel returns the self-model snapshot for a persona
|
||||
func (s *PersonaService) GetSelfModel(ctx context.Context, personaID, tenantID uuid.UUID) (*types.SelfModelSnapshot, error) {
|
||||
snapshot := &types.SelfModelSnapshot{
|
||||
IdentityConstraints: make([]types.IdentityConstraint, 0),
|
||||
Commitments: make([]types.PersonaCommitment, 0),
|
||||
ConscienceStandards: make([]types.ConscienceStandard, 0),
|
||||
}
|
||||
|
||||
// Identity constraints
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT constraint_type, constraint_text, description, source, strength
|
||||
FROM persona.identity_constraints
|
||||
WHERE persona_id = $1 AND tenant_id = $2 AND is_active = true
|
||||
ORDER BY strength DESC
|
||||
`, personaID, tenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("constraints query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var c types.IdentityConstraint
|
||||
var strength *float64
|
||||
if err := rows.Scan(&c.ConstraintType, &c.ConstraintText, &c.Description, &c.Source, &strength); err != nil {
|
||||
return nil, fmt.Errorf("constraint scan failed: %w", err)
|
||||
}
|
||||
if strength != nil {
|
||||
c.Strength = *strength
|
||||
} else {
|
||||
c.Strength = 1.0
|
||||
}
|
||||
snapshot.IdentityConstraints = append(snapshot.IdentityConstraints, c)
|
||||
}
|
||||
|
||||
// Commitments
|
||||
//
|
||||
// persona_commitments tracks session-bound commitments the assistant
|
||||
// has made during conversation; it has no `source` or `strength`
|
||||
// columns (the active flag is `status='active'`, not `is_active`).
|
||||
// Synthesise both fields for the snapshot so the SelfModel contract
|
||||
// stays stable for callers.
|
||||
commitRows, err := s.pool.Query(ctx, `
|
||||
SELECT commitment_text, COALESCE(commitment_type, '')
|
||||
FROM persona.persona_commitments
|
||||
WHERE persona_id = $1 AND tenant_id = $2 AND status = 'active'
|
||||
ORDER BY created_at DESC
|
||||
`, personaID, tenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("commitments query failed: %w", err)
|
||||
}
|
||||
defer commitRows.Close()
|
||||
|
||||
commitSource := "learned"
|
||||
for commitRows.Next() {
|
||||
var c types.PersonaCommitment
|
||||
if err := commitRows.Scan(&c.CommitmentText, &c.CommitmentType); err != nil {
|
||||
return nil, fmt.Errorf("commitment scan failed: %w", err)
|
||||
}
|
||||
c.Source = &commitSource
|
||||
c.Strength = 1.0
|
||||
snapshot.Commitments = append(snapshot.Commitments, c)
|
||||
}
|
||||
|
||||
// Conscience standards
|
||||
stdRows, err := s.pool.Query(ctx, `
|
||||
SELECT standard_text, standard_type, moral_foundation, strength
|
||||
FROM persona.conscience_standards
|
||||
WHERE persona_id = $1 AND tenant_id = $2 AND is_active = true
|
||||
ORDER BY strength DESC
|
||||
`, personaID, tenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("standards query failed: %w", err)
|
||||
}
|
||||
defer stdRows.Close()
|
||||
|
||||
for stdRows.Next() {
|
||||
var s types.ConscienceStandard
|
||||
var strength *float64
|
||||
if err := stdRows.Scan(&s.StandardText, &s.StandardType, &s.MoralFoundation, &strength); err != nil {
|
||||
return nil, fmt.Errorf("standard scan failed: %w", err)
|
||||
}
|
||||
if strength != nil {
|
||||
s.Strength = *strength
|
||||
} else {
|
||||
s.Strength = 1.0
|
||||
}
|
||||
snapshot.ConscienceStandards = append(snapshot.ConscienceStandards, s)
|
||||
}
|
||||
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// SearchExperiences returns experiences for a persona ordered by importance
|
||||
func (s *PersonaService) SearchExperiences(ctx context.Context, personaID, tenantID uuid.UUID, limit int) ([]types.Experience, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, event_summary, event_type, occurred_at, place,
|
||||
actors, outcome, outcome_detail,
|
||||
emotional_valence, lesson_learned, importance_score
|
||||
FROM persona.experiences
|
||||
WHERE persona_id = $1 AND tenant_id = $2
|
||||
ORDER BY importance_score DESC, occurred_at DESC
|
||||
LIMIT $3
|
||||
`, personaID, tenantID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("experiences query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
experiences := make([]types.Experience, 0)
|
||||
for rows.Next() {
|
||||
var e types.Experience
|
||||
if err := rows.Scan(
|
||||
&e.ID, &e.EventSummary, &e.EventType, &e.OccurredAt, &e.Place,
|
||||
&e.Actors, &e.Outcome, &e.OutcomeDetail,
|
||||
&e.EmotionalValence, &e.LessonLearned, &e.ImportanceScore,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("experience scan failed: %w", err)
|
||||
}
|
||||
experiences = append(experiences, e)
|
||||
}
|
||||
|
||||
return experiences, nil
|
||||
}
|
||||
|
||||
// GetEvaluations returns evaluations for a session
|
||||
func (s *PersonaService) GetEvaluations(ctx context.Context, sessionID uuid.UUID, limit int) ([]types.Evaluation, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT e.role_fidelity, e.voice_consistency,
|
||||
e.safety_compliance, e.character_break,
|
||||
e.drift_score, e.evaluator_model, e.evaluated_at
|
||||
FROM persona.evaluations e
|
||||
JOIN persona.messages m ON m.id = e.message_id
|
||||
WHERE m.session_id = $1
|
||||
ORDER BY e.evaluated_at DESC
|
||||
LIMIT $2
|
||||
`, sessionID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("evaluations query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
evaluations := make([]types.Evaluation, 0)
|
||||
for rows.Next() {
|
||||
var e types.Evaluation
|
||||
if err := rows.Scan(
|
||||
&e.RoleFidelity, &e.VoiceConsistency,
|
||||
&e.SafetyCompliance, &e.CharacterBreak,
|
||||
&e.DriftScore, &e.EvaluatorModel, &e.EvaluatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("evaluation scan failed: %w", err)
|
||||
}
|
||||
evaluations = append(evaluations, e)
|
||||
}
|
||||
|
||||
return evaluations, nil
|
||||
}
|
||||
|
||||
// GetMoralPattern returns moral assessments for a session
|
||||
func (s *PersonaService) GetMoralPattern(ctx context.Context, sessionID, tenantID uuid.UUID) ([]types.MoralAssessment, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT activated_foundations, assessment_text,
|
||||
has_tension, tension_foundations,
|
||||
resolution_foundation, confidence
|
||||
FROM persona.moral_assessments
|
||||
WHERE session_id = $1 AND tenant_id = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
`, sessionID, tenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("moral pattern query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
assessments := make([]types.MoralAssessment, 0)
|
||||
for rows.Next() {
|
||||
var a types.MoralAssessment
|
||||
var activatedFoundations []byte
|
||||
if err := rows.Scan(
|
||||
&activatedFoundations, &a.AssessmentText,
|
||||
&a.HasTension, &a.TensionFoundations,
|
||||
&a.ResolutionFoundation, &a.Confidence,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("moral assessment scan failed: %w", err)
|
||||
}
|
||||
a.ActivatedFoundations = json.RawMessage(activatedFoundations)
|
||||
assessments = append(assessments, a)
|
||||
}
|
||||
|
||||
return assessments, nil
|
||||
}
|
||||
Reference in New Issue
Block a user