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 }