Files
Jay Brown 72de690894 Merged in feature/chatbot1 (pull request #223)
Chatbot functionality

* baseline working

* missing test file

* more tests
2026-05-07 20:56:18 +00:00

250 lines
8.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bot
import (
"encoding/json"
"time"
"github.com/google/uuid"
)
// Session is the service-layer representation of a chatbot session.
// Handlers translate this into BotSessionResponse for the wire. Fields
// match plan §3 / §4: lastTurn = last_terminal_ordinal (the highest
// ordinal whose status is not in_flight), never the in-flight ordinal.
type Session struct {
ID uuid.UUID
ClientID string
CreatedBy string
Title string
State json.RawMessage
LastTurn int32
CreatedAt time.Time
UpdatedAt time.Time
Scope SessionScope
RecentTurns []TurnPreview
IsDeleted bool
}
// SessionScope is the persisted (documents, folders) tuple. Returned as
// id arrays per plan §3 — folder expansion to documents lives in the
// M3 FastAPI payload assembly path, not here.
type SessionScope struct {
DocumentIDs []uuid.UUID
FolderIDs []uuid.UUID
}
// TurnPreview is a single recent-turn row surfaced in the session-list
// response. M2 returns the columns the UI needs for a preview card.
type TurnPreview struct {
Ordinal int32
Prompt string
Completion *string
Status string
CreatedAt time.Time
}
// ListSessionsInput drives ListSessionsForOwner / ListSessionsForSuperAdmin.
// Limit and Offset are validated up-front by the handler; the service
// re-validates as defense in depth so non-HTTP callers cannot bypass
// the [1, 200] / >= 0 bounds plan §3 mandates.
type ListSessionsInput struct {
Limit int
Offset int
}
// PatchScopeInput carries the body of PATCH /scope. The service
// deduplicates each array, rejects an id that appears in both add and
// remove sets, and rejects cross-client ids.
type PatchScopeInput struct {
AddDocuments []uuid.UUID
RemoveDocuments []uuid.UUID
AddFolders []uuid.UUID
RemoveFolders []uuid.UUID
}
// ---------- M3: FastAPI agent service wire types ----------
//
// Field shapes are pinned by plans/chatbot.stuff/openapi.chatbot.swagger.json
// (the "Aaria Agent Service" v0.1.0 contract). Plan §7 defines the
// request/response mapping; these structs serialize 1:1 to the swagger
// schema with snake_case JSON tags.
// ChatRequest is the body sent to POST /agent/chat. Plan §7 mapping
// table: request_id is "{sessionId}:{ordinal}" stable across retries;
// user_id is the Cognito sub treated as opaque telemetry; session_id is
// bot_sessions.id as a string; conversation_history is the last N
// completed turns × 2 messages; session_state is bot_sessions.state from
// tx1 (nil when the session has no state); scope is the assembled
// document/folder set.
type ChatRequest struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
SessionID string `json:"session_id"`
Message string `json:"message"`
ConversationHistory []ConversationMessage `json:"conversation_history"`
SessionState *SessionState `json:"session_state,omitempty"`
Scope ChatScope `json:"scope"`
}
// ChatResponse is the body returned by POST /agent/chat. Plan §7
// mandates that updated_session_state == null preserves the prior state
// and an explicit {} overwrites it; the *json.RawMessage type is the
// standard Go idiom for distinguishing those two cases at unmarshal.
type ChatResponse struct {
RequestID string `json:"request_id"`
Status string `json:"status"`
Answer *Answer `json:"answer,omitempty"`
UpdatedSessionState *json.RawMessage `json:"updated_session_state,omitempty"`
Metadata *Metadata `json:"metadata,omitempty"`
Error *ErrorPayload `json:"error,omitempty"`
}
// ChatScope is the assembled document/folder payload sent to FastAPI.
// Both fields are emitted as non-null arrays (the swagger marks them
// required) — empty input produces "[]", not "null".
type ChatScope struct {
DocumentIDs []string `json:"document_ids"`
FolderIDs []string `json:"folder_ids"`
}
// ConversationMessage is one entry in conversation_history. Plan §7:
// each completed turn produces a (user, assistant) pair with the turn's
// created_at as the timestamp.
type ConversationMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Timestamp time.Time `json:"timestamp"`
}
// Answer is the agent's response payload. v1 plan §3 mandates that
// citations live in answer.text markdown; structured_data and
// follow_up_suggestions are response-only fields (logged or surfaced,
// not persisted).
type Answer struct {
Text string `json:"text"`
ResultType string `json:"result_type"`
StructuredData json.RawMessage `json:"structured_data,omitempty"`
FollowUpSuggestions []string `json:"follow_up_suggestions,omitempty"`
Confidence float64 `json:"confidence"`
}
// Metadata is the per-call telemetry from the agent service. The
// authoritative swagger declares Agents as a MAP keyed by agent name
// (NOT an array). SummarizeMetadata sums tokens by iterating map values.
type Metadata struct {
TotalLatencyMs int `json:"total_latency_ms"`
RetryCount int `json:"retry_count"`
Agents map[string]AgentTelemetry `json:"agents"`
}
// AgentTelemetry is one entry in the Metadata.Agents map. Passed is a
// pointer so the field can round-trip null → unset as the swagger allows.
type AgentTelemetry struct {
LatencyMs int `json:"latency_ms"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
Passed *bool `json:"passed,omitempty"`
}
// ErrorPayload is the error-shape returned by FastAPI for a 200 response
// with status:"error" or carried alongside the error sentinel surfaced
// by the client when the upstream fails the application path.
type ErrorPayload struct {
Code string `json:"code"`
Message string `json:"message"`
}
// HealthResponse is the body returned by GET /health. Plan §7 mandates
// status == "ok" plus non-empty service and version.
type HealthResponse struct {
Status string `json:"status"`
Service string `json:"service"`
Version string `json:"version"`
}
// SessionState is the agent-side session-state object. The swagger
// defines it as an open object with no enumerated keys; we use a
// json.RawMessage alias so the client passes the bytes through
// untouched and the M4 tx2 path can apply StripCachedResults /
// SessionStateOversize gates without re-decoding. M4 may refine if
// structured access becomes useful.
type SessionState = json.RawMessage
// ChatCallResult is what FastAPIClient.Chat returns on success. The
// service layer reads SentAt/ReceivedAt for the wall-clock fallback when
// metadata is absent (plan §7 mapping table) and AttemptCount when
// persisting bot_turns.error per plan §6.
type ChatCallResult struct {
Response *ChatResponse
SentAt time.Time
ReceivedAt time.Time
AttemptCount int
}
// AttemptCounter is implemented by every error returned by
// FastAPIClient.Chat so the service layer can record the attempt count
// in bot_turns.error without unwrapping driver-specific types. Plan §7
// mandates the error path expose AttemptCount alongside the result path.
type AttemptCounter interface {
error
AttemptCount() int
}
// ---------- M4: AddTurn types ----------
// AddTurnInput drives Service.AddTurn. Caller specifies Ordinal: the
// handler computes it from session state via prompt-match semantics
// before calling the service; the concurrency test passes Ordinal
// directly. Plan §6 algorithm is by-ordinal.
type AddTurnInput struct {
SessionID uuid.UUID
ClientID string
Actor string
Ordinal int
Prompt string
}
// AddTurnResult is the per-call outcome the handler converts into
// BotTurnResponse. Status is one of the bot_turns.status enum values.
// Error is the persisted error JSON when status in (errored, abandoned,
// session_deleted), nil otherwise.
type AddTurnResult struct {
Ordinal int32
Status string
Completion *string
LatencyMs int
TokensIn int
TokensOut int
Error []byte
AttemptCount int
CreatedAt time.Time
}
// CompleteTurnInput is the argument shape for Service.CompleteBotTurnWithAttempt.
// Plan §11 stale-tx2 test exercises this directly so a stale callback
// (whose AttemptID does not match the persisted row) returns
// ErrTurnSuperseded.
type CompleteTurnInput struct {
SessionID uuid.UUID
Ordinal int
AttemptID uuid.UUID
Completion string
LatencyMs int
TokensIn int
TokensOut int
}
// Turn is the service-layer representation of one bot_turns row. The
// handler converts this into BotTurnResponse for the wire.
type Turn struct {
Ordinal int32
Prompt string
Completion *string
Status string
LatencyMs *int32
TokensIn *int32
TokensOut *int32
Error []byte
CreatedAt time.Time
}