feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package extraction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/outbox"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const DirectStoryEvent = "call.story_extraction_requested"
|
||||
|
||||
type directStorySnapshot struct {
|
||||
extractionSnapshot
|
||||
revision, epoch int64
|
||||
ready, processed, reviewed bool
|
||||
}
|
||||
|
||||
func loadDirectStorySnapshot(ctx context.Context, q queryer, sid string) (directStorySnapshot, error) {
|
||||
var v directStorySnapshot
|
||||
err := q.QueryRow(ctx, `SELECT c.tenant_id::text,c.subject_id::text,c.call_state,c.transcript_revision,c.privacy_epoch,
|
||||
c.ended_at IS NOT NULL AND c.transcript_state IN ('COMPLETE','PARTIAL'),call_story_authorized(c.id),
|
||||
EXISTS(SELECT 1 FROM call_story_extraction e WHERE e.session_id=c.id AND e.transcript_revision=c.transcript_revision AND e.privacy_epoch=c.privacy_epoch),
|
||||
EXISTS(SELECT 1 FROM story_draft sd WHERE sd.call_session_id=c.id AND (sd.status IN ('IN_REVIEW','CONFIRMED','REJECTED')
|
||||
OR (sd.status='CANDIDATE' AND EXISTS(SELECT 1 FROM fact_candidate fc WHERE fc.story_id=sd.id AND (fc.version>1 OR fc.status<>'CANDIDATE')))))
|
||||
FROM call_session c WHERE c.id=$1::uuid AND c.budget_snapshot->>'protocol'='DIRECT-CHAT-1'`, sid).Scan(
|
||||
&v.tenantID, &v.subjectID, &v.status, &v.revision, &v.epoch, &v.ready, &v.storyExtractionAllowed, &v.processed, &v.reviewed)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return v, nil
|
||||
}
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.exists = true
|
||||
if !v.storyExtractionAllowed || !v.ready || v.processed {
|
||||
return v, nil
|
||||
}
|
||||
rows, err := q.Query(ctx, `SELECT u.utterance_id,u.text_content,u.interrupted,u.source_revision
|
||||
FROM call_utterance u JOIN call_session c ON c.id=u.session_id WHERE u.session_id=$1::uuid AND u.speaker='USER'
|
||||
AND u.text_content<>'[deleted]' AND length(trim(u.text_content))>0
|
||||
AND NOT EXISTS(SELECT 1 FROM memory_source_suppression ms CROSS JOIN LATERAL jsonb_array_elements(ms.source_refs) r
|
||||
WHERE ms.subject_id=c.subject_id AND r->>'session_id'=c.id::text AND (r->>'utterance_id' IS NULL OR r->>'utterance_id'=u.utterance_id))
|
||||
ORDER BY u.source_at,u.utterance_id`, sid)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var item turn
|
||||
if err = rows.Scan(&item.id, &item.text, &item.interrupted, &item.sourceRevision); err != nil {
|
||||
return v, err
|
||||
}
|
||||
item.publicID, item.speaker = item.id, "subject"
|
||||
v.turns = append(v.turns, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return v, err
|
||||
}
|
||||
v.digest = snapshotDigest(v.extractionSnapshot)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// HandleDirectStory shares the existing story generator and review tables, but
|
||||
// never turns provider summaries or assistant speech into the elder's story.
|
||||
func (p *Processor) HandleDirectStory(ctx context.Context, event outbox.Event) error {
|
||||
var request struct {
|
||||
GenerationVersion int64 `json:"generation_version"`
|
||||
PrivacyEpoch int64 `json:"privacy_epoch"`
|
||||
}
|
||||
if json.Unmarshal(event.Payload, &request) != nil || request.GenerationVersion <= 0 || request.PrivacyEpoch < 0 {
|
||||
return errors.New("invalid direct story extraction request")
|
||||
}
|
||||
snapshot, err := loadDirectStorySnapshot(ctx, p.pool, event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !snapshot.exists || !snapshot.storyExtractionAllowed || snapshot.processed || snapshot.revision != request.GenerationVersion || snapshot.epoch != request.PrivacyEpoch {
|
||||
return nil
|
||||
}
|
||||
if !snapshot.ready {
|
||||
return outbox.Retryable(ErrConversationContentNotReady)
|
||||
}
|
||||
var generated GenerationResult
|
||||
var previous []PreviousStory
|
||||
multi := false
|
||||
if g, ok := p.generator.(interface{ SupportsEventStories() bool }); ok {
|
||||
multi = g.SupportsEventStories()
|
||||
}
|
||||
outcome := "EMPTY"
|
||||
if snapshot.reviewed && !multi {
|
||||
outcome = "REVIEWED"
|
||||
} else if len(snapshot.turns) > 0 {
|
||||
generated, previous, err = p.generateEventsSafely(ctx, snapshot.extractionSnapshot, time.Now().In(time.FixedZone("CST", 8*3600)).Format("2006-01-02"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outcome = "NO_STORY"
|
||||
if generated.Meaningful && generated.StoryWorthy && generated.AllowStory {
|
||||
outcome = "STORY"
|
||||
}
|
||||
}
|
||||
// The external model call has no open transaction. Lock privacy first, then
|
||||
// the call, and re-read in a new statement after any lock wait.
|
||||
tx, err := p.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(context.Background())
|
||||
if outcome != "REVIEWED" && (generated.Stories != nil || generated.DailyEvents != nil) {
|
||||
if err = lockStoryContext(ctx, tx, snapshot.extractionSnapshot, previous); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var locked string
|
||||
if err = tx.QueryRow(ctx, `SELECT subject_id::text FROM memory_user_state WHERE subject_id=$1::uuid FOR UPDATE`, snapshot.subjectID).Scan(&locked); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = tx.QueryRow(ctx, `SELECT id::text FROM call_session WHERE id=$1::uuid FOR UPDATE`, event.AggregateID).Scan(&locked); err != nil {
|
||||
return err
|
||||
}
|
||||
current, err := loadDirectStorySnapshot(ctx, tx, event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !current.exists || !current.storyExtractionAllowed || current.processed || current.revision != request.GenerationVersion || current.epoch != request.PrivacyEpoch {
|
||||
return nil
|
||||
}
|
||||
if !current.ready {
|
||||
return outbox.Retryable(ErrConversationContentNotReady)
|
||||
}
|
||||
if current.reviewed && !multi {
|
||||
outcome = "REVIEWED"
|
||||
} else if current.digest != snapshot.digest {
|
||||
return outbox.Retryable(ErrConversationContentChanged)
|
||||
}
|
||||
if outcome != "REVIEWED" {
|
||||
// Late final subtitles replace only untouched candidates. Human edits,
|
||||
// confirmations and rejections are never overwritten by background work.
|
||||
if _, err = tx.Exec(ctx, `UPDATE fact_candidate SET status='SUPERSEDED',version=version+1,updated_at=now() WHERE story_id IN
|
||||
(SELECT id FROM story_draft WHERE call_session_id=$1::uuid AND status='CANDIDATE' AND generation_version<$2)`, event.AggregateID, current.revision); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `UPDATE story_draft SET status='SUPERSEDED',version=version+1,updated_at=now()
|
||||
WHERE call_session_id=$1::uuid AND status='CANDIDATE' AND generation_version<$2`, event.AggregateID, current.revision); err != nil {
|
||||
return err
|
||||
}
|
||||
if generated.Stories != nil || generated.DailyEvents != nil {
|
||||
if err = storeEventStories(ctx, tx, "", event.AggregateID, "", current.epoch, current.extractionSnapshot, generated, current.revision, previous); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if outcome == "STORY" {
|
||||
if err = storeStoryCandidate(ctx, tx, "", event.AggregateID, current.epoch, current.extractionSnapshot, generated, current.revision); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO call_story_extraction(session_id,transcript_revision,privacy_epoch,outcome,model_ref,prompt_version)
|
||||
VALUES($1::uuid,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING`, event.AggregateID, current.revision, current.epoch, outcome, generated.ModelRef, generated.PromptVersion); err != nil {
|
||||
return fmt.Errorf("store direct story extraction result: %w", err)
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user