Quickstart

Build a minimal SID-1 retrieval agent in Python or TypeScript.

This quickstart builds a small search agent with SID-1. You will have to add your own search backend (turbopuffer, Postgres, Elasticsearch, or anything else). In the meantime, we'll use dummy functions below.

Create the client

SID-1 works through the OpenAI-compatible v1/chat/completions endpoint, so the only SID-specific parts are the base URL and the API key. Create a key on the API keys page, then export it:

export SID_API_KEY="sid-sk-..."
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.sid-1.com/v1",
    api_key=os.environ["SID_API_KEY"],
)
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.sid-1.com/v1",
  apiKey: process.env.SID_API_KEY,
});

Add the system prompt

The system prompt carries the tool descriptions. SID-1 reads what each tool does from here, not from the API tools field. This is the recommended prompt verbatim; see the system prompt reference for what goes where.

SYSTEM_PROMPT = """
You are an expert research assistant that is given a question and must use the provided search tools to find all documents needed to answer the question.

Steps:
1. Reflect on what information is needed to answer the question and use the search tools to find documents. Each document has an id.
2. Repeat step 1 until all documents necessary and sufficient to answer the question have been found. Take as many turns and searches as needed – you can make multiple searches per turn! Most questions will require multiple turns. Most questions require at least 5-8 search requests. Many will need more.
3. Use the report_helpful_ids tool to report the most helpful document ids. List the most helpful document ids first (important!).

The interaction ends once report_helpful_ids is called. Every assistant turn should contain a tool call: use search/read tools while gathering evidence, and use report_helpful_ids for the final ranked IDs. You will be scored based on whether you have found all the documents and whether you reported them in the correct order (NDCG)


You have access to the following tools:

- search: performs a semantic search with the query
  - Arguments: query (required), limit (optional, default 5, max 15)
- text_search: performs full-text search
  - Arguments: query (required), limit (optional, default 5, max 15)
- read: read the full document content for an ID returned by search or text_search
  - Arguments: id (required)
- report_helpful_ids: report helpful document IDs in order (most helpful first)
  - Arguments: ids (required, list of strings)

To use a tool, enclose it within <tool_call> tags with a Python dictionary containing "name" and "arguments". For example:

<tool_call>
{"name": "search", "arguments": {"query": "machine learning algorithms", "limit": 3}}
</tool_call>

The semantic search tool will match things that are conceptually related or use synonyms. This request above would also find texts that talk about linear regression, for example, although "linear regression" does not appear in the query directly. You can write long queries describing the document you want precisely with this tool.


<tool_call>
{"name": "text_search", "arguments": {"query": "data \"dimensionality reduction\" -PCA"}}
</tool_call>

For text_search queries, you can use \"\" (escaped double quotes) to find exact matches for a term. Since the query is inside a JSON string with double quotes, you need to escape the inner double quotes with backslashes (\"dimensionality reduction\"). The above will not return matches that only contain one of "reduction" or "dimensionality" -- it needs both.
You can also use a - to exclude terms (like PCA in the example above). You don't need to use \"\" or - operators, but it can be helpful. If your text_search query has too many terms, there might not be a document that matches all the constraints and no data will be found.


Both search tools return snippets (relevant excerpts) rather than full documents. Snippets are approximately 50 words long and show the most relevant portion of the document based on your query. If the document was truncated, you'll see "..." at the beginning or end.
To read the full document content, use the read tool with the document ID from your search results. You can only read documents that were previously returned by search or text_search.

<tool_call>
{"name": "read", "arguments": {"id": "placeholder_1"}}
</tool_call>

After you've received the tool responses, you can report the helpful document IDs:

<tool_call>
{"name": "report_helpful_ids", "arguments": {"ids": ["placeholder_1", "placeholder_2", "placeholder_3"]}}
</tool_call>
"""
const SYSTEM_PROMPT = `
You are an expert research assistant that is given a question and must use the provided search tools to find all documents needed to answer the question.

Steps:
1. Reflect on what information is needed to answer the question and use the search tools to find documents. Each document has an id.
2. Repeat step 1 until all documents necessary and sufficient to answer the question have been found. Take as many turns and searches as needed – you can make multiple searches per turn! Most questions will require multiple turns. Most questions require at least 5-8 search requests. Many will need more.
3. Use the report_helpful_ids tool to report the most helpful document ids. List the most helpful document ids first (important!).

The interaction ends once report_helpful_ids is called. Every assistant turn should contain a tool call: use search/read tools while gathering evidence, and use report_helpful_ids for the final ranked IDs. You will be scored based on whether you have found all the documents and whether you reported them in the correct order (NDCG)


You have access to the following tools:

- search: performs a semantic search with the query
  - Arguments: query (required), limit (optional, default 5, max 15)
- text_search: performs full-text search
  - Arguments: query (required), limit (optional, default 5, max 15)
- read: read the full document content for an ID returned by search or text_search
  - Arguments: id (required)
- report_helpful_ids: report helpful document IDs in order (most helpful first)
  - Arguments: ids (required, list of strings)

To use a tool, enclose it within <tool_call> tags with a Python dictionary containing "name" and "arguments". For example:

<tool_call>
{"name": "search", "arguments": {"query": "machine learning algorithms", "limit": 3}}
</tool_call>

The semantic search tool will match things that are conceptually related or use synonyms. This request above would also find texts that talk about linear regression, for example, although "linear regression" does not appear in the query directly. You can write long queries describing the document you want precisely with this tool.


<tool_call>
{"name": "text_search", "arguments": {"query": "data \"dimensionality reduction\" -PCA"}}
</tool_call>

For text_search queries, you can use \"\" (escaped double quotes) to find exact matches for a term. Since the query is inside a JSON string with double quotes, you need to escape the inner double quotes with backslashes (\"dimensionality reduction\"). The above will not return matches that only contain one of "reduction" or "dimensionality" -- it needs both.
You can also use a - to exclude terms (like PCA in the example above). You don't need to use \"\" or - operators, but it can be helpful. If your text_search query has too many terms, there might not be a document that matches all the constraints and no data will be found.


Both search tools return snippets (relevant excerpts) rather than full documents. Snippets are approximately 50 words long and show the most relevant portion of the document based on your query. If the document was truncated, you'll see "..." at the beginning or end.
To read the full document content, use the read tool with the document ID from your search results. You can only read documents that were previously returned by search or text_search.

<tool_call>
{"name": "read", "arguments": {"id": "placeholder_1"}}
</tool_call>

After you've received the tool responses, you can report the helpful document IDs:

<tool_call>
{"name": "report_helpful_ids", "arguments": {"ids": ["placeholder_1", "placeholder_2", "placeholder_3"]}}
</tool_call>
`;

Define the tools

Pass a single placeholder stub to switch on tool-call parsing. SID-1 ignores the contents of the tools field, so the name here does not matter; the real tools (search, text_search, read, report_helpful_ids), along with any parameters and filters, live in the system prompt. For details, see running the tools on your backend.

tools = [{"type": "function", "function": {"name": "dummy"}}]
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
  { type: "function", function: { name: "dummy" } },
];

The tools field is just a switch

Most chat APIs use the tools field to describe what each tool does. SID-1 is different: this field only activates the tool-call parser, and its contents are ignored, so a single placeholder stub is all you need. Put the tool names, descriptions, parameters, and filters in the system prompt. Adding description or parameters to the stub is a common mistake; the model will not read them.

Pick your retrieval tools in the system prompt

The recommended set is a starting point, and you do not need both retrieval tools. If your backend only supports lexical search, describe only text_search; if it only supports semantic search, describe only search. You make these changes in the system prompt, not in the tools field. See customize the tools for tool-set changes, filters, and multiple corpora.

Inspect the tool calls

SID-1 is expected to use tools on every assistant turn, and usually returns several tool calls per turn. The assistant message includes a tool_calls array, where each entry has the tool name and a JSON string of arguments:

[
  {
    "id": "chatcmpl-tool-1a2b3c4d",
    "type": "function",
    "function": {
      "name": "search",
      "arguments": "{\"query\": \"indoor 4 minute mile record city\", \"limit\": 5}"
    }
  },
  {
    "id": "chatcmpl-tool-5e6f7a8b",
    "type": "function",
    "function": {
      "name": "search",
      "arguments": "{\"query\": \"outdoor 4 minute mile record city\", \"limit\": 5}"
    }
  },
  {
    "id": "chatcmpl-tool-9c0d1e2f",
    "type": "function",
    "function": {
      "name": "text_search",
      "arguments": "{\"query\": \"four-minute mile world record\", \"limit\": 10}"
    }
  }
]

Extract the name and parse the arguments before dispatching to your handler. The full loop below shows where this fits.

import json

response = client.chat.completions.create(model="sid-1", messages=messages, tools=tools)
assistant_msg = response.choices[0].message

if not assistant_msg.tool_calls:
    raise RuntimeError("Expected SID-1 to return at least one tool call.")

for tc in assistant_msg.tool_calls:
    name = tc.function.name
    arguments = json.loads(tc.function.arguments)
    print(name, arguments)
const response = await client.chat.completions.create({ model: "sid-1", messages, tools });
const assistantMsg = response.choices[0].message;

if (!assistantMsg.tool_calls?.length) {
  throw new Error("Expected SID-1 to return at least one tool call.");
}

for (const tc of assistantMsg.tool_calls) {
  const name = tc.function.name;
  const arguments_ = JSON.parse(tc.function.arguments);
  console.log(name, arguments_);
}

Implement the search tools

These three functions are where you connect SID-1 to your retrieval backend. The only field the protocol depends on is each document's id: the model cites it and re-reads documents by it across turns. search and text_search return snippets; read returns the full document.

The stubs below raise until you implement them. Replace each body with a call to your index.

def search(query: str, limit: int = 5) -> list[dict]:
    """Semantic search. Return the documents your backend matched."""
    raise NotImplementedError("Connect `search` to your retrieval backend.")


def text_search(query: str, limit: int = 5) -> list[dict]:
    """Full-text search. Return the documents your backend matched."""
    raise NotImplementedError("Connect `text_search` to your retrieval backend.")


def read(id: str) -> dict:
    """Fetch one full document by id. Truncate the content to ~3000 tokens."""
    raise NotImplementedError("Connect `read` to your retrieval backend.")
async function search(query: string, limit = 5): Promise<Record<string, unknown>[]> {
  // Semantic search. Return the documents your backend matched.
  throw new Error("Connect `search` to your retrieval backend.");
}

async function textSearch(query: string, limit = 5): Promise<Record<string, unknown>[]> {
  // Full-text search. Return the documents your backend matched.
  throw new Error("Connect `textSearch` to your retrieval backend.");
}

async function read(id: string): Promise<Record<string, unknown>> {
  // Fetch one full document by id. Truncate the content to ~3000 tokens.
  throw new Error("Connect `read` to your retrieval backend.");
}

Format the results

Return each document as an XML <doc> block, newline-concatenated. This is how the model cites stable document IDs across turns. id is required; title is optional. The same formatter is described in formatting tool results.

def format_docs_xml(docs: list[dict]) -> str:
    parts = []
    for doc in docs:
        title = doc.get("title", "")
        doc_id = doc.get("id", "")
        content = doc.get("content", "")
        parts.append(f'<doc id="{doc_id}" title="{title}">\n{content}\n</doc>')
    return "\n".join(parts)
function formatDocsXml(docs: Record<string, unknown>[]): string {
  return docs
    .map((doc) => `<doc id="${doc.id ?? ""}" title="${doc.title ?? ""}">\n${doc.content ?? ""}\n</doc>`)
    .join("\n");
}

Execute the tool calls

SID-1 responds with one or more tool calls per turn. This executor dispatches each call to the matching function, formats the result, and signals when the model has reported its final IDs.

import json


def execute_tool_call(tc) -> tuple[str, dict, str, bool]:
    name = tc.function.name
    arguments = json.loads(tc.function.arguments)

    if name == "search":
        docs = search(arguments["query"], arguments.get("limit", 5))
        return tc.id, arguments, format_docs_xml(docs), False

    if name == "text_search":
        docs = text_search(arguments["query"], arguments.get("limit", 5))
        return tc.id, arguments, format_docs_xml(docs), False

    if name == "read":
        return tc.id, arguments, format_docs_xml([read(arguments["id"])]), False

    if name == "report_helpful_ids":
        return tc.id, arguments, json.dumps(arguments["ids"]), True

    return tc.id, arguments, f"Unknown tool: {name}", False
interface ToolResult {
  toolCallId: string;
  args: Record<string, unknown>;
  text: string;
  shouldStop: boolean;
}

async function executeToolCall(
  tc: OpenAI.Chat.Completions.ChatCompletionMessageToolCall,
): Promise<ToolResult> {
  const name = tc.function.name;
  const args = JSON.parse(tc.function.arguments);

  if (name === "search") {
    const docs = await search(args.query, args.limit ?? 5);
    return { toolCallId: tc.id, args, text: formatDocsXml(docs), shouldStop: false };
  }

  if (name === "text_search") {
    const docs = await textSearch(args.query, args.limit ?? 5);
    return { toolCallId: tc.id, args, text: formatDocsXml(docs), shouldStop: false };
  }

  if (name === "read") {
    const doc = await read(args.id);
    return { toolCallId: tc.id, args, text: formatDocsXml([doc]), shouldStop: false };
  }

  if (name === "report_helpful_ids") {
    return { toolCallId: tc.id, args, text: JSON.stringify(args.ids), shouldStop: true };
  }

  return { toolCallId: tc.id, args, text: `Unknown tool: ${name}`, shouldStop: false };
}

Run the loop

The loop follows the loop overview: generate, execute, format, append, repeat. Build the message list from the system prompt and the user query. SID-1 should return at least one tool call every turn, and often returns several; run them in parallel. In Python, ThreadPoolExecutor runs synchronous search functions concurrently. In TypeScript, Promise.all does the same for async handlers. After each turn, append a turn-budget notice to the last tool message so SID-1 knows how many turns remain before it must call report_helpful_ids; see tell SID-1 how many turns remain.

from concurrent.futures import ThreadPoolExecutor

MAX_TURNS = 10


def turn_budget_notice(turns_left: int) -> str:
    notice = f"\n\nYou have {turns_left} out of {MAX_TURNS} turns left."
    if turns_left == 1:
        notice += " You must call `report_helpful_ids` in the next turn."
    return notice


query = "In which cities were the indoor and outdoor 4 minute mile records set?"

messages = [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": query},
]

reported_ids = None

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(model="sid-1", messages=messages, tools=tools)
    assistant_msg = response.choices[0].message

    msg_dict = {"role": "assistant", "content": assistant_msg.content or ""}
    if assistant_msg.tool_calls:
        msg_dict["tool_calls"] = [
            {
                "id": tc.id,
                "type": "function",
                "function": {"name": tc.function.name, "arguments": tc.function.arguments},
            }
            for tc in assistant_msg.tool_calls
        ]
    messages.append(msg_dict)

    if not assistant_msg.tool_calls:
        raise RuntimeError("Expected SID-1 to return at least one tool call.")

    with ThreadPoolExecutor() as executor:
        results = list(executor.map(execute_tool_call, assistant_msg.tool_calls))

    for tool_call_id, arguments, result_text, should_stop in results:
        messages.append({"role": "tool", "tool_call_id": tool_call_id, "content": result_text})
        if should_stop:
            reported_ids = arguments["ids"]

    if reported_ids is not None:
        break

    # Tell SID-1 how many turns remain on the last tool message of the turn.
    turns_left = MAX_TURNS - (turn + 1)
    if turns_left >= 1:
        messages[-1]["content"] += turn_budget_notice(turns_left)
const MAX_TURNS = 10;

function turnBudgetNotice(turnsLeft: number): string {
  let notice = `\n\nYou have ${turnsLeft} out of ${MAX_TURNS} turns left.`;
  if (turnsLeft === 1) {
    notice += " You must call `report_helpful_ids` in the next turn.";
  }
  return notice;
}

const query = "In which cities were the indoor and outdoor 4 minute mile records set?";

const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
  { role: "system", content: SYSTEM_PROMPT },
  { role: "user", content: query },
];

let reportedIds: string[] | null = null;

for (let turn = 0; turn < MAX_TURNS; turn++) {
  const response = await client.chat.completions.create({ model: "sid-1", messages, tools });
  const assistantMsg = response.choices[0].message;
  messages.push(assistantMsg);

  if (!assistantMsg.tool_calls?.length) {
    throw new Error("Expected SID-1 to return at least one tool call.");
  }

  const results = await Promise.all(assistantMsg.tool_calls.map(executeToolCall));

  let lastToolMessage: OpenAI.Chat.Completions.ChatCompletionToolMessageParam | null = null;
  for (const { toolCallId, args, text, shouldStop } of results) {
    const toolMessage = { role: "tool" as const, tool_call_id: toolCallId, content: text };
    messages.push(toolMessage);
    lastToolMessage = toolMessage;
    if (shouldStop) reportedIds = args.ids as string[];
  }

  if (reportedIds !== null) break;

  // Tell SID-1 how many turns remain on the last tool message of the turn.
  const turnsLeft = MAX_TURNS - (turn + 1);
  if (turnsLeft >= 1 && lastToolMessage) {
    lastToolMessage.content += turnBudgetNotice(turnsLeft);
  }
}

When SID-1 calls report_helpful_ids, it passes back a ranked list of documents, with the most relevant document first. The number of documents returned can vary: SID-1 may return as many or as few as it believes are relevant to the query. The system is designed to favor over-inclusion, since missing key information is more costly for downstream applications than returning superfluous documents. For further details, see the technical report section on rewards.

Full code

Implement the three search functions against your backend, then run the file.

Save as quickstart.py and run it with uv:

uv run --with 'openai>=1' quickstart.py
import os
import json
from concurrent.futures import ThreadPoolExecutor

from openai import OpenAI

client = OpenAI(
    base_url="https://api.sid-1.com/v1",
    api_key=os.environ["SID_API_KEY"],
)

MAX_TURNS = 10

SYSTEM_PROMPT = """
You are an expert research assistant that is given a question and must use the provided search tools to find all documents needed to answer the question.

Steps:
1. Reflect on what information is needed to answer the question and use the search tools to find documents. Each document has an id.
2. Repeat step 1 until all documents necessary and sufficient to answer the question have been found. Take as many turns and searches as needed – you can make multiple searches per turn! Most questions will require multiple turns. Most questions require at least 5-8 search requests. Many will need more.
3. Use the report_helpful_ids tool to report the most helpful document ids. List the most helpful document ids first (important!).

The interaction ends once report_helpful_ids is called. Every assistant turn should contain a tool call: use search/read tools while gathering evidence, and use report_helpful_ids for the final ranked IDs. You will be scored based on whether you have found all the documents and whether you reported them in the correct order (NDCG)


You have access to the following tools:

- search: performs a semantic search with the query
  - Arguments: query (required), limit (optional, default 5, max 15)
- text_search: performs full-text search
  - Arguments: query (required), limit (optional, default 5, max 15)
- read: read the full document content for an ID returned by search or text_search
  - Arguments: id (required)
- report_helpful_ids: report helpful document IDs in order (most helpful first)
  - Arguments: ids (required, list of strings)

To use a tool, enclose it within <tool_call> tags with a Python dictionary containing "name" and "arguments". For example:

<tool_call>
{"name": "search", "arguments": {"query": "machine learning algorithms", "limit": 3}}
</tool_call>

The semantic search tool will match things that are conceptually related or use synonyms. This request above would also find texts that talk about linear regression, for example, although "linear regression" does not appear in the query directly. You can write long queries describing the document you want precisely with this tool.


<tool_call>
{"name": "text_search", "arguments": {"query": "data \"dimensionality reduction\" -PCA"}}
</tool_call>

For text_search queries, you can use \"\" (escaped double quotes) to find exact matches for a term. Since the query is inside a JSON string with double quotes, you need to escape the inner double quotes with backslashes (\"dimensionality reduction\"). The above will not return matches that only contain one of "reduction" or "dimensionality" -- it needs both.
You can also use a - to exclude terms (like PCA in the example above). You don't need to use \"\" or - operators, but it can be helpful. If your text_search query has too many terms, there might not be a document that matches all the constraints and no data will be found.


Both search tools return snippets (relevant excerpts) rather than full documents. Snippets are approximately 50 words long and show the most relevant portion of the document based on your query. If the document was truncated, you'll see "..." at the beginning or end.
To read the full document content, use the read tool with the document ID from your search results. You can only read documents that were previously returned by search or text_search.

<tool_call>
{"name": "read", "arguments": {"id": "placeholder_1"}}
</tool_call>

After you've received the tool responses, you can report the helpful document IDs:

<tool_call>
{"name": "report_helpful_ids", "arguments": {"ids": ["placeholder_1", "placeholder_2", "placeholder_3"]}}
</tool_call>
"""

# --- Connect these three functions to your retrieval backend ---

def search(query: str, limit: int = 5) -> list[dict]:
    """Semantic search. Return the documents your backend matched."""
    raise NotImplementedError("Connect `search` to your retrieval backend.")


def text_search(query: str, limit: int = 5) -> list[dict]:
    """Full-text search. Return the documents your backend matched."""
    raise NotImplementedError("Connect `text_search` to your retrieval backend.")


def read(id: str) -> dict:
    """Fetch one full document by id. Truncate the content to ~3000 tokens."""
    raise NotImplementedError("Connect `read` to your retrieval backend.")


def format_docs_xml(docs: list[dict]) -> str:
    parts = []
    for doc in docs:
        title = doc.get("title", "")
        doc_id = doc.get("id", "")
        content = doc.get("content", "")
        parts.append(f'<doc id="{doc_id}" title="{title}">\n{content}\n</doc>')
    return "\n".join(parts)


def turn_budget_notice(turns_left: int) -> str:
    notice = f"\n\nYou have {turns_left} out of {MAX_TURNS} turns left."
    if turns_left == 1:
        notice += " You must call `report_helpful_ids` in the next turn."
    return notice


def execute_tool_call(tc) -> tuple[str, dict, str, bool]:
    name = tc.function.name
    arguments = json.loads(tc.function.arguments)
    print(f"  Tool: {name}({arguments})")

    if name == "search":
        docs = search(arguments["query"], arguments.get("limit", 5))
        return tc.id, arguments, format_docs_xml(docs), False

    if name == "text_search":
        docs = text_search(arguments["query"], arguments.get("limit", 5))
        return tc.id, arguments, format_docs_xml(docs), False

    if name == "read":
        return tc.id, arguments, format_docs_xml([read(arguments["id"])]), False

    if name == "report_helpful_ids":
        return tc.id, arguments, json.dumps(arguments["ids"]), True

    return tc.id, arguments, f"Unknown tool: {name}", False


query = "In which cities were the indoor and outdoor 4 minute mile records set?"

messages = [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": query},
]

tools = [{"type": "function", "function": {"name": "dummy"}}]

reported_ids = None

for turn in range(MAX_TURNS):
    print(f"\n--- Turn {turn + 1} ---")

    response = client.chat.completions.create(model="sid-1", messages=messages, tools=tools)
    assistant_msg = response.choices[0].message
    print(f"Content: {assistant_msg.content}")

    msg_dict = {"role": "assistant", "content": assistant_msg.content or ""}
    if assistant_msg.tool_calls:
        msg_dict["tool_calls"] = [
            {
                "id": tc.id,
                "type": "function",
                "function": {"name": tc.function.name, "arguments": tc.function.arguments},
            }
            for tc in assistant_msg.tool_calls
        ]
    messages.append(msg_dict)

    if not assistant_msg.tool_calls:
        raise RuntimeError("Expected SID-1 to return at least one tool call.")

    with ThreadPoolExecutor() as executor:
        results = list(executor.map(execute_tool_call, assistant_msg.tool_calls))

    for tool_call_id, arguments, result_text, should_stop in results:
        messages.append({"role": "tool", "tool_call_id": tool_call_id, "content": result_text})
        print(f"  Result: {result_text}")
        if should_stop:
            reported_ids = arguments["ids"]

    if reported_ids is not None:
        break

    turns_left = MAX_TURNS - (turn + 1)
    if turns_left >= 1:
        messages[-1]["content"] += turn_budget_notice(turns_left)

if reported_ids is None:
    raise RuntimeError("SID-1 did not call report_helpful_ids before the loop ended.")

print(f"\nReported IDs: {reported_ids}")
print("\n--- Reported documents ---")
for doc_id in reported_ids:
    doc = read(doc_id)
    print(f"\n[{doc.get('id', '')}] {doc.get('title', '')}")
    print(doc.get("content", ""))

Save as quickstart.ts, install the SDK, and run it with tsx:

npm install openai@^5
npx tsx quickstart.ts
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.sid-1.com/v1",
  apiKey: process.env.SID_API_KEY,
});

const MAX_TURNS = 10;

const SYSTEM_PROMPT = `
You are an expert research assistant that is given a question and must use the provided search tools to find all documents needed to answer the question.

Steps:
1. Reflect on what information is needed to answer the question and use the search tools to find documents. Each document has an id.
2. Repeat step 1 until all documents necessary and sufficient to answer the question have been found. Take as many turns and searches as needed – you can make multiple searches per turn! Most questions will require multiple turns. Most questions require at least 5-8 search requests. Many will need more.
3. Use the report_helpful_ids tool to report the most helpful document ids. List the most helpful document ids first (important!).

The interaction ends once report_helpful_ids is called. Every assistant turn should contain a tool call: use search/read tools while gathering evidence, and use report_helpful_ids for the final ranked IDs. You will be scored based on whether you have found all the documents and whether you reported them in the correct order (NDCG)


You have access to the following tools:

- search: performs a semantic search with the query
  - Arguments: query (required), limit (optional, default 5, max 15)
- text_search: performs full-text search
  - Arguments: query (required), limit (optional, default 5, max 15)
- read: read the full document content for an ID returned by search or text_search
  - Arguments: id (required)
- report_helpful_ids: report helpful document IDs in order (most helpful first)
  - Arguments: ids (required, list of strings)

To use a tool, enclose it within <tool_call> tags with a Python dictionary containing "name" and "arguments". For example:

<tool_call>
{"name": "search", "arguments": {"query": "machine learning algorithms", "limit": 3}}
</tool_call>

The semantic search tool will match things that are conceptually related or use synonyms. This request above would also find texts that talk about linear regression, for example, although "linear regression" does not appear in the query directly. You can write long queries describing the document you want precisely with this tool.


<tool_call>
{"name": "text_search", "arguments": {"query": "data \"dimensionality reduction\" -PCA"}}
</tool_call>

For text_search queries, you can use \"\" (escaped double quotes) to find exact matches for a term. Since the query is inside a JSON string with double quotes, you need to escape the inner double quotes with backslashes (\"dimensionality reduction\"). The above will not return matches that only contain one of "reduction" or "dimensionality" -- it needs both.
You can also use a - to exclude terms (like PCA in the example above). You don't need to use \"\" or - operators, but it can be helpful. If your text_search query has too many terms, there might not be a document that matches all the constraints and no data will be found.


Both search tools return snippets (relevant excerpts) rather than full documents. Snippets are approximately 50 words long and show the most relevant portion of the document based on your query. If the document was truncated, you'll see "..." at the beginning or end.
To read the full document content, use the read tool with the document ID from your search results. You can only read documents that were previously returned by search or text_search.

<tool_call>
{"name": "read", "arguments": {"id": "placeholder_1"}}
</tool_call>

After you've received the tool responses, you can report the helpful document IDs:

<tool_call>
{"name": "report_helpful_ids", "arguments": {"ids": ["placeholder_1", "placeholder_2", "placeholder_3"]}}
</tool_call>
`;

// --- Connect these three functions to your retrieval backend ---

async function search(query: string, limit = 5): Promise<Record<string, unknown>[]> {
  // Semantic search. Return the documents your backend matched.
  throw new Error("Connect `search` to your retrieval backend.");
}

async function textSearch(query: string, limit = 5): Promise<Record<string, unknown>[]> {
  // Full-text search. Return the documents your backend matched.
  throw new Error("Connect `textSearch` to your retrieval backend.");
}

async function read(id: string): Promise<Record<string, unknown>> {
  // Fetch one full document by id. Truncate the content to ~3000 tokens.
  throw new Error("Connect `read` to your retrieval backend.");
}

function formatDocsXml(docs: Record<string, unknown>[]): string {
  return docs
    .map((doc) => `<doc id="${doc.id ?? ""}" title="${doc.title ?? ""}">\n${doc.content ?? ""}\n</doc>`)
    .join("\n");
}

interface ToolResult {
  toolCallId: string;
  args: Record<string, unknown>;
  text: string;
  shouldStop: boolean;
}

function turnBudgetNotice(turnsLeft: number): string {
  let notice = `\n\nYou have ${turnsLeft} out of ${MAX_TURNS} turns left.`;
  if (turnsLeft === 1) {
    notice += " You must call `report_helpful_ids` in the next turn.";
  }
  return notice;
}

async function executeToolCall(
  tc: OpenAI.Chat.Completions.ChatCompletionMessageToolCall,
): Promise<ToolResult> {
  const name = tc.function.name;
  const args = JSON.parse(tc.function.arguments);
  console.log(`  Tool: ${name}(${JSON.stringify(args)})`);

  if (name === "search") {
    const docs = await search(args.query, args.limit ?? 5);
    return { toolCallId: tc.id, args, text: formatDocsXml(docs), shouldStop: false };
  }

  if (name === "text_search") {
    const docs = await textSearch(args.query, args.limit ?? 5);
    return { toolCallId: tc.id, args, text: formatDocsXml(docs), shouldStop: false };
  }

  if (name === "read") {
    const doc = await read(args.id);
    return { toolCallId: tc.id, args, text: formatDocsXml([doc]), shouldStop: false };
  }

  if (name === "report_helpful_ids") {
    return { toolCallId: tc.id, args, text: JSON.stringify(args.ids), shouldStop: true };
  }

  return { toolCallId: tc.id, args, text: `Unknown tool: ${name}`, shouldStop: false };
}

const query = "In which cities were the indoor and outdoor 4 minute mile records set?";

const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
  { role: "system", content: SYSTEM_PROMPT },
  { role: "user", content: query },
];

const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
  { type: "function", function: { name: "dummy" } },
];

let reportedIds: string[] | null = null;

for (let turn = 0; turn < MAX_TURNS; turn++) {
  console.log(`\n--- Turn ${turn + 1} ---`);

  const response = await client.chat.completions.create({ model: "sid-1", messages, tools });
  const assistantMsg = response.choices[0].message;
  console.log(`Content: ${assistantMsg.content}`);
  messages.push(assistantMsg);

  if (!assistantMsg.tool_calls?.length) {
    throw new Error("Expected SID-1 to return at least one tool call.");
  }

  const results = await Promise.all(assistantMsg.tool_calls.map(executeToolCall));

  let lastToolMessage: OpenAI.Chat.Completions.ChatCompletionToolMessageParam | null = null;
  for (const { toolCallId, args, text, shouldStop } of results) {
    const toolMessage = { role: "tool" as const, tool_call_id: toolCallId, content: text };
    messages.push(toolMessage);
    lastToolMessage = toolMessage;
    console.log(`  Result: ${text}`);
    if (shouldStop) reportedIds = args.ids as string[];
  }

  if (reportedIds !== null) break;

  const turnsLeft = MAX_TURNS - (turn + 1);
  if (turnsLeft >= 1 && lastToolMessage) {
    lastToolMessage.content += turnBudgetNotice(turnsLeft);
  }
}

if (reportedIds === null) {
  throw new Error("SID-1 did not call report_helpful_ids before the loop ended.");
}

console.log(`\nReported IDs: ${JSON.stringify(reportedIds)}`);
console.log("\n--- Reported documents ---");
for (const docId of reportedIds) {
  const doc = await read(docId);
  console.log(`\n[${doc.id ?? ""}] ${doc.title ?? ""}`);
  console.log(doc.content ?? "");
}

Content