Skip to content

Refine & Feedback

Gaia provides two post-query operations that improve answer quality over time: Refine narrows an answer to specific documents, and Feedback signals whether an answer was helpful. Both use the queryUid returned by the initial /ask call.


Refine: Focused Answers from Selected Documents

The Problem

When Gaia answers a question, it retrieves multiple documents and synthesizes an answer from all of them. Sometimes the retrieved set includes marginally relevant documents that dilute the answer. Refine lets the user (or your application logic) select the most relevant documents and regenerate the answer using only those.

How It Works

Text Only
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ 1. Initial /ask │────▶│ 2. User reviews  │────▶│ 3. POST /refine │
│    Returns docs │     │    documents,    │     │    with selected│
│    + answer     │     │    selects best  │     │    docIds       │
│    + queryUid   │     │    ones          │     │    → new answer │
└─────────────────┘     └─────────────────┘     └─────────────────┘

API: POST /ask/refine

Request body:

JSON
{
  "queryUid": "abc-123",
  "datasetNames": ["quarterly-reports"],
  "queryString": "What was our Q4 revenue?",
  "docIds": ["doc-7", "doc-12"]
}
Field Type Description
queryUid string The queryUid from the original /ask response
datasetNames string[] Same dataset(s) used in the original query
queryString string The original (or modified) question
docIds string[] Document IDs to use for the refined answer

Response:

JSON
{
  "responseString": "Based on the selected financial statements, Q4 revenue was $4.2B...",
  "queryUid": "def-456",
  "documents": [
    {
      "docId": "doc-7",
      "filename": "q4-financials.pdf",
      "snippet": "Total revenue for Q4 was $4.2 billion..."
    }
  ]
}

Backend Implementation

Using the SDK:

Python
from fastapi import APIRouter, Depends
from gaia_sdk import GaiaClient
from backend.api.dependencies import get_gaia_client
from pydantic import BaseModel, Field

router = APIRouter()


class RefineRequest(BaseModel):
    query_uid: str = Field(alias="queryUid")
    dataset_names: list[str] = Field(alias="datasetNames")
    query: str = Field(alias="queryString")
    doc_ids: list[str] = Field(alias="docIds")

    model_config = {"populate_by_name": True}


class RefineResponse(BaseModel):
    response: str | None
    query_uid: str | None
    documents: list | None = None


@router.post("/ask/refine", response_model=RefineResponse, tags=["Ask"])
async def refine(
    request: RefineRequest,
    client: GaiaClient = Depends(get_gaia_client),
):
    result = await client.refine(
        query_uid=request.query_uid,
        dataset_names=request.dataset_names,
        query=request.query,
        doc_ids=request.doc_ids,
    )
    return RefineResponse(
        response=result.response_string,
        query_uid=result.query_uid,
        documents=result.documents,
    )

Using raw httpx:

Python
async def refine_answer(
    api_key: str,
    query_uid: str,
    dataset_names: list[str],
    query: str,
    doc_ids: list[str],
) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://helios.cohesity.com/v2/mcm/gaia/ask/refine",
            headers={"apiKey": api_key, "Content-Type": "application/json"},
            json={
                "queryUid": query_uid,
                "datasetNames": dataset_names,
                "queryString": query,
                "docIds": doc_ids,
            },
        )
        response.raise_for_status()
        return response.json()

Frontend: Document Selection UI

Build a component that shows source documents from the initial answer and lets the user select which ones to use for refinement.

TSX
// src/components/DocumentSelector.tsx

import { useState } from "react";

interface Document {
  docId: string;
  filename: string;
  snippet: string;
  score: number;
}

interface Props {
  documents: Document[];
  onRefine: (selectedDocIds: string[]) => void;
  isLoading?: boolean;
}

export function DocumentSelector({ documents, onRefine, isLoading }: Props) {
  const [selected, setSelected] = useState<Set<string>>(new Set());

  const toggle = (docId: string) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(docId)) {
        next.delete(docId);
      } else {
        next.add(docId);
      }
      return next;
    });
  };

  return (
    <div className="border rounded-lg p-4 space-y-3">
      <h3 className="font-semibold">Source Documents</h3>
      <p className="text-sm text-gray-500">
        Select relevant documents to refine the answer.
      </p>
      <ul className="space-y-2">
        {documents.map((doc) => (
          <li
            key={doc.docId}
            className={`border rounded p-3 cursor-pointer transition ${
              selected.has(doc.docId)
                ? "border-purple-500 bg-purple-50"
                : "border-gray-200 hover:border-gray-300"
            }`}
            onClick={() => toggle(doc.docId)}
          >
            <div className="flex items-center justify-between">
              <span className="font-medium text-sm">{doc.filename}</span>
              <span className="text-xs text-gray-400">
                Score: {doc.score?.toFixed(2)}
              </span>
            </div>
            <p className="text-sm text-gray-600 mt-1 line-clamp-2">
              {doc.snippet}
            </p>
          </li>
        ))}
      </ul>
      <button
        onClick={() => onRefine(Array.from(selected))}
        disabled={selected.size === 0 || isLoading}
        className="w-full rounded bg-purple-600 px-4 py-2 text-white text-sm font-medium hover:bg-purple-700 disabled:opacity-50"
      >
        {isLoading ? "Refining..." : `Refine with ${selected.size} document(s)`}
      </button>
    </div>
  );
}

The API call:

TypeScript
// src/api/gaia.ts

export async function refine(
  queryUid: string,
  datasetNames: string[],
  query: string,
  docIds: string[],
): Promise<AskResponse> {
  return api.post<AskResponse>("/ask/refine", {
    queryUid,
    datasetNames,
    queryString: query,
    docIds,
  });
}

Feedback: Thumbs Up / Thumbs Down

Why Feedback Matters

Feedback helps Gaia's underlying models improve over time. When users rate answers, Gaia can adjust ranking and retrieval for future queries on the same datasets.

API: POST /query-feedback

Request body:

JSON
{
  "queryUid": "abc-123",
  "isGood": true,
  "feedbackText": "Accurate and well-sourced answer."
}
Field Type Description
queryUid string The queryUid from the /ask response
isGood boolean true for positive, false for negative feedback
feedbackText string (optional) Free-text explanation from the user

Response: 200 OK with an empty body or acknowledgment.

Backend Implementation

Python
class FeedbackRequest(BaseModel):
    query_uid: str = Field(alias="queryUid")
    is_good: bool = Field(alias="isGood")
    feedback_text: str | None = Field(None, alias="feedbackText")

    model_config = {"populate_by_name": True}


@router.post("/feedback", tags=["Feedback"])
async def send_feedback(
    request: FeedbackRequest,
    client: GaiaClient = Depends(get_gaia_client),
):
    await client.send_feedback(
        query_uid=request.query_uid,
        is_good=request.is_good,
        feedback_text=request.feedback_text,
    )
    return {"status": "ok"}

Frontend: Feedback Buttons

TSX
// src/components/FeedbackButtons.tsx

import { useState } from "react";
import { api } from "../api/client";

interface Props {
  queryUid: string;
}

export function FeedbackButtons({ queryUid }: Props) {
  const [submitted, setSubmitted] = useState<boolean | null>(null);

  const handleFeedback = async (isGood: boolean) => {
    try {
      await api.post("/feedback", { queryUid, isGood });
      setSubmitted(isGood);
    } catch {
      // best effort — don't block UI on feedback failure
    }
  };

  if (submitted !== null) {
    return (
      <span className="text-sm text-gray-400">
        {submitted ? "👍 Thanks!" : "👎 We'll improve."}
      </span>
    );
  }

  return (
    <div className="flex gap-2">
      <button
        onClick={() => handleFeedback(true)}
        className="text-sm text-gray-500 hover:text-green-600"
        title="Good answer"
      >
        👍
      </button>
      <button
        onClick={() => handleFeedback(false)}
        className="text-sm text-gray-500 hover:text-red-600"
        title="Bad answer"
      >
        👎
      </button>
    </div>
  );
}

Integrate into ChatMessage:

TSX
{!message.isStreaming && message.queryUid && (
  <div className="mt-2 flex items-center gap-3">
    <FeedbackButtons queryUid={message.queryUid} />
  </div>
)}

Putting It Together: The Refine + Feedback Flow

Text Only
1. User asks:  "What was our Q4 revenue?"
   → POST /ask → response + documents + queryUid

2. User sees answer + source documents
   → Clicks 👍 or 👎
   → POST /query-feedback { queryUid, isGood }

3. User selects 2 of 5 documents as most relevant
   → Clicks "Refine"
   → POST /ask/refine { queryUid, docIds: [...] }
   → New, more focused answer

4. User rates the refined answer
   → POST /query-feedback { queryUid: newQueryUid, isGood }

Refine returns a new queryUid

The refined answer has its own queryUid. Use this new UID when sending feedback on the refined answer — not the original query's UID.


Next Steps