openapi: 3.1.0
info:
  title: Decisions AI — Internal Edge Functions
  version: 0.1.0
  summary: The four user-facing edge functions of the Decisions AI internal API.
  description: |
    This spec only covers the edge functions. The bulk of the API surface
    (every table CRUD, every Realtime channel) is auto-generated by Supabase
    PostgREST and Supabase Realtime; consume those via `@moei/decisions-sdk`
    or directly with `@supabase/supabase-js`. The full reference, including
    the schema and Realtime catalog, lives at <https://decisions.heos.ae/api.md>.
  contact:
    name: Decisions AI
    url: https://decisions.heos.ae/api.md
servers:
  - url: https://lydtdudifolcdzunlvtu.supabase.co/functions/v1
    description: Production
security:
  - apiKey: []
    bearerAuth: []
paths:
  /run-pipeline:
    post:
      operationId: runPipeline
      summary: Kick off / resume the multi-agent worker for a decision
      description: |
        Idempotent and fire-and-forget. Returns immediately (HTTP 200/202)
        once the worker has been launched (or already-running). The actual
        agent panel runs server-side inside `EdgeRuntime.waitUntil()`,
        persists incremental progress to `runs` and `run_steps`, and
        survives both the browser closing and the function's 400 s wall.
        A `pg_cron` job named `resume_stale_runs` re-kicks abandoned runs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RunPipelineRequest"
      responses:
        "200":
          description: Worker launched (or already running).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunPipelineResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "405":
          description: Method not allowed.
      x-codeSamples:
        - lang: shell
          label: curl
          source: |
            curl -sS -X POST "$DAI_URL/functions/v1/run-pipeline" \
              -H "apikey: $DAI_KEY" \
              -H "authorization: Bearer $DAI_KEY" \
              -H "content-type: application/json" \
              -d '{"decision_id":"f3b1e9c2-2a8d-4f3a-9e9f-3a1f1c8d2c0a"}'
        - lang: js
          label: SDK
          source: |
            import { runPipeline } from "@moei/decisions-sdk";
            const { status } = await runPipeline(client, decisionId);

  /chat:
    post:
      operationId: chat
      summary: Stream a refinement chat reply for a finished decision (SSE)
      description: |
        Server-Sent Events stream. The user message must be persisted to
        `chat_messages` by the client before calling — this endpoint only
        streams and persists the assistant reply. The final assistant row
        is inserted automatically once the stream completes; subscribers
        on the `chat_messages` Realtime channel see it without consuming
        the SSE stream.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatRequest"
      responses:
        "200":
          description: SSE stream.
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Newline-delimited SSE frames. Each frame is
                  `data: <json>\n\n` where `<json>` matches one of the
                  `ChatStreamEvent` shapes below. The final frame is
                  `data: [DONE]`.
              examples:
                stream:
                  value: |
                    data: {"type":"message_started","messageId":"a..."}

                    data: {"type":"message_delta","messageId":"a...","delta":"The "}

                    data: {"type":"message_delta","messageId":"a...","delta":"top three risks "}

                    data: {"type":"message_complete","messageId":"a...","content":"The top three risks ..."}

                    data: [DONE]
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: shell
          label: curl
          source: |
            curl -sS -N -X POST "$DAI_URL/functions/v1/chat" \
              -H "apikey: $DAI_KEY" -H "authorization: Bearer $DAI_KEY" \
              -H "content-type: application/json" -H "accept: text/event-stream" \
              -d '{"decision_id":"f3b1e9c2-...","message":"Summarise the top three risks."}'
        - lang: js
          label: SDK
          source: |
            import { chat } from "@moei/decisions-sdk";
            await chat(client, {
              decisionId,
              message: "Summarise the top three risks.",
              onEvent: (e) => {
                if (e.type === "message_delta") process.stdout.write(e.delta);
              },
            });

  /ingest-document:
    post:
      operationId: ingestDocument
      summary: Extract text from a previously uploaded document
      description: |
        Downloads the file from the `documents` Storage bucket, extracts text
        (PDF via pdf.js, DOCX via mammoth, plain text directly, images leave
        `extracted_text` empty so vision agents read bytes inline), and
        writes the result to `documents.extracted_text` and `documents.summary`.
        Hard 50 MB upper bound. `uploadDocument()` already calls this for you.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IngestDocumentRequest"
      responses:
        "200":
          description: Extraction succeeded (or was empty for an image).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestDocumentResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "413":
          description: Document exceeds the 50 MB upper bound.
          content:
            application/json:
              schema:
                type: object
                required: [error, size_bytes]
                properties:
                  error:
                    type: string
                    example: document exceeds 50MB limit
                  size_bytes:
                    type: integer
        "422":
          description: Extraction failed (e.g. corrupt PDF). Suppressed for images.
          content:
            application/json:
              schema:
                type: object
                required: [ok, error, preview]
                properties:
                  ok:
                    type: boolean
                    enum: [false]
                  error:
                    type: string
                  preview:
                    type: string
                    enum: [""]

  /suggest-followups:
    post:
      operationId: suggestFollowups
      summary: Generate 4 follow-up chat prompts for a finished decision
      description: |
        Cached on `decisions.suggested_followups`. Pass `force: true` to
        regenerate. If the briefing isn't ready yet (`final_markdown`
        shorter than 120 chars) the endpoint returns
        `{"suggestions":[],"cached":false}` with HTTP 200 — not an error.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SuggestFollowupsRequest"
      responses:
        "200":
          description: Suggestions (possibly empty if the briefing isn't ready).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuggestFollowupsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          description: Anthropic call failed.
          content:
            application/json:
              schema:
                type: object
                required: [error, suggestions]
                properties:
                  error:
                    type: string
                  suggestions:
                    type: array
                    items:
                      type: string

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: apikey
      description: Supabase publishable anon key.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: anon-key
      description: Same value as `apikey`. Both headers must be present.
  responses:
    BadRequest:
      description: Required field missing or malformed JSON body.
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: string
                example: decision_id required
    NotFound:
      description: Referenced row does not exist.
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error:
                type: string
                example: decision not found
  schemas:
    RunPipelineRequest:
      type: object
      required: [decision_id]
      properties:
        decision_id:
          type: string
          format: uuid
          description: Primary key of `public.decisions`.
    RunPipelineResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [launched, "already-running"]
        decision_id:
          type: string
          format: uuid

    ChatRequest:
      type: object
      required: [decision_id, message]
      properties:
        decision_id:
          type: string
          format: uuid
        message:
          type: string
          description: The user's message. Must already be persisted to `chat_messages` if you want it to appear in the backlog.
    ChatStreamEvent:
      oneOf:
        - $ref: "#/components/schemas/ChatStreamMessageStarted"
        - $ref: "#/components/schemas/ChatStreamMessageDelta"
        - $ref: "#/components/schemas/ChatStreamMessageComplete"
        - $ref: "#/components/schemas/ChatStreamMessageFailed"
      discriminator:
        propertyName: type
        mapping:
          message_started: "#/components/schemas/ChatStreamMessageStarted"
          message_delta: "#/components/schemas/ChatStreamMessageDelta"
          message_complete: "#/components/schemas/ChatStreamMessageComplete"
          message_failed: "#/components/schemas/ChatStreamMessageFailed"
    ChatStreamMessageStarted:
      type: object
      required: [type, messageId]
      properties:
        type:
          type: string
          enum: [message_started]
        messageId:
          type: string
          format: uuid
    ChatStreamMessageDelta:
      type: object
      required: [type, messageId, delta]
      properties:
        type:
          type: string
          enum: [message_delta]
        messageId:
          type: string
          format: uuid
        delta:
          type: string
    ChatStreamMessageComplete:
      type: object
      required: [type, messageId, content]
      properties:
        type:
          type: string
          enum: [message_complete]
        messageId:
          type: string
          format: uuid
        content:
          type: string
    ChatStreamMessageFailed:
      type: object
      required: [type, messageId, error]
      properties:
        type:
          type: string
          enum: [message_failed]
        messageId:
          type: string
          format: uuid
        error:
          type: string

    IngestDocumentRequest:
      type: object
      required: [document_id]
      properties:
        document_id:
          type: string
          format: uuid
    IngestDocumentResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          enum: [true]
        preview:
          type: string
          description: First 400 chars of the extracted text.

    SuggestFollowupsRequest:
      type: object
      required: [decision_id]
      properties:
        decision_id:
          type: string
          format: uuid
        force:
          type: boolean
          default: false
          description: If true, ignore the cached value on `decisions.suggested_followups`.
    SuggestFollowupsResponse:
      type: object
      required: [suggestions, cached]
      properties:
        suggestions:
          type: array
          items:
            type: string
          description: 0-6 short follow-up prompts in the briefing's language.
        cached:
          type: boolean
