72de690894
Chatbot functionality * baseline working * missing test file * more tests
250 lines
8.8 KiB
Go
250 lines
8.8 KiB
Go
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
|
||
}
|