info:
  title: Galya API
  version: 0.01.0
  description: >
    Galya is a preference intelligence and taste infrastructure API.
    It models entities (users, places, brands, content, etc.) as nodes in a
    Subspace Taste Graph and exposes endpoints for indexing content, searching
    and reranking by affinity, generating taste-personalized answers, and
    explaining entity affinities for agent consumption.

servers:
  - url: https://api.galya.io/v1
    description: Production

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  schemas:

    ContentType:
      type: string
      enum: [image, text, audio, video]

    EntityType:
      type: string
      description: >
        Built-in entity types plus any custom types added via POST /entity/type.
      enum: [content, place, brand, user, website, file]

    ContentObject:
      type: object
      description: >
        A raw content artifact. Used as input for indexing and as unindexed
        candidates in search/rerank. Not a graph node itself — becomes a
        content entity upon indexing via POST /index.
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        url:
          type: string
          format: uri
          description: Natural dedup key. If a content entity with this URL already exists in the namespace, /index will resolve to it rather than create a duplicate.
        content:
          type: string
        type:
          $ref: '#/components/schemas/ContentType'
      required: [url, type]

    Affinity:
      type: object
      properties:
        score:
          type: number
          format: float
        cluster_id:
          type: string
      required: [score, cluster_id]

    Entity:
      type: object
      description: >
        A node in the Subspace Taste Graph. Accumulates affinity, gets
        clustered, and is returned in search/rerank results. Content objects
        become content entities after indexing. All other types are created
        via POST /entity.
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        type:
          $ref: '#/components/schemas/EntityType'
        linked_content:
          type: array
          description: Explicit, declared affiliations to raw content artifacts.
          items:
            $ref: '#/components/schemas/ContentObject'
        linked_entities:
          type: array
          description: Explicit, declared affiliations to other graph nodes.
          items:
            $ref: '#/components/schemas/Entity'
      required: [id, name, type]

    Cluster:
      type: object
      properties:
        id:
          type: string
        entity_type:
          $ref: '#/components/schemas/EntityType'
        affinities:
          type: array
          items:
            $ref: '#/components/schemas/Affinity'
        clustered_entities:
          type: array
          items:
            $ref: '#/components/schemas/Entity'
      required: [id, entity_type]

    Domain:
      type: string
      enum: [discovery, design, decisions, data]

    Task:
      type: string
      enum: [coding-agent, commerce-agent, design-agent, insight-agent]

    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
      required: [error, code]

paths:

  /search:
    post:
      summary: Search entities by taste affinity
      description: >
        Searches the namespace for entities ranked by affinity relative to the
        taste profile of a specified entity. Optionally accepts unindexed
        content objects as additional real-time candidates.
      parameters:
        - name: relative_to_entity_id
          in: query
          required: true
          description: The entity whose taste profile is used to rank results.
          schema:
            type: string
        - name: in_terms_of_entity_type
          in: query
          required: true
          description: The entity type to search and rank.
          schema:
            $ref: '#/components/schemas/EntityType'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: Natural language query from the user.
                additional_candidates:
                  type: array
                  description: >
                    Unindexed content artifacts to include in the search space
                    and score against the entity's taste in real-time. Use when
                    content has not yet been indexed into the graph. These are
                    not persisted after the call.
                  items:
                    $ref: '#/components/schemas/ContentObject'
              required: [query]
      responses:
        '200':
          description: Ranked list of entities by affinity.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    description: Ranked highest to lowest affinity relative to the specified entity.
                    items:
                      $ref: '#/components/schemas/Entity'
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /rerank:
    post:
      summary: Rerank a provided candidate set by taste affinity
      description: >
        Reranks a caller-supplied list of content objects by affinity relative
        to the taste profile of a specified entity. Unlike /search, no graph
        lookup is performed — the candidate set is entirely caller-provided.
        Candidates are not persisted after the call.
      parameters:
        - name: relative_to_entity_id
          in: query
          required: true
          description: The entity whose taste profile is used to rank the candidates.
          schema:
            type: string
        - name: in_terms_of_entity_type
          in: query
          required: true
          description: The entity type context for affinity scoring.
          schema:
            $ref: '#/components/schemas/EntityType'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                candidates:
                  type: array
                  description: Content objects to rerank by affinity. These are not persisted after the call.
                  items:
                    $ref: '#/components/schemas/ContentObject'
              required: [candidates]
      responses:
        '200':
          description: Reranked list of entities by affinity.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    description: Ranked highest to lowest affinity relative to the specified entity.
                    items:
                      $ref: '#/components/schemas/Entity'
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /ask:
    post:
      summary: Taste-personalized natural language answer
      description: >
        Returns a direct natural language answer personalized to the taste
        profile of the specified entity. Acts as a self-personalizing LLM
        replacement. Best for discovery and commerce use cases.
        OpenAI-compatible response schema (openresponses) — full compatibility
        is planned.
      parameters:
        - name: relative_to_entity_id
          in: query
          required: true
          description: The entity whose taste profile personalizes the answer.
          schema:
            type: string
        - name: in_terms_of_entity_type
          in: query
          required: true
          description: The entity type context for affinity.
          schema:
            $ref: '#/components/schemas/EntityType'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: Natural language query from the user.
              required: [query]
      responses:
        '200':
          description: Personalized natural language answer.
          content:
            application/json:
              schema:
                type: object
                properties:
                  answer:
                    type: object
                    description: OpenAI openresponses-compatible output.
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /explain:
    post:
      summary: Explain entity affinities for agent consumption
      description: >
        Returns a natural language explanation of an entity's affinities,
        structured for agent consumption. Tailored by domain and task context.
        Simple input/output — no openresponses schema.
      parameters:
        - name: relative_to_entity_id
          in: query
          required: true
          description: The entity to explain affinities for.
          schema:
            type: string
        - name: in_terms_of_entity_type
          in: query
          required: true
          description: The entity type context for affinity.
          schema:
            $ref: '#/components/schemas/EntityType'
        - name: domain
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/Domain'
        - name: task
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/Task'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: Natural language query from the user.
              required: [query]
      responses:
        '200':
          description: Natural language explanation of entity affinities.
          content:
            application/json:
              schema:
                type: object
                properties:
                  explanation:
                    type: string
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /index:
    post:
      summary: Index a content object into the graph
      description: >
        Ingests a content object into the graph, creating or resolving a
        content entity. If a content entity with the same URL already exists
        in the namespace, returns the existing entity ID (idempotent).
        Optionally connects the content entity to an existing entity.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  $ref: '#/components/schemas/ContentObject'
                entity_id:
                  type: string
                  description: >
                    Optional. Entity to connect the content entity to after
                    indexing. If omitted, the content entity is created as a
                    standalone node.
              required: [content]
      responses:
        '200':
          description: Content entity created or resolved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  entity_id:
                    type: string
                    description: The entity ID of the created or resolved content entity.
                  created:
                    type: boolean
                    description: True if a new entity was created, false if resolved from an existing URL.
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /index/batch:
    post:
      summary: Batch index content objects into the graph
      description: >
        Same as POST /index but accepts lists of content objects and optional
        entity_ids. Each content object is processed independently —
        idempotency applies per URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: array
                  items:
                    $ref: '#/components/schemas/ContentObject'
                entity_ids:
                  type: array
                  description: Optional. Parallel list of entity IDs to connect each content object to. Use null for standalone nodes.
                  items:
                    type: string
                    nullable: true
              required: [content]
      responses:
        '200':
          description: Batch index results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        entity_id:
                          type: string
                        created:
                          type: boolean
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /entity:
    post:
      summary: Create a new entity
      description: >
        Creates a non-content entity in the namespace. Content entities must
        be created via POST /index, not this endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                entity_type:
                  $ref: '#/components/schemas/EntityType'
                name:
                  type: string
                description:
                  type: string
              required: [entity_type, name]
      responses:
        '201':
          description: Entity created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  entity_id:
                    type: string
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: Retrieve an entity
      parameters:
        - name: entity_id
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Entity object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Entity'
        '404':
          description: Entity not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    delete:
      summary: Delete an entity
      parameters:
        - name: entity_id
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Entity deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          description: Entity not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /entity/type:
    post:
      summary: Create a custom entity type
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                entity_type:
                  type: string
                  description: The new entity type name to create.
              required: [entity_type]
      responses:
        '201':
          description: Entity type created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          description: Bad request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: Retrieve a custom entity type definition
      parameters:
        - name: entity_type
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Entity type definition.
          content:
            application/json:
              schema:
                type: object
                properties:
                  entity_type:
                    type: string
        '404':
          description: Entity type not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /clusters:
    get:
      summary: List entity clusters in the namespace
      description: >
        Retrieves the most popular entity clusters scoped to the namespace.
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
        - name: entity_type
          in: query
          required: false
          description: Filter clusters by entity type.
          schema:
            $ref: '#/components/schemas/EntityType'
      responses:
        '200':
          description: List of cluster objects.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Cluster'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /cluster:
    get:
      summary: Retrieve a specific cluster
      description: Retrieves a particular entity cluster scoped to the namespace.
      parameters:
        - name: cluster_id
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Cluster object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cluster'
        '404':
          description: Cluster not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
