`, with blank lines separating events:
```
data: {"choices":[{"delta":{"content":"The"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" resurrection"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" is"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":"..."},"finish_reason":"stop"}]}
```
The stream ends when a chunk arrives with a non-null `finish_reason` (typically `"stop"`).
## Two Approaches: Direct vs. Proxy
This tutorial covers two ways to consume the stream:
| Approach | How it works | When to use |
| - | - | - |
| **Terminal** | Your server calls the API directly and prints tokens | Background jobs, CLIs, server-side rendering |
| **Proxy** | A lightweight server relays SSE to any external client | Web apps, any case where browser JS would expose credentials |
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account with API credentials
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* **Authentication setup** — complete the [Authentication Tutorial](/tutorials/authentication) first
Authentication uses your `GLOO_API_KEY` environment variable — set it in your
`.env` file and it's used directly as a Bearer token. No token exchange
needed.
## Getting Started with the Starter Project
This tutorial uses a hands-on approach where you'll build the streaming client incrementally. The starter code provides complete scaffolding with TODO markers guiding each step.
### Download the Starter Code
Choose your preferred language and download the starter project:
Python 3.9+ · requests · Flask
Node.js 18+ · native fetch · Express
TypeScript 5+ · typed SSE chunks
PHP 8.1+ · cURL write callback
Go 1.20+ · bufio.Scanner · http.Flusher
Java 17+ · HttpClient · Maven
### Quick Setup
```bash Python theme={null}
cd starter/python
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env with your GLOO_API_KEY
```
```bash JavaScript theme={null}
cd starter/javascript
npm install
cp .env.example .env
# Edit .env with your GLOO_API_KEY
```
```bash TypeScript theme={null}
cd starter/typescript
npm install
cp .env.example .env
# Edit .env with your GLOO_API_KEY
```
```bash PHP theme={null}
cd starter/php
composer install
cp .env.example .env
# Edit .env with your GLOO_API_KEY
```
```bash Go theme={null}
cd starter/go
go mod download
cp .env.example .env
# Edit .env with your GLOO_API_KEY
```
```bash Java theme={null}
cd starter/java
mvn clean compile
cp .env.example .env
# Edit .env with your GLOO_API_KEY
```
### Test Your Setup
Run the entry point — it should load your credentials and confirm the stubs are in place:
```bash Python theme={null}
python main.py
```
```bash JavaScript theme={null}
npm start
```
```bash TypeScript theme={null}
npm start
```
```bash PHP theme={null}
composer start
```
```bash Go theme={null}
go run main.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.Main"
```
You should see credentials load successfully, followed by `NotImplementedError` (or equivalent) from the first stub — confirming that setup is complete and you're ready to implement.
## Architecture Overview
### Component Architecture
```mermaid theme={null}
%%{init: {'theme':'dark', 'themeVariables': { 'fontSize':'18px'}}}%%
graph TB
Auth["Environment
GLOO_API_KEY from .env
Loaded once at startup"]
StreamClient["Streaming Client
Make Streaming Request
Parse SSE Line
Extract Token Content
Stream Completion
Handle Stream Error"]
Renderer["Terminal Renderer
Render Stream to Terminal
Unbuffered stdout output
Typing effect"]
Proxy["Proxy Server
POST /api/stream
Relays SSE upstream
Keeps credentials server-side"]
BrowserClient["Browser Client
fetch() + ReadableStream
Markdown re-parse pattern
Pre-built index.html"]
GlooAPI["Gloo AI API
POST /ai/v2/guarded/chat/completions
stream: true
SSE response"]
StreamClient --> Renderer
StreamClient --> GlooAPI
Proxy --> GlooAPI
BrowserClient --> Proxy
classDef prebuilt fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#9ca3af
classDef teaching fill:#4a5568,stroke:#cbd5e0,stroke-width:3px,color:#fff
classDef external fill:#1a202c,stroke:#cbd5e0,stroke-width:2px,color:#9ca3af
class BrowserClient prebuilt
class StreamClient,Renderer,Proxy teaching
class Auth,GlooAPI external
```
### Implementation Roadmap
| Step | What You Build | Track | Validates |
| :-: | - | - | - |
| **1** | Environment setup | Shared | Auth loads; streaming endpoint reachable |
| **2** | Handle stream errors | Shared | 401/403/429 errors thrown before stream read |
| **3** | Streaming request + SSE parsing | Shared | HTTP connection opens; SSE lines parsed; `[DONE]` detected |
| **4** | Token extraction + accumulation | Shared | Token text extracted; full response assembled with timing |
| **5** | Render stream to terminal | Terminal | Tokens print live to terminal |
| **6** | Proxy stream handler | Proxy | SSE relayed through server |
| 7 † | Testing & browser demo | Proxy | End-to-end validation |
† No new implementation — run the demo, test the proxy via API, and explore
the browser client.
Steps 1–5 build the streaming client. Step 6 adds the server-side proxy. Step 7 walks through the browser demo.
Let's get started!
***
## Step 1: Environment Setup & Auth Verification
Your API key is loaded from the `GLOO_API_KEY` environment variable. Before implementing any streaming logic, confirm it works with the streaming endpoint.
### What You'll Verify
1. Credentials load correctly from `.env`
2. Your API key is set and valid
3. A request to the completions endpoint returns `200 OK` with `Content-Type: text/event-stream`
### Testing Your Setup
Run the Step 1 checkpoint now:
```bash Python theme={null}
python tests/step1_auth_test.py
```
```bash JavaScript theme={null}
npm run test:step1
```
```bash TypeScript theme={null}
npm run test:step1
```
```bash PHP theme={null}
php tests/Step1AuthTest.php
```
```bash Go theme={null}
go run tests/step1_auth.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.tests.Step1AuthTest"
```
### ✓ Checkpoint: Auth Verification
Your output should look similar to the following:
```
🧪 Testing: Environment Setup & Auth Verification
✓ GLOO_API_KEY loaded
Test 1: Verifying API key...
✓ API key valid
Test 2: Verifying streaming endpoint...
✓ Status: 200 OK
✓ Content-Type: text/event-stream; charset=utf-8
✅ Auth and streaming endpoint verified.
Next: Making the Streaming Request
```
**If tests fail**, check:
* `.env` file exists in the language directory (not just `.env.example`)
* `GLOO_API_KEY` is set correctly.
* You've completed the [Authentication Tutorial](/tutorials/authentication) prerequisites
***
## Step 2: Streaming-Aware Error Handling
Now implement the stream error handler, a focused function that maps HTTP status codes to descriptive exceptions before any stream data is read.
### Key Concepts
#### Two-Phase Error Handling
Streaming introduces two distinct error phases:
**Phase 1 — Pre-stream (before reading bytes)**: The HTTP status tells you everything. A 401 means invalid API key; a 429 means slow down. Check the status immediately and throw a specific error before touching the body. This is what the stream error handler does.
**Phase 2 — Mid-stream (while reading bytes)**: The connection is live when something fails — network drop, server restart, timeout. Catch these in the accumulation loop with a try/catch around the read loop. If you've already accumulated partial text, preserve it and return what you have rather than discarding the work.
Separating these phases makes errors debuggable: pre-stream errors have status codes; mid-stream errors have partial content.
### Implementation Guide
Open your streaming client file and find the error handler method, it's a small, focused function with one case per status code. Review the TODO comments, then implement the function:
```python Python theme={null}
# File: streaming/stream_client.py
def handle_stream_error(status_code: int, response_body: str = "") -> None:
if status_code == 401:
raise Exception("Authentication failed (401): Invalid or expired token")
elif status_code == 403:
raise Exception("Authorization failed (403): Insufficient permissions")
elif status_code == 429:
raise Exception("Rate limit exceeded (429): Too many requests")
elif status_code != 200:
raise Exception(f"API error ({status_code}): {response_body[:200]}")
```
```javascript JavaScript theme={null}
// File: src/streaming/streamClient.js
export function handleStreamError(statusCode, responseBody = '') {
if (statusCode === 401) {
throw new Error('Authentication failed (401): Invalid or expired token');
} else if (statusCode === 403) {
throw new Error('Authorization failed (403): Insufficient permissions');
} else if (statusCode === 429) {
throw new Error('Rate limit exceeded (429): Too many requests');
} else if (statusCode !== 200) {
throw new Error(
`API error (${statusCode}): ${String(responseBody).slice(0, 200)}`,
);
}
}
```
```typescript TypeScript theme={null}
// File: src/streaming/streamClient.ts
export function handleStreamError(statusCode: number, responseBody = ''): void {
if (statusCode === 401) {
throw new Error('Authentication failed (401): Invalid or expired token');
} else if (statusCode === 403) {
throw new Error('Authorization failed (403): Insufficient permissions');
} else if (statusCode === 429) {
throw new Error('Rate limit exceeded (429): Too many requests');
} else if (statusCode !== 200) {
throw new Error(
`API error (${statusCode}): ${String(responseBody).slice(0, 200)}`,
);
}
}
```
```php PHP theme={null}
// File: src/Streaming/StreamClient.php
public static function handleStreamError(int $statusCode, string $responseBody = ''): void
{
if ($statusCode === 401) {
throw new \RuntimeException('Authentication failed (401): Invalid or expired token');
} elseif ($statusCode === 403) {
throw new \RuntimeException('Authorization failed (403): Insufficient permissions');
} elseif ($statusCode === 429) {
throw new \RuntimeException('Rate limit exceeded (429): Too many requests');
} elseif ($statusCode !== 200) {
$preview = substr($responseBody, 0, 200);
throw new \RuntimeException("API error ({$statusCode}): {$preview}");
}
}
```
```go Go theme={null}
// File: pkg/streaming/client.go
func HandleStreamError(statusCode int, responseBody string) error {
switch statusCode {
case http.StatusOK:
return nil
case http.StatusUnauthorized:
return fmt.Errorf("authentication failed (401): invalid or expired token")
case http.StatusForbidden:
return fmt.Errorf("authorization failed (403): insufficient permissions")
case http.StatusTooManyRequests:
return fmt.Errorf("rate limit exceeded (429): too many requests")
default:
preview := responseBody
if len(preview) > 200 {
preview = preview[:200]
}
return fmt.Errorf("API error (%d): %s", statusCode, preview)
}
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/streaming/StreamClient.java
public static void handleStreamError(int statusCode, String responseBody) {
switch (statusCode) {
case 200 -> { /* ok */ }
case 401 -> throw new RuntimeException(
"Authentication failed (401): Invalid or expired token"
);
case 403 -> throw new RuntimeException(
"Authorization failed (403): Insufficient permissions"
);
case 429 -> throw new RuntimeException(
"Rate limit exceeded (429): Too many requests"
);
default -> {
String preview = responseBody != null && responseBody.length() > 200
? responseBody.substring(0, 200) : responseBody;
throw new RuntimeException("API error (" + statusCode + "): " + preview);
}
}
}
```
The code does the following:
* Throws an authentication error on 401 if the API key is missing, invalid, or revoked
* Throws an authorization error on 403 if the token is valid but lacks permission for this resource
* Throws a rate limit error on 429 if the request was rejected before the API spent any compute
* Throws a generic error for any other non-200 status, including the response body for diagnostic context
* Returns without throwing on 200 so the caller can proceed to read the stream
### ✓ Checkpoint: Error Handling
Run the error handling test:
```bash Python theme={null}
python tests/step2_error_handling_test.py
```
```bash JavaScript theme={null}
npm run test:step2
```
```bash TypeScript theme={null}
npm run test:step2
```
```bash PHP theme={null}
php tests/Step2ErrorHandlingTest.php
```
```bash Go theme={null}
go run tests/step2_error_handling.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.tests.Step2ErrorHandlingTest"
```
Your output should look similar to the following:
```
🧪 Testing: Streaming Error Handling
Test 1: handle_stream_error(401)...
✓ 401 raises: Authentication failed (401): Invalid or expired token
Test 2: handle_stream_error(403)...
✓ 403 raises: Authorization failed (403): Insufficient permissions
Test 3: handle_stream_error(429)...
✓ 429 raises: Rate limit exceeded (429): Too many requests
Test 4: handle_stream_error(200) — success, no exception...
✓ 200 OK — no exception raised
Test 5: handle_stream_error(500)...
✓ 500 throws with body: API error (500): Internal Server Error
✅ Two-phase error handling working.
Next: Streaming Requests & SSE Parsing
```
**If tests fail**, check:
* Status 200 must **not** raise an exception
* The error message for non-200 includes the status code
* The response body is truncated (first 200 chars) to avoid enormous error messages
***
## Step 3: Streaming Requests & SSE Parsing
Time to wire up the streaming connection. You'll open a persistent HTTP connection to the completions API and write the parser that converts raw SSE lines into something you can actually work with.
### What You'll Implement
1. A function to initiate a streaming request
2. A function to parse individual SSE lines
### Making the Streaming Request
#### Why `stream: true` Changes Everything
Without `stream: true`, the API buffers the entire response and returns it as a single JSON object. With `stream: true`, it switches to SSE mode: the connection stays open and bytes arrive incrementally as the model generates them.
This is why you return the **raw response object** rather than parsed JSON — the body isn't fully available yet. The caller will read it line by line in the next steps.
#### Fail Fast Before Reading
Checking the HTTP status code *before* starting to read the stream is important for a clean user experience. A 401 response will never produce SSE data — it returns a JSON error body. If you skipped the status check and tried to parse lines from a 401 response, you'd get confusing parse errors instead of a clear "authentication failed" message.
#### Implementation Guide
Still in the same streaming client file, find the streaming request method, review the TODO comments, then implement the changes outlined in the code block:
```python Python theme={null}
# File: streaming/stream_client.py
def make_streaming_request(message: str, token: str):
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
payload = {
"messages": [{"role": "user", "content": message}],
"auto_routing": True,
"stream": True,
}
response = requests.post(API_URL, headers=headers, json=payload, stream=True)
handle_stream_error(
response.status_code,
response.text if response.status_code != 200 else "",
)
return response
```
```javascript JavaScript theme={null}
// File: src/streaming/streamClient.js
export async function makeStreamingRequest(message, token) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: [{ role: 'user', content: message }],
auto_routing: true,
stream: true,
}),
});
if (!response.ok) {
const body = await response.text();
handleStreamError(response.status, body);
}
return response;
}
```
```typescript TypeScript theme={null}
// File: src/streaming/streamClient.ts
export async function makeStreamingRequest(
message: string,
token: string,
): Promise {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: [{ role: 'user', content: message }],
auto_routing: true,
stream: true,
}),
});
if (!response.ok) {
const body = await response.text();
handleStreamError(response.status, body);
}
return response;
}
```
```php PHP theme={null}
// File: src/Streaming/StreamClient.php
public static function makeStreamingRequest(string $message, string $token, callable $writeCallback): void
{
$payload = json_encode([
'messages' => [['role' => 'user', 'content' => $message]],
'auto_routing' => true,
'stream' => true,
]);
$headers = [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
];
$statusCode = 0;
$ch = curl_init(self::API_URL);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$statusCode) {
if (preg_match('/HTTP\/\d+\.?\d*\s+(\d+)/', $header, $m)) {
$statusCode = (int)$m[1];
}
return strlen($header);
},
CURLOPT_WRITEFUNCTION => function ($ch, $data) use (&$statusCode, $writeCallback) {
if ($statusCode && $statusCode !== 200) {
self::handleStreamError($statusCode, $data);
}
$writeCallback($data);
return strlen($data);
},
]);
$result = curl_exec($ch);
$error = $result === false ? curl_error($ch) : '';
curl_close($ch);
if ($error) {
throw new \RuntimeException("Streaming request failed: {$error}");
}
}
```
```go Go theme={null}
// File: pkg/streaming/client.go
func MakeStreamingRequest(message, token string) (*http.Response, error) {
payload := map[string]any{
"messages": []map[string]string{{"role": "user", "content": message}},
"auto_routing": true,
"stream": true,
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to encode request: %w", err)
}
req, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("streaming request failed: %w", err)
}
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, HandleStreamError(resp.StatusCode, string(bodyBytes))
}
return resp, nil
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/streaming/StreamClient.java
public static HttpResponse makeStreamingRequest(
String message, String token
) {
String payload = GSON.toJson(Map.of(
"messages", List.of(Map.of("role", "user", "content", message)),
"auto_routing", true,
"stream", true
));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
try {
HttpResponse response = HTTP_CLIENT.send(
request, HttpResponse.BodyHandlers.ofInputStream()
);
if (response.statusCode() != 200) {
String body = new String(response.body().readAllBytes(), StandardCharsets.UTF_8);
handleStreamError(response.statusCode(), body);
}
return response;
} catch (Exception e) {
if (e instanceof RuntimeException re) throw re;
throw new RuntimeException("Streaming request failed: " + e.getMessage(), e);
}
}
```
The code does the following:
* Sets `Authorization` and `Content-Type` headers using the provided token
* Builds the request payload with `stream: true` to enable SSE mode and `auto_routing: true` to let Gloo select the best model
* Checks the HTTP status before reading any response data, raising a descriptive error for non-200 responses
* Returns the raw response object so the caller can iterate its body line by line
**PHP note**: cURL's streaming architecture doesn't allow inspecting the HTTP
status before the write callback fires. The status check happens on the first
data chunk instead. This is the idiomatic PHP pattern for streaming with cURL.
### Parsing SSE Lines
#### The SSE Wire Format
SSE is a simple text protocol. Each event is one line starting with `data: `, terminated by a blank line. In practice, the Gloo AI stream looks like:
```
data: {"id":"...","choices":[{"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"...","choices":[{"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"...","choices":[{"delta":{"content":" world"},"finish_reason":"stop"}]}
```
Blank lines are separators, not errors — they're common and should be silently skipped. Lines that don't start with `data: ` (such as `event:` or `:` comment lines) should also be skipped.
#### Defensive Parsing
The JSON parse is wrapped in a try/catch. Mid-stream network hiccups can produce partial lines — you don't want a single malformed chunk to crash the entire stream. Return `null` for unparseable lines and let the accumulation loop move on.
#### Implementation Guide
You're still working with the streaming client file. Find the SSE line parser method, review the TODO comments, then implement:
```python Python theme={null}
# File: streaming/stream_client.py
def parse_sse_line(line: str):
if not line or not line.strip():
return None
if not line.startswith("data: "):
return None
data = line[6:] # strip 'data: ' prefix
if data.strip() == "[DONE]":
return "[DONE]"
try:
return json.loads(data)
except json.JSONDecodeError:
return None
```
```javascript JavaScript theme={null}
// File: src/streaming/streamClient.js
export function parseSseLine(line) {
if (!line || !line.trim()) return null;
if (!line.startsWith('data: ')) return null;
const data = line.slice(6); // strip 'data: '
if (data.trim() === '[DONE]') return '[DONE]';
try {
return JSON.parse(data);
} catch {
return null;
}
}
```
```typescript TypeScript theme={null}
// File: src/streaming/streamClient.ts
export function parseSseLine(line: string): null | '[DONE]' | SSEChunk {
if (!line || !line.trim()) return null;
if (!line.startsWith('data: ')) return null;
const data = line.slice(6); // strip 'data: '
if (data.trim() === '[DONE]') return '[DONE]';
try {
return JSON.parse(data) as SSEChunk;
} catch {
return null;
}
}
```
```php PHP theme={null}
// File: src/Streaming/StreamClient.php
public static function parseSseLine(string $line): mixed
{
if (!$line || !trim($line)) {
return null;
}
if (!str_starts_with($line, 'data: ')) {
return null;
}
$data = substr($line, 6); // strip 'data: '
if (trim($data) === '[DONE]') {
return '[DONE]';
}
$decoded = json_decode($data, true);
return $decoded ?? null;
}
```
```go Go theme={null}
// File: pkg/streaming/client.go
func ParseSSELine(line string) any {
if strings.TrimSpace(line) == "" {
return nil
}
if !strings.HasPrefix(line, "data: ") {
return nil
}
data := line[6:] // strip "data: "
if strings.TrimSpace(data) == "[DONE]" {
return "[DONE]"
}
var chunk SSEChunk
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
return nil
}
return &chunk
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/streaming/StreamClient.java
public static Object parseSseLine(String line) {
if (line == null || line.isBlank()) return null;
if (!line.startsWith("data: ")) return null;
String data = line.substring(6); // strip "data: "
if (data.trim().equals("[DONE]")) return "[DONE]";
try {
return GSON.fromJson(data, new TypeToken
The code does the following:
* Returns `null` for blank lines and lines that don't start with `data: `, signalling the caller to skip to the next line
* Strips the `data: ` prefix to isolate the raw JSON payload
* Detects the `[DONE]` sentinel before attempting JSON parsing and returns it as a string to signal the end of the stream
* Parses the payload as JSON and returns the result, or `null` if parsing fails — never throws on malformed input
### ✓ Checkpoint: Streaming Request & SSE Parsing
Run the validation test for this step:
```bash Python theme={null}
python tests/step3_sse_parsing_test.py
```
```bash JavaScript theme={null}
npm run test:step3
```
```bash TypeScript theme={null}
npm run test:step3
```
```bash PHP theme={null}
php tests/Step3SseParsingTest.php
```
```bash Go theme={null}
go run tests/step3_sse_parsing.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.tests.Step3SseParsingTest"
```
Your output should look similar to the following:
```
🧪 Testing: Streaming Request & SSE Line Parsing
✓ API key loaded
Test 1: parse_sse_line — blank line...
✓ Blank line → None
Test 2: parse_sse_line — non-data line...
✓ Non-data line → None
Test 3: parse_sse_line — [DONE] sentinel...
✓ data: [DONE] → '[DONE]'
Test 4: parse_sse_line — valid JSON data line...
✓ data: {json} → parsed dict
Test 5: parse_sse_line — malformed JSON...
✓ Malformed JSON → None (gracefully handled)
Test 6: make_streaming_request() — live connection...
✓ Streaming connection opened (status 200)
Test 7: Iterating SSE lines and detecting stream termination...
✓ Processed 5 lines, 2 data chunks
✓ Stream terminated cleanly (finish_reason=stop)
Test 8: Bad credentials → authentication error before reading stream...
✓ Bad credentials caught (pre-stream): Authorization failed (403): Insufficient permissions
✅ Streaming request and SSE parsing working.
Next: Token Extraction & Accumulation
```
**If tests fail**, check:
* The streaming request function sets `stream` to `true` in the payload
* The SSE line parser strips exactly 6 characters (`"data: "` has a space after the colon)
* The `[DONE]` check happens before the JSON parse
***
## Step 4: Token Extraction & Accumulation
Next you'll add the pieces to pull the token out of each parsed SSE chunk, and the accumulation loop that stitches everything together into a complete result.
### What You'll Implement
1. A function to extract token content from a parsed SSE chunk
2. A function to collect the full stream into a result object
### Extracting Token Content
#### Why Content Can Be Absent
Not every SSE chunk carries text. The first chunk establishes the role (`delta: {"role": "assistant"}`), while the final chunk carries the finish reason with an empty or absent delta. Only chunks in the middle carry actual content.
This is why you return an empty string rather than throwing since an absent `content` field is completely normal. The accumulation loop skips empty strings when counting tokens.
#### Null-Safe Navigation
Different languages handle missing keys differently. In Python, `.get()` returns `None` without raising; in JavaScript/TypeScript, optional chaining (`?.`) does the same. In Go and Java the struct is fully typed, so missing content simply maps to the zero value. The goal in all languages is the same: never throw when a field is absent.
#### Implementation Guide
Still in the streaming client file, find the token content extractor, review the TODO comments, then implement:
```python Python theme={null}
# File: streaming/stream_client.py
def extract_token_content(chunk: dict) -> str:
try:
choices = chunk.get("choices", [])
if not choices:
return ""
delta = choices[0].get("delta", {})
return delta.get("content") or ""
except (IndexError, AttributeError, KeyError):
return ""
```
```javascript JavaScript theme={null}
// File: src/streaming/streamClient.js
export function extractTokenContent(chunk) {
try {
const choices = chunk?.choices;
if (!choices || choices.length === 0) return '';
const content = choices[0]?.delta?.content;
return content || '';
} catch {
return '';
}
}
```
```typescript TypeScript theme={null}
// File: src/streaming/streamClient.ts
export function extractTokenContent(chunk: SSEChunk): string {
try {
const choices = chunk?.choices;
if (!choices || choices.length === 0) return '';
const content = choices[0]?.delta?.content;
return content || '';
} catch {
return '';
}
}
```
```php PHP theme={null}
// File: src/Streaming/StreamClient.php
public static function extractTokenContent(array $chunk): string
{
try {
$choices = $chunk['choices'] ?? [];
if (empty($choices)) {
return '';
}
$content = $choices[0]['delta']['content'] ?? '';
return $content ?: '';
} catch (\Throwable) {
return '';
}
}
```
```go Go theme={null}
// File: pkg/streaming/client.go
func ExtractTokenContent(chunk *SSEChunk) string {
if chunk == nil || len(chunk.Choices) == 0 {
return ""
}
return chunk.Choices[0].Delta.Content
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/streaming/StreamClient.java
@SuppressWarnings("unchecked")
public static String extractTokenContent(Map chunk) {
try {
List
The code does the following:
* Returns an empty string immediately if `choices` is absent or empty — the first and last chunks often carry no content
* Reads `delta.content` from the first choice, returning an empty string if the field is absent or `null`
* Handles any unexpected chunk structure by returning an empty string rather than throwing, keeping the accumulation loop running cleanly
### Accumulating the Full Response
#### Two Ways to Consume a Stream
You can either accumulate all tokens into a string (what the function in this step does) or print each token immediately as it arrives (what the function in Step 5 does). The choice depends on whether you need the full text before taking action:
* **Accumulate**: useful when you need to parse the full response, log it, or return it from an API
* **Print immediately**: useful for CLI tools and browser UIs where you want the typing effect
#### The Line Buffer (JS/TS/PHP)
In Python and Go, the HTTP libraries provide line-at-a-time iteration. In JavaScript, TypeScript, and PHP, you read raw bytes and split on `\n` yourself. This requires a **line buffer**: keep any incomplete final chunk in a variable and prepend it to the next read's output. Without it, tokens near chunk boundaries get split across two parse calls.
```js theme={null}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? ''; // save incomplete last line
```
#### Implementation Guide
Open the streaming client file and find the accumulation loop method. This one brings together everything from the previous steps, with the TODO comments showing each stage. Take a moment to trace through the structure before implementing.
```python Python theme={null}
# File: streaming/stream_client.py
def stream_completion(message: str, token: str) -> dict:
start_time = time.time()
response = make_streaming_request(message, token)
full_text = ""
token_count = 0
finish_reason = "unknown"
try:
for raw_line in response.iter_lines(decode_unicode=True):
chunk = parse_sse_line(raw_line)
if chunk is None:
continue
if chunk == "[DONE]":
break
content = extract_token_content(chunk)
if content:
full_text += content
token_count += 1
choices = chunk.get("choices", [])
if choices and choices[0].get("finish_reason"):
finish_reason = choices[0]["finish_reason"]
except Exception:
if full_text:
pass # preserve partial output on error
else:
raise
duration_ms = int((time.time() - start_time) * 1000)
return {
"text": full_text,
"token_count": token_count,
"duration_ms": duration_ms,
"finish_reason": finish_reason,
}
```
```javascript JavaScript theme={null}
// File: src/streaming/streamClient.js
export async function streamCompletion(message, token) {
const startTime = Date.now();
const response = await makeStreamingRequest(message, token);
let fullText = '';
let tokenCount = 0;
let finishReason = 'unknown';
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const chunk = parseSseLine(line);
if (chunk === null) continue;
if (chunk === '[DONE]') break;
const content = extractTokenContent(chunk);
if (content) {
fullText += content;
tokenCount += 1;
}
const choices = chunk?.choices;
if (choices && choices[0]?.finish_reason) {
finishReason = choices[0].finish_reason;
}
}
}
} finally {
reader.releaseLock();
}
return {
text: fullText,
token_count: tokenCount,
duration_ms: Date.now() - startTime,
finish_reason: finishReason,
};
}
```
```typescript TypeScript theme={null}
// File: src/streaming/streamClient.ts
export async function streamCompletion(
message: string,
token: string,
): Promise {
const startTime = Date.now();
const response = await makeStreamingRequest(message, token);
let fullText = '';
let tokenCount = 0;
let finishReason = 'unknown';
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const chunk = parseSseLine(line);
if (chunk === null) continue;
if (chunk === '[DONE]') break;
const content = extractTokenContent(chunk);
if (content) {
fullText += content;
tokenCount += 1;
}
const choices = chunk?.choices;
if (choices && choices[0]?.finish_reason) {
finishReason = choices[0].finish_reason;
}
}
}
} finally {
reader.releaseLock();
}
return {
text: fullText,
token_count: tokenCount,
duration_ms: Date.now() - startTime,
finish_reason: finishReason,
};
}
```
```php PHP theme={null}
// File: src/Streaming/StreamClient.php
public static function streamCompletion(string $message, string $token): array
{
$startTime = microtime(true);
$fullText = '';
$tokenCount = 0;
$finishReason = 'unknown';
$lineBuffer = '';
$done = false;
$writeCallback = function (string $data) use (
&$fullText, &$tokenCount, &$finishReason, &$lineBuffer, &$done
): void {
if ($done) return;
$lineBuffer .= $data;
$lines = explode("\n", $lineBuffer);
$lineBuffer = array_pop($lines); // save incomplete last line
foreach ($lines as $line) {
$chunk = self::parseSseLine($line);
if ($chunk === null) continue;
if ($chunk === '[DONE]') { $done = true; break; }
$content = self::extractTokenContent($chunk);
if ($content !== '') {
$fullText .= $content;
$tokenCount++;
}
$choices = $chunk['choices'] ?? [];
if (!empty($choices) && !empty($choices[0]['finish_reason'])) {
$finishReason = $choices[0]['finish_reason'];
}
}
};
self::makeStreamingRequest($message, $token, $writeCallback);
return [
'text' => $fullText,
'token_count' => $tokenCount,
'duration_ms' => (int)((microtime(true) - $startTime) * 1000),
'finish_reason' => $finishReason,
];
}
```
```go Go theme={null}
// File: pkg/streaming/client.go
func StreamCompletion(message, token string) (*StreamResult, error) {
start := time.Now()
resp, err := MakeStreamingRequest(message, token)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var (
fullText strings.Builder
tokenCount int
finishReason = "unknown"
)
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
parsed := ParseSSELine(line)
if parsed == nil {
continue
}
if s, ok := parsed.(string); ok && s == "[DONE]" {
break
}
chunk, ok := parsed.(*SSEChunk)
if !ok {
continue
}
content := ExtractTokenContent(chunk)
if content != "" {
fullText.WriteString(content)
tokenCount++
}
if len(chunk.Choices) > 0 && chunk.Choices[0].FinishReason != nil {
finishReason = *chunk.Choices[0].FinishReason
}
}
if err := scanner.Err(); err != nil && fullText.Len() == 0 {
return nil, fmt.Errorf("error reading stream: %w", err)
}
return &StreamResult{
Text: fullText.String(),
TokenCount: tokenCount,
DurationMs: time.Since(start).Milliseconds(),
FinishReason: finishReason,
}, nil
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/streaming/StreamClient.java
@SuppressWarnings("unchecked")
public static StreamResult streamCompletion(String message, String token) {
Instant start = Instant.now();
HttpResponse response = makeStreamingRequest(message, token);
StringBuilder fullText = new StringBuilder();
int tokenCount = 0;
String finishReason = "unknown";
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body(), StandardCharsets.UTF_8)
)) {
String line;
while ((line = reader.readLine()) != null) {
Object parsed = parseSseLine(line);
if (parsed == null) continue;
if ("[DONE]".equals(parsed)) break;
Map chunk = (Map) parsed;
String content = extractTokenContent(chunk);
if (!content.isEmpty()) {
fullText.append(content);
tokenCount++;
}
List> choices = (List>) chunk.get("choices");
if (choices != null && !choices.isEmpty()) {
Object fr = choices.get(0).get("finish_reason");
if (fr != null && !fr.toString().equals("null")) {
finishReason = fr.toString();
}
}
}
} catch (Exception e) {
if (fullText.length() == 0) {
throw new RuntimeException("Stream read failed: " + e.getMessage(), e);
}
// Preserve partial output on mid-stream error
}
long durationMs = Instant.now().toEpochMilli() - start.toEpochMilli();
return new StreamResult(fullText.toString(), tokenCount, durationMs, finishReason);
}
```
The code does the following:
* Records the start time before opening the stream so elapsed duration includes connection overhead
* Initializes accumulators for the full text, token count, and finish reason
* Iterates the stream line by line, parsing each with the SSE parser and skipping `null` lines
* Stops the loop when a non-null `finish_reason` is detected or a `[DONE]` sentinel arrives
* Returns a single result object containing the assembled text, token count, elapsed duration in milliseconds, and finish reason
### ✓ Checkpoint: Token Extraction & Accumulation
Run the validation test for this step:
```bash Python theme={null}
python tests/step4_accumulation_test.py
```
```bash JavaScript theme={null}
npm run test:step4
```
```bash TypeScript theme={null}
npm run test:step4
```
```bash PHP theme={null}
php tests/Step4AccumulationTest.php
```
```bash Go theme={null}
go run tests/step4_accumulation.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.tests.Step4AccumulationTest"
```
Your output should look similar to the following:
```
🧪 Testing: Token Extraction & Accumulation
Test 1: extract_token_content — normal chunk...
✓ Normal chunk → 'Hello'
Test 2: extract_token_content — null content delta...
✓ Null content → ''
Test 3: extract_token_content — empty delta (role-only chunk)...
✓ Empty delta → ''
Test 4: extract_token_content — no choices...
✓ Empty choices → ''
Test 5: extract_token_content — finish_reason chunk...
✓ finish_reason chunk → '' (no content tokens from finish chunk)
Test 6: stream_completion — full response assembly...
✓ Delta content extraction working
✓ Null delta handled gracefully
✓ finish_reason detected: stop
✓ Duration tracked: 2098ms
✓ Token count: 2 tokens
Response preview: '1 2 3 4 5'
✅ Full response assembled.
Next: Typing-Effect Renderer
```
**If tests fail**, check:
* The token content extractor returns `""` (not `None`/`null`) when content is absent
* The accumulation loop reads `finish_reason` from `choices[0]`, not from the top-level chunk
* The line buffer (`buffer = lines.pop()`) is in place for JS/TS/PHP
***
## Step 5: Typing-Effect Terminal Renderer
Now implement the terminal renderer, a function that prints each token immediately to stdout without a newline, creating a live typing effect in the terminal.
This step demonstrates an important pattern: consuming the stream directly rather than accumulating it first. The renderer calls the streaming request, SSE parsing, and token extraction functions, but skips the accumulation loop entirely.
### Key Concepts
#### Unbuffered Output
By default, most languages buffer stdout which means that output is held until the buffer fills or the program exits. For a typing effect you need every token to appear immediately. Each language has its own way to force this:
| Language | Unbuffered write |
| - | - |
| Python | `print(content, end="", flush=True)` |
| JavaScript / TypeScript | `process.stdout.write(content)` |
| PHP | `echo $content; ob_flush(); flush();` |
| Go | `fmt.Fprint(os.Stdout, content)` (stdout is unbuffered by default) |
| Java | `System.out.print(content); System.out.flush();` |
#### Direct Stream Consumption vs. Accumulation
The stream completion function from Step 4 accumulates everything and returns once the stream is complete. The terminal renderer function prints as it goes, the user sees output before the model has finished generating. Both patterns are valid; the right choice depends on whether the output needs to be complete before it's useful.
### Implementation Guide
Open the renderer file referenced in the code block. Unlike the streaming client, this file has a single method to implement. Review the TODO comments, then implement:
```python Python theme={null}
# File: browser/renderer.py
def render_stream_to_terminal(message: str, token: str) -> None:
print(f"Prompt: {message}\n")
print("Response: ", end="", flush=True)
response = make_streaming_request(message, token)
total_tokens = 0
finish_reason = "unknown"
for raw_line in response.iter_lines(decode_unicode=True):
chunk = parse_sse_line(raw_line)
if chunk is None:
continue
if chunk == "[DONE]":
break
content = extract_token_content(chunk)
if content:
print(content, end="", flush=True)
total_tokens += 1
choices = chunk.get("choices", [])
if choices and choices[0].get("finish_reason"):
finish_reason = choices[0]["finish_reason"]
print()
print(f"\n[{total_tokens} tokens, finish_reason={finish_reason}]")
```
```javascript JavaScript theme={null}
// File: src/browser/renderer.js
export async function renderStreamToTerminal(message, token) {
process.stdout.write(`Prompt: ${message}\n\nResponse: `);
const response = await makeStreamingRequest(message, token);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let totalTokens = 0;
let finishReason = 'unknown';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const chunk = parseSseLine(line);
if (chunk === null) continue;
if (chunk === '[DONE]') break;
const content = extractTokenContent(chunk);
if (content) {
process.stdout.write(content);
totalTokens += 1;
}
const choices = chunk?.choices;
if (choices && choices[0]?.finish_reason) {
finishReason = choices[0].finish_reason;
}
}
}
} finally {
reader.releaseLock();
}
process.stdout.write(
`\n\n[${totalTokens} tokens, finish_reason=${finishReason}]\n`,
);
}
```
```typescript TypeScript theme={null}
// File: src/browser/renderer.ts
export async function renderStreamToTerminal(
message: string,
token: string,
): Promise {
process.stdout.write(`Prompt: ${message}\n\nResponse: `);
const response = await makeStreamingRequest(message, token);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
let totalTokens = 0;
let finishReason = 'unknown';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const chunk = parseSseLine(line);
if (chunk === null) continue;
if (chunk === '[DONE]') break;
const content = extractTokenContent(chunk);
if (content) {
process.stdout.write(content);
totalTokens += 1;
}
const choices = chunk?.choices;
if (choices && choices[0]?.finish_reason) {
finishReason = choices[0].finish_reason;
}
}
}
} finally {
reader.releaseLock();
}
process.stdout.write(
`\n\n[${totalTokens} tokens, finish_reason=${finishReason}]\n`,
);
}
```
```php PHP theme={null}
// File: src/Browser/Renderer.php
public static function renderStreamToTerminal(string $message, string $token): void
{
echo "Prompt: {$message}\n\nResponse: ";
$totalTokens = 0;
$finishReason = 'unknown';
$lineBuffer = '';
$done = false;
$writeCallback = function (string $data) use (
&$totalTokens, &$finishReason, &$lineBuffer, &$done
): void {
if ($done) return;
$lineBuffer .= $data;
$lines = explode("\n", $lineBuffer);
$lineBuffer = array_pop($lines);
foreach ($lines as $line) {
$chunk = StreamClient::parseSseLine($line);
if ($chunk === null) continue;
if ($chunk === '[DONE]') { $done = true; break; }
$content = StreamClient::extractTokenContent($chunk);
if ($content !== '') {
echo $content;
if (ob_get_level() > 0) ob_flush();
flush();
$totalTokens++;
}
$choices = $chunk['choices'] ?? [];
if (!empty($choices) && !empty($choices[0]['finish_reason'])) {
$finishReason = $choices[0]['finish_reason'];
}
}
};
StreamClient::makeStreamingRequest($message, $token, $writeCallback);
echo "\n\n[{$totalTokens} tokens, finish_reason={$finishReason}]\n";
}
```
```go Go theme={null}
// File: pkg/browser/renderer.go
func RenderStreamToTerminal(message, token string) error {
fmt.Printf("Prompt: %s\n\nResponse: ", message)
resp, err := streaming.MakeStreamingRequest(message, token)
if err != nil {
return err
}
defer resp.Body.Close()
totalTokens := 0
finishReason := "unknown"
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
parsed := streaming.ParseSSELine(line)
if parsed == nil {
continue
}
if s, ok := parsed.(string); ok && s == "[DONE]" {
break
}
chunk, ok := parsed.(*streaming.SSEChunk)
if !ok {
continue
}
content := streaming.ExtractTokenContent(chunk)
if content != "" {
fmt.Fprint(os.Stdout, content)
totalTokens++
}
if len(chunk.Choices) > 0 && chunk.Choices[0].FinishReason != nil {
finishReason = *chunk.Choices[0].FinishReason
}
}
fmt.Printf("\n\n[%d tokens, finish_reason=%s]\n", totalTokens, finishReason)
return nil
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/browser/Renderer.java
@SuppressWarnings("unchecked")
public static void renderStreamToTerminal(String message, String token) {
System.out.print("Prompt: " + message + "\n\nResponse: ");
System.out.flush();
HttpResponse response = StreamClient.makeStreamingRequest(message, token);
int totalTokens = 0;
String finishReason = "unknown";
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body(), StandardCharsets.UTF_8)
)) {
String line;
while ((line = reader.readLine()) != null) {
Object parsed = StreamClient.parseSseLine(line);
if (parsed == null) continue;
if ("[DONE]".equals(parsed)) break;
Map chunk = (Map) parsed;
String content = StreamClient.extractTokenContent(chunk);
if (!content.isEmpty()) {
System.out.print(content);
System.out.flush();
totalTokens++;
}
List> choices = (List>) chunk.get("choices");
if (choices != null && !choices.isEmpty()) {
Object fr = choices.get(0).get("finish_reason");
if (fr != null && !fr.toString().equals("null")) {
finishReason = fr.toString();
}
}
}
} catch (Exception e) {
System.err.println("Error reading stream: " + e.getMessage());
}
System.out.println("\n\n[" + totalTokens + " tokens, finish_reason=" + finishReason + "]");
}
```
The code does the following:
* Prints the user's message as a prompt header before the response begins
* Opens the stream and iterates SSE lines directly, without an accumulation loop, so tokens are available to print as soon as they arrive
* Writes each token to stdout without a trailing newline and flushes immediately, producing a character-by-character typing effect
* Prints a summary line with the total token count and finish reason after the stream ends
### ✓ Checkpoint: Terminal Renderer
Run the validation test:
```bash Python theme={null}
python tests/step5_renderer_test.py
```
```bash JavaScript theme={null}
npm run test:step5
```
```bash TypeScript theme={null}
npm run test:step5
```
```bash PHP theme={null}
php tests/Step5RendererTest.php
```
```bash Go theme={null}
go run tests/step5_renderer.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.tests.Step5RendererTest"
```
Your output should look similar to the following:
```
🧪 Testing: Typing-Effect Renderer
✓ API key loaded
Test 1: render_stream_to_terminal() — streaming to terminal...
Prompt: Reply with exactly: Hello streaming world
Response: Hello streaming world
[2 tokens, finish_reason=stop]
✓ Prompt header printed
✓ Response label printed
✓ Token summary found: 2 tokens, finish_reason=stop
✅ Typing-effect renderer working.
Next: Server-Side Proxy
```
With a short prompt like this, tokens arrive so quickly that the typing effect
may not be visible — the response appears all at once. That's expected. In
production, longer AI responses make the effect clear: each token renders as
it arrives rather than waiting for the full response. This is the pattern your
chat UI will use.
**If tests fail**, check:
* Each token is written with no trailing newline
* `flush()` or equivalent is called after each write
* The summary line format is `[N tokens, finish_reason=X]`
***
## Step 6: Server-Side Proxy
In this step you'll implement the proxy server's stream handler. This is the route that receives requests from browser clients, forwards them upstream to Gloo AI with the server-side API key, and pipes the SSE response back.
### Key Concepts
#### Why a Proxy?
Browser JavaScript cannot safely include API credentials because anything in client code is visible to anyone who opens DevTools. A proxy server is the standard solution: the browser POSTs to your server, your server adds the API key and POSTs to Gloo AI, and the SSE stream flows back through your server to the browser.
An additional benefit: the proxy can add rate limiting, logging, and multi-tenant auth logic without touching client code.
#### SSE Headers That Matter
Three headers tell the browser (and any reverse proxies like nginx) that this is a live stream, not a buffered response:
| Header | Value | Why |
| - | - | - |
| `Content-Type` | `text/event-stream` | Identifies the SSE protocol |
| `Cache-Control` | `no-cache` | Prevents browser caching of the stream |
| `X-Accel-Buffering` | `no` | Disables nginx buffering so bytes arrive immediately |
#### Language-Specific Flushing
Each language needs an explicit flush mechanism to push bytes to the client immediately:
| Language | Flush mechanism |
| - | - |
| Python (Flask) | `yield` from a generator — Flask flushes on each `yield` |
| JavaScript/TypeScript | `res.write()` — Express sends immediately |
| PHP | `flush()` after each write |
| Go | `flusher.Flush()` — requires `http.Flusher` interface |
| Java | `out.flush()` after each write |
### Implementation Guide
Open the proxy server file referenced in the code block. The server setup and routing are already in place. Find the stream handler method (or route handler, depending on the language), review the TODO comments, and implement the relay logic:
```python Python theme={null}
# File: proxy/server.py
@app.route("/api/stream", methods=["POST", "OPTIONS"])
def stream_proxy():
if request.method == "OPTIONS":
return Response(status=204)
request_data = request.get_json() or {}
def generate():
try:
auth_token = os.getenv("GLOO_API_KEY")
headers = {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json",
}
payload = {**request_data, "stream": True}
with requests.post(
API_URL, headers=headers, json=payload, stream=True
) as resp:
if resp.status_code != 200:
yield f'data: {{"error": "API error {resp.status_code}"}}\n\n'
return
for line in resp.iter_lines():
if line:
decoded = line.decode("utf-8")
yield f"{decoded}\n\n"
except Exception as e:
yield f'data: {{"error": "{str(e)}"}}\n\n'
return Response(
generate(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
```
```javascript JavaScript theme={null}
// File: src/proxy/server.js
app.post('/api/stream', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Connection', 'keep-alive');
try {
const token = process.env.GLOO_API_KEY;
const payload = { ...req.body, stream: true };
const upstream = await fetch(API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!upstream.ok) {
const errText = await upstream.text();
res.write(
`data: {"error": "API error ${upstream.status}: ${errText.slice(0, 100)}"}\n\n`,
);
res.end();
return;
}
const reader = upstream.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
for (const line of text.split('\n')) {
if (line.trim()) {
res.write(`${line}\n\n`);
}
}
}
reader.releaseLock();
} catch (err) {
res.write(`data: {"error": "${err.message}"}\n\n`);
} finally {
res.end();
}
});
```
```typescript TypeScript theme={null}
// File: src/proxy/server.ts
app.post('/api/stream', async (req: Request, res: Response): Promise => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Connection', 'keep-alive');
try {
const token = process.env.GLOO_API_KEY;
const payload = { ...req.body, stream: true };
const upstream = await fetch(API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!upstream.ok) {
const errText = await upstream.text();
res.write(
`data: {"error": "API error ${upstream.status}: ${errText.slice(0, 100)}"}\n\n`,
);
res.end();
return;
}
const reader = upstream.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
for (const line of text.split('\n')) {
if (line.trim()) {
res.write(`${line}\n\n`);
}
}
}
reader.releaseLock();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
res.write(`data: {"error": "${message}"}\n\n`);
} finally {
res.end();
}
});
```
```php PHP theme={null}
// File: src/Proxy/Server.php
public static function handle(): void
{
$corsOrigin = $_ENV['PROXY_CORS_ORIGIN'] ?? 'http://localhost:3000';
header('Access-Control-Allow-Origin: ' . $corsOrigin);
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: POST, OPTIONS');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
if ($path === '/health') {
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['status' => 'ok', 'service' => 'completions-streaming-proxy']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
while (ob_get_level() > 0) ob_end_flush();
try {
$authToken = $_ENV['GLOO_API_KEY'];
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$body['stream'] = true;
$ch = curl_init(self::API_URL);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $authToken,
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => false,
CURLOPT_WRITEFUNCTION => function ($ch, $data) {
foreach (explode("\n", $data) as $line) {
if (trim($line)) {
echo $line . "\n\n";
flush();
}
}
return strlen($data);
},
]);
curl_exec($ch);
curl_close($ch);
} catch (\Throwable $e) {
echo 'data: {"error": "' . addslashes($e->getMessage()) . '"}' . "\n\n";
flush();
}
}
```
```go Go theme={null}
// File: pkg/proxy/server.go
func streamProxy(w http.ResponseWriter, r *http.Request) {
corsOrigin := os.Getenv("PROXY_CORS_ORIGIN")
if corsOrigin == "" {
corsOrigin = "http://localhost:3000"
}
w.Header().Set("Access-Control-Allow-Origin", corsOrigin)
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
flusher := w.(http.Flusher)
token := os.Getenv("GLOO_API_KEY")
body, _ := io.ReadAll(r.Body)
var reqBody map[string]any
json.Unmarshal(body, &reqBody)
reqBody["stream"] = true
payload, _ := json.Marshal(reqBody)
req, _ := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{}).Do(req)
if err != nil {
fmt.Fprintf(w, "data: {\"error\": \"%s\"}\n\n", err.Error())
flusher.Flush()
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Fprintf(w, "data: {\"error\": \"API error %d\"}\n\n", resp.StatusCode)
flusher.Flush()
return
}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
if line := scanner.Text(); line != "" {
fmt.Fprintf(w, "%s\n\n", line)
flusher.Flush()
}
}
}
```
```java Java theme={null}
// File: src/main/java/com/gloo/streaming/proxy/ProxyServer.java
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", corsOrigin);
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type, Authorization");
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS");
if ("OPTIONS".equals(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
return;
}
if (!"POST".equals(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(405, -1);
return;
}
exchange.getResponseHeaders().add("Content-Type", "text/event-stream");
exchange.getResponseHeaders().add("Cache-Control", "no-cache");
exchange.getResponseHeaders().add("X-Accel-Buffering", "no");
exchange.sendResponseHeaders(200, 0);
OutputStream out = exchange.getResponseBody();
try {
String authToken = System.getenv("GLOO_API_KEY");
String rawBody = new String(
exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8
);
Map body = GSON.fromJson(rawBody, Map.class);
if (body == null) body = new java.util.HashMap<>();
body.put("stream", true);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Authorization", "Bearer " + authToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
.build();
HttpResponse upstream = HTTP_CLIENT.send(
request, HttpResponse.BodyHandlers.ofInputStream()
);
if (upstream.statusCode() != 200) {
String errMsg = "data: {\"error\": \"API error " + upstream.statusCode() + "\"}\n\n";
out.write(errMsg.getBytes(StandardCharsets.UTF_8));
out.flush();
return;
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(upstream.body(), StandardCharsets.UTF_8)
)) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.isBlank()) {
out.write((line + "\n\n").getBytes(StandardCharsets.UTF_8));
out.flush();
}
}
}
} catch (Exception e) {
String errMsg = "data: {\"error\": \"" + e.getMessage().replace("\"", "'") + "\"}\n\n";
out.write(errMsg.getBytes(StandardCharsets.UTF_8));
out.flush();
} finally {
out.close();
}
} catch (Exception e) {
System.err.println("Handler error: " + e.getMessage());
}
}
```
The code does the following:
* Sets `Content-Type: text/event-stream`, `Cache-Control: no-cache`, and `X-Accel-Buffering: no` before writing any response data
* Handles `OPTIONS` preflight requests immediately so browsers can POST cross-origin
* Reads the API key from the server's environment, keeping credentials server-side
* Reads the incoming request body, injects `stream: true`, and forwards the request to the Gloo AI API
* Relays each non-blank SSE line to the client and flushes immediately so tokens reach the browser as they arrive
* Writes a structured error SSE frame if the upstream request fails, avoiding a silent stream close
PHP, Go, and Java use a generic HTTP handler that receives all request
methods, so they include an explicit 405 check before the streaming logic.
Python, JavaScript, and TypeScript register the route for POST only, so the
framework rejects other methods automatically.
### ✓ Checkpoint: Proxy Server
Run the proxy server validation test:
```bash Python theme={null}
python tests/step6_proxy_test.py
```
```bash JavaScript theme={null}
npm run test:step6
```
```bash TypeScript theme={null}
npm run test:step6
```
```bash PHP theme={null}
php tests/Step6ProxyTest.php
```
```bash Go theme={null}
go run tests/step6_proxy.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.tests.Step6ProxyTest"
```
Your output should look similar to the following:
```
🧪 Testing: Server-Side Proxy
Test 1: Starting proxy server on port 3001...
* Serving Flask app 'proxy.server'
* Debug mode: off
✓ Proxy server running at http://localhost:3001
Test 2: /health endpoint...
✓ /health returns: {'service': 'completions-streaming-proxy', 'status': 'ok'}
Test 3: POST /api/stream — Content-Type header...
✓ Content-Type: text/event-stream; charset=utf-8
Test 4: SSE line format (data: prefix)...
✓ All lines have 'data: ' prefix (3 data chunks received)
✓ Stream terminated cleanly (finish_reason=stop)
Test 5: CORS headers on response...
✓ Access-Control-Allow-Origin: http://localhost:3000
✅ Proxy server relaying SSE end-to-end.
Proxy complete: credentials stay server-side, client receives SSE.
```
**If tests fail**, check:
* CORS headers are set before sending the response headers (Java)
* `X-Accel-Buffering: no` is present (required to disable nginx buffering)
* Go: the flusher interface assertion must succeed — this panics if the `ResponseWriter` doesn't support flushing
* PHP: clear any existing output buffers before setting SSE headers
***
## Step 7: Testing Your Complete Implementation
With all six steps implemented, you can now run the full demo, test the proxy server via API, and explore the browser demo.
### Run the Demo Script
The entry point runs both examples back-to-back: first it accumulates a full response and prints it, then it streams a second response to the terminal with a typing effect.
```bash Python theme={null}
python main.py
```
```bash JavaScript theme={null}
npm start
```
```bash TypeScript theme={null}
npm start
```
```bash PHP theme={null}
composer start
```
```bash Go theme={null}
go run main.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.Main"
```
Your output should look similar to:
```
Streaming AI Responses in Real Time
Environment variables loaded
Example: Streaming a completion (accumulate full text)...
Full response:
The resurrection of Jesus Christ is a cornerstone of Christian
faith, holding profound significance for believers. It's not
merely a historical event but a theological truth that reshapes
our understanding of God, humanity, and...
Received 16 tokens in 6864ms
Finish reason: stop
Example: Typing-effect rendering...
Prompt: Tell me about Christian discipleship.
Response: Christian discipleship is a transformative journey of
following Jesus Christ, learning from His teachings, and striving
to live a life that reflects His character and mission...
[11 tokens, finish_reason=stop]
```
### Test the Proxy Server via API
Start the proxy server in one terminal:
```bash Python theme={null}
python proxy/server.py
```
```bash JavaScript theme={null}
npm run proxy
```
```bash TypeScript theme={null}
npm run proxy
```
```bash PHP theme={null}
composer proxy
```
```bash Go theme={null}
go run cmd/proxy/main.go
```
```bash Java theme={null}
mvn -q compile exec:java -Dexec.mainClass="com.gloo.streaming.proxy.ProxyServer"
```
Then send a request from another terminal using curl:
```bash theme={null}
curl -X POST http://localhost:3001/api/stream \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Hello!"}], "auto_routing": true}'
```
You will see the SSE stream arrive line by line:
```
data: {"id": "gen-abc123", "choices": [{"delta": {"content": "Hello", "function_call": null, "refusal": null, "role": "assistant", "tool_calls": null}, "finish_reason": null, "index": 0, "logprobs": null, "native_finish_reason": null}], "created": 1774527271, "model": "google/gemini-2.5-flash", "object": "chat.completion.chunk", "service_tier": null, "system_fingerprint": null, "usage": null, "provider": "Gloo AI", "ttft_ms": 940.61}
data: {"id": "gen-abc123", "choices": [{"delta": {"content": "! How", "function_call": null, "refusal": null, "role": "assistant", "tool_calls": null}, "finish_reason": null, "index": 0, "logprobs": null, "native_finish_reason": null}], "created": 1774527271, "model": "google/gemini-2.5-flash", "object": "chat.completion.chunk", "service_tier": null, "system_fingerprint": null, "usage": null, "provider": "Gloo AI"}
data: {"id": "gen-abc123", "choices": [{"delta": {"content": " can I help you today?", "function_call": null, "refusal": null, "role": "assistant", "tool_calls": null}, "finish_reason": null, "index": 0, "logprobs": null, "native_finish_reason": null}], "created": 1774527271, "model": "google/gemini-2.5-flash", "object": "chat.completion.chunk", "service_tier": null, "system_fingerprint": null, "usage": null, "provider": "Gloo AI"}
data: {"id": "gen-abc123", "choices": [{"delta": {"content": "", "function_call": null, "refusal": null, "role": "assistant", "tool_calls": null}, "finish_reason": "stop", "index": 0, "logprobs": null, "native_finish_reason": "STOP"}], "created": 1774527271, "model": "google/gemini-2.5-flash", "object": "chat.completion.chunk", "service_tier": null, "system_fingerprint": null, "usage": null, "provider": "Gloo AI"}
```
Each line is a JSON-encoded delta. The final chunk signals the end of the stream with a non-null `finish_reason`.
### Browser Demo
The browser demo is a standalone HTML file separate from the language starter projects — no install step required.
Download or clone this directory alongside your language starter
The file connects to the proxy over HTTP, so it works with **any language's proxy server**.
With the proxy already running on port 3001, serve the browser client from the `frontend-example/` directory using whichever tool you have available:
```bash theme={null}
# Node
npx serve
# Python
python -m http.server 3000
# PHP
php -S localhost:3000
```
Do not open `index.html` directly via `File > Open`. When loaded as a `file://` URL, the browser reports `Origin: null`, which the proxy's CORS policy rejects. You must serve the file over HTTP so the origin is `http://localhost:3000`.
Then open `http://localhost:3000` in your browser, type a question, and click **Send** — tokens appear one by one as they arrive from the proxy.
#### How the Browser Connects to the Stream
Browsers have a built-in API called `EventSource` designed for receiving server-sent events — but it only supports `GET` requests. Since the completions API requires a `POST` body containing the message text, `EventSource` can't be used here. Instead, the demo page uses `fetch()` with a `ReadableStream`, which supports any HTTP method:
```js theme={null}
// File: frontend-example/index.html
const response = await fetch('http://localhost:3001/api/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [{ role: 'user', content: message }] }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
```
The `ReadableStream` API works identically to what you used in the terminal renderer — the same line buffer, SSE parser, and token extractor pattern applies.
#### Markdown Rendering
AI responses often contain markdown. Inserting raw tokens directly into the DOM produces broken mid-stream output — `**bo` appears before `ld**` closes the bold span. The correct pattern is to **accumulate tokens and re-parse the full buffer on each token**:
```js theme={null}
// File: frontend-example/index.html
let buffer = '';
// On each token:
buffer += content;
outputEl.innerHTML = DOMPurify.sanitize(marked.parse(buffer));
```
`marked.parse()` runs on every token — slightly redundant but always produces valid HTML. `DOMPurify.sanitize()` prevents XSS from any HTML in the AI response.
For production, serve the browser client from the same origin as the proxy, or
set `PROXY_CORS_ORIGIN` in your `.env` to match your frontend domain.
For React applications, the [Vercel AI SDK](https://sdk.vercel.ai/) `useChat`
hook handles streaming, markdown rendering, and state management out of the
box — it's a higher-level alternative to building this pattern manually.
***
## Troubleshooting
**Stream hangs and never produces output**
: Verify `"stream": true` is in the request payload. Without it, the API returns a single buffered JSON response so the connection may appear to hang while waiting for a response format that never arrives.
**Garbled or split tokens**
: The line buffer is missing or incorrect. In JS/TS/PHP, raw bytes must be accumulated and split on `\n` before parsing. Make sure `buffer = lines.pop()` saves the incomplete last fragment.
**`Authentication failed (401)`**
: Your `.env` file is missing `GLOO_API_KEY`, or the values are incorrect. Run the Step 1 checkpoint to verify credentials load correctly.
**Browser blocks direct API calls (CORS error)**
: Browsers enforce same-origin policy. Direct calls from browser JavaScript to `platform.ai.gloo.com` will be blocked. Use the proxy server (Step 6) so API calls happen server-side.
**`Failed to fetch` when serving the browser demo on a port other than 3000**
: The proxy allows requests only from `http://localhost:3000` by default. If your file server uses a different port (e.g. VS Code / Cursor Live Server on port 5500, or `python -m http.server 8080`), the browser's `Origin` header won't match and the proxy blocks the request. Fix: set `PROXY_CORS_ORIGIN` in your `.env` to the **exact** origin shown in your browser's address bar, then restart the proxy.
```bash theme={null}
# .env — must be an exact match including hostname
PROXY_CORS_ORIGIN=http://127.0.0.1:5500 # Cursor / VS Code Live Server
```
Note that `http://localhost:5500` and `http://127.0.0.1:5500` are treated as different origins by the browser even though they resolve to the same address. Copy the origin directly from the address bar to avoid a mismatch.
**PHP output appears all at once**
: PHP's output buffering is active. Call `ob_end_flush()` (or `while (ob_get_level() > 0) ob_end_flush()`) before the SSE loop to disable buffering.
**Go panics on `w.(http.Flusher)`**
: Your `http.ResponseWriter` doesn't implement `http.Flusher`. This shouldn't happen with the standard `net/http` server, but will happen with some test wrappers. Make sure you're using `http.ResponseWriter` directly.
**Mid-stream disconnect loses all output**
: Wrap the read loop in try/catch (or check errors in Go). If `fullText` already has content when the error occurs, return it rather than re-raising — partial responses are usually more useful than nothing.
**Broken markdown mid-stream**
: Do not insert raw tokens into `innerHTML`. Accumulate the full buffer and call `marked.parse(buffer)` on every token — this ensures the markdown is always valid HTML at each step.
***
## View the Completed Project
If you want to see a working reference before or after completing the steps, the final project is available in the tutorial repository:
Browse the complete implementation in all six languages — Python, JavaScript,
TypeScript, PHP, Go, and Java.
***
## Next Steps
* **[Grounded Completions](/tutorials/completions-grounded)** — add retrieved context from your content library to improve response accuracy
* **[Tool Use](/tutorials/completions-tool-use)** — combine streaming with function calling for real-time tool-augmented responses
* **[Completions API reference](/api-guides/completions-v2)** — explore all available parameters including `tradition`, `model_family`, and `model`
# Completions with Tool Use
Source: https://docs.gloo.com/tutorials/completions-tool-use
Learn how to extend model capabilities via the Completions API with tool use.
This recipe provides an example of Completions API tool use for structured output. Instead of receiving unstructured text responses, you can force the AI to return predictable, machine-readable data that your application can easily parse and use.
We'll build a goal-setting assistant that takes a user's goal in plain English and returns a structured growth plan with actionable steps and timelines.
**Completions V1 is Deprecated**: This tutorial now uses the V2 API. If you're using V1 (`/ai/v1/chat/completions`), please migrate to V2 for better performance and intelligent routing.
[View V2 Guide](/api-guides/completions-v2) | [Migration Steps](/api-guides/completions-v2#migrating-from-completions-v1)
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first
The Completions API with tool use requires an API key for authentication. If you haven't set up authentication yet, follow the [Authentication Tutorial](/tutorials/authentication).
***
## Step 1: Define Your Tool Schema
The first step is to define a function schema that describes the structured output you want. This tells the AI exactly what format to return:
```json theme={null}
{
"tools": [
{
"type": "function",
"function": {
"name": "create_growth_plan",
"description": "Creates a structured personal growth plan with a title and a series of actionable steps.",
"parameters": {
"type": "object",
"properties": {
"goal_title": {
"type": "string",
"description": "A concise, encouraging title for the user's goal."
},
"steps": {
"type": "array",
"description": "A list of concrete steps the user should take.",
"items": {
"type": "object",
"properties": {
"step_number": { "type": "integer" },
"action": {
"type": "string",
"description": "The specific, actionable task for this step."
},
"timeline": {
"type": "string",
"description": "A suggested timeframe for this step (e.g., 'Week 1-2')."
}
},
"required": ["step_number", "action", "timeline"]
}
}
},
"required": ["goal_title", "steps"]
}
}
}
]
}
```
***
## Step 2: Make the API Call
Now make the API request with the tool definition and set `tool_choice: "required"` to force the AI to use your tool:
```json theme={null}
{
"auto_routing": true,
"messages": [
{
"role": "user",
"content": "I want to grow in my faith."
}
],
"tools": [
// ... The tool definition from Step 1 goes here ...
],
"tool_choice": "required"
}
```
**Auto-Routing**: Setting `auto_routing: true` lets Gloo AI automatically select the optimal model for your tool use request. This is the recommended V2 approach. You can also specify a direct model like `"model": "gloo-anthropic-claude-sonnet-4.5"` if you need explicit control.
### Example Response
The API will return structured data in the `tool_calls` array:
```json theme={null}
{
"id": "chatcmpl-ecc49558",
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": null,
"role": "assistant",
"tool_calls": [
{
"id": "tooluse_c0lmjtOJSlOYQZPBsp4biQ",
"function": {
"arguments": "{\"goal_title\": \"Growing Deeper in Faith\", \"steps\": [{\"step_number\": 1, \"action\": \"Establish a consistent daily quiet time with God through prayer and Bible reading, starting with 10-15 minutes each morning\", \"timeline\": \"Week 1-2\"}, {\"step_number\": 2, \"action\": \"Choose a Bible reading plan or devotional to provide structure for your study time\", \"timeline\": \"Week 2-3\"}, {\"step_number\": 3, \"action\": \"Find a local church community or small group where you can worship, learn, and build relationships with other believers\", \"timeline\": \"Week 3-4\"}, {\"step_number\": 4, \"action\": \"Begin journaling your prayers, thoughts, and insights from Scripture to track your spiritual growth\", \"timeline\": \"Week 4-5\"}, {\"step_number\": 5, \"action\": \"Look for opportunities to serve others in your community or church as a way to live out your faith\", \"timeline\": \"Month 2\"}, {\"step_number\": 6, \"action\": \"Seek out a mentor or accountability partner who can encourage you and help you grow in your walk with God\", \"timeline\": \"Month 2-3\"}]}",
"name": "create_growth_plan"
},
"type": "function"
}
]
}
}
],
"created": 1755118870,
"model": "gloo-anthropic-claude-sonnet-4.5",
"object": "chat.completion",
"usage": {
"completion_tokens": 391,
"prompt_tokens": 1631,
"total_tokens": 2022
}
}
```
Notice how the `arguments` field contains a JSON string that matches your tool schema perfectly!
***
## Complete Examples
The following examples combine authentication, making the API call with tool use, and parsing the structured response into a complete, runnable script for each language.
You'll want to first set up your environment variables in either an `.env` file:
```
GLOO_API_KEY=YOUR_API_KEY
```
Or export them in your shell for Go and Java:
```bash theme={null}
export GLOO_API_KEY="your_actual_api_key_here"
```
```python Python theme={null}
import requests
import json
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# --- Configuration ---
# It's recommended to load your API key from an environment variable
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
def create_goal_setting_request(user_goal):
"""Creates a goal-setting request with tool use."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
payload = {
"auto_routing": True,
"messages": [{"role": "user", "content": user_goal}],
"tools": [
{
"type": "function",
"function": {
"name": "create_growth_plan",
"description": "Creates a structured personal growth plan with a title and a series of actionable steps.",
"parameters": {
"type": "object",
"properties": {
"goal_title": {
"type": "string",
"description": "A concise, encouraging title for the user's goal."
},
"steps": {
"type": "array",
"description": "A list of concrete steps the user should take.",
"items": {
"type": "object",
"properties": {
"step_number": {"type": "integer"},
"action": {
"type": "string",
"description": "The specific, actionable task for this step."
},
"timeline": {
"type": "string",
"description": "A suggested timeframe for this step (e.g., 'Week 1-2')."
}
},
"required": ["step_number", "action", "timeline"]
}
}
},
"required": ["goal_title", "steps"]
}
}
}
],
"tool_choice": "required"
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
def parse_growth_plan(api_response):
"""Parses the API response and extracts the structured growth plan."""
try:
tool_call = api_response['choices'][0]['message']['tool_calls'][0]
function_args = json.loads(tool_call['function']['arguments'])
return function_args
except (KeyError, IndexError, json.JSONDecodeError) as e:
raise ValueError(f"Failed to parse growth plan: {e}")
def display_growth_plan(growth_plan):
"""Displays the growth plan in a user-friendly format."""
print(f"\n🎯 {growth_plan['goal_title']}")
print("=" * (len(growth_plan['goal_title']) + 4))
for step in growth_plan['steps']:
print(f"\n{step['step_number']}. {step['action']}")
print(f" ⏰ Timeline: {step['timeline']}")
# --- Main Execution ---
if __name__ == "__main__":
try:
user_goal = "I want to grow in my faith."
print(f"Creating growth plan for: '{user_goal}'")
# Make API call with tool use
response = create_goal_setting_request(user_goal)
# Parse the structured response
growth_plan = parse_growth_plan(response)
# Display the results
display_growth_plan(growth_plan)
# Also show raw JSON for developers
print(f"\n📊 Raw JSON output:")
print(json.dumps(growth_plan, indent=2))
except requests.exceptions.HTTPError as err:
print(f"An HTTP error occurred: {err}")
except Exception as err:
print(f"An error occurred: {err}")
```
```javascript JavaScript theme={null}
// Load environment variables from .env file
require('dotenv').config();
const axios = require('axios');
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
async function createGoalSettingRequest(userGoal) {
const payload = {
auto_routing: true,
messages: [{ role: "user", content: userGoal }],
tools: [
{
type: "function",
function: {
name: "create_growth_plan",
description: "Creates a structured personal growth plan with a title and a series of actionable steps.",
parameters: {
type: "object",
properties: {
goal_title: {
type: "string",
description: "A concise, encouraging title for the user's goal."
},
steps: {
type: "array",
description: "A list of concrete steps the user should take.",
items: {
type: "object",
properties: {
step_number: { type: "integer" },
action: {
type: "string",
description: "The specific, actionable task for this step."
},
timeline: {
type: "string",
description: "A suggested timeframe for this step (e.g., 'Week 1-2')."
}
},
required: ["step_number", "action", "timeline"]
}
}
},
required: ["goal_title", "steps"]
}
}
}
],
tool_choice: "required"
};
const response = await axios.post(API_URL, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
function parseGrowthPlan(apiResponse) {
try {
const toolCall = apiResponse.choices[0].message.tool_calls[0];
return JSON.parse(toolCall.function.arguments);
} catch (error) {
throw new Error(`Failed to parse growth plan: ${error.message}`);
}
}
function displayGrowthPlan(growthPlan) {
console.log(`\n🎯 ${growthPlan.goal_title}`);
console.log("=".repeat(growthPlan.goal_title.length + 4));
growthPlan.steps.forEach(step => {
console.log(`\n${step.step_number}. ${step.action}`);
console.log(` ⏰ Timeline: ${step.timeline}`);
});
}
// --- Main Execution ---
async function main() {
try {
const userGoal = "I want to grow in my faith.";
console.log(`Creating growth plan for: '${userGoal}'`);
// Make API call with tool use
const response = await createGoalSettingRequest(userGoal);
// Parse the structured response
const growthPlan = parseGrowthPlan(response);
// Display the results
displayGrowthPlan(growthPlan);
// Also show raw JSON for developers
console.log(`\n📊 Raw JSON output:`);
console.log(JSON.stringify(growthPlan, null, 2));
} catch (error) {
console.error("An error occurred:", error.response ? error.response.data : error.message);
}
}
main();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Type definitions
interface GrowthStep {
step_number: number;
action: string;
timeline: string;
}
interface GrowthPlan {
goal_title: string;
steps: GrowthStep[];
}
interface ToolCall {
id: string;
type: string;
function: {
name: string;
arguments: string;
};
}
interface ApiResponse {
choices: Array<{
message: {
tool_calls: ToolCall[];
};
}>;
}
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
async function createGoalSettingRequest(userGoal: string): Promise {
const payload = {
auto_routing: true,
messages: [{ role: "user", content: userGoal }],
tools: [
{
type: "function",
function: {
name: "create_growth_plan",
description: "Creates a structured personal growth plan with a title and a series of actionable steps.",
parameters: {
type: "object",
properties: {
goal_title: {
type: "string",
description: "A concise, encouraging title for the user's goal."
},
steps: {
type: "array",
description: "A list of concrete steps the user should take.",
items: {
type: "object",
properties: {
step_number: { type: "integer" },
action: {
type: "string",
description: "The specific, actionable task for this step."
},
timeline: {
type: "string",
description: "A suggested timeframe for this step (e.g., 'Week 1-2')."
}
},
required: ["step_number", "action", "timeline"]
}
}
},
required: ["goal_title", "steps"]
}
}
}
],
tool_choice: "required"
};
const response = await axios.post(API_URL, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
function parseGrowthPlan(apiResponse: ApiResponse): GrowthPlan {
try {
const toolCall = apiResponse.choices[0].message.tool_calls[0];
return JSON.parse(toolCall.function.arguments) as GrowthPlan;
} catch (error: any) {
throw new Error(`Failed to parse growth plan: ${error.message}`);
}
}
function displayGrowthPlan(growthPlan: GrowthPlan): void {
console.log(`\n🎯 ${growthPlan.goal_title}`);
console.log("=".repeat(growthPlan.goal_title.length + 4));
growthPlan.steps.forEach(step => {
console.log(`\n${step.step_number}. ${step.action}`);
console.log(` ⏰ Timeline: ${step.timeline}`);
});
}
// --- Main Execution ---
async function main(): Promise {
try {
const userGoal = "I want to grow in my faith.";
console.log(`Creating growth plan for: '${userGoal}'`);
// Make API call with tool use
const response = await createGoalSettingRequest(userGoal);
// Parse the structured response
const growthPlan = parseGrowthPlan(response);
// Display the results
displayGrowthPlan(growthPlan);
// Also show raw JSON for developers
console.log(`\n📊 Raw JSON output:`);
console.log(JSON.stringify(growthPlan, null, 2));
} catch (error: any) {
console.error("An error occurred:", error.response ? error.response.data : error.message);
}
}
main();
```
```php PHP theme={null}
load();
// --- Configuration ---
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$API_URL = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions';
// Validate credentials
if ($API_KEY === 'YOUR_API_KEY' || empty($API_KEY)) {
echo "Error: GLOO_API_KEY must be set\n";
echo "Either:\n";
echo "1. Create a .env file with your credentials:\n";
echo " GLOO_API_KEY=your_api_key_here\n";
echo "2. Export them as environment variables:\n";
echo " export GLOO_API_KEY=\"your_api_key_here\"\n";
exit(1);
}
function createGoalSettingRequest($userGoal, $apiUrl, $apiKey) {
$tools = [
[
'type' => 'function',
'function' => [
'name' => 'create_growth_plan',
'description' => 'Creates a structured personal growth plan with a title and a series of actionable steps.',
'parameters' => [
'type' => 'object',
'properties' => [
'goal_title' => [
'type' => 'string',
'description' => 'A concise, encouraging title for the user\'s goal.'
],
'steps' => [
'type' => 'array',
'description' => 'A list of concrete steps the user should take.',
'items' => [
'type' => 'object',
'properties' => [
'step_number' => ['type' => 'integer'],
'action' => [
'type' => 'string',
'description' => 'The specific, actionable task for this step.'
],
'timeline' => [
'type' => 'string',
'description' => 'A suggested timeframe for this step (e.g., \'Week 1-2\').'
]
],
'required' => ['step_number', 'action', 'timeline']
]
]
],
'required' => ['goal_title', 'steps']
]
]
]
];
$payload = json_encode([
'auto_routing' => true,
'messages' => [['role' => 'user', 'content' => $userGoal]],
'tools' => $tools,
'tool_choice' => 'required'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) throw new Exception(curl_error($ch));
curl_close($ch);
return json_decode($result, true);
}
function parseGrowthPlan($apiResponse) {
try {
$toolCall = $apiResponse['choices'][0]['message']['tool_calls'][0];
return json_decode($toolCall['function']['arguments'], true);
} catch (Exception $e) {
throw new Exception("Failed to parse growth plan: " . $e->getMessage());
}
}
function displayGrowthPlan($growthPlan) {
echo "\n🎯 " . $growthPlan['goal_title'] . "\n";
echo str_repeat("=", strlen($growthPlan['goal_title']) + 4) . "\n";
foreach ($growthPlan['steps'] as $step) {
echo "\n" . $step['step_number'] . ". " . $step['action'] . "\n";
echo " ⏰ Timeline: " . $step['timeline'] . "\n";
}
}
// --- Main Execution ---
try {
$userGoal = "I want to grow in my faith.";
echo "Creating growth plan for: '$userGoal'\n";
// Make API call with tool use
$response = createGoalSettingRequest($userGoal, $API_URL, $API_KEY);
// Parse the structured response
$growthPlan = parseGrowthPlan($response);
// Display the results
displayGrowthPlan($growthPlan);
// Also show raw JSON for developers
echo "\n📊 Raw JSON output:\n";
echo json_encode($growthPlan, JSON_PRETTY_PRINT) . "\n";
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/joho/godotenv"
)
// --- Configuration ---
var (
apiKey string
apiURL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
)
// --- Data Structures ---
type GrowthStep struct {
StepNumber int `json:"step_number"`
Action string `json:"action"`
Timeline string `json:"timeline"`
}
type GrowthPlan struct {
GoalTitle string `json:"goal_title"`
Steps []GrowthStep `json:"steps"`
}
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
type ApiResponse struct {
Choices []struct {
Message struct {
ToolCalls []ToolCall `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
// --- Function Definitions ---
func createGoalSettingRequest(userGoal string) (*ApiResponse, error) {
tools := []map[string]interface{}{
{
"type": "function",
"function": map[string]interface{}{
"name": "create_growth_plan",
"description": "Creates a structured personal growth plan with a title and a series of actionable steps.",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"goal_title": map[string]interface{}{
"type": "string",
"description": "A concise, encouraging title for the user's goal.",
},
"steps": map[string]interface{}{
"type": "array",
"description": "A list of concrete steps the user should take.",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"step_number": map[string]string{"type": "integer"},
"action": map[string]string{
"type": "string",
"description": "The specific, actionable task for this step.",
},
"timeline": map[string]string{
"type": "string",
"description": "A suggested timeframe for this step (e.g., 'Week 1-2').",
},
},
"required": []string{"step_number", "action", "timeline"},
},
},
},
"required": []string{"goal_title", "steps"},
},
},
},
}
payload := map[string]interface{}{
"auto_routing": true,
"messages": []map[string]string{{"role": "user", "content": userGoal}},
"tools": tools,
"tool_choice": "required",
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body))
}
var result ApiResponse
json.Unmarshal(body, &result)
return &result, nil
}
func parseGrowthPlan(apiResponse *ApiResponse) (*GrowthPlan, error) {
if len(apiResponse.Choices) == 0 || len(apiResponse.Choices[0].Message.ToolCalls) == 0 {
return nil, fmt.Errorf("no tool calls found in response")
}
toolCall := apiResponse.Choices[0].Message.ToolCalls[0]
var growthPlan GrowthPlan
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &growthPlan); err != nil {
return nil, fmt.Errorf("failed to parse growth plan: %v", err)
}
return &growthPlan, nil
}
func displayGrowthPlan(growthPlan *GrowthPlan) {
fmt.Printf("\n🎯 %s\n", growthPlan.GoalTitle)
fmt.Printf("%s\n", strings.Repeat("=", len(growthPlan.GoalTitle)+4))
for _, step := range growthPlan.Steps {
fmt.Printf("\n%d. %s\n", step.StepNumber, step.Action)
fmt.Printf(" ⏰ Timeline: %s\n", step.Timeline)
}
}
// Helper to get environment variables
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
// Initialize loads environment variables and validates configuration
func init() {
// Load environment variables from .env file if it exists
_ = godotenv.Load()
// Get credentials from environment
apiKey = getEnv("GLOO_API_KEY", "")
// Validate that credentials are provided
if apiKey == "" {
fmt.Println("Error: GLOO_API_KEY must be set")
fmt.Println("Either:")
fmt.Println("1. Create a .env file with your credentials:")
fmt.Println(" GLOO_API_KEY=your_api_key_here")
fmt.Println("2. Export them as environment variables:")
fmt.Println(" export GLOO_API_KEY=\"your_api_key_here\"")
os.Exit(1)
}
}
// --- Main Execution ---
func main() {
userGoal := "I want to grow in my faith."
fmt.Printf("Creating growth plan for: '%s'\n", userGoal)
// Make API call with tool use
response, err := createGoalSettingRequest(userGoal)
if err != nil {
fmt.Printf("Error creating growth plan: %v\n", err)
return
}
// Parse the structured response
growthPlan, err := parseGrowthPlan(response)
if err != nil {
fmt.Printf("Error parsing growth plan: %v\n", err)
return
}
// Display the results
displayGrowthPlan(growthPlan)
// Also show raw JSON for developers
fmt.Printf("\n📊 Raw JSON output:\n")
jsonBytes, _ := json.MarshalIndent(growthPlan, "", " ")
fmt.Printf("%s\n", string(jsonBytes))
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import io.github.cdimascio.dotenv.Dotenv;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
// --- Main Application Class ---
public class Main {
public static void main(String[] args) {
// Validate credentials before proceeding
if (!CompletionsToolUse.validateCredentials()) {
System.exit(1);
}
CompletionsToolUse toolUse = new CompletionsToolUse();
try {
String userGoal = "I want to grow in my faith.";
System.out.println("Creating growth plan for: '" + userGoal + "'");
// Make API call with tool use
ApiResponse response = toolUse.createGoalSettingRequest(userGoal);
// Parse the structured response
GrowthPlan growthPlan = toolUse.parseGrowthPlan(response);
// Display the results
toolUse.displayGrowthPlan(growthPlan);
// Also show raw JSON for developers
System.out.println("\n📊 Raw JSON output:");
System.out.println(toolUse.gson.toJson(growthPlan));
} catch (Exception e) {
e.printStackTrace();
}
}
}
// --- Completions Tool Use Class ---
class CompletionsToolUse {
// Load environment variables from .env file if it exists
private static final Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
private static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
private static final String API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
private final HttpClient httpClient = HttpClient.newHttpClient();
public final Gson gson = new Gson();
// Validate that credentials are provided
public static boolean validateCredentials() {
if ("YOUR_API_KEY".equals(API_KEY) || API_KEY == null || API_KEY.trim().isEmpty()) {
System.err.println("Error: GLOO_API_KEY must be set");
System.err.println("Either:");
System.err.println("1. Create a .env file with your credentials:");
System.err.println(" GLOO_API_KEY=your_api_key_here");
System.err.println("2. Export them as environment variables:");
System.err.println(" export GLOO_API_KEY=\"your_api_key_here\"");
return false;
}
return true;
}
// --- Data Classes ---
static class GrowthStep {
int step_number;
String action;
String timeline;
}
static class GrowthPlan {
String goal_title;
List steps;
}
static class ToolCall {
String id;
String type;
Function function;
static class Function {
String name;
String arguments;
}
}
static class ApiResponse {
List choices;
static class Choice {
Message message;
static class Message {
List tool_calls;
}
}
}
// --- API Methods ---
public ApiResponse createGoalSettingRequest(String userGoal) throws IOException, InterruptedException {
// Create the tool definition using JsonObject for proper structure
JsonObject toolSchema = new JsonObject();
toolSchema.addProperty("type", "function");
JsonObject function = new JsonObject();
function.addProperty("name", "create_growth_plan");
function.addProperty("description", "Creates a structured personal growth plan with a title and a series of actionable steps.");
JsonObject parameters = new JsonObject();
parameters.addProperty("type", "object");
JsonObject properties = new JsonObject();
JsonObject goalTitle = new JsonObject();
goalTitle.addProperty("type", "string");
goalTitle.addProperty("description", "A concise, encouraging title for the user's goal.");
properties.add("goal_title", goalTitle);
JsonObject steps = new JsonObject();
steps.addProperty("type", "array");
steps.addProperty("description", "A list of concrete steps the user should take.");
JsonObject items = new JsonObject();
items.addProperty("type", "object");
JsonObject itemProperties = new JsonObject();
JsonObject stepNumber = new JsonObject();
stepNumber.addProperty("type", "integer");
itemProperties.add("step_number", stepNumber);
JsonObject action = new JsonObject();
action.addProperty("type", "string");
action.addProperty("description", "The specific, actionable task for this step.");
itemProperties.add("action", action);
JsonObject timeline = new JsonObject();
timeline.addProperty("type", "string");
timeline.addProperty("description", "A suggested timeframe for this step (e.g., 'Week 1-2').");
itemProperties.add("timeline", timeline);
items.add("properties", itemProperties);
JsonArray required = new JsonArray();
required.add("step_number");
required.add("action");
required.add("timeline");
items.add("required", required);
steps.add("items", items);
properties.add("steps", steps);
parameters.add("properties", properties);
JsonArray requiredParams = new JsonArray();
requiredParams.add("goal_title");
requiredParams.add("steps");
parameters.add("required", requiredParams);
function.add("parameters", parameters);
toolSchema.add("function", function);
JsonArray tools = new JsonArray();
tools.add(toolSchema);
JsonObject message = new JsonObject();
message.addProperty("role", "user");
message.addProperty("content", userGoal);
JsonArray messages = new JsonArray();
messages.add(message);
JsonObject payload = new JsonObject();
payload.addProperty("auto_routing", true);
payload.add("messages", messages);
payload.add("tools", tools);
payload.addProperty("tool_choice", "required");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("API call failed: " + response.body());
}
return gson.fromJson(response.body(), ApiResponse.class);
}
public GrowthPlan parseGrowthPlan(ApiResponse apiResponse) {
try {
ToolCall toolCall = apiResponse.choices.get(0).message.tool_calls.get(0);
return gson.fromJson(toolCall.function.arguments, GrowthPlan.class);
} catch (Exception e) {
throw new RuntimeException("Failed to parse growth plan: " + e.getMessage(), e);
}
}
public void displayGrowthPlan(GrowthPlan growthPlan) {
System.out.println("\n🎯 " + growthPlan.goal_title);
System.out.println("=".repeat(growthPlan.goal_title.length() + 4));
for (GrowthStep step : growthPlan.steps) {
System.out.println("\n" + step.step_number + ". " + step.action);
System.out.println(" ⏰ Timeline: " + step.timeline);
}
}
}
```
***
## Testing Your Implementation
To test any of the complete examples:
1. **Set up your environment variables** with your actual Gloo AI credentials
2. **Install dependencies** according to each language's requirements
3. **Run the script** and observe the structured output
The script will:
* Authenticate using your API key
* Make a tool-use API call with the goal "I want to grow in my faith"
* Parse the JSON response into a structured format
* Display the growth plan in a user-friendly format
* Show the raw JSON for developers
***
## Key Benefits
This approach provides several advantages over simple chat completions:
1. **Predictable Structure**: The response always follows your defined schema
2. **Machine-Readable**: Easy to parse and use in applications
3. **Type Safety**: Clear data types for each field
4. **Validation**: The API enforces your schema requirements
5. **Flexibility**: Can adapt to any structured output needs
***
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions.
## Next Steps
Now that you understand how to use tool use for structured output with V2, consider exploring:
1. **[Completions V2 Guide](/api-guides/completions-v2)** - Learn about auto-routing and model selection options
2. **[Tool Use Guide](/api-guides/tool-use)** - For more advanced tool use patterns and multi-step workflows
3. **[Completions API Reference](/api-reference/studio/post-completions)** - Full API documentation
4. **Custom Schemas** - Adapt this pattern for your specific use cases
# Using the Completions V1 API (Deprecated)
Source: https://docs.gloo.com/tutorials/completions-v1
Learn how to integrate with the Completions API.
**This API is deprecated**. Follow the [Completions V2 API](/tutorials/completions-v2) tutorial instead.
This guide provides a practical, step-by-step guide for authenticating with the Gloo AI API and making a basic call to the Chat Completions endpoint.
The process involves two main steps:
1. **Get an API Key:** Create an API key in Gloo AI Studio.
2. **Make an API Call:** Use the API key to make an authenticated request to the `/chat/completions` endpoint.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first
The Completions API requires an API key for authentication.
***
## Make a Chat Completion Call
Once you have a valid API key (using the authentication methods from the [Authentication Tutorial](/tutorials/authentication)), you can call the Completions API. The examples below show a basic request.
```python Python theme={null}
import time
import requests
def make_chat_completion_request(api_key="YOUR_API_KEY"):
"""Makes a chat completion request using the API key."""
api_url = "https://platform.ai.gloo.com/ai/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": "us.anthropic.claude-sonnet-4-20250514-v1:0",
"messages": [
{"role": "user", "content": "How can I be joyful in hard times?"}
]
}
response = requests.post(api_url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
```
```javascript JavaScript theme={null}
const axios = require('axios');
// const API_KEY = process.env.GLOO_API_KEY;
async function makeChatCompletionRequest() {
try {
const apiUrl = "https://platform.ai.gloo.com/ai/v1/chat/completions";
const payload = {
model: "us.anthropic.claude-sonnet-4-20250514-v1:0",
messages: [
{ role: "user", content: "How can I be joyful in hard times?" },
],
};
const response = await axios.post(apiUrl, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error("Error making chat completion request:", error.response ? error.response.data : error.message);
throw error;
}
}
```
```typescript TypeScript theme={null}
import axios from 'axios';
// Type definitions
interface ChatCompletionRequest {
model: string;
messages: Array<{ role: string; content: string }>;
}
interface ChatCompletionResponse {
choices: Array<{
message: {
role: string;
content: string;
};
}>;
}
// const API_KEY = process.env.GLOO_API_KEY;
async function makeChatCompletionRequest(): Promise {
try {
const apiUrl = "https://platform.ai.gloo.com/ai/v1/chat/completions";
const payload: ChatCompletionRequest = {
model: "us.anthropic.claude-sonnet-4-20250514-v1:0",
messages: [
{ role: "user", content: "How can I be joyful in hard times?" },
],
};
const response = await axios.post(apiUrl, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error: any) {
console.error("Error making chat completion request:", error.response ? error.response.data : error.message);
throw error;
}
}
```
```php PHP theme={null}
'us.anthropic.claude-sonnet-4-20250514-v1:0',
'messages' => [
['role' => 'user', 'content' => 'How can I be joyful in hard times?']
]
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func makeChatCompletionRequest() (map[string]interface{}, error) {
apiUrl := "https://platform.ai.gloo.com/ai/v1/chat/completions"
payload := map[string]interface{}{
"model": "us.anthropic.claude-sonnet-4-20250514-v1:0",
"messages": []map[string]string{
{"role": "user", "content": "How can I be joyful in hard times?"},
},
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body))
}
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
```
```java Java theme={null}
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.google.gson.Gson;
public class GlooApiClient {
public static String makeChatCompletionRequest() throws IOException, InterruptedException {
String apiUrl = "https://platform.ai.gloo.com/ai/v1/chat/completions";
String payload = """
{
"model": "us.anthropic.claude-sonnet-4-20250514-v1:0",
"messages": [
{
"role": "user",
"content": "How can I be joyful in hard times?"
}
]
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("API call failed: " + response.body());
}
return response.body();
}
}
```
***
## Complete Examples
The following examples combine the API key and the API call into a single, runnable script for each language.
You'll want to first set up your environment variables in either an `.env` file:
```
GLOO_API_KEY=YOUR_API_KEY
```
Or export them in your shell for Go and Java:
```bash theme={null}
export GLOO_API_KEY="your_actual_api_key_here"
```
```python Python theme={null}
import requests
import time
import base64
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# --- Configuration ---
# It's recommended to load credentials from environment variables
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v1/chat/completions"
# --- Function Definitions ---
def make_chat_completion_request():
"""Makes a chat completion request using the API key."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": "us.anthropic.claude-sonnet-4-20250514-v1:0",
"messages": [{"role": "user", "content": "How can I be joyful in hard times?"}]
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
# --- Main Execution ---
if __name__ == "__main__":
try:
print("Making first API call...")
completion1 = make_chat_completion_request()
print("First call successful:", completion1['choices'][0]['message']['content'])
print("\nMaking second API call...")
completion2 = make_chat_completion_request()
print("Second call successful:", completion2['choices'][0]['message']['content'])
except requests.exceptions.HTTPError as err:
print(f"An HTTP error occurred: {err}")
except Exception as err:
print(f"An error occurred: {err}")
```
```javascript JavaScript theme={null}
// Load environment variables from .env file
require('dotenv').config();
const axios = require('axios');
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v1/chat/completions";
// --- Function Definitions ---
async function makeChatCompletionRequest() {
const payload = {
model: "us.anthropic.claude-sonnet-4-20250514-v1:0",
messages: [{ role: "user", content: "How can I be joyful in hard times?" }],
};
const response = await axios.post(API_URL, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
// --- Main Execution ---
async function main() {
try {
console.log("Making first API call...");
const completion1 = await makeChatCompletionRequest();
console.log("First call successful:", completion1.choices[0].message.content);
console.log("\nMaking second API call...");
const completion2 = await makeChatCompletionRequest();
console.log("Second call successful:", completion2.choices[0].message.content);
} catch (error) {
console.error("An error occurred:", error.response ? error.response.data : error.message);
}
}
main();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Type definitions
interface ChatCompletionRequest {
model: string;
messages: Array<{ role: string; content: string }>;
}
interface ChatCompletionResponse {
choices: Array<{
message: {
role: string;
content: string;
};
}>;
}
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v1/chat/completions";
// --- Function Definitions ---
async function makeChatCompletionRequest(): Promise {
const payload: ChatCompletionRequest = {
model: "us.anthropic.claude-sonnet-4-20250514-v1:0",
messages: [{ role: "user", content: "How can I be joyful in hard times?" }],
};
const response = await axios.post(API_URL, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
// --- Main Execution ---
async function main(): Promise {
try {
console.log("Making first API call...");
const completion1 = await makeChatCompletionRequest();
console.log("First call successful:", completion1.choices[0].message.content);
console.log("\nMaking second API call...");
const completion2 = await makeChatCompletionRequest();
console.log("Second call successful:", completion2.choices[0].message.content);
} catch (error: any) {
console.error("An error occurred:", error.response ? error.response.data : error.message);
}
}
main();
```
```php PHP theme={null}
load();
// --- Configuration ---
$API_KEY = getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY';
$API_URL = 'https://platform.ai.gloo.com/ai/v1/chat/completions';
// --- Function Definitions ---
function makeChatCompletionRequest($apiUrl, $apiKey) {
$payload = json_encode([
'model' => 'us.anthropic.claude-sonnet-4-20250514-v1:0',
'messages' => [['role' => 'user', 'content' => 'How can I be joyful in hard times?']]
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) throw new Exception(curl_error($ch));
curl_close($ch);
return json_decode($result, true);
}
// --- Main Execution ---
try {
echo "Making first API call...\n";
$completion1 = makeChatCompletionRequest($API_URL, $API_KEY);
echo "First call successful: " . $completion1['choices'][0]['message']['content'] . "\n";
echo "\nMaking second API call...\n";
$completion2 = makeChatCompletionRequest($API_URL, $API_KEY);
echo "Second call successful: " . $completion2['choices'][0]['message']['content'] . "\n";
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage();
}
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
// --- Configuration ---
var (
apiKey = getEnv("GLOO_API_KEY", "YOUR_API_KEY")
apiURL = "https://platform.ai.gloo.com/ai/v1/chat/completions"
)
// --- Function Definitions ---
func makeChatCompletionRequest() (map[string]interface{}, error) {
payload := map[string]interface{}{
"model": "us.anthropic.claude-sonnet-4-20250514-v1:0",
"messages": []map[string]string{
{"role": "user", "content": "How can I be joyful in hard times?"},
},
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body))
}
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
// Helper to get environment variables
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
// --- Main Execution ---
func main() {
fmt.Println("Making first API call...")
completion1, err := makeChatCompletionRequest()
if err != nil {
fmt.Println("Error on first call:", err)
return
}
// Safely parse the response
if choices, ok := completion1["choices"].([]interface{}); ok && len(choices) > 0 {
if choice, ok := choices[0].(map[string]interface{}); ok {
if message, ok := choice["message"].(map[string]interface{}); ok {
fmt.Println("First call successful:", message["content"])
}
}
}
fmt.Println("\nMaking second API call...")
completion2, err := makeChatCompletionRequest()
if err != nil {
fmt.Println("Error on second call:", err)
return
}
if choices, ok := completion2["choices"].([]interface{}); ok && len(choices) > 0 {
if choice, ok := choices[0].(map[string]interface{}); ok {
if message, ok := choice["message"].(map[string]interface{}); ok {
fmt.Println("Second call successful:", message["content"])
}
}
}
}
```
```java Java theme={null}
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
// --- Main Application Class ---
public class Main {
public static void main(String[] args) {
GlooApiClient client = new GlooApiClient();
try {
System.out.println("Making first API call...");
String content1 = client.getChatCompletionContent();
System.out.println("First call successful: " + content1);
System.out.println("\nMaking second API call...");
String content2 = client.getChatCompletionContent();
System.out.println("Second call successful: " + content2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// --- API Client Class (GlooApiClient.java) ---
class GlooApiClient {
private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY");
private static final String API_URL = "https://platform.ai.gloo.com/ai/v1/chat/completions";
private final HttpClient httpClient = HttpClient.newHttpClient();
private final Gson gson = new Gson();
// --- Public Method ---
public String getChatCompletionContent() throws IOException, InterruptedException {
String payload = "{\"model\": \"us.anthropic.claude-sonnet-4-20250514-v1:0\", \"messages\": [{\"role\": \"user\", \"content\": \"How can I be joyful in hard times?\"}]}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("API call failed: " + response.body());
// Parse the response to extract the message content
Map responseMap = gson.fromJson(response.body(), Map.class);
List> choices = (List>) responseMap.get("choices");
Map message = (Map) choices.get(0).get("message");
return (String) message.get("content");
}
}
```
***
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions.
## Next Steps
Now that you understand how to use the Completions API, consider exploring:
1. **[Authentication Tutorial](/tutorials/authentication)** - For detailed authentication setup
2. **[Completions API](/api-reference/completions/v1)** - For additional API information
3. **[Chat Tutorial](/tutorials/chat)** - For stateful chat interactions
4. **[Tool Use](/api-guides/tool-use)** - For enhanced completion capabilities
# Using the Completions API
Source: https://docs.gloo.com/tutorials/completions-v2
Learn how to integrate with the Completions V2 API featuring auto-routing and intelligent model selection.
This guide provides a practical, step-by-step tutorial for using the Gloo AI Completions V2 API with its powerful routing features.
**Why V2?** Completions V2 offers auto-routing for optimal model selection, model family preferences, and tradition-aware responses—all while maintaining compatibility with the standard chat completions format.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first
***
## Choose Your Routing Strategy
Completions V2 offers three routing modes:
| Mode | Use Case | Parameter |
| - | - | - |
| **AI Core** (Recommended) | Let Gloo AI automatically select the best model | `"auto_routing": true` |
| **AI Core Select** | Choose a provider family, let Gloo pick the model | `"model_family": "anthropic"` |
| **AI Select** | Specify an exact model | `"model": "gloo-openai-gpt-5-mini"` |
***
## Example 1: Auto-Routing (Recommended)
Let Gloo AI analyze your query and automatically select the optimal model:
```python Python theme={null}
import requests
def make_v2_completion_auto(api_key):
"""Makes a V2 completion request with auto-routing."""
api_url = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"messages": [
{"role": "user", "content": "How does the Old Testament connect to the New Testament?"}
],
"auto_routing": True,
"tradition": "evangelical" # Optional: evangelical, catholic, or mainline
}
response = requests.post(api_url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
```
```javascript JavaScript theme={null}
async function makeV2CompletionAuto() {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "How does the Old Testament connect to the New Testament?" }
],
auto_routing: true,
tradition: "evangelical" // Optional: evangelical, catholic, or mainline
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
return response.json();
}
```
```typescript TypeScript theme={null}
async function makeV2CompletionAuto(): Promise {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "How does the Old Testament connect to the New Testament?" }
],
auto_routing: true,
tradition: "evangelical" // Optional: evangelical, catholic, or mainline
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
return response.json();
}
```
```php PHP theme={null}
[
['role' => 'user', 'content' => 'How does the Old Testament connect to the New Testament?']
],
'auto_routing' => true,
'tradition' => 'evangelical' // Optional: evangelical, catholic, or mainline
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
?>
```
```go Go theme={null}
func makeV2CompletionAuto() (map[string]interface{}, error) {
apiUrl := "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
payload := map[string]interface{}{
"messages": []map[string]string{
{"role": "user", "content": "How does the Old Testament connect to the New Testament?"},
},
"auto_routing": true,
"tradition": "evangelical", // Optional: evangelical, catholic, or mainline
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
```
```java Java theme={null}
public String makeV2CompletionAuto() throws IOException, InterruptedException {
String apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
String payload = """
{
"messages": [
{"role": "user", "content": "How does the Old Testament connect to the New Testament?"}
],
"auto_routing": true,
"tradition": "evangelical"
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
```
***
## Example 2: Model Family Selection
Specify a provider family and let Gloo AI pick the best model within that family:
```python Python theme={null}
def make_v2_completion_family(token_info):
"""Makes a V2 completion request with model family selection."""
api_url = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"messages": [
{"role": "user", "content": "Draft a short sermon outline on forgiveness."}
],
"model_family": "anthropic", # Options: openai, anthropic, google, open source
"stream": False
}
response = requests.post(api_url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
```
```javascript JavaScript theme={null}
async function makeV2CompletionFamily() {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "Draft a short sermon outline on forgiveness." }
],
model_family: "anthropic", // Options: openai, anthropic, google, open source
stream: false
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
return response.json();
}
```
```typescript TypeScript theme={null}
async function makeV2CompletionFamily(): Promise {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "Draft a short sermon outline on forgiveness." }
],
model_family: "anthropic", // Options: openai, anthropic, google, open source
stream: false
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
return response.json();
}
```
```php PHP theme={null}
[
['role' => 'user', 'content' => 'Draft a short sermon outline on forgiveness.']
],
'model_family' => 'anthropic', // Options: openai, anthropic, google, open source
'stream' => false
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
?>
```
```go Go theme={null}
func makeV2CompletionFamily() (map[string]interface{}, error) {
apiUrl := "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
payload := map[string]interface{}{
"messages": []map[string]string{
{"role": "user", "content": "Draft a short sermon outline on forgiveness."},
},
"model_family": "anthropic", // Options: openai, anthropic, google, open source
"stream": false,
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
```
```java Java theme={null}
public String makeV2CompletionFamily() throws IOException, InterruptedException {
String apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
String payload = """
{
"messages": [
{"role": "user", "content": "Draft a short sermon outline on forgiveness."}
],
"model_family": "anthropic",
"stream": false
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
```
Available model families: `openai`, `anthropic`, `google`, `open source`
***
## Example 3: Direct Model Selection
Choose a specific model for full control:
```python Python theme={null}
def make_v2_completion_direct(token_info):
"""Makes a V2 completion request with direct model selection."""
api_url = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"messages": [
{"role": "user", "content": "Summarize the book of Romans in 3 sentences."}
],
"model": "gloo-anthropic-claude-sonnet-4.5",
"temperature": 0.7,
"max_tokens": 500
}
response = requests.post(api_url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
```
```javascript JavaScript theme={null}
async function makeV2CompletionDirect() {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "Summarize the book of Romans in 3 sentences." }
],
model: "gloo-anthropic-claude-sonnet-4.5",
temperature: 0.7,
max_tokens: 500
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
return response.json();
}
```
```typescript TypeScript theme={null}
async function makeV2CompletionDirect(): Promise {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "Summarize the book of Romans in 3 sentences." }
],
model: "gloo-anthropic-claude-sonnet-4.5",
temperature: 0.7,
max_tokens: 500
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
return response.json();
}
```
```php PHP theme={null}
[
['role' => 'user', 'content' => 'Summarize the book of Romans in 3 sentences.']
],
'model' => 'gloo-anthropic-claude-sonnet-4.5',
'temperature' => 0.7,
'max_tokens' => 500
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
?>
```
```go Go theme={null}
func makeV2CompletionDirect() (map[string]interface{}, error) {
apiUrl := "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
payload := map[string]interface{}{
"messages": []map[string]string{
{"role": "user", "content": "Summarize the book of Romans in 3 sentences."},
},
"model": "gloo-anthropic-claude-sonnet-4.5",
"temperature": 0.7,
"max_tokens": 500,
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
```
```java Java theme={null}
public String makeV2CompletionDirect() throws IOException, InterruptedException {
String apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
String payload = """
{
"messages": [
{"role": "user", "content": "Summarize the book of Romans in 3 sentences."}
],
"model": "gloo-anthropic-claude-sonnet-4.5",
"temperature": 0.7,
"max_tokens": 500
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
```
See the [Supported Model IDs](/api-guides/supported-models) page for all available models.
***
## Understanding the Response
V2 responses include additional routing metadata:
```json theme={null}
{
"id": "chatcmpl-xyz",
"object": "chat.completion",
"created": 1733184562,
"model": "gloo-anthropic-claude-sonnet-4.5",
"routing_mechanism": "auto_routing",
"routing_tier": "tier_2",
"routing_confidence": 0.87,
"tradition": "evangelical",
"provider": "Anthropic",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The response content..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 125,
"completion_tokens": 78,
"total_tokens": 203
}
}
```
Key metadata fields:
* `routing_mechanism`: How the model was selected (`auto_routing`, `model_family`, or `direct_model_selection`)
* `routing_tier`: The complexity tier determined by auto-routing (`tier_1`, `tier_2`, `tier_3`)
* `routing_confidence`: Confidence score for the routing decision (0.0-1.0)
* `tradition`: The theological perspective applied (if specified)
***
## Streaming Responses
Enable streaming for real-time responses:
```python Python theme={null}
import requests
def make_v2_completion_streaming(token_info):
"""Makes a streaming V2 completion request."""
api_url = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"messages": [
{"role": "user", "content": "Explain the significance of the resurrection."}
],
"auto_routing": True,
"stream": True
}
with requests.post(api_url, headers=headers, json=payload, stream=True) as response:
response.raise_for_status()
for line in response.iter_lines():
if line:
print(line.decode('utf-8'))
```
```javascript JavaScript theme={null}
async function makeV2CompletionStreaming() {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "Explain the significance of the resurrection." }
],
auto_routing: true,
stream: true
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}
}
```
```typescript TypeScript theme={null}
async function makeV2CompletionStreaming(): Promise {
const apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
const payload = {
messages: [
{ role: "user", content: "Explain the significance of the resurrection." }
],
auto_routing: true,
stream: true
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
const reader = response.body?.getReader();
if (!reader) throw new Error('No reader available');
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}
}
```
```php PHP theme={null}
[
['role' => 'user', 'content' => 'Explain the significance of the resurrection.']
],
'auto_routing' => true,
'stream' => true
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
// Stream the response
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
echo $data;
return strlen($data);
});
curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception(curl_error($ch));
}
curl_close($ch);
}
?>
```
```go Go theme={null}
func makeV2CompletionStreaming() error {
apiUrl := "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
payload := map[string]interface{}{
"messages": []map[string]string{
{"role": "user", "content": "Explain the significance of the resurrection."},
},
"auto_routing": true,
"stream": true,
}
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonPayload))
if err != nil {
return err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
reader := bufio.NewReader(resp.Body)
for {
line, err := reader.ReadBytes('\n')
if err != nil {
break
}
fmt.Print(string(line))
}
return nil
}
```
```java Java theme={null}
public void makeV2CompletionStreaming() throws IOException, InterruptedException {
String apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
String payload = """
{
"messages": [
{"role": "user", "content": "Explain the significance of the resurrection."}
],
"auto_routing": true,
"stream": true
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse> response = client.send(
request,
HttpResponse.BodyHandlers.ofLines()
);
response.body().forEach(System.out::println);
}
```
***
## Complete Examples
The following examples combine all three routing strategies into a single, runnable script for each language.
You'll want to first set up your environment variables in either an `.env` file:
```
GLOO_API_KEY=YOUR_API_KEY
```
Or export them in your shell for Go and Java:
```bash theme={null}
export GLOO_API_KEY="your_actual_api_key_here"
```
```python Python theme={null}
import requests
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# --- Configuration ---
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
# --- V2 Completion Functions ---
def make_v2_auto_routing(message, tradition="evangelical"):
"""Example 1: Auto-routing - Let Gloo AI select the optimal model."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
payload = {
"messages": [{"role": "user", "content": message}],
"auto_routing": True,
"tradition": tradition
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
def make_v2_model_family(message, model_family="anthropic"):
"""Example 2: Model family selection - Choose a provider family."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
payload = {
"messages": [{"role": "user", "content": message}],
"model_family": model_family
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
def make_v2_direct_model(message, model="gloo-anthropic-claude-sonnet-4.5"):
"""Example 3: Direct model selection - Specify an exact model."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
payload = {
"messages": [{"role": "user", "content": message}],
"model": model,
"temperature": 0.7,
"max_tokens": 500
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
# --- Main Execution ---
if __name__ == "__main__":
try:
# Example 1: Auto-routing
print("=== Example 1: Auto-Routing ===")
result1 = make_v2_auto_routing("How does the Old Testament connect to the New Testament?")
print(f"Model used: {result1.get('model')}")
print(f"Routing: {result1.get('routing_mechanism')}")
print(f"Response: {result1['choices'][0]['message']['content'][:200]}...")
# Example 2: Model family selection
print("\n=== Example 2: Model Family Selection ===")
result2 = make_v2_model_family("Draft a short sermon outline on forgiveness.", "anthropic")
print(f"Model used: {result2.get('model')}")
print(f"Response: {result2['choices'][0]['message']['content'][:200]}...")
# Example 3: Direct model selection
print("\n=== Example 3: Direct Model Selection ===")
result3 = make_v2_direct_model("Summarize the book of Romans in 3 sentences.")
print(f"Model used: {result3.get('model')}")
print(f"Response: {result3['choices'][0]['message']['content'][:200]}...")
except requests.exceptions.HTTPError as err:
print(f"An HTTP error occurred: {err}")
except Exception as err:
print(f"An error occurred: {err}")
```
```javascript JavaScript theme={null}
// Load environment variables from .env file
require('dotenv').config();
const axios = require('axios');
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
// --- V2 Completion Functions ---
async function makeV2AutoRouting(message, tradition = "evangelical") {
// Example 1: Auto-routing - Let Gloo AI select the optimal model
const response = await axios.post(API_URL, {
messages: [{ role: "user", content: message }],
auto_routing: true,
tradition: tradition
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
async function makeV2ModelFamily(message, modelFamily = "anthropic") {
// Example 2: Model family selection - Choose a provider family
const response = await axios.post(API_URL, {
messages: [{ role: "user", content: message }],
model_family: modelFamily
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
async function makeV2DirectModel(message, model = "gloo-anthropic-claude-sonnet-4.5") {
// Example 3: Direct model selection - Specify an exact model
const response = await axios.post(API_URL, {
messages: [{ role: "user", content: message }],
model: model,
temperature: 0.7,
max_tokens: 500
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
// --- Main Execution ---
async function main() {
try {
// Example 1: Auto-routing
console.log("=== Example 1: Auto-Routing ===");
const result1 = await makeV2AutoRouting("How does the Old Testament connect to the New Testament?");
console.log(`Model used: ${result1.model}`);
console.log(`Routing: ${result1.routing_mechanism}`);
console.log(`Response: ${result1.choices[0].message.content.substring(0, 200)}...`);
// Example 2: Model family selection
console.log("\n=== Example 2: Model Family Selection ===");
const result2 = await makeV2ModelFamily("Draft a short sermon outline on forgiveness.", "anthropic");
console.log(`Model used: ${result2.model}`);
console.log(`Response: ${result2.choices[0].message.content.substring(0, 200)}...`);
// Example 3: Direct model selection
console.log("\n=== Example 3: Direct Model Selection ===");
const result3 = await makeV2DirectModel("Summarize the book of Romans in 3 sentences.");
console.log(`Model used: ${result3.model}`);
console.log(`Response: ${result3.choices[0].message.content.substring(0, 200)}...`);
} catch (error) {
console.error("An error occurred:", error.response ? error.response.data : error.message);
}
}
main();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// --- Type Definitions ---
interface V2CompletionResponse {
model: string;
routing_mechanism?: string;
routing_tier?: string;
routing_confidence?: number;
tradition?: string;
choices: Array<{
message: {
role: string;
content: string;
};
}>;
}
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
// --- V2 Completion Functions ---
async function makeV2AutoRouting(message: string, tradition: string = "evangelical"): Promise {
// Example 1: Auto-routing - Let Gloo AI select the optimal model
const response = await axios.post(API_URL, {
messages: [{ role: "user", content: message }],
auto_routing: true,
tradition: tradition
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
async function makeV2ModelFamily(message: string, modelFamily: string = "anthropic"): Promise {
// Example 2: Model family selection - Choose a provider family
const response = await axios.post(API_URL, {
messages: [{ role: "user", content: message }],
model_family: modelFamily
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
async function makeV2DirectModel(message: string, model: string = "gloo-anthropic-claude-sonnet-4.5"): Promise {
// Example 3: Direct model selection - Specify an exact model
const response = await axios.post(API_URL, {
messages: [{ role: "user", content: message }],
model: model,
temperature: 0.7,
max_tokens: 500
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
// --- Main Execution ---
async function main(): Promise {
try {
// Example 1: Auto-routing
console.log("=== Example 1: Auto-Routing ===");
const result1 = await makeV2AutoRouting("How does the Old Testament connect to the New Testament?");
console.log(`Model used: ${result1.model}`);
console.log(`Routing: ${result1.routing_mechanism}`);
console.log(`Response: ${result1.choices[0].message.content.substring(0, 200)}...`);
// Example 2: Model family selection
console.log("\n=== Example 2: Model Family Selection ===");
const result2 = await makeV2ModelFamily("Draft a short sermon outline on forgiveness.", "anthropic");
console.log(`Model used: ${result2.model}`);
console.log(`Response: ${result2.choices[0].message.content.substring(0, 200)}...`);
// Example 3: Direct model selection
console.log("\n=== Example 3: Direct Model Selection ===");
const result3 = await makeV2DirectModel("Summarize the book of Romans in 3 sentences.");
console.log(`Model used: ${result3.model}`);
console.log(`Response: ${result3.choices[0].message.content.substring(0, 200)}...`);
} catch (error: any) {
console.error("An error occurred:", error.response ? error.response.data : error.message);
}
}
main();
```
```php PHP theme={null}
load();
// --- Configuration ---
$API_KEY = $_ENV['GLOO_API_KEY'] ?? getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY';
$API_URL = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions';
// --- V2 Completion Functions ---
function makeV2AutoRouting($message, $tradition, $apiUrl) {
global $API_KEY;
// Example 1: Auto-routing - Let Gloo AI select the optimal model
$payload = json_encode([
'messages' => [['role' => 'user', 'content' => $message]],
'auto_routing' => true,
'tradition' => $tradition
]);
return makeRequest($apiUrl, $payload, $API_KEY);
}
function makeV2ModelFamily($message, $modelFamily, $apiUrl) {
global $API_KEY;
// Example 2: Model family selection - Choose a provider family
$payload = json_encode([
'messages' => [['role' => 'user', 'content' => $message]],
'model_family' => $modelFamily
]);
return makeRequest($apiUrl, $payload, $API_KEY);
}
function makeV2DirectModel($message, $model, $apiUrl) {
global $API_KEY;
// Example 3: Direct model selection - Specify an exact model
$payload = json_encode([
'messages' => [['role' => 'user', 'content' => $message]],
'model' => $model,
'temperature' => 0.7,
'max_tokens' => 500
]);
return makeRequest($apiUrl, $payload, $API_KEY);
}
function makeRequest($apiUrl, $payload, $apiKey) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) throw new Exception(curl_error($ch));
curl_close($ch);
return json_decode($result, true);
}
// --- Main Execution ---
try {
// Example 1: Auto-routing
echo "=== Example 1: Auto-Routing ===\n";
$result1 = makeV2AutoRouting(
"How does the Old Testament connect to the New Testament?",
"evangelical",
$API_URL
);
echo "Model used: " . $result1['model'] . "\n";
echo "Routing: " . $result1['routing_mechanism'] . "\n";
echo "Response: " . substr($result1['choices'][0]['message']['content'], 0, 200) . "...\n";
// Example 2: Model family selection
echo "\n=== Example 2: Model Family Selection ===\n";
$result2 = makeV2ModelFamily(
"Draft a short sermon outline on forgiveness.",
"anthropic",
$API_URL
);
echo "Model used: " . $result2['model'] . "\n";
echo "Response: " . substr($result2['choices'][0]['message']['content'], 0, 200) . "...\n";
// Example 3: Direct model selection
echo "\n=== Example 3: Direct Model Selection ===\n";
$result3 = makeV2DirectModel(
"Summarize the book of Romans in 3 sentences.",
"gloo-anthropic-claude-sonnet-4.5",
$API_URL
);
echo "Model used: " . $result3['model'] . "\n";
echo "Response: " . substr($result3['choices'][0]['message']['content'], 0, 200) . "...\n";
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage();
}
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)
// --- Configuration ---
var (
apiKey = getEnv("GLOO_API_KEY", "YOUR_API_KEY")
apiURL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
)
// --- V2 Completion Functions ---
func makeV2AutoRouting(message, tradition string) (map[string]interface{}, error) {
// Example 1: Auto-routing - Let Gloo AI select the optimal model
payload := map[string]interface{}{
"messages": []map[string]string{{"role": "user", "content": message}},
"auto_routing": true,
"tradition": tradition,
}
return makeRequest(payload)
}
func makeV2ModelFamily(message, modelFamily string) (map[string]interface{}, error) {
// Example 2: Model family selection - Choose a provider family
payload := map[string]interface{}{
"messages": []map[string]string{{"role": "user", "content": message}},
"model_family": modelFamily,
}
return makeRequest(payload)
}
func makeV2DirectModel(message, model string) (map[string]interface{}, error) {
// Example 3: Direct model selection - Specify an exact model
payload := map[string]interface{}{
"messages": []map[string]string{{"role": "user", "content": message}},
"model": model,
"temperature": 0.7,
"max_tokens": 500,
}
return makeRequest(payload)
}
func makeRequest(payload map[string]interface{}) (map[string]interface{}, error) {
jsonPayload, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body))
}
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func getResponseContent(result map[string]interface{}) string {
if choices, ok := result["choices"].([]interface{}); ok && len(choices) > 0 {
if choice, ok := choices[0].(map[string]interface{}); ok {
if message, ok := choice["message"].(map[string]interface{}); ok {
if content, ok := message["content"].(string); ok {
if len(content) > 200 {
return content[:200] + "..."
}
return content
}
}
}
}
return ""
}
// --- Main Execution ---
func main() {
// Example 1: Auto-routing
fmt.Println("=== Example 1: Auto-Routing ===")
result1, err := makeV2AutoRouting("How does the Old Testament connect to the New Testament?", "evangelical")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Model used: %v\n", result1["model"])
fmt.Printf("Routing: %v\n", result1["routing_mechanism"])
fmt.Printf("Response: %s\n", getResponseContent(result1))
// Example 2: Model family selection
fmt.Println("\n=== Example 2: Model Family Selection ===")
result2, err := makeV2ModelFamily("Draft a short sermon outline on forgiveness.", "anthropic")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Model used: %v\n", result2["model"])
fmt.Printf("Response: %s\n", getResponseContent(result2))
// Example 3: Direct model selection
fmt.Println("\n=== Example 3: Direct Model Selection ===")
result3, err := makeV2DirectModel("Summarize the book of Romans in 3 sentences.", "gloo-anthropic-claude-sonnet-4.5")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Model used: %v\n", result3["model"])
fmt.Printf("Response: %s\n", getResponseContent(result3))
}
```
```java Java theme={null}
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GlooV2Example {
// --- Configuration ---
private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY");
private static final String API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
private final HttpClient httpClient = HttpClient.newHttpClient();
private final Gson gson = new Gson();
// --- V2 Completion Functions ---
public Map makeV2AutoRouting(String message, String tradition) throws IOException, InterruptedException {
// Example 1: Auto-routing - Let Gloo AI select the optimal model
Map payload = new HashMap<>();
Map messageEntry = new HashMap<>();
messageEntry.put("role", "user");
messageEntry.put("content", message);
payload.put("messages", List.of(messageEntry));
payload.put("auto_routing", true);
payload.put("tradition", tradition);
return makeRequest(gson.toJson(payload));
}
public Map makeV2ModelFamily(String message, String modelFamily) throws IOException, InterruptedException {
// Example 2: Model family selection - Choose a provider family
Map payload = new HashMap<>();
Map messageEntry = new HashMap<>();
messageEntry.put("role", "user");
messageEntry.put("content", message);
payload.put("messages", List.of(messageEntry));
payload.put("model_family", modelFamily);
return makeRequest(gson.toJson(payload));
}
public Map makeV2DirectModel(String message, String model) throws IOException, InterruptedException {
// Example 3: Direct model selection - Specify an exact model
Map payload = new HashMap<>();
Map messageEntry = new HashMap<>();
messageEntry.put("role", "user");
messageEntry.put("content", message);
payload.put("messages", List.of(messageEntry));
payload.put("model", model);
payload.put("temperature", 0.7);
payload.put("max_tokens", 500);
return makeRequest(gson.toJson(payload));
}
private Map makeRequest(String payload) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("API call failed: " + response.body());
return gson.fromJson(response.body(), Map.class);
}
private String getResponseContent(Map result) {
List> choices = (List>) result.get("choices");
Map message = (Map) choices.get(0).get("message");
String content = (String) message.get("content");
return content.length() > 200 ? content.substring(0, 200) + "..." : content;
}
// --- Main Execution ---
public static void main(String[] args) {
GlooV2Example client = new GlooV2Example();
try {
// Example 1: Auto-routing
System.out.println("=== Example 1: Auto-Routing ===");
Map result1 = client.makeV2AutoRouting(
"How does the Old Testament connect to the New Testament?", "evangelical");
System.out.println("Model used: " + result1.get("model"));
System.out.println("Routing: " + result1.get("routing_mechanism"));
System.out.println("Response: " + client.getResponseContent(result1));
// Example 2: Model family selection
System.out.println("\n=== Example 2: Model Family Selection ===");
Map result2 = client.makeV2ModelFamily(
"Draft a short sermon outline on forgiveness.", "anthropic");
System.out.println("Model used: " + result2.get("model"));
System.out.println("Response: " + client.getResponseContent(result2));
// Example 3: Direct model selection
System.out.println("\n=== Example 3: Direct Model Selection ===");
Map result3 = client.makeV2DirectModel(
"Summarize the book of Romans in 3 sentences.", "gloo-anthropic-claude-sonnet-4.5");
System.out.println("Model used: " + result3.get("model"));
System.out.println("Response: " + client.getResponseContent(result3));
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
***
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions.
## Next Steps
Now that you understand the Completions V2 API, explore:
1. **[Completions V2 Guide](/api-guides/completions-v2)** - Full API documentation
2. **[Supported Model IDs](/api-guides/supported-models)** - All available models
3. **[Tool Use](/api-guides/tool-use)** - Function calling with completions
4. **[Chat Tutorial](/tutorials/chat)** - Stateful chat interactions
# Overview
Source: https://docs.gloo.com/tutorials/overview
Welcome to our tutorials! These guides provide a hands-on approach to building on the Gloo AI platform. Choose a tutorial below to get started.
Learn how to authenticate with the Gloo AI API using an API key.
Build with the Guarded Responses API — recommended for new integrations. Covers typed output parsing, tradition values-alignment, SSE streaming, and vision input.
Learn how to integrate with the Completions API.
Learn how to stream Gloo AI completions token-by-token using SSE, build a typing-effect renderer, and set up a server-side proxy to keep credentials secure.
Learn how to extend model capabilities via the Completions API with tool use.
Build knowledge-grounded chatbots that answer from your content using Retrieval-Augmented Generation to reduce hallucinations.
Build custom search experiences using the Search API as an alternative to the Discovery Widget. Full control over branding and UI.
Surface relevant resources using the Recommendations API. Covers publisher-scoped ranked items and cross-publisher affiliate network discovery.
Build automated pipelines that ingest content via the Upload Files API and make it available for search and AI interaction.
Use AI coding assistants to quickly build applications with Gloo AI.
A connected series: set up a publisher, ingest and enrich content, verify indexing, and wire it into search and grounded completions.
# Part 1: Set Up the Pipeline
Source: https://docs.gloo.com/tutorials/rag-pipeline-part-1
Create a publisher, ingest content with metadata, and verify indexing — the foundation of an end-to-end RAG pipeline on Gloo AI.
This is **Part 1 of the Build an End-to-End RAG Pipeline series**. Across three parts, you'll connect Gloo AI's features into one continuous workflow: the publisher and content you set up here are the same ones you'll manage in Part 2 and harden in Part 3.
In this part you'll create a publisher, upload content with descriptive metadata, and verify that it's fully indexed and ready for retrieval.
## The Full Journey
Building a production RAG application means connecting several Gloo AI features into one continuous path. The diagram below shows that path end to end: you set up your content and keep adding to it, Gloo answers questions against it, and you keep it current over time. Each stage below maps to a step in this series or a focused tutorial.
This series covers stages 1-3 and 7-8 in depth. Stages 4-6 are covered in [Building Custom Search](/tutorials/search) and [Grounded Completions with RAG](/tutorials/completions-grounded).
## Pipeline at a Glance
Create the publisher that owns your content — covered below.
Upload files and enrich them — covered below, with a deep dive in [Upload
Files to Data Engine](/tutorials/upload-files).
Poll item status until your content is searchable — covered below.
Query your content — deep dive: [Building Custom Search](/tutorials/search).
Answer questions from your content with citations — deep dive: [Grounded
Completions with RAG](/tutorials/completions-grounded).
Update, bulk-edit, and delete content — [Part
2](/tutorials/rag-pipeline-part-2).
Error handling and retry patterns — [Part
3](/tutorials/rag-pipeline-part-3).
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* **API key** - Set the `GLOO_API_KEY` environment variable to your key from the [API Credentials page](/studio/manage-api-credentials)
All API calls in this series authenticate with an API key. The snippets below
read `GLOO_API_KEY` and set `API_KEY` so the same variable works in [Part
2](/tutorials/rag-pipeline-part-2) and [Part
3](/tutorials/rag-pipeline-part-3).
***
## Step 1: Create Your Publisher
Content in the Data Engine belongs to a **publisher**. Create one in Gloo AI Studio:
1. In [Gloo AI Studio](https://studio.ai.gloo.com), click your user account in the bottom-left corner and select **Manage Organizations**
2. Select the organization you want to add the publisher to, then click **View Publishers**
3. Click **Create Publisher** and give the new publisher a name
4. Copy the **Publisher ID** (a UUID) — every API call in this series uses it
See [Manage Publishers](/studio/manage-publishers) for the full Studio walkthrough.
Use one dedicated publisher for this series. Parts 2 and 3 operate on the
content you upload here, and a dedicated publisher keeps those operations
cleanly separated from your production content.
## Step 2: Upload Content with a Producer ID
Upload a file to **POST** `/ingestion/v2/files`. The `producer_id` query parameter attaches your own stable identifier to the item — this is what makes the pipeline manageable later: re-running the upload detects a duplicate instead of creating a copy, and in Part 2 you'll update, bulk-edit, and delete these same items.
This series uses a short Markdown article as its sample content: grab [`building-stronger-communities.md` from the cookbook repository](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/blob/main/rag-pipeline-part-1/sample_files/building-stronger-communities.md) and save it next to your script. (Any Markdown, text, PDF, or Word file of your own works too — just adjust the filename.) Then upload it:
Content not on local disk? See [Preparing Content for
Ingestion](/best-practices/ingestion-pattern) for the source-agnostic pattern
— pulling from a CMS, API, database, file library, or feed, and how to
structure content before it reaches this upload step.
```python Python theme={null}
import os
import requests
API_KEY = os.getenv("GLOO_API_KEY")
token = API_KEY # Used by parts 2 and 3
PUBLISHER_ID = "your_publisher_id"
PRODUCER_ID = "rag-pipeline-part1-building-stronger-communities"
# Upload the file with a stable producer ID
with open("building-stronger-communities.md", "rb") as f:
response = requests.post(
"https://platform.ai.gloo.com/ingestion/v2/files",
headers={"Authorization": f"Bearer {token}"},
params={"producer_id": PRODUCER_ID},
files={"files": ("building-stronger-communities.md", f)},
data={"publisher_id": PUBLISHER_ID},
)
result = response.json()
# A fresh upload returns the new item ID in "ingesting";
# re-uploading the same content returns it in "duplicates".
item_id = (result["ingesting"] or result["duplicates"])[0]
print(f"Item ID: {item_id}")
```
```javascript JavaScript theme={null}
import { readFile } from 'node:fs/promises';
const API_KEY = process.env.GLOO_API_KEY;
const token = API_KEY; // Used by parts 2 and 3
const PUBLISHER_ID = 'your_publisher_id';
const PRODUCER_ID = 'rag-pipeline-part1-building-stronger-communities';
// Upload the file with a stable producer ID
const form = new FormData();
form.append('publisher_id', PUBLISHER_ID);
form.append(
'files',
new Blob([await readFile('building-stronger-communities.md')]),
'building-stronger-communities.md',
);
const response = await fetch(
`https://platform.ai.gloo.com/ingestion/v2/files?producer_id=${encodeURIComponent(PRODUCER_ID)}`,
{ method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form },
);
const result = await response.json();
// A fresh upload returns the new item ID in "ingesting";
// re-uploading the same content returns it in "duplicates".
const itemId = (
result.ingesting.length ? result.ingesting : result.duplicates
)[0];
console.log(`Item ID: ${itemId}`);
```
```typescript TypeScript theme={null}
import { readFileSync } from 'node:fs';
const API_KEY = process.env.GLOO_API_KEY;
const token = API_KEY; // Used by parts 2 and 3
const PUBLISHER_ID = 'your_publisher_id';
const PRODUCER_ID = 'rag-pipeline-part1-building-stronger-communities';
interface UploadResponse {
ingesting: string[];
duplicates: string[];
}
(async () => {
// Upload the file with a stable producer ID
const form = new FormData();
form.append('publisher_id', PUBLISHER_ID);
form.append(
'files',
new Blob([readFileSync('building-stronger-communities.md')]),
'building-stronger-communities.md',
);
const response = await fetch(
`https://platform.ai.gloo.com/ingestion/v2/files?producer_id=${encodeURIComponent(PRODUCER_ID)}`,
{
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: form,
},
);
const result = (await response.json()) as UploadResponse;
// A fresh upload returns the new item ID in "ingesting";
// re-uploading the same content returns it in "duplicates".
const itemId = (
result.ingesting.length ? result.ingesting : result.duplicates
)[0];
console.log(`Item ID: ${itemId}`);
})();
```
```php PHP theme={null}
true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
CURLOPT_POSTFIELDS => [
'publisher_id' => $publisherId,
'files' => new CURLFile('building-stronger-communities.md', 'text/markdown'),
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
// A fresh upload returns the new item ID in "ingesting";
// re-uploading the same content returns it in "duplicates".
$itemId = ($result['ingesting'] ?: $result['duplicates'])[0];
echo "Item ID: $itemId\n";
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"os"
"strings"
)
const (
publisherID = "your_publisher_id"
producerID = "rag-pipeline-part1-building-stronger-communities"
)
func main() {
apiKey := os.Getenv("GLOO_API_KEY")
token := apiKey // Used by parts 2 and 3
// Upload the file with a stable producer ID
fileBytes, _ := os.ReadFile("building-stronger-communities.md")
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
writer.WriteField("publisher_id", publisherID)
part, _ := writer.CreateFormFile("files", "building-stronger-communities.md")
part.Write(fileBytes)
writer.Close()
uploadURL := "https://platform.ai.gloo.com/ingestion/v2/files?producer_id=" +
url.QueryEscape(producerID)
req, _ := http.NewRequest("POST", uploadURL, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// A fresh upload returns the new item ID in "ingesting";
// re-uploading the same content returns it in "duplicates".
var result struct {
Ingesting []string `json:"ingesting"`
Duplicates []string `json:"duplicates"`
}
json.NewDecoder(resp.Body).Decode(&result)
itemID := append(result.Ingesting, result.Duplicates...)[0]
fmt.Println("Item ID:", itemID)
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
public class UploadContent {
static final String PUBLISHER_ID = "your_publisher_id";
static final String PRODUCER_ID = "rag-pipeline-part1-building-stronger-communities";
public static void main(String[] args) throws Exception {
HttpClient http = HttpClient.newHttpClient();
Gson gson = new Gson();
String token = System.getenv("GLOO_API_KEY"); // Used by parts 2 and 3
// Upload the file with a stable producer ID (manual multipart body)
String boundary = "----GlooBoundary";
var body = new java.io.ByteArrayOutputStream();
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"publisher_id\"\r\n\r\n"
+ PUBLISHER_ID + "\r\n").getBytes(StandardCharsets.UTF_8));
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"files\"; "
+ "filename=\"building-stronger-communities.md\"\r\n"
+ "Content-Type: text/markdown\r\n\r\n").getBytes(StandardCharsets.UTF_8));
body.write(Files.readAllBytes(Path.of("building-stronger-communities.md")));
body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
HttpRequest uploadRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/ingestion/v2/files?producer_id="
+ URLEncoder.encode(PRODUCER_ID, StandardCharsets.UTF_8)))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
.build();
JsonObject result = gson
.fromJson(http.send(uploadRequest, HttpResponse.BodyHandlers.ofString()).body(),
JsonObject.class);
// A fresh upload returns the new item ID in "ingesting";
// re-uploading the same content returns it in "duplicates".
var ingesting = result.getAsJsonArray("ingesting");
String itemId = (ingesting.size() > 0
? ingesting
: result.getAsJsonArray("duplicates")).get(0).getAsString();
System.out.println("Item ID: " + itemId);
}
}
```
### What You'll See
```
Item ID: 822308cc-72fc-478d-a2eb-fbdf01a6a15d
```
Keep this item ID — the next two steps use it, and Parts 2 and 3 operate on this same item.
For multi-file uploads, batch processing, and supported file types, see the
[Upload Files to Data Engine](/tutorials/upload-files) deep dive.
## Step 3: Enrich with Metadata
Attach descriptive metadata with **PATCH** `/engine/v2/item`. Good metadata pays off downstream: titles and authors appear in search results and source citations, and tags let you organize and bulk-manage content in Part 2.
```python Python theme={null}
metadata = {
"publisher_id": PUBLISHER_ID,
"item_id": item_id,
"item_title": "Building Stronger Communities Through Service",
"item_summary": "Practical guidance for starting and sustaining community service efforts.",
"author": ["Gloo AI Docs Team"],
"item_tags": ["community", "service", "rag-pipeline-series"],
}
response = requests.patch(
"https://platform.ai.gloo.com/engine/v2/item",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json=metadata,
)
response.raise_for_status()
print("Metadata set")
```
```javascript JavaScript theme={null}
const metadata = {
publisher_id: PUBLISHER_ID,
item_id: itemId,
item_title: 'Building Stronger Communities Through Service',
item_summary:
'Practical guidance for starting and sustaining community service efforts.',
author: ['Gloo AI Docs Team'],
item_tags: ['community', 'service', 'rag-pipeline-series'],
};
const metadataResponse = await fetch(
'https://platform.ai.gloo.com/engine/v2/item',
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(metadata),
},
);
if (!metadataResponse.ok) throw new Error(`HTTP ${metadataResponse.status}`);
console.log('Metadata set');
```
```typescript TypeScript theme={null}
const metadata = {
publisher_id: PUBLISHER_ID,
item_id: itemId,
item_title: 'Building Stronger Communities Through Service',
item_summary:
'Practical guidance for starting and sustaining community service efforts.',
author: ['Gloo AI Docs Team'],
item_tags: ['community', 'service', 'rag-pipeline-series'],
};
const metadataResponse = await fetch(
'https://platform.ai.gloo.com/engine/v2/item',
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(metadata),
},
);
if (!metadataResponse.ok) throw new Error(`HTTP ${metadataResponse.status}`);
console.log('Metadata set');
```
```php PHP theme={null}
$metadata = [
'publisher_id' => $publisherId,
'item_id' => $itemId,
'item_title' => 'Building Stronger Communities Through Service',
'item_summary' => 'Practical guidance for starting and sustaining community service efforts.',
'author' => ['Gloo AI Docs Team'],
'item_tags' => ['community', 'service', 'rag-pipeline-series'],
];
$ch = curl_init('https://platform.ai.gloo.com/engine/v2/item');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($metadata),
]);
curl_exec($ch);
curl_close($ch);
echo "Metadata set\n";
```
```go Go theme={null}
metadata := map[string]any{
"publisher_id": publisherID,
"item_id": itemID,
"item_title": "Building Stronger Communities Through Service",
"item_summary": "Practical guidance for starting and sustaining community service efforts.",
"author": []string{"Gloo AI Docs Team"},
"item_tags": []string{"community", "service", "rag-pipeline-series"},
}
payload, _ := json.Marshal(metadata)
req, _ = http.NewRequest("PATCH", "https://platform.ai.gloo.com/engine/v2/item",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
resp.Body.Close()
fmt.Println("Metadata set")
```
```java Java theme={null}
String metadataJson = """
{
"publisher_id": "%s",
"item_id": "%s",
"item_title": "Building Stronger Communities Through Service",
"item_summary": "Practical guidance for starting and sustaining community service efforts.",
"author": ["Gloo AI Docs Team"],
"item_tags": ["community", "service", "rag-pipeline-series"]
}
""".formatted(PUBLISHER_ID, itemId);
HttpRequest metadataRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/item"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(metadataJson))
.build();
http.send(metadataRequest, HttpResponse.BodyHandlers.ofString());
System.out.println("Metadata set");
```
Metadata can be set as soon as the item ID exists — you don't need to wait for
ingestion to finish. You can also target a single item by `producer_id`
instead of `item_id`.
## Step 4: Verify Indexing
Ingestion is **asynchronous**: the upload response means your file is queued, not searchable. Poll **GET** `/engine/v2/items/{item_id}` until `status` reaches `COMPLETED` — typically a few minutes for a small file. While processing you'll see intermediate states such as `CHUNKING` (see [How Chunking Works](/api-guides/chunking) for what happens during this stage).
```python Python theme={null}
import time
POLL_INTERVAL_SECONDS = 15
POLL_TIMEOUT_SECONDS = 600
deadline = time.time() + POLL_TIMEOUT_SECONDS
while time.time() < deadline:
item = requests.get(
f"https://platform.ai.gloo.com/engine/v2/items/{item_id}",
headers={"Authorization": f"Bearer {token}"},
).json()
status = item.get("status", "unknown")
print(f"Status: {status}")
if status.upper() == "COMPLETED":
print(f"Indexed: {item['item_title']} (tags: {', '.join(item['item_tags'])})")
break
if status.upper() in ("FAILED", "ERROR"):
raise RuntimeError(f"Ingestion failed with status: {status}")
time.sleep(POLL_INTERVAL_SECONDS)
else:
raise TimeoutError(f"Not indexed within {POLL_TIMEOUT_SECONDS}s")
```
```javascript JavaScript theme={null}
const POLL_INTERVAL_MS = 15_000;
const POLL_TIMEOUT_MS = 600_000;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const deadline = Date.now() + POLL_TIMEOUT_MS;
let indexed = false;
while (Date.now() < deadline) {
const itemResponse = await fetch(
`https://platform.ai.gloo.com/engine/v2/items/${itemId}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
const item = await itemResponse.json();
const status = item.status ?? 'unknown';
console.log(`Status: ${status}`);
if (status.toUpperCase() === 'COMPLETED') {
console.log(
`Indexed: ${item.item_title} (tags: ${item.item_tags.join(', ')})`,
);
indexed = true;
break;
}
if (['FAILED', 'ERROR'].includes(status.toUpperCase())) {
throw new Error(`Ingestion failed with status: ${status}`);
}
await sleep(POLL_INTERVAL_MS);
}
if (!indexed) throw new Error(`Not indexed within ${POLL_TIMEOUT_MS / 1000}s`);
```
```typescript TypeScript theme={null}
const POLL_INTERVAL_MS = 15_000;
const POLL_TIMEOUT_MS = 600_000;
const sleep = (ms: number): Promise =>
new Promise((resolve) => setTimeout(resolve, ms));
interface ItemMetadata {
status: string;
item_title: string | null;
item_tags: string[];
}
const deadline = Date.now() + POLL_TIMEOUT_MS;
let indexed = false;
while (Date.now() < deadline) {
const itemResponse = await fetch(
`https://platform.ai.gloo.com/engine/v2/items/${itemId}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
const item = (await itemResponse.json()) as ItemMetadata;
const status = item.status ?? 'unknown';
console.log(`Status: ${status}`);
if (status.toUpperCase() === 'COMPLETED') {
console.log(
`Indexed: ${item.item_title} (tags: ${item.item_tags.join(', ')})`,
);
indexed = true;
break;
}
if (['FAILED', 'ERROR'].includes(status.toUpperCase())) {
throw new Error(`Ingestion failed with status: ${status}`);
}
await sleep(POLL_INTERVAL_MS);
}
if (!indexed) throw new Error(`Not indexed within ${POLL_TIMEOUT_MS / 1000}s`);
```
```php PHP theme={null}
const POLL_INTERVAL_SECONDS = 15;
const POLL_TIMEOUT_SECONDS = 600;
$deadline = time() + POLL_TIMEOUT_SECONDS;
$indexed = false;
while (time() < $deadline) {
$ch = curl_init("https://platform.ai.gloo.com/engine/v2/items/$itemId");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
]);
$item = json_decode(curl_exec($ch), true);
curl_close($ch);
$status = $item['status'] ?? 'unknown';
echo "Status: $status\n";
if (strtoupper($status) === 'COMPLETED') {
echo "Indexed: {$item['item_title']} (tags: " . implode(', ', $item['item_tags']) . ")\n";
$indexed = true;
break;
}
if (in_array(strtoupper($status), ['FAILED', 'ERROR'], true)) {
throw new RuntimeException("Ingestion failed with status: $status");
}
sleep(POLL_INTERVAL_SECONDS);
}
if (!$indexed) {
throw new RuntimeException('Not indexed within ' . POLL_TIMEOUT_SECONDS . 's');
}
```
```go Go theme={null}
const (
pollInterval = 15 * time.Second
pollTimeout = 600 * time.Second
)
deadline := time.Now().Add(pollTimeout)
indexed := false
for time.Now().Before(deadline) {
req, _ := http.NewRequest("GET",
"https://platform.ai.gloo.com/engine/v2/items/"+itemID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var item struct {
Status string `json:"status"`
ItemTitle string `json:"item_title"`
ItemTags []string `json:"item_tags"`
}
json.NewDecoder(resp.Body).Decode(&item)
resp.Body.Close()
fmt.Println("Status:", item.Status)
if strings.EqualFold(item.Status, "COMPLETED") {
fmt.Printf("Indexed: %s (tags: %s)\n", item.ItemTitle, strings.Join(item.ItemTags, ", "))
indexed = true
break
}
if strings.EqualFold(item.Status, "FAILED") || strings.EqualFold(item.Status, "ERROR") {
panic("Ingestion failed with status: " + item.Status)
}
time.Sleep(pollInterval)
}
if !indexed {
panic("Not indexed within timeout")
}
```
```java Java theme={null}
Duration pollInterval = Duration.ofSeconds(15);
Duration pollTimeout = Duration.ofSeconds(600);
Instant deadline = Instant.now().plus(pollTimeout);
boolean indexed = false;
while (Instant.now().isBefore(deadline)) {
HttpRequest statusRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/items/" + itemId))
.header("Authorization", "Bearer " + token)
.GET()
.build();
JsonObject item = gson
.fromJson(http.send(statusRequest, HttpResponse.BodyHandlers.ofString()).body(),
JsonObject.class);
String status = item.has("status") ? item.get("status").getAsString() : "unknown";
System.out.println("Status: " + status);
if (status.equalsIgnoreCase("COMPLETED")) {
System.out.println("Indexed: " + item.get("item_title").getAsString());
indexed = true;
break;
}
if (status.equalsIgnoreCase("FAILED") || status.equalsIgnoreCase("ERROR")) {
throw new IllegalStateException("Ingestion failed with status: " + status);
}
Thread.sleep(pollInterval.toMillis());
}
if (!indexed) {
throw new IllegalStateException("Not indexed within timeout");
}
```
### What You'll See
For a fresh upload (about 6 minutes for the sample file):
```
Status: QUEUED
Status: CHUNKING
...
Status: COMPLETED
Indexed: Building Stronger Communities Through Service (tags: community, service, rag-pipeline-series)
```
If you re-run against already-indexed content, the first poll returns `COMPLETED` immediately. The metadata you set in Step 3 round-trips on the same response — confirming title, author, and tags are attached to the indexed item.
## Run the Complete Example
The cookbook contains the full pipeline — upload, metadata, and polling with token caching and error handling — as one runnable program in all six languages. From the [cookbook repository](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook), install dependencies, copy `.env.example` to `.env` and add your credentials, then run it:
```bash Python theme={null}
cd rag-pipeline-part-1/python
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then add your API key and Publisher ID
python main.py
```
```bash JavaScript theme={null}
cd rag-pipeline-part-1/javascript
npm install
cp .env.example .env # then add your API key and Publisher ID
npm start
```
```bash TypeScript theme={null}
cd rag-pipeline-part-1/typescript
npm install
cp .env.example .env # then add your API key and Publisher ID
npm start
```
```bash PHP theme={null}
cd rag-pipeline-part-1/php
composer install
cp .env.example .env # then add your API key and Publisher ID
php index.php
```
```bash Go theme={null}
cd rag-pipeline-part-1/go
go mod tidy
cp .env.example .env # then add your API key and Publisher ID
go run main.go
```
```bash Java theme={null}
cd rag-pipeline-part-1/java
mvn compile
cp .env.example .env # then add your API key and Publisher ID
mvn -q exec:java
```
You'll see:
```
Step 1: Uploading sample content...
Queued for ingestion: 822308cc-72fc-478d-a2eb-fbdf01a6a15d
Step 2: Setting item metadata...
Metadata set: title, summary, author, 3 tags
Step 3: Verifying indexing (polling)...
Status: QUEUED
Status: COMPLETED
Pipeline content is indexed and ready.
Item ID: 822308cc-72fc-478d-a2eb-fbdf01a6a15d
Title: Building Stronger Communities Through Service
Author: Gloo AI Docs Team
Tags: community, service, rag-pipeline-series
Status: COMPLETED
```
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript,
TypeScript, Python, PHP, Go, Java) with setup instructions and the sample
content file.
The code snippets above are **simplified and self-contained** — designed for
readability and easy copy-paste. The cookbook examples add duplicate handling
and structured error handling. Both implement the same APIs and patterns.
## Troubleshooting
### Error: 401 Unauthorized
Your API key is missing or invalid. Verify the `GLOO_API_KEY` environment variable is set correctly. See the [Authentication Tutorial](/tutorials/authentication).
### Error: 400 publisher\_not\_found
The `publisher_id` doesn't exist or isn't accessible with your credentials. Copy the Publisher ID (a UUID) from **Studio > Data Engine > Publishers**.
### Error: 400 too\_many\_files
`producer_id` applies to a single file. When uploading multiple files in one request, omit it.
### Polling times out
Larger files take longer to process. Increase the timeout, or check **Studio > Ingestion Analytics** to see whether the publisher is still processing.
## Next Steps
Your pipeline now has indexed, metadata-rich content. Wire it into retrieval:
1. **[Building Custom Search](/tutorials/search)** — query this content semantically with the Search API
2. **[Grounded Completions with RAG](/tutorials/completions-grounded)** — answer questions from this content with source citations
3. **[Part 2: Content Lifecycle](/tutorials/rag-pipeline-part-2)** — update, bulk-edit, and delete the items you created here
4. **\[Part 3: Verification, Error Handling & Resilience]\((/tutorials/rag-pipeline-part-3)** — production-grade retry and verification patterns
# Part 2: Content Lifecycle
Source: https://docs.gloo.com/tutorials/rag-pipeline-part-2
Update, bulk-edit, and delete Data Engine content safely — scoped to exactly the items you created.
This is **Part 2 of the Build an End-to-End RAG Pipeline series**. In Part 1 you ingested content and verified it was indexed. Real content doesn't stay still: titles get corrected, you re-tag in bulk, and you remove outdated material. This part covers those lifecycle operations — and how to run them safely against a publisher that may also hold content you don't want to touch.
The key idea is **scoping**: every edit and delete here targets the exact items this recipe created, identified by the item IDs you captured when you uploaded them — never the whole publisher.
## Pipeline at a Glance
Create the publisher that owns your content — [Part 1](/tutorials/rag-pipeline-part-1).
Upload files and enrich them — [Part 1](/tutorials/rag-pipeline-part-1).
Poll item status until your content is searchable — [Part 1](/tutorials/rag-pipeline-part-1).
Query your content — deep dive: [Building Custom Search](/tutorials/search).
Answer questions from your content with citations — deep dive: [Grounded Completions with RAG](/tutorials/completions-grounded).
Update, bulk-edit, and delete content — **covered below**.
Error handling and retry patterns — [Part 3](/tutorials/rag-pipeline-part-3).
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* A **publisher** with credentials — see [Part 1](/tutorials/rag-pipeline-part-1) to create one
* Familiarity with uploading and indexing content from [Part 1](/tutorials/rag-pipeline-part-1)
This recipe seeds its own sample content, so it runs standalone — you don't need to have completed Part 1 first. It reuses Part 1's upload and indexing patterns for that seeding step.
***
## How Scoping Keeps This Safe
A publisher can hold content from many sources. Bulk edits and deletes are powerful, so this recipe never operates publisher-wide. Instead it follows one rule:
> Keep the **item ID** the API returns when you upload each item, and scope every bulk edit and delete to that exact list of item IDs.
The upload response gives you each item's ID directly (Part 1, Step 2). Those IDs are authoritative handles for content you own, so building your bulk operations from them guarantees you only ever touch your own items.
There's also a lookup endpoint, **POST** `/engine/v2/publisher/{publisher_id}/items/by-producer`, that resolves your producer IDs back to item IDs — handy when a separate process only has the producer IDs it assigned. Be aware it's **cached (\~5 minutes) and isn't invalidated when items change**, so right after an upload or delete it can return stale or already-deleted item IDs. Treat the upload response (or a `GET` on the item) as the source of truth; don't rely on `by-producer` immediately after a write.
## Step 1: Set Up Sample Content
This recipe manages three short articles, each uploaded under a stable producer ID. The upload response returns each item's ID — capture it; that's the handle every later step uses.
| Producer ID | Initial title |
| - | - |
| `rag-pipeline-part2-volunteer-onboarding` | Onboarding New Volunteers |
| `rag-pipeline-part2-measuring-impact` | Measuring Community Impact |
| `rag-pipeline-part2-sustaining-engagement` | Sustaining Long-Term Engagement |
The files live in [`sample_files/`](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/rag-pipeline-part-2/sample_files). Upload each one, set its initial metadata, and keep the returned `item_id` in a map keyed by producer ID:
```python Python theme={null}
import requests
# token is set in Part 1 — see rag-pipeline-part-1.mdx
PUBLISHER_ID = "your_publisher_id"
SEED_ITEMS = [
{"file": "volunteer-onboarding.md",
"producer_id": "rag-pipeline-part2-volunteer-onboarding",
"item_title": "Onboarding New Volunteers",
"item_tags": ["volunteers", "rag-pipeline-series"]},
{"file": "measuring-community-impact.md",
"producer_id": "rag-pipeline-part2-measuring-impact",
"item_title": "Measuring Community Impact",
"item_tags": ["measurement", "rag-pipeline-series"]},
{"file": "sustaining-engagement.md",
"producer_id": "rag-pipeline-part2-sustaining-engagement",
"item_title": "Sustaining Long-Term Engagement",
"item_tags": ["engagement", "rag-pipeline-series"]},
]
# Upload each file and KEEP the returned item_id — your authoritative handle
mapping = {}
for item in SEED_ITEMS:
with open(f"sample_files/{item['file']}", "rb") as f:
result = requests.post(
"https://platform.ai.gloo.com/ingestion/v2/files",
headers={"Authorization": f"Bearer {token}"},
params={"producer_id": item["producer_id"]},
files={"files": (item["file"], f)},
data={"publisher_id": PUBLISHER_ID},
).json()
item_id = (result["ingesting"] or result["duplicates"])[0]
mapping[item["producer_id"]] = item_id
# Set initial metadata (see Part 1, Step 3)
requests.patch(
"https://platform.ai.gloo.com/engine/v2/item",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"publisher_id": PUBLISHER_ID, "item_id": item_id,
"item_title": item["item_title"], "item_tags": item["item_tags"]},
)
print(f"Uploaded {item['producer_id']} -> {item_id}")
item_ids = list(mapping.values())
# Wait for every item to finish indexing before editing (see Part 1, Step 4).
```
```javascript JavaScript theme={null}
import { readFile } from "node:fs/promises";
// token is set in Part 1 — see rag-pipeline-part-1.mdx
const PUBLISHER_ID = "your_publisher_id";
const SEED_ITEMS = [
{ file: "volunteer-onboarding.md", producer_id: "rag-pipeline-part2-volunteer-onboarding",
item_title: "Onboarding New Volunteers", item_tags: ["volunteers", "rag-pipeline-series"] },
{ file: "measuring-community-impact.md", producer_id: "rag-pipeline-part2-measuring-impact",
item_title: "Measuring Community Impact", item_tags: ["measurement", "rag-pipeline-series"] },
{ file: "sustaining-engagement.md", producer_id: "rag-pipeline-part2-sustaining-engagement",
item_title: "Sustaining Long-Term Engagement", item_tags: ["engagement", "rag-pipeline-series"] },
];
// Upload each file and KEEP the returned item_id — your authoritative handle
const mapping = {};
for (const item of SEED_ITEMS) {
const form = new FormData();
form.append("publisher_id", PUBLISHER_ID);
form.append("files", new Blob([await readFile(`sample_files/${item.file}`)]), item.file);
const result = await (
await fetch(
`https://platform.ai.gloo.com/ingestion/v2/files?producer_id=${encodeURIComponent(item.producer_id)}`,
{ method: "POST", headers: { Authorization: `Bearer ${token}` }, body: form }
)
).json();
const itemId = (result.ingesting.length ? result.ingesting : result.duplicates)[0];
mapping[item.producer_id] = itemId;
// Set initial metadata (see Part 1, Step 3)
await fetch("https://platform.ai.gloo.com/engine/v2/item", {
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ publisher_id: PUBLISHER_ID, item_id: itemId,
item_title: item.item_title, item_tags: item.item_tags }),
});
console.log(`Uploaded ${item.producer_id} -> ${itemId}`);
}
const itemIds = Object.values(mapping);
// Wait for every item to finish indexing before editing (see Part 1, Step 4).
```
```typescript TypeScript theme={null}
import { readFile } from "node:fs/promises";
// token is set in Part 1 — see rag-pipeline-part-1.mdx
const PUBLISHER_ID = "your_publisher_id";
interface SeedItem {
file: string;
producer_id: string;
item_title: string;
item_tags: string[];
}
const SEED_ITEMS: SeedItem[] = [
{ file: "volunteer-onboarding.md", producer_id: "rag-pipeline-part2-volunteer-onboarding",
item_title: "Onboarding New Volunteers", item_tags: ["volunteers", "rag-pipeline-series"] },
{ file: "measuring-community-impact.md", producer_id: "rag-pipeline-part2-measuring-impact",
item_title: "Measuring Community Impact", item_tags: ["measurement", "rag-pipeline-series"] },
{ file: "sustaining-engagement.md", producer_id: "rag-pipeline-part2-sustaining-engagement",
item_title: "Sustaining Long-Term Engagement", item_tags: ["engagement", "rag-pipeline-series"] },
];
(async () => {
// Upload each file and KEEP the returned item_id — your authoritative handle
const mapping: Record = {};
for (const item of SEED_ITEMS) {
const form = new FormData();
form.append("publisher_id", PUBLISHER_ID);
form.append("files", new Blob([await readFile(`sample_files/${item.file}`)]), item.file);
const result = (await (
await fetch(
`https://platform.ai.gloo.com/ingestion/v2/files?producer_id=${encodeURIComponent(item.producer_id)}`,
{ method: "POST", headers: { Authorization: `Bearer ${token}` }, body: form }
)
).json()) as { ingesting: string[]; duplicates: string[] };
const itemId = (result.ingesting.length ? result.ingesting : result.duplicates)[0];
mapping[item.producer_id] = itemId;
// Set initial metadata (see Part 1, Step 3)
await fetch("https://platform.ai.gloo.com/engine/v2/item", {
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ publisher_id: PUBLISHER_ID, item_id: itemId,
item_title: item.item_title, item_tags: item.item_tags }),
});
console.log(`Uploaded ${item.producer_id} -> ${itemId}`);
}
const itemIds = Object.values(mapping);
// Wait for every item to finish indexing before editing (see Part 1, Step 4).
})();
```
```php PHP theme={null}
'volunteer-onboarding.md', 'producer_id' => 'rag-pipeline-part2-volunteer-onboarding',
'item_title' => 'Onboarding New Volunteers', 'item_tags' => ['volunteers', 'rag-pipeline-series']],
['file' => 'measuring-community-impact.md', 'producer_id' => 'rag-pipeline-part2-measuring-impact',
'item_title' => 'Measuring Community Impact', 'item_tags' => ['measurement', 'rag-pipeline-series']],
['file' => 'sustaining-engagement.md', 'producer_id' => 'rag-pipeline-part2-sustaining-engagement',
'item_title' => 'Sustaining Long-Term Engagement', 'item_tags' => ['engagement', 'rag-pipeline-series']],
];
// Upload each file and KEEP the returned item_id — your authoritative handle
$mapping = [];
foreach ($seedItems as $item) {
$url = 'https://platform.ai.gloo.com/ingestion/v2/files?producer_id=' . urlencode($item['producer_id']);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
CURLOPT_POSTFIELDS => [
'publisher_id' => $publisherId,
'files' => new CURLFile("sample_files/{$item['file']}", 'text/markdown'),
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
$itemId = ($result['ingesting'] ?: $result['duplicates'])[0];
$mapping[$item['producer_id']] = $itemId;
// Set initial metadata (see Part 1, Step 3)
$ch = curl_init('https://platform.ai.gloo.com/engine/v2/item');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['publisher_id' => $publisherId, 'item_id' => $itemId,
'item_title' => $item['item_title'], 'item_tags' => $item['item_tags']]),
]);
curl_exec($ch);
curl_close($ch);
echo "Uploaded {$item['producer_id']} -> $itemId\n";
}
$itemIds = array_values($mapping);
// Wait for every item to finish indexing before editing (see Part 1, Step 4).
```
```go Go theme={null}
// Seeding reuses Part 1's upload + polling. The key point: capture the item_id
// the upload returns and keep it keyed by producer ID.
mapping := map[string]string{}
itemIDs := []string{}
for _, item := range seedItems {
fileBytes, _ := os.ReadFile("sample_files/" + item.File)
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
writer.WriteField("publisher_id", publisherID)
part, _ := writer.CreateFormFile("files", item.File)
part.Write(fileBytes)
writer.Close()
uploadURL := "https://platform.ai.gloo.com/ingestion/v2/files?producer_id=" +
url.QueryEscape(item.ProducerID)
req, _ := http.NewRequest("POST", uploadURL, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, _ := http.DefaultClient.Do(req)
var result struct {
Ingesting []string `json:"ingesting"`
Duplicates []string `json:"duplicates"`
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
itemID := append(result.Ingesting, result.Duplicates...)[0]
mapping[item.ProducerID] = itemID
itemIDs = append(itemIDs, itemID)
// Set initial metadata (see Part 1, Step 3), then wait for indexing (Part 1, Step 4).
fmt.Printf("Uploaded %s -> %s\n", item.ProducerID, itemID)
}
```
```java Java theme={null}
// Seeding reuses Part 1's upload + polling. The key point: capture the item_id
// the upload returns and keep it keyed by producer ID.
Map mapping = new LinkedHashMap<>();
List itemIds = new ArrayList<>();
for (SeedItem item : seedItems) {
String boundary = "----GlooBoundary" + java.util.UUID.randomUUID();
var body = new java.io.ByteArrayOutputStream();
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"publisher_id\"\r\n\r\n"
+ PUBLISHER_ID + "\r\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"files\"; filename=\"" + item.file() + "\"\r\n"
+ "Content-Type: text/markdown\r\n\r\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));
body.write(java.nio.file.Files.readAllBytes(java.nio.file.Path.of("sample_files", item.file())));
body.write(("\r\n--" + boundary + "--\r\n").getBytes(java.nio.charset.StandardCharsets.UTF_8));
HttpRequest uploadRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/ingestion/v2/files?producer_id="
+ URLEncoder.encode(item.producerId(), StandardCharsets.UTF_8)))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
.build();
JsonObject result = gson.fromJson(
http.send(uploadRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
JsonArray ingesting = result.getAsJsonArray("ingesting");
String itemId = (ingesting.size() > 0 ? ingesting : result.getAsJsonArray("duplicates"))
.get(0).getAsString();
mapping.put(item.producerId(), itemId);
itemIds.add(itemId);
// Set initial metadata (see Part 1, Step 3), then wait for indexing (Part 1, Step 4).
System.out.printf("Uploaded %s -> %s%n", item.producerId(), itemId);
}
```
### What You'll See
```
Uploaded rag-pipeline-part2-volunteer-onboarding -> 0bfc3629-137d-4797-aac6-a2836ca39930
Uploaded rag-pipeline-part2-measuring-impact -> 8e9f12bc-4ef2-4e77-b1aa-293935a67c9a
Uploaded rag-pipeline-part2-sustaining-engagement -> ca849be9-cf4c-4423-9369-3793736cd75a
```
Ingestion is asynchronous — wait until every item reaches `COMPLETED` (the polling loop from [Part 1, Step 4](/tutorials/rag-pipeline-part-1)) before editing it. The complete cookbook program includes this wait.
The following steps reuse `token`, `PUBLISHER_ID`, `mapping`, and `item_ids` from here.
## Step 2: Update a Single Item
Correct one item's metadata with **PATCH** `/engine/v2/item`. Identify the target by `item_id` (or by `producer_id` — either works for a single item). Only the fields you send change; everything else is left as-is.
```python Python theme={null}
target_id = mapping["rag-pipeline-part2-volunteer-onboarding"]
response = requests.patch(
"https://platform.ai.gloo.com/engine/v2/item",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={
"publisher_id": PUBLISHER_ID,
"item_id": target_id,
"item_title": "Onboarding New Volunteers: A First-Day Playbook",
"item_summary": "A practical first-day checklist for welcoming and retaining new volunteers.",
},
)
response.raise_for_status()
print("Single item updated")
```
```javascript JavaScript theme={null}
const targetId = mapping["rag-pipeline-part2-volunteer-onboarding"];
const updateResponse = await fetch("https://platform.ai.gloo.com/engine/v2/item", {
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
publisher_id: PUBLISHER_ID,
item_id: targetId,
item_title: "Onboarding New Volunteers: A First-Day Playbook",
item_summary: "A practical first-day checklist for welcoming and retaining new volunteers.",
}),
});
if (!updateResponse.ok) throw new Error(`HTTP ${updateResponse.status}`);
console.log("Single item updated");
```
```typescript TypeScript theme={null}
const targetId = mapping["rag-pipeline-part2-volunteer-onboarding"];
const updateResponse = await fetch("https://platform.ai.gloo.com/engine/v2/item", {
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
publisher_id: PUBLISHER_ID,
item_id: targetId,
item_title: "Onboarding New Volunteers: A First-Day Playbook",
item_summary: "A practical first-day checklist for welcoming and retaining new volunteers.",
}),
});
if (!updateResponse.ok) throw new Error(`HTTP ${updateResponse.status}`);
console.log("Single item updated");
```
```php PHP theme={null}
$targetId = $mapping['rag-pipeline-part2-volunteer-onboarding'];
$ch = curl_init('https://platform.ai.gloo.com/engine/v2/item');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'publisher_id' => $publisherId,
'item_id' => $targetId,
'item_title' => 'Onboarding New Volunteers: A First-Day Playbook',
'item_summary' => 'A practical first-day checklist for welcoming and retaining new volunteers.',
]),
]);
curl_exec($ch);
curl_close($ch);
echo "Single item updated\n";
```
```go Go theme={null}
targetID := mapping["rag-pipeline-part2-volunteer-onboarding"]
update, _ := json.Marshal(map[string]any{
"publisher_id": publisherID,
"item_id": targetID,
"item_title": "Onboarding New Volunteers: A First-Day Playbook",
"item_summary": "A practical first-day checklist for welcoming and retaining new volunteers.",
})
req, _ = http.NewRequest("PATCH", "https://platform.ai.gloo.com/engine/v2/item",
bytes.NewReader(update))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ = http.DefaultClient.Do(req)
resp.Body.Close()
fmt.Println("Single item updated")
```
```java Java theme={null}
String targetId = mapping.get("rag-pipeline-part2-volunteer-onboarding");
String updateJson = """
{
"publisher_id": "%s",
"item_id": "%s",
"item_title": "Onboarding New Volunteers: A First-Day Playbook",
"item_summary": "A practical first-day checklist for welcoming and retaining new volunteers."
}
""".formatted(PUBLISHER_ID, targetId);
HttpRequest updateRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/item"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(updateJson))
.build();
http.send(updateRequest, HttpResponse.BodyHandlers.ofString());
System.out.println("Single item updated");
```
## Step 3: Bulk-Edit Multiple Items
Apply changes to several items at once with **PATCH** `/engine/v2/items?publisher_id=...`. The request has two parts: a `filter` selecting which items to touch, and a list of `ops` describing the changes. Here the filter is the exact `item_ids` you captured in Step 1 — that's what keeps the operation scoped to your content.
Each op has an `op` (`append`, `replace`, or `remove`), a `field`, and a `value`. Below, every item gets a `reviewed-q2-2026` tag appended and its author replaced.
```python Python theme={null}
response = requests.patch(
"https://platform.ai.gloo.com/engine/v2/items",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
params={"publisher_id": PUBLISHER_ID},
json={
"filter": {"item_ids": item_ids},
"ops": [
{"op": "append", "field": "item_tags", "value": ["reviewed-q2-2026"]},
{"op": "replace", "field": "author", "value": ["Community Programs Team"]},
],
},
)
response.raise_for_status()
result = response.json()
print(f"Matched {result['total_matched']}, patched {result['total_patched']}, "
f"failed {result['total_failed']}")
```
```javascript JavaScript theme={null}
const bulkResponse = await fetch(
`https://platform.ai.gloo.com/engine/v2/items?publisher_id=${encodeURIComponent(PUBLISHER_ID)}`,
{
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
filter: { item_ids: itemIds },
ops: [
{ op: "append", field: "item_tags", value: ["reviewed-q2-2026"] },
{ op: "replace", field: "author", value: ["Community Programs Team"] },
],
}),
}
);
if (!bulkResponse.ok) throw new Error(`HTTP ${bulkResponse.status}`);
const result = await bulkResponse.json();
console.log(
`Matched ${result.total_matched}, patched ${result.total_patched}, failed ${result.total_failed}`
);
```
```typescript TypeScript theme={null}
const bulkResponse = await fetch(
`https://platform.ai.gloo.com/engine/v2/items?publisher_id=${encodeURIComponent(PUBLISHER_ID)}`,
{
method: "PATCH",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
filter: { item_ids: itemIds },
ops: [
{ op: "append", field: "item_tags", value: ["reviewed-q2-2026"] },
{ op: "replace", field: "author", value: ["Community Programs Team"] },
],
}),
}
);
if (!bulkResponse.ok) throw new Error(`HTTP ${bulkResponse.status}`);
const result = (await bulkResponse.json()) as {
total_matched: number;
total_patched: number;
total_failed: number;
};
console.log(
`Matched ${result.total_matched}, patched ${result.total_patched}, failed ${result.total_failed}`
);
```
```php PHP theme={null}
$ch = curl_init('https://platform.ai.gloo.com/engine/v2/items?publisher_id=' . urlencode($publisherId));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'filter' => ['item_ids' => $itemIds],
'ops' => [
['op' => 'append', 'field' => 'item_tags', 'value' => ['reviewed-q2-2026']],
['op' => 'replace', 'field' => 'author', 'value' => ['Community Programs Team']],
],
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Matched {$result['total_matched']}, patched {$result['total_patched']}, "
. "failed {$result['total_failed']}\n";
```
```go Go theme={null}
bulk, _ := json.Marshal(map[string]any{
"filter": map[string]any{"item_ids": itemIDs},
"ops": []map[string]any{
{"op": "append", "field": "item_tags", "value": []string{"reviewed-q2-2026"}},
{"op": "replace", "field": "author", "value": []string{"Community Programs Team"}},
},
})
bulkURL := "https://platform.ai.gloo.com/engine/v2/items?publisher_id=" + url.QueryEscape(publisherID)
req, _ = http.NewRequest("PATCH", bulkURL, bytes.NewReader(bulk))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ = http.DefaultClient.Do(req)
defer resp.Body.Close()
var result struct {
TotalMatched int `json:"total_matched"`
TotalPatched int `json:"total_patched"`
TotalFailed int `json:"total_failed"`
}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Printf("Matched %d, patched %d, failed %d\n",
result.TotalMatched, result.TotalPatched, result.TotalFailed)
```
```java Java theme={null}
String bulkJson = """
{
"filter": { "item_ids": %s },
"ops": [
{ "op": "append", "field": "item_tags", "value": ["reviewed-q2-2026"] },
{ "op": "replace", "field": "author", "value": ["Community Programs Team"] }
]
}
""".formatted(gson.toJson(itemIds));
HttpRequest bulkRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/items?publisher_id="
+ URLEncoder.encode(PUBLISHER_ID, StandardCharsets.UTF_8)))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(bulkJson))
.build();
JsonObject result = gson.fromJson(
http.send(bulkRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
System.out.printf("Matched %d, patched %d, failed %d%n",
result.get("total_matched").getAsInt(),
result.get("total_patched").getAsInt(),
result.get("total_failed").getAsInt());
```
### What You'll See
```
Matched 3, patched 3, failed 0
```
## Step 4: Verify Your Changes
Re-fetch each item with **GET** `/engine/v2/items/{item_id}` to confirm the edits.
Edits succeed immediately, but the read path is **eventually consistent**: a freshly patched item can take a few seconds to reflect the change on a subsequent GET. Verify by re-fetching until the change appears, rather than reading once.
```python Python theme={null}
import time
def get_item(item_id):
r = requests.get(
f"https://platform.ai.gloo.com/engine/v2/items/{item_id}",
headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status()
return r.json()
for item_id in item_ids:
item = get_item(item_id)
for _ in range(20): # read-after-write retry
if "reviewed-q2-2026" in (item.get("item_tags") or []):
break
time.sleep(3)
item = get_item(item_id)
print(item["item_title"])
print(f" author: {', '.join(item.get('author') or [])}")
print(f" tags: {', '.join(item.get('item_tags') or [])}")
```
```javascript JavaScript theme={null}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function getItem(itemId) {
const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}
for (const itemId of itemIds) {
let item = await getItem(itemId);
for (let attempt = 0; attempt < 20; attempt++) {
// read-after-write retry
if ((item.item_tags ?? []).includes("reviewed-q2-2026")) break;
await sleep(3000);
item = await getItem(itemId);
}
console.log(item.item_title);
console.log(` author: ${(item.author ?? []).join(", ")}`);
console.log(` tags: ${(item.item_tags ?? []).join(", ")}`);
}
```
```typescript TypeScript theme={null}
const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms));
interface ItemMetadata {
item_title: string;
author: string[];
item_tags: string[];
}
async function getItem(itemId: string): Promise {
const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return (await r.json()) as ItemMetadata;
}
for (const itemId of itemIds) {
let item = await getItem(itemId);
for (let attempt = 0; attempt < 20; attempt++) {
// read-after-write retry
if ((item.item_tags ?? []).includes("reviewed-q2-2026")) break;
await sleep(3000);
item = await getItem(itemId);
}
console.log(item.item_title);
console.log(` author: ${(item.author ?? []).join(", ")}`);
console.log(` tags: ${(item.item_tags ?? []).join(", ")}`);
}
```
```php PHP theme={null}
function getItem(string $itemId, string $token): array
{
$ch = curl_init("https://platform.ai.gloo.com/engine/v2/items/$itemId");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
]);
$item = json_decode(curl_exec($ch), true);
curl_close($ch);
return $item;
}
foreach ($itemIds as $itemId) {
$item = getItem($itemId, $token);
for ($attempt = 0; $attempt < 20; $attempt++) { // read-after-write retry
if (in_array('reviewed-q2-2026', $item['item_tags'] ?? [], true)) {
break;
}
sleep(3);
$item = getItem($itemId, $token);
}
echo "{$item['item_title']}\n";
echo ' author: ' . implode(', ', $item['author'] ?? []) . "\n";
echo ' tags: ' . implode(', ', $item['item_tags'] ?? []) . "\n";
}
```
```go Go theme={null}
getItem := func(itemID string) map[string]any {
req, _ := http.NewRequest("GET",
"https://platform.ai.gloo.com/engine/v2/items/"+itemID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var item map[string]any
json.NewDecoder(resp.Body).Decode(&item)
return item
}
hasTag := func(item map[string]any, tag string) bool {
tags, _ := item["item_tags"].([]any)
for _, t := range tags {
if t == tag {
return true
}
}
return false
}
for _, itemID := range itemIDs {
item := getItem(itemID)
for attempt := 0; attempt < 20; attempt++ { // read-after-write retry
if hasTag(item, "reviewed-q2-2026") {
break
}
time.Sleep(3 * time.Second)
item = getItem(itemID)
}
fmt.Println(item["item_title"])
fmt.Println(" author:", item["author"])
fmt.Println(" tags: ", item["item_tags"])
}
```
```java Java theme={null}
for (String itemId : mapping.values()) {
JsonObject item = null;
for (int attempt = 0; attempt < 20; attempt++) { // read-after-write retry
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/items/" + itemId))
.header("Authorization", "Bearer " + token)
.GET()
.build();
item = gson.fromJson(
http.send(getRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
if (item.getAsJsonArray("item_tags").toString().contains("reviewed-q2-2026")) break;
Thread.sleep(3000);
}
System.out.println(item.get("item_title").getAsString());
System.out.println(" author: " + item.getAsJsonArray("author"));
System.out.println(" tags: " + item.getAsJsonArray("item_tags"));
}
```
### What You'll See
```
Onboarding New Volunteers: A First-Day Playbook
author: Community Programs Team
tags: volunteers, rag-pipeline-series, reviewed-q2-2026
Measuring Community Impact
author: Community Programs Team
tags: measurement, rag-pipeline-series, reviewed-q2-2026
Sustaining Long-Term Engagement
author: Community Programs Team
tags: engagement, rag-pipeline-series, reviewed-q2-2026
```
The single-item update from Step 2 (the new title on the first item) and the bulk edits from Step 3 (new author and added tag on all three) are both reflected.
## Step 5: Delete Items
Remove items with **DELETE** `/engine/v2/items`, passing the `item_ids` to delete. This both demonstrates deletion and cleans up the content this recipe created.
```python Python theme={null}
response = requests.delete(
"https://platform.ai.gloo.com/engine/v2/items",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"item_ids": item_ids},
)
response.raise_for_status()
deletion = response.json()
print(f"Requested {deletion['total_requested']}, deleted {deletion['total_deleted']}, "
f"failed {deletion['total_failed']}")
# GET is authoritative for deletion: it returns 404 once an item is gone.
for item_id in item_ids:
r = requests.get(
f"https://platform.ai.gloo.com/engine/v2/items/{item_id}",
headers={"Authorization": f"Bearer {token}"},
)
print(f"{item_id} -> {r.status_code}")
```
```javascript JavaScript theme={null}
const deleteResponse = await fetch("https://platform.ai.gloo.com/engine/v2/items", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ item_ids: itemIds }),
});
if (!deleteResponse.ok) throw new Error(`HTTP ${deleteResponse.status}`);
const deletion = await deleteResponse.json();
console.log(
`Requested ${deletion.total_requested}, deleted ${deletion.total_deleted}, failed ${deletion.total_failed}`
);
// GET is authoritative for deletion: it returns 404 once an item is gone.
for (const itemId of itemIds) {
const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
headers: { Authorization: `Bearer ${token}` },
});
console.log(`${itemId} -> ${r.status}`);
}
```
```typescript TypeScript theme={null}
const deleteResponse = await fetch("https://platform.ai.gloo.com/engine/v2/items", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ item_ids: itemIds }),
});
if (!deleteResponse.ok) throw new Error(`HTTP ${deleteResponse.status}`);
const deletion = (await deleteResponse.json()) as {
total_requested: number;
total_deleted: number;
total_failed: number;
};
console.log(
`Requested ${deletion.total_requested}, deleted ${deletion.total_deleted}, failed ${deletion.total_failed}`
);
// GET is authoritative for deletion: it returns 404 once an item is gone.
for (const itemId of itemIds) {
const r = await fetch(`https://platform.ai.gloo.com/engine/v2/items/${itemId}`, {
headers: { Authorization: `Bearer ${token}` },
});
console.log(`${itemId} -> ${r.status}`);
}
```
```php PHP theme={null}
$ch = curl_init('https://platform.ai.gloo.com/engine/v2/items');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['item_ids' => $itemIds]),
]);
$deletion = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Requested {$deletion['total_requested']}, deleted {$deletion['total_deleted']}, "
. "failed {$deletion['total_failed']}\n";
// GET is authoritative for deletion: it returns 404 once an item is gone.
foreach ($itemIds as $itemId) {
$ch = curl_init("https://platform.ai.gloo.com/engine/v2/items/$itemId");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "$itemId -> $status\n";
}
```
```go Go theme={null}
del, _ := json.Marshal(map[string][]string{"item_ids": itemIDs})
req, _ = http.NewRequest("DELETE", "https://platform.ai.gloo.com/engine/v2/items",
bytes.NewReader(del))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, _ = http.DefaultClient.Do(req)
var deletion struct {
TotalRequested int `json:"total_requested"`
TotalDeleted int `json:"total_deleted"`
TotalFailed int `json:"total_failed"`
}
json.NewDecoder(resp.Body).Decode(&deletion)
resp.Body.Close()
fmt.Printf("Requested %d, deleted %d, failed %d\n",
deletion.TotalRequested, deletion.TotalDeleted, deletion.TotalFailed)
// GET is authoritative for deletion: it returns 404 once an item is gone.
for _, itemID := range itemIDs {
req, _ := http.NewRequest("GET",
"https://platform.ai.gloo.com/engine/v2/items/"+itemID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, _ := http.DefaultClient.Do(req)
resp.Body.Close()
fmt.Printf("%s -> %d\n", itemID, resp.StatusCode)
}
```
```java Java theme={null}
HttpRequest deleteRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/items"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("DELETE", HttpRequest.BodyPublishers.ofString(
gson.toJson(Map.of("item_ids", itemIds))))
.build();
JsonObject deletion = gson.fromJson(
http.send(deleteRequest, HttpResponse.BodyHandlers.ofString()).body(), JsonObject.class);
System.out.printf("Requested %d, deleted %d, failed %d%n",
deletion.get("total_requested").getAsInt(),
deletion.get("total_deleted").getAsInt(),
deletion.get("total_failed").getAsInt());
// GET is authoritative for deletion: it returns 404 once an item is gone.
for (String itemId : itemIds) {
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(URI.create("https://platform.ai.gloo.com/engine/v2/items/" + itemId))
.header("Authorization", "Bearer " + token)
.GET()
.build();
int status = http.send(getRequest, HttpResponse.BodyHandlers.ofString()).statusCode();
System.out.printf("%s -> %d%n", itemId, status);
}
```
### What You'll See
```
Requested 3, deleted 3, failed 0
0bfc3629-137d-4797-aac6-a2836ca39930 -> 404
8e9f12bc-4ef2-4e77-b1aa-293935a67c9a -> 404
ca849be9-cf4c-4423-9369-3793736cd75a -> 404
```
A `GET` on the item ID is authoritative for deletion — it returns 404 as soon as the item is gone. Don't use the `by-producer` lookup to confirm a delete: it's cached and can keep returning the deleted item's ID for a few minutes.
## Run the Complete Example
The cookbook runs the whole lifecycle — seed, update, bulk-edit, verify, and delete — as one program in all six languages. From the [cookbook repository](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook), install dependencies, copy `.env.example` to `.env` and add your credentials, then run it:
```bash Python theme={null}
cd rag-pipeline-part-2/python
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then add your API key and Publisher ID
python main.py
```
```bash JavaScript theme={null}
cd rag-pipeline-part-2/javascript
npm install
cp .env.example .env # then add your API key and Publisher ID
npm start
```
```bash TypeScript theme={null}
cd rag-pipeline-part-2/typescript
npm install
cp .env.example .env # then add your API key and Publisher ID
npm start
```
```bash PHP theme={null}
cd rag-pipeline-part-2/php
composer install
cp .env.example .env # then add your API key and Publisher ID
php index.php
```
```bash Go theme={null}
cd rag-pipeline-part-2/go
go mod tidy
cp .env.example .env # then add your API key and Publisher ID
go run main.go
```
```bash Java theme={null}
cd rag-pipeline-part-2/java
mvn compile
cp .env.example .env # then add your API key and Publisher ID
mvn -q exec:java
```
You'll see:
```
Step 1: Seeding sample content...
Uploaded rag-pipeline-part2-volunteer-onboarding -> 0bfc3629-137d-4797-aac6-a2836ca39930
Uploaded rag-pipeline-part2-measuring-impact -> 8e9f12bc-4ef2-4e77-b1aa-293935a67c9a
Uploaded rag-pipeline-part2-sustaining-engagement -> ca849be9-cf4c-4423-9369-3793736cd75a
Waiting for ingestion to complete (this can take a few minutes)...
All seed items indexed.
Step 2: Updating a single item...
Updated title and summary for rag-pipeline-part2-volunteer-onboarding
Step 3: Bulk-editing all seeded items...
Matched 3, patched 3, failed 0
Step 4: Verifying changes...
Onboarding New Volunteers: A First-Day Playbook
author: Community Programs Team
tags: volunteers, rag-pipeline-series, reviewed-q2-2026
Measuring Community Impact
author: Community Programs Team
tags: measurement, rag-pipeline-series, reviewed-q2-2026
Sustaining Long-Term Engagement
author: Community Programs Team
tags: engagement, rag-pipeline-series, reviewed-q2-2026
Step 5: Deleting items (cleanup)...
Requested 3, deleted 3, failed 0
All items confirmed deleted: true
Lifecycle complete. The publisher is back to its pre-recipe state.
```
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions and the sample content files.
The code snippets above are **simplified and self-contained** — designed for readability and easy copy-paste. The cookbook examples add token caching, the indexing wait, and structured error handling. Both implement the same APIs and patterns.
## Troubleshooting
### Error: 400 Invalid request — Item ID or Producer ID with Publisher ID required
A single-item `PATCH /engine/v2/item` needs `publisher_id` plus either `item_id` or `producer_id`. Include one of the identifiers.
### Bulk patch reports fewer matched than expected
The `filter` didn't match all your items. Confirm your `item_ids` are the ones returned by the uploads and that the items still exist (a prior run may have deleted them).
### Verification shows stale metadata
Reads are eventually consistent. Re-fetch until the change appears (as shown in Step 4) rather than reading once immediately after an edit.
### Editing right after upload returns 404 item\_not\_found
If you looked the item up with `by-producer`, its result may be stale (it's cached \~5 minutes and not invalidated on writes). Use the `item_id` from the upload response instead — that's authoritative.
## Next Steps
You can now manage content through its full lifecycle, safely scoped to the items you own. To round out the pipeline:
1. **[Part 3: Verification, Error Handling & Resilience](/tutorials/rag-pipeline-part-3)** — interpret API error responses and add retry patterns for production
2. **[Building Custom Search](/tutorials/search)** — surface your updated content to users
3. **[Grounded Completions with RAG](/tutorials/completions-grounded)** — answer questions from your content with citations
# Part 3: Verification, Error Handling & Resilience
Source: https://docs.gloo.com/tutorials/rag-pipeline-part-3
Harden your Gloo AI integration: interpret API errors, retry transient failures with backoff, and verify ingestion health.
This is **Part 3 of the Build an End-to-End RAG Pipeline series**. Parts 1 and 2 walked the happy path. Production code can't assume it: requests fail, services blip, and you need to confirm that an operation actually took effect. This part builds a small **resilient client** and uses it to interpret API errors, retry transient failures, and verify ingestion health.
Gloo AI does not include a monitoring or health-check endpoint. Resilience is built from the same item APIs you've already used, plus disciplined error handling on the client side.
## Pipeline at a Glance
Create the publisher that owns your content — [Part 1](/tutorials/rag-pipeline-part-1).
Upload files and enrich them — [Part 1](/tutorials/rag-pipeline-part-1).
Poll item status until your content is searchable — [Part 1](/tutorials/rag-pipeline-part-1).
Query your content — deep dive: [Building Custom Search](/tutorials/search).
Answer questions from your content with citations — deep dive: [Grounded Completions with RAG](/tutorials/completions-grounded).
Update, bulk-edit, and delete content — [Part 2](/tutorials/rag-pipeline-part-2).
Error handling and retry patterns — **covered below**.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* A **publisher** with credentials — see [Part 1](/tutorials/rag-pipeline-part-1)
* Familiarity with the item APIs from [Part 1](/tutorials/rag-pipeline-part-1) and [Part 2](/tutorials/rag-pipeline-part-2)
***
## Step 1: Parse Errors and Retry
A resilient client does two things on every request: it turns failures into a **normalized error** (status, code, message) so callers can react to them, and it **retries only transient failures** — server errors (500, 502, 503, 504) and network blips — with exponential backoff. Client errors (400, 401, 403, 404, 422) are bugs in the request, not blips, so they fail fast.
The Data Engine returns a few error shapes — `{"detail": {"code", "message"}}`, `{"detail": "..."}`, and `{"error", "message"}` — so the parser normalizes all of them.
```python Python theme={null}
import time
import requests
RETRYABLE = {500, 502, 503, 504}
MAX_RETRIES = 4
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"[{status} {code}] {message}")
self.status, self.code, self.message = status, code, message
def parse_error(response):
try:
body = response.json()
except ValueError:
return None, response.reason
detail = body.get("detail") if isinstance(body, dict) else None
if isinstance(detail, dict):
return detail.get("code"), detail.get("message") or response.reason
if isinstance(detail, str):
return None, detail
return body.get("error"), body.get("message") or response.reason
def request(method, url, token, **kwargs):
for attempt in range(MAX_RETRIES + 1):
response = requests.request(
method, url, headers={"Authorization": f"Bearer {token}"}, timeout=30, **kwargs
)
if response.status_code in RETRYABLE and attempt < MAX_RETRIES:
delay = 2 ** attempt
print(f" Attempt {attempt + 1} failed ({response.status_code}); retrying in {delay}s")
time.sleep(delay)
continue
if not response.ok:
code, message = parse_error(response)
raise ApiError(response.status_code, code, message)
return response.json()
```
```javascript JavaScript theme={null}
const RETRYABLE = new Set([500, 502, 503, 504]);
const MAX_RETRIES = 4;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
class ApiError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
async function parseError(response) {
const text = await response.text();
let body;
try {
body = JSON.parse(text);
} catch {
return [null, response.statusText];
}
const detail = body?.detail;
if (detail && typeof detail === "object") return [detail.code ?? null, detail.message || response.statusText];
if (typeof detail === "string") return [null, detail];
return [body?.error ?? null, body?.message || response.statusText];
}
async function request(method, url, token, body) {
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
const response = await fetch(url, {
method,
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (RETRYABLE.has(response.status) && attempt < MAX_RETRIES) {
const delay = 2 ** attempt;
console.log(` Attempt ${attempt + 1} failed (${response.status}); retrying in ${delay}s`);
await sleep(delay * 1000);
continue;
}
if (!response.ok) {
const [code, message] = await parseError(response);
throw new ApiError(response.status, code, message);
}
return response.json();
}
}
```
```typescript TypeScript theme={null}
const RETRYABLE = new Set([500, 502, 503, 504]);
const MAX_RETRIES = 4;
const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms));
class ApiError extends Error {
constructor(readonly status: number | null, readonly code: string | null, message: string) {
super(message);
}
}
async function parseError(response: Response): Promise<[string | null, string]> {
const text = await response.text();
let body: any;
try {
body = JSON.parse(text);
} catch {
return [null, response.statusText];
}
const detail = body?.detail;
if (detail && typeof detail === "object") return [detail.code ?? null, detail.message || response.statusText];
if (typeof detail === "string") return [null, detail];
return [body?.error ?? null, body?.message || response.statusText];
}
async function request(method: string, url: string, token: string, body?: unknown): Promise {
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
const response = await fetch(url, {
method,
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (RETRYABLE.has(response.status) && attempt < MAX_RETRIES) {
const delay = 2 ** attempt;
console.log(` Attempt ${attempt + 1} failed (${response.status}); retrying in ${delay}s`);
await sleep(delay * 1000);
continue;
}
if (!response.ok) {
const [code, message] = await parseError(response);
throw new ApiError(response.status, code, message);
}
return response.json();
}
}
```
```php PHP theme={null}
'Bad Request', 403 => 'Forbidden', 404 => 'Not Found', 500 => 'Internal Server Error'];
$reason = $reasons[$status] ?? "HTTP $status";
$decoded = json_decode($body, true);
$detail = is_array($decoded) ? ($decoded['detail'] ?? null) : null;
if (is_array($detail)) return [$detail['code'] ?? null, ($detail['message'] ?? '') ?: $reason];
if (is_string($detail)) return [null, $detail];
return [$decoded['error'] ?? null, ($decoded['message'] ?? '') ?: $reason];
}
function request(string $method, string $url, string $token, ?array $body = null) {
for ($attempt = 0; $attempt <= MAX_RETRIES; $attempt++) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $body !== null ? json_encode($body) : null,
CURLOPT_TIMEOUT => 30,
]);
$responseBody = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (in_array($status, RETRYABLE, true) && $attempt < MAX_RETRIES) {
$delay = 2 ** $attempt;
echo " Attempt " . ($attempt + 1) . " failed ($status); retrying in {$delay}s\n";
sleep($delay);
continue;
}
if ($status >= 400) {
[$code, $message] = parseError($status, $responseBody);
throw new ApiError($status, $code, $message);
}
return json_decode($responseBody, true);
}
}
```
```go Go theme={null}
var retryable = map[int]bool{500: true, 502: true, 503: true, 504: true}
const maxRetries = 4
type ApiError struct {
Status int
Code string
Message string
}
func (e *ApiError) Error() string { return fmt.Sprintf("[%d %s] %s", e.Status, e.Code, e.Message) }
func parseError(status int, body []byte) (string, string) {
reason := http.StatusText(status)
var decoded map[string]any
if json.Unmarshal(body, &decoded) != nil {
return "", reason
}
if detail, ok := decoded["detail"].(map[string]any); ok {
return str(detail["code"]), orReason(str(detail["message"]), reason)
}
if detail, ok := decoded["detail"].(string); ok {
return "", detail
}
return str(decoded["error"]), orReason(str(decoded["message"]), reason)
}
func request(method, url, token string, body []byte) (map[string]any, error) {
for attempt := 0; attempt <= maxRetries; attempt++ {
var reader io.Reader
if body != nil {
reader = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, url, reader)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, &ApiError{0, "network_error", err.Error()}
}
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if retryable[resp.StatusCode] && attempt < maxRetries {
delay := 1 << attempt
fmt.Printf(" Attempt %d failed (%d); retrying in %ds\n", attempt+1, resp.StatusCode, delay)
time.Sleep(time.Duration(delay) * time.Second)
continue
}
if resp.StatusCode >= 400 {
code, message := parseError(resp.StatusCode, respBody)
return nil, &ApiError{resp.StatusCode, code, message}
}
var result map[string]any
json.Unmarshal(respBody, &result)
return result, nil
}
return nil, &ApiError{0, "retries_exhausted", "Exhausted retries"}
}
```
```java Java theme={null}
static final Set RETRYABLE = Set.of(500, 502, 503, 504);
static final int MAX_RETRIES = 4;
static class ApiError extends RuntimeException {
final Integer status;
final String code;
ApiError(Integer status, String code, String message) {
super(message);
this.status = status;
this.code = code;
}
}
static String[] parseError(int status, String body) {
String reason = switch (status) {
case 400 -> "Bad Request"; case 403 -> "Forbidden"; case 404 -> "Not Found";
default -> "HTTP " + status;
};
JsonObject obj = GSON.fromJson(body, JsonObject.class);
JsonElement detail = obj == null ? null : obj.get("detail");
if (detail != null && detail.isJsonObject()) {
JsonObject d = detail.getAsJsonObject();
return new String[] {jsonString(d, "code"), orReason(jsonString(d, "message"), reason)};
}
if (detail != null && detail.isJsonPrimitive()) return new String[] {null, detail.getAsString()};
return new String[] {jsonString(obj, "error"), orReason(jsonString(obj, "message"), reason)};
}
static JsonObject request(String method, String url, String token, String body)
throws IOException, InterruptedException {
for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method(method, body != null
? HttpRequest.BodyPublishers.ofString(body)
: HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (RETRYABLE.contains(resp.statusCode()) && attempt < MAX_RETRIES) {
int delay = 1 << attempt;
System.out.printf(" Attempt %d failed (%d); retrying in %ds%n", attempt + 1, resp.statusCode(), delay);
Thread.sleep(delay * 1000L);
continue;
}
if (resp.statusCode() >= 400) {
String[] parsed = parseError(resp.statusCode(), resp.body());
throw new ApiError(resp.statusCode(), parsed[0], parsed[1]);
}
return GSON.fromJson(resp.body(), JsonObject.class);
}
throw new ApiError(null, "retries_exhausted", "Exhausted retries");
}
```
These snippets are **simplified for readability**. The cookbook client also retries on a `401`, retries network-level failures, and applies the same retry policy to multipart uploads. Both implement the same patterns.
## Step 2: Interpret API Error Responses
With `request` and `parse_error` in place, error handling becomes uniform: catch the normalized error and read its `status`, `code`, and `message`. The calls below deliberately trigger three common failures — a missing item (404), a malformed ID (400), and a rejected token (403).
```python Python theme={null}
import uuid
cases = [
("Missing item (random UUID)", f"{ITEMS_URL}/{uuid.uuid4()}", token),
("Malformed item ID", f"{ITEMS_URL}/not-a-valid-uuid", token),
("Rejected bearer token", f"{ITEMS_URL}/{uuid.uuid4()}", "invalid-token"),
]
for label, url, tok in cases:
try:
request("GET", url, tok)
print(f" {label}: unexpectedly succeeded")
except ApiError as e:
print(f" {label}: status={e.status} code={e.code!r} message={e.message!r}")
```
```javascript JavaScript theme={null}
import { randomUUID } from "node:crypto";
const cases = [
["Missing item (random UUID)", `${ITEMS_URL}/${randomUUID()}`, token],
["Malformed item ID", `${ITEMS_URL}/not-a-valid-uuid`, token],
["Rejected bearer token", `${ITEMS_URL}/${randomUUID()}`, "invalid-token"],
];
for (const [label, url, tok] of cases) {
try {
await request("GET", url, tok);
console.log(` ${label}: unexpectedly succeeded`);
} catch (e) {
console.log(` ${label}: status=${e.status} code='${e.code}' message='${e.message}'`);
}
}
```
```typescript TypeScript theme={null}
import { randomUUID } from "node:crypto";
const cases: Array<[string, string, string]> = [
["Missing item (random UUID)", `${ITEMS_URL}/${randomUUID()}`, token],
["Malformed item ID", `${ITEMS_URL}/not-a-valid-uuid`, token],
["Rejected bearer token", `${ITEMS_URL}/${randomUUID()}`, "invalid-token"],
];
for (const [label, url, tok] of cases) {
try {
await request("GET", url, tok);
console.log(` ${label}: unexpectedly succeeded`);
} catch (e) {
if (e instanceof ApiError) {
console.log(` ${label}: status=${e.status} code='${e.code}' message='${e.message}'`);
}
}
}
```
```php PHP theme={null}
$cases = [
['Missing item (random UUID)', ITEMS_URL . '/' . guidv4(), $token],
['Malformed item ID', ITEMS_URL . '/not-a-valid-uuid', $token],
['Rejected bearer token', ITEMS_URL . '/' . guidv4(), 'invalid-token'],
];
foreach ($cases as [$label, $url, $tok]) {
try {
request('GET', $url, $tok);
echo " $label: unexpectedly succeeded\n";
} catch (ApiError $e) {
echo " $label: status={$e->status} code='{$e->apiCode}' message='{$e->getMessage()}'\n";
}
}
```
```go Go theme={null}
cases := []struct{ label, url, token string }{
{"Missing item (random UUID)", itemsURL + "/" + newUUID(), token},
{"Malformed item ID", itemsURL + "/not-a-valid-uuid", token},
{"Rejected bearer token", itemsURL + "/" + newUUID(), "invalid-token"},
}
for _, tc := range cases {
_, err := request("GET", tc.url, tc.token, nil)
if err == nil {
fmt.Printf(" %s: unexpectedly succeeded\n", tc.label)
continue
}
var e *ApiError
if errors.As(err, &e) {
fmt.Printf(" %s: status=%d code='%s' message='%s'\n", tc.label, e.Status, e.Code, e.Message)
}
}
```
```java Java theme={null}
record Case(String label, String url, String token) {}
List cases = List.of(
new Case("Missing item (random UUID)", ITEMS_URL + "/" + UUID.randomUUID(), token),
new Case("Malformed item ID", ITEMS_URL + "/not-a-valid-uuid", token),
new Case("Rejected bearer token", ITEMS_URL + "/" + UUID.randomUUID(), "invalid-token"));
for (Case tc : cases) {
try {
request("GET", tc.url(), tc.token(), null);
System.out.println(" " + tc.label() + ": unexpectedly succeeded");
} catch (ApiError e) {
System.out.printf(" %s: status=%s code='%s' message='%s'%n",
tc.label(), e.status, e.code, e.getMessage());
}
}
```
### What You'll See
```
Missing item (random UUID): status=404 code='Item not found' message='The requested item does not exist or has been permanently deleted'
Malformed item ID: status=400 code='Invalid item ID format' message='Item ID must be a valid UUID format'
Rejected bearer token: status=403 code='Forbidden - insufficient permissions' message='Forbidden'
```
A rejected or malformed token returns **403**, not 401. A genuinely *expired* token typically returns 401 — which is why the cookbook client refreshes the token once on a 401 and retries. It does **not** refresh on 403, since 403 can be a legitimate permission denial (for example, an item that belongs to another publisher).
## Step 3: Retry Transient Failures
Transient failures — a 503, a dropped connection — should be retried, not surfaced. The retry loop from Step 1 already does this; here it is in isolation, recovering from a service that fails twice before succeeding.
A healthy API won't return a 5xx on demand, so this example **simulates** a transient failure to exercise the backoff path. In production the same path handles real 5xx responses and network errors.
```python Python theme={null}
RETRYABLE_DELAYS = [2 ** i for i in range(MAX_RETRIES)]
calls = 0
def flaky():
global calls
calls += 1
if calls < 3:
raise ApiError(503, "service_unavailable", "Service temporarily unavailable")
return {"ok": True}
for attempt in range(MAX_RETRIES + 1):
try:
flaky()
break
except ApiError as e:
if e.status in RETRYABLE and attempt < MAX_RETRIES:
delay = RETRYABLE_DELAYS[attempt]
print(f" Attempt {attempt + 1} failed ({e.status}: {e.code}); retrying in {delay}s")
time.sleep(delay)
else:
raise
print(f" Succeeded after {calls} attempts")
```
```javascript JavaScript theme={null}
let calls = 0;
const flaky = () => {
calls += 1;
if (calls < 3) throw new ApiError(503, "service_unavailable", "Service temporarily unavailable");
return { ok: true };
};
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
flaky();
break;
} catch (e) {
if (RETRYABLE.has(e.status) && attempt < MAX_RETRIES) {
const delay = 2 ** attempt;
console.log(` Attempt ${attempt + 1} failed (${e.status}: ${e.code}); retrying in ${delay}s`);
await sleep(delay * 1000);
} else throw e;
}
}
console.log(` Succeeded after ${calls} attempts`);
```
```typescript TypeScript theme={null}
let calls = 0;
const flaky = (): { ok: boolean } => {
calls += 1;
if (calls < 3) throw new ApiError(503, "service_unavailable", "Service temporarily unavailable");
return { ok: true };
};
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
flaky();
break;
} catch (e) {
if (e instanceof ApiError && e.status !== null && RETRYABLE.has(e.status) && attempt < MAX_RETRIES) {
const delay = 2 ** attempt;
console.log(` Attempt ${attempt + 1} failed (${e.status}: ${e.code}); retrying in ${delay}s`);
await sleep(delay * 1000);
} else throw e;
}
}
console.log(` Succeeded after ${calls} attempts`);
```
```php PHP theme={null}
$calls = 0;
$flaky = function () use (&$calls) {
$calls++;
if ($calls < 3) throw new ApiError(503, 'service_unavailable', 'Service temporarily unavailable');
return ['ok' => true];
};
for ($attempt = 0; $attempt <= MAX_RETRIES; $attempt++) {
try {
$flaky();
break;
} catch (ApiError $e) {
if (in_array($e->status, RETRYABLE, true) && $attempt < MAX_RETRIES) {
$delay = 2 ** $attempt;
echo " Attempt " . ($attempt + 1) . " failed ({$e->status}: {$e->apiCode}); retrying in {$delay}s\n";
sleep($delay);
} else {
throw $e;
}
}
}
echo " Succeeded after $calls attempts\n";
```
```go Go theme={null}
calls := 0
flaky := func() error {
calls++
if calls < 3 {
return &ApiError{503, "service_unavailable", "Service temporarily unavailable"}
}
return nil
}
for attempt := 0; attempt <= maxRetries; attempt++ {
err := flaky()
if err == nil {
break
}
var e *ApiError
if errors.As(err, &e) && retryable[e.Status] && attempt < maxRetries {
delay := 1 << attempt
fmt.Printf(" Attempt %d failed (%d: %s); retrying in %ds\n", attempt+1, e.Status, e.Code, delay)
time.Sleep(time.Duration(delay) * time.Second)
} else {
log.Fatal(err)
}
}
fmt.Printf(" Succeeded after %d attempts\n", calls)
```
```java Java theme={null}
int[] calls = {0};
Runnable flaky = () -> {
calls[0]++;
if (calls[0] < 3) throw new ApiError(503, "service_unavailable", "Service temporarily unavailable");
};
for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
flaky.run();
break;
} catch (ApiError e) {
if (e.status != null && RETRYABLE.contains(e.status) && attempt < MAX_RETRIES) {
int delay = 1 << attempt;
System.out.printf(" Attempt %d failed (%d: %s); retrying in %ds%n",
attempt + 1, e.status, e.code, delay);
Thread.sleep(delay * 1000L);
} else {
throw e;
}
}
}
System.out.printf(" Succeeded after %d attempts%n", calls[0]);
```
### What You'll See
```
Attempt 1 failed (503: service_unavailable); retrying in 1s
Attempt 2 failed (503: service_unavailable); retrying in 2s
Succeeded after 3 attempts
```
## Step 4: Verify Ingestion Health
There's no health endpoint, so you verify by checking the items you care about. Upload a batch, wait for indexing, then fetch each item's status and roll it up into a summary — treating a 404 as "not found" rather than an error so one missing item doesn't abort the check. The example deliberately includes a random ID to show that path.
```python Python theme={null}
# item_ids: uploaded and indexed via the resilient client (see the cookbook)
to_check = item_ids + [str(uuid.uuid4())]
summary = {"completed": 0, "pending": 0, "failed": 0, "not_found": 0}
for item_id in to_check:
try:
status = request("GET", f"{ITEMS_URL}/{item_id}", token).get("status", "").upper()
if status == "COMPLETED":
summary["completed"] += 1
elif status in ("FAILED", "ERROR"):
summary["failed"] += 1
else:
summary["pending"] += 1
except ApiError as e:
if e.status == 404:
summary["not_found"] += 1
else:
raise
print(f" Health: {summary['completed']} completed, {summary['pending']} pending, "
f"{summary['failed']} failed, {summary['not_found']} not found")
```
```javascript JavaScript theme={null}
// itemIds: uploaded and indexed via the resilient client (see the cookbook)
const toCheck = [...itemIds, randomUUID()];
const summary = { completed: 0, pending: 0, failed: 0, notFound: 0 };
for (const itemId of toCheck) {
try {
const status = ((await request("GET", `${ITEMS_URL}/${itemId}`, token)).status ?? "").toUpperCase();
if (status === "COMPLETED") summary.completed += 1;
else if (["FAILED", "ERROR"].includes(status)) summary.failed += 1;
else summary.pending += 1;
} catch (e) {
if (e.status === 404) summary.notFound += 1;
else throw e;
}
}
console.log(
` Health: ${summary.completed} completed, ${summary.pending} pending, ` +
`${summary.failed} failed, ${summary.notFound} not found`
);
```
```typescript TypeScript theme={null}
// itemIds: uploaded and indexed via the resilient client (see the cookbook)
const toCheck = [...itemIds, randomUUID()];
const summary = { completed: 0, pending: 0, failed: 0, notFound: 0 };
for (const itemId of toCheck) {
try {
const status = ((await request("GET", `${ITEMS_URL}/${itemId}`, token)).status ?? "").toUpperCase();
if (status === "COMPLETED") summary.completed += 1;
else if (["FAILED", "ERROR"].includes(status)) summary.failed += 1;
else summary.pending += 1;
} catch (e) {
if (e instanceof ApiError && e.status === 404) summary.notFound += 1;
else throw e;
}
}
console.log(
` Health: ${summary.completed} completed, ${summary.pending} pending, ` +
`${summary.failed} failed, ${summary.notFound} not found`
);
```
```php PHP theme={null}
// $itemIds: uploaded and indexed via the resilient client (see the cookbook)
$toCheck = array_merge($itemIds, [guidv4()]);
$summary = ['completed' => 0, 'pending' => 0, 'failed' => 0, 'not_found' => 0];
foreach ($toCheck as $itemId) {
try {
$status = strtoupper(request('GET', ITEMS_URL . '/' . $itemId, $token)['status'] ?? '');
if ($status === 'COMPLETED') $summary['completed']++;
elseif (in_array($status, ['FAILED', 'ERROR'], true)) $summary['failed']++;
else $summary['pending']++;
} catch (ApiError $e) {
if ($e->status === 404) $summary['not_found']++;
else throw $e;
}
}
echo " Health: {$summary['completed']} completed, {$summary['pending']} pending, "
. "{$summary['failed']} failed, {$summary['not_found']} not found\n";
```
```go Go theme={null}
// itemIDs: uploaded and indexed via the resilient client (see the cookbook)
toCheck := append(append([]string{}, itemIDs...), newUUID())
completed, pending, failed, notFound := 0, 0, 0, 0
for _, id := range toCheck {
item, err := request("GET", itemsURL+"/"+id, token, nil)
if err != nil {
var e *ApiError
if errors.As(err, &e) && e.Status == 404 {
notFound++
continue
}
log.Fatal(err)
}
switch strings.ToUpper(str(item["status"])) {
case "COMPLETED":
completed++
case "FAILED", "ERROR":
failed++
default:
pending++
}
}
fmt.Printf(" Health: %d completed, %d pending, %d failed, %d not found\n",
completed, pending, failed, notFound)
```
```java Java theme={null}
// itemIds: uploaded and indexed via the resilient client (see the cookbook)
List toCheck = new ArrayList<>(itemIds);
toCheck.add(UUID.randomUUID().toString());
int completed = 0, pending = 0, failed = 0, notFound = 0;
for (String itemId : toCheck) {
try {
String status = request("GET", ITEMS_URL + "/" + itemId, token, null)
.get("status").getAsString().toUpperCase();
if (status.equals("COMPLETED")) completed++;
else if (status.equals("FAILED") || status.equals("ERROR")) failed++;
else pending++;
} catch (ApiError e) {
if (e.status != null && e.status == 404) notFound++;
else throw e;
}
}
System.out.printf(" Health: %d completed, %d pending, %d failed, %d not found%n",
completed, pending, failed, notFound);
```
### What You'll See
```
Health: 2 completed, 0 pending, 0 failed, 1 not found
```
The two uploaded items report `COMPLETED`; the random ID is counted as `not found` instead of throwing.
## Run the Complete Example
The cookbook ties it together — a resilient client that handles errors, retries, and a full upload → verify → clean-up health check — in all six languages. From the [cookbook repository](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook), install dependencies, copy `.env.example` to `.env` and add your credentials, then run it:
```bash Python theme={null}
cd rag-pipeline-part-3/python
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then add your API key and Publisher ID
python main.py
```
```bash JavaScript theme={null}
cd rag-pipeline-part-3/javascript
npm install
cp .env.example .env # then add your API key and Publisher ID
npm start
```
```bash TypeScript theme={null}
cd rag-pipeline-part-3/typescript
npm install
cp .env.example .env # then add your API key and Publisher ID
npm start
```
```bash PHP theme={null}
cd rag-pipeline-part-3/php
composer install
cp .env.example .env # then add your API key and Publisher ID
php index.php
```
```bash Go theme={null}
cd rag-pipeline-part-3/go
go mod tidy
cp .env.example .env # then add your API key and Publisher ID
go run main.go
```
```bash Java theme={null}
cd rag-pipeline-part-3/java
mvn compile
cp .env.example .env # then add your API key and Publisher ID
mvn -q exec:java
```
You'll see:
```
Step 1: Resilient client ready (token refresh, error parsing, retry/backoff).
Step 2: Interpreting API error responses...
Missing item (random UUID): status=404 code='Item not found' message='The requested item does not exist or has been permanently deleted'
Malformed item ID: status=400 code='Invalid item ID format' message='Item ID must be a valid UUID format'
Rejected bearer token: status=403 code='Forbidden - insufficient permissions' message='Forbidden'
Step 3: Retrying transient failures with backoff...
Attempt 1 failed (503: service_unavailable); retrying in 1s
Attempt 2 failed (503: service_unavailable); retrying in 2s
Succeeded after 3 attempts
Step 4: Verifying ingestion health...
Uploading and indexing a batch...
Waiting for 2 item(s) to finish indexing...
Health: 2 completed, 0 pending, 0 failed, 1 not found
Cleaned up 2 item(s)
Done. The resilient client handled errors, retries, and verification end to end.
```
## Working Code Sample
Clone or browse the complete resilient client for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions and the sample content files.
## Troubleshooting
### A request fails immediately instead of retrying
That's intended for client errors (400, 401, 403, 404, 422) — they won't succeed on retry. Only 500/502/503/504 and network failures are retried.
### Retries never stop / take too long
Check your backoff cap. With base-2 exponential backoff and `MAX_RETRIES = 4`, the waits are 1s, 2s, 4s, 8s. Lower `MAX_RETRIES` or cap the delay for latency-sensitive paths.
### Empty error message
Some error responses carry a code but no message. The parser falls back to the HTTP reason phrase (for example, `Forbidden`) so you always have something to log.
### 403 on a token you believe is valid
A malformed or rejected token returns 403. Re-check the token; if it's genuinely expired you'll typically get a 401, which the cookbook client refreshes automatically. See the [Authentication Tutorial](/tutorials/authentication).
## Next Steps
That completes the **Build an End-to-End RAG Pipeline** series — you've set up a publisher and ingested content (Part 1), managed its lifecycle (Part 2), and made the integration resilient (Part 3). From here:
1. **[Building Custom Search](/tutorials/search)** — surface your content to users
2. **[Grounded Completions with RAG](/tutorials/completions-grounded)** — answer questions from your content with citations
3. Fold the resilient client into your own service so every Gloo AI call gets consistent error handling and retries.
# Realtime Upload (Deprecated)
Source: https://docs.gloo.com/tutorials/realtime-ingestion
Learn how to build an automated pipeline that ingests content via the Realtime API.
**This API is deprecated.** Follow the [Upload Files to Data
Engine](/tutorials/upload-files) tutorial instead.
This recipe shows how to build an automated content ingestion pipeline using the Realtime API. You'll learn to set up real-time content monitoring, process various content types, and create production-ready ingestion workflows that automatically make content available for search and AI interaction.
We'll build a complete content pipeline that watches for new files, extracts metadata, and uploads content to the Gloo AI platform with proper error handling and monitoring.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first
The Realtime Ingestion API requires an API key for authentication. If you
haven't set up authentication yet, follow the [Authentication
Tutorial](/tutorials/authentication).
***
## V1 Realtime Upload \[Deprecated]
### Key Features
* **Real-time Processing**: Content is processed upon upload
* **Rich Metadata**: Support for comprehensive content categorization
* **Flexible Content Types**: Articles, documents, media, and structured content
* **Automatic Indexing**: Content becomes searchable instantly
### Step 1: Understanding the Realtime API
The Realtime Ingestion API allows you to upload content that gets processed and made available for search and AI interaction. The primary endpoint is:
**POST** `/ingestion/v1/real_time_upload`
#### Key Features
* **Real-time Processing**: Content is processed upon upload
* **Rich Metadata**: Support for comprehensive content categorization
* **Flexible Content Types**: Articles, documents, media, and structured content
* **Automatic Indexing**: Content becomes searchable instantly upon file process completion.
#### Required Fields
```json theme={null}
{
"content": "Your content text here",
"publisherId": "Your publisher id here"
}
```
#### Optional Metadata Fields
* **Content Details**: `author`, `publication_date`, `item_title`, `item_subtitle`, `item_summary`
* **Categorization**: `type`, `pub_type`, `denomination`, `item_tags`
* **Media**: `item_image`, `item_url`, `hosted_url`
* **Access Control**: `drm`, `evergreen`
* **Hierarchical Structure**: `h2_title`, `h3_title` for document sections
***
### Step 2: Basic Content Upload
Let's start with a simple content upload example. This demonstrates the core API call with proper authentication and error handling.
#### API Request Structure
The `kallm` DRM value shown in the examples below is deprecated and no longer
supported. Use `aspen` only.
```json theme={null}
{
"content": "Your article content...",
"publisherId": "b6ada63e-f1ce-4a16-80da-9864f58c5bd7",
"item_title": "Developer Happiness",
"item_subtitle": "Beyond the Code",
"author": ["Aspiring Developer"],
"publication_date": "2025-08-26",
"type": "Article",
"pub_type": "technical",
"item_tags": ["documentation", "reference"],
"evergreen": true,
"drm": ["aspen", "kallm"]
}
```
#### Expected Response
```json theme={null}
{
"success": true,
"message": "Processing started, data will be uploaded shortly.",
"task_id": null,
"batch_id": null,
"processing_details": null
}
```
***
### Step 3: Verifying Content Upload
After uploading content, you can check on progress through Gloo AI Studio.
1. Log in to Gloo AI Studio.
2. Navigate to the **Data Engine** section from the main Studio sidebar.
3. Click on **Your Data**.
***
### Step 4: Setting Up File Monitoring
For automated content ingestion, you'll want to monitor directories for new files and automatically process them. This creates a real-time content pipeline.
#### File Watching Strategy
1. **Monitor Target Directory**: Watch for new files or changes
2. **Extract Metadata**: Parse filename and content for metadata
3. **Validate Content**: Ensure required fields are present
4. **Upload with Retry**: Handle failures gracefully
5. **Log Results**: Track successful and failed uploads
***
### Step 5: Batch Processing Pipeline
For processing multiple files or handling large volumes of content, batch processing provides better performance and resource management.
### Batch Processing Benefits
* **Rate Limiting**: Control API call frequency
* **Progress Tracking**: Monitor processing status
* **Error Recovery**: Retry failed uploads
* **Resource Management**: Efficient memory and network usage
***
### Step 6: Production Considerations
### Error Handling
* **Authentication Failures**: API key validation and retry
* **Rate Limiting**: Exponential backoff
* **Network Issues**: Connection retry logic
* **Validation Errors**: Content preprocessing
#### Monitoring and Logging
* **Success Metrics**: Upload counts and timing
* **Error Tracking**: Failed uploads with reasons
* **Performance Monitoring**: API response times
* **Health Checks**: System status monitoring
***
### Complete Examples
The following examples combine authentication, file processing, and error handling into complete, production-ready solutions for each language.
First, set up your environment variables in a `.env` file:
```
GLOO_API_KEY=YOUR_API_KEY
```
Or export them in your shell for Go and Java:
```bash theme={null}
export GLOO_API_KEY="your_actual_api_key_here"
```
```python Python theme={null}
import requests
import time
import json
import os
from datetime import datetime
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# --- Configuration ---
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ingestion/v1/real_time_upload"
PUBLISHER_ID = "b6ada63e-f1ce-4a16-80da-9864f58c5bd7" # Replace with your publisher ID
def upload_content(content_data):
"""Uploads content to the Realtime API."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
response = requests.post(API_URL, headers=headers, json=content_data)
response.raise_for_status()
return response.json()
def process_file(file_path):
"""Processes a single file and uploads its content."""
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Extract metadata from filename and content
filename = os.path.basename(file_path)
title = filename.replace('.txt', '').replace('.md', '').replace('_', ' ').title()
content_data = {
"content": content,
"publisherId": PUBLISHER_ID,
"item_title": title,
"author": ["Automated Ingestion"],
"publication_date": datetime.now().strftime("%Y-%m-%d"),
"type": "Article",
"pub_type": "technical",
"item_tags": ["automated", "ingestion"],
"evergreen": True,
"drm": ["aspen", "kallm"]
}
result = upload_content(content_data)
print(f"✅ Successfully uploaded: {title}")
print(f" Response: {result['message']}")
return True
except Exception as e:
print(f"❌ Failed to process {file_path}: {e}")
return False
class ContentHandler(FileSystemEventHandler):
"""Handles file system events for content monitoring."""
def on_created(self, event):
if not event.is_directory and event.src_path.endswith(('.txt', '.md')):
print(f"📄 New file detected: {event.src_path}")
time.sleep(1) # Allow file write to complete
process_file(event.src_path)
def start_file_watcher(watch_directory):
"""Starts monitoring a directory for new content files."""
if not os.path.exists(watch_directory):
os.makedirs(watch_directory)
print(f"Created watch directory: {watch_directory}")
event_handler = ContentHandler()
observer = Observer()
observer.schedule(event_handler, watch_directory, recursive=True)
print(f"🔍 Monitoring directory: {watch_directory}")
print(" Supported file types: .txt, .md")
print(" Press Ctrl+C to stop")
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
print("\n👋 Stopping file monitor...")
observer.join()
def batch_process_directory(directory_path):
"""Processes all supported files in a directory."""
if not os.path.exists(directory_path):
print(f"Directory does not exist: {directory_path}")
return
processed = 0
failed = 0
for filename in os.listdir(directory_path):
if filename.endswith(('.txt', '.md')):
file_path = os.path.join(directory_path, filename)
if process_file(file_path):
processed += 1
else:
failed += 1
# Rate limiting - avoid overwhelming the API
time.sleep(1)
print(f"\n📊 Batch processing complete:")
print(f" ✅ Processed: {processed} files")
print(f" ❌ Failed: {failed} files")
# --- Example Usage ---
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage:")
print(" python main.py watch # Monitor directory for new files")
print(" python main.py batch # Process all files in directory")
print(" python main.py single # Process single file")
sys.exit(1)
command = sys.argv[1]
if command == "watch" and len(sys.argv) > 2:
start_file_watcher(sys.argv[2])
elif command == "batch" and len(sys.argv) > 2:
batch_process_directory(sys.argv[2])
elif command == "single" and len(sys.argv) > 2:
process_file(sys.argv[2])
else:
print("Invalid command or missing arguments")
```
```javascript JavaScript theme={null}
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
require('dotenv').config();
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const API_URL = 'https://platform.ai.gloo.com/ingestion/v1/real_time_upload';
const PUBLISHER_ID = 'b6ada63e-f1ce-4a16-80da-9864f58c5bd7'; // Replace with your publisher ID
async function uploadContent(contentData) {
const response = await axios.post(API_URL, contentData, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
async function processFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const filename = path.basename(filePath);
const title = filename
.replace(/\.(txt|md)$/, '')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase());
const contentData = {
content: content,
publisherId: PUBLISHER_ID,
item_title: title,
author: ['Automated Ingestion'],
publication_date: new Date().toISOString().split('T')[0],
type: 'Article',
pub_type: 'technical',
item_tags: ['automated', 'ingestion'],
evergreen: true,
drm: ['aspen', 'kallm'],
};
const result = await uploadContent(contentData);
console.log(`✅ Successfully uploaded: ${title}`);
console.log(` Response: ${result.message}`);
return true;
} catch (error) {
console.error(
`❌ Failed to process ${filePath}:`,
error.response ? error.response.data : error.message,
);
return false;
}
}
function startFileWatcher(watchDirectory) {
if (!fs.existsSync(watchDirectory)) {
fs.mkdirSync(watchDirectory, { recursive: true });
console.log(`Created watch directory: ${watchDirectory}`);
}
console.log(`🔍 Monitoring directory: ${watchDirectory}`);
console.log(' Supported file types: .txt, .md');
console.log(' Press Ctrl+C to stop');
const watcher = chokidar.watch(watchDirectory, {
ignored: /^\./,
persistent: true,
});
watcher.on('add', async (filePath) => {
if (path.extname(filePath).match(/\.(txt|md)$/)) {
console.log(`📄 New file detected: ${filePath}`);
// Small delay to ensure file write is complete
setTimeout(() => processFile(filePath), 1000);
}
});
process.on('SIGINT', () => {
console.log('\n👋 Stopping file monitor...');
watcher.close();
process.exit(0);
});
}
async function batchProcessDirectory(directoryPath) {
if (!fs.existsSync(directoryPath)) {
console.log(`Directory does not exist: ${directoryPath}`);
return;
}
const files = fs.readdirSync(directoryPath);
const supportedFiles = files.filter((file) => file.match(/\.(txt|md)$/));
let processed = 0;
let failed = 0;
for (const filename of supportedFiles) {
const filePath = path.join(directoryPath, filename);
if (await processFile(filePath)) {
processed++;
} else {
failed++;
}
// Rate limiting - avoid overwhelming the API
await new Promise((resolve) => setTimeout(resolve, 1000));
}
console.log('\n📊 Batch processing complete:');
console.log(` ✅ Processed: ${processed} files`);
console.log(` ❌ Failed: ${failed} files`);
}
// --- Main Execution ---
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('Usage:');
console.log(
' node index.js watch # Monitor directory for new files',
);
console.log(
' node index.js batch # Process all files in directory',
);
console.log(' node index.js single # Process single file');
process.exit(1);
}
const command = args[0];
try {
switch (command) {
case 'watch':
if (args[1]) startFileWatcher(args[1]);
else console.log('Please specify a directory to watch');
break;
case 'batch':
if (args[1]) await batchProcessDirectory(args[1]);
else console.log('Please specify a directory to process');
break;
case 'single':
if (args[1]) await processFile(args[1]);
else console.log('Please specify a file to process');
break;
default:
console.log('Invalid command. Use watch, batch, or single');
}
} catch (error) {
console.error(
'An error occurred:',
error.response ? error.response.data : error.message,
);
}
}
main();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as fs from 'fs';
import * as path from 'path';
import chokidar from 'chokidar';
import * as dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Type definitions
interface ContentData {
content: string;
publisherId: string;
item_title: string;
item_subtitle?: string;
author: string[];
publication_date: string;
type: string;
pub_type: string;
item_tags: string[];
evergreen: boolean;
drm: string[];
item_summary?: string;
item_image?: string;
item_url?: string;
}
interface ApiResponse {
success: boolean;
message: string;
task_id: string | null;
batch_id: string | null;
processing_details: any | null;
}
// --- Configuration ---
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const API_URL = 'https://platform.ai.gloo.com/ingestion/v1/real_time_upload';
const PUBLISHER_ID = 'b6ada63e-f1ce-4a16-80da-9864f58c5bd7'; // Replace with your publisher ID
async function uploadContent(contentData: ContentData): Promise {
const response = await axios.post(API_URL, contentData, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
async function processFile(filePath: string): Promise {
try {
const content = fs.readFileSync(filePath, 'utf8');
const filename = path.basename(filePath);
const title = filename
.replace(/\.(txt|md)$/, '')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase());
const contentData: ContentData = {
content: content,
publisherId: PUBLISHER_ID,
item_title: title,
author: ['Automated Ingestion'],
publication_date: new Date().toISOString().split('T')[0],
type: 'Article',
pub_type: 'technical',
item_tags: ['automated', 'ingestion'],
evergreen: true,
drm: ['aspen', 'kallm'],
};
const result = await uploadContent(contentData);
console.log(`✅ Successfully uploaded: ${title}`);
console.log(` Response: ${result.message}`);
return true;
} catch (error: any) {
console.error(
`❌ Failed to process ${filePath}:`,
error.response ? error.response.data : error.message,
);
return false;
}
}
function startFileWatcher(watchDirectory: string): void {
if (!fs.existsSync(watchDirectory)) {
fs.mkdirSync(watchDirectory, { recursive: true });
console.log(`Created watch directory: ${watchDirectory}`);
}
console.log(`🔍 Monitoring directory: ${watchDirectory}`);
console.log(' Supported file types: .txt, .md');
console.log(' Press Ctrl+C to stop');
const watcher = chokidar.watch(watchDirectory, {
ignored: /^\./,
persistent: true,
});
watcher.on('add', async (filePath: string) => {
if (path.extname(filePath).match(/\.(txt|md)$/)) {
console.log(`📄 New file detected: ${filePath}`);
// Small delay to ensure file write is complete
setTimeout(() => processFile(filePath), 1000);
}
});
process.on('SIGINT', () => {
console.log('\n👋 Stopping file monitor...');
watcher.close();
process.exit(0);
});
}
async function batchProcessDirectory(directoryPath: string): Promise {
if (!fs.existsSync(directoryPath)) {
console.log(`Directory does not exist: ${directoryPath}`);
return;
}
const files = fs.readdirSync(directoryPath);
const supportedFiles = files.filter((file) => file.match(/\.(txt|md)$/));
let processed = 0;
let failed = 0;
for (const filename of supportedFiles) {
const filePath = path.join(directoryPath, filename);
if (await processFile(filePath)) {
processed++;
} else {
failed++;
}
// Rate limiting - avoid overwhelming the API
await new Promise((resolve) => setTimeout(resolve, 1000));
}
console.log('\n📊 Batch processing complete:');
console.log(` ✅ Processed: ${processed} files`);
console.log(` ❌ Failed: ${failed} files`);
}
// --- Main Execution ---
async function main(): Promise {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('Usage:');
console.log(
' npx ts-node index.ts watch # Monitor directory for new files',
);
console.log(
' npx ts-node index.ts batch # Process all files in directory',
);
console.log(
' npx ts-node index.ts single # Process single file',
);
process.exit(1);
}
const command = args[0];
try {
switch (command) {
case 'watch':
if (args[1]) startFileWatcher(args[1]);
else console.log('Please specify a directory to watch');
break;
case 'batch':
if (args[1]) await batchProcessDirectory(args[1]);
else console.log('Please specify a directory to process');
break;
case 'single':
if (args[1]) await processFile(args[1]);
else console.log('Please specify a file to process');
break;
default:
console.log('Invalid command. Use watch, batch, or single');
}
} catch (error: any) {
console.error(
'An error occurred:',
error.response ? error.response.data : error.message,
);
}
}
main();
```
```php PHP theme={null}
load();
// --- Configuration ---
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$API_URL = 'https://platform.ai.gloo.com/ingestion/v1/real_time_upload';
$PUBLISHER_ID = 'b6ada63e-f1ce-4a16-80da-9864f58c5bd7'; // Replace with your publisher ID
// Validate credentials
if ($API_KEY === 'YOUR_API_KEY' || empty($API_KEY)) {
echo "Error: GLOO_API_KEY must be set\n";
echo "Create a .env file with your credentials:\n";
echo "GLOO_API_KEY=your_api_key_here\n";
exit(1);
}
$httpClient = new Client();
function uploadContent($contentData, $apiUrl, $apiKey, $httpClient) {
try {
$response = $httpClient->post($apiUrl, [
'json' => $contentData,
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json'
]
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
throw new Exception("Upload failed: " . $e->getMessage());
}
}
function processFile($filePath, $publisherId, $apiUrl, $apiKey, $httpClient) {
try {
if (!file_exists($filePath)) {
throw new Exception("File does not exist: $filePath");
}
$content = file_get_contents($filePath);
$filename = pathinfo($filePath, PATHINFO_FILENAME);
$title = str_replace(['_', '-'], ' ', $filename);
$title = ucwords($title);
$contentData = [
'content' => $content,
'publisherId' => $publisherId,
'item_title' => $title,
'author' => ['Automated Ingestion'],
'publication_date' => date('Y-m-d'),
'type' => 'Article',
'pub_type' => 'technical',
'item_tags' => ['automated', 'ingestion'],
'evergreen' => true,
'drm' => ['aspen', 'kallm']
];
$result = uploadContent($contentData, $apiUrl, $apiKey, $httpClient);
echo "✅ Successfully uploaded: $title\n";
echo " Response: " . $result['message'] . "\n";
return true;
} catch (Exception $e) {
echo "❌ Failed to process $filePath: " . $e->getMessage() . "\n";
return false;
}
}
function watchDirectory($watchDirectory, $publisherId, $apiUrl, $apiKey, $httpClient) {
if (!is_dir($watchDirectory)) {
mkdir($watchDirectory, 0755, true);
echo "Created watch directory: $watchDirectory\n";
}
echo "🔍 Monitoring directory: $watchDirectory\n";
echo " Supported file types: .txt, .md\n";
echo " Press Ctrl+C to stop\n";
$processedFiles = [];
while (true) {
$files = glob($watchDirectory . '/*.{txt,md}', GLOB_BRACE);
foreach ($files as $file) {
$fileTime = filemtime($file);
if (!isset($processedFiles[$file]) || $processedFiles[$file] < $fileTime) {
echo "📄 New or updated file detected: $file\n";
sleep(1); // Allow file write to complete
if (processFile($file, $publisherId, $apiUrl, $apiKey, $httpClient)) {
$processedFiles[$file] = $fileTime;
}
}
}
sleep(2); // Check every 2 seconds
}
}
function batchProcessDirectory($directoryPath, $publisherId, $apiUrl, $apiKey, $httpClient) {
if (!is_dir($directoryPath)) {
echo "Directory does not exist: $directoryPath\n";
return;
}
$files = glob($directoryPath . '/*.{txt,md}', GLOB_BRACE);
$processed = 0;
$failed = 0;
foreach ($files as $file) {
if (processFile($file, $publisherId, $apiUrl, $apiKey, $httpClient)) {
$processed++;
} else {
$failed++;
}
// Rate limiting - avoid overwhelming the API
sleep(1);
}
echo "\n📊 Batch processing complete:\n";
echo " ✅ Processed: $processed files\n";
echo " ❌ Failed: $failed files\n";
}
// --- Main Execution ---
if ($argc < 2) {
echo "Usage:\n";
echo " php index.php watch # Monitor directory for new files\n";
echo " php index.php batch # Process all files in directory\n";
echo " php index.php single # Process single file\n";
exit(1);
}
$command = $argv[1];
try {
switch ($command) {
case 'watch':
if (isset($argv[2])) {
watchDirectory($argv[2], $PUBLISHER_ID, $API_URL, $API_KEY, $httpClient);
} else {
echo "Please specify a directory to watch\n";
}
break;
case 'batch':
if (isset($argv[2])) {
batchProcessDirectory($argv[2], $PUBLISHER_ID, $API_URL, $API_KEY, $httpClient);
} else {
echo "Please specify a directory to process\n";
}
break;
case 'single':
if (isset($argv[2])) {
processFile($argv[2], $PUBLISHER_ID, $API_URL, $API_KEY, $httpClient);
} else {
echo "Please specify a file to process\n";
}
break;
default:
echo "Invalid command. Use watch, batch, or single\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/joho/godotenv"
)
// --- Configuration ---
var (
apiKey string
apiURL = "https://platform.ai.gloo.com/ingestion/v1/real_time_upload"
publisherID = "b6ada63e-f1ce-4a16-80da-9864f58c5bd7" // Replace with your publisher ID
)
// --- Data Structures ---
type ContentData struct {
Content string `json:"content"`
PublisherID string `json:"publisherId"`
ItemTitle string `json:"item_title"`
Author []string `json:"author"`
PublicationDate string `json:"publication_date"`
Type string `json:"type"`
PubType string `json:"pub_type"`
ItemTags []string `json:"item_tags"`
Evergreen bool `json:"evergreen"`
DRM []string `json:"drm"`
}
type ApiResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
TaskID *string `json:"task_id"`
BatchID *string `json:"batch_id"`
ProcessingDetails interface{} `json:"processing_details"`
}
// --- Function Definitions ---
func uploadContent(contentData *ContentData) (*ApiResponse, error) {
jsonPayload, _ := json.Marshal(contentData)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+apiKey)
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body))
}
var result ApiResponse
json.Unmarshal(body, &result)
return &result, nil
}
func processFile(filePath string) bool {
content, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Printf("❌ Failed to read file %s: %v\n", filePath, err)
return false
}
filename := filepath.Base(filePath)
title := strings.TrimSuffix(filename, filepath.Ext(filename))
title = strings.ReplaceAll(title, "_", " ")
title = strings.ReplaceAll(title, "-", " ")
title = strings.Title(title)
contentData := &ContentData{
Content: string(content),
PublisherID: publisherID,
ItemTitle: title,
Author: []string{"Automated Ingestion"},
PublicationDate: time.Now().Format("2006-01-02"),
Type: "Article",
PubType: "technical",
ItemTags: []string{"automated", "ingestion"},
Evergreen: true,
DRM: []string{"aspen", "kallm"},
}
result, err := uploadContent(contentData)
if err != nil {
fmt.Printf("❌ Failed to process %s: %v\n", filePath, err)
return false
}
fmt.Printf("✅ Successfully uploaded: %s\n", title)
fmt.Printf(" Response: %s\n", result.Message)
return true
}
func watchDirectory(watchDir string) error {
// Create directory if it doesn't exist
if _, err := os.Stat(watchDir); os.IsNotExist(err) {
os.MkdirAll(watchDir, 0755)
fmt.Printf("Created watch directory: %s\n", watchDir)
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer watcher.Close()
fmt.Printf("🔍 Monitoring directory: %s\n", watchDir)
fmt.Println(" Supported file types: .txt, .md")
fmt.Println(" Press Ctrl+C to stop")
err = watcher.Add(watchDir)
if err != nil {
return err
}
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create {
ext := filepath.Ext(event.Name)
if ext == ".txt" || ext == ".md" {
fmt.Printf("📄 New file detected: %s\n", event.Name)
time.Sleep(1 * time.Second) // Allow file write to complete
processFile(event.Name)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
fmt.Printf("Error: %v\n", err)
}
}
}()
<-done
return nil
}
func batchProcessDirectory(dirPath string) {
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
fmt.Printf("Directory does not exist: %s\n", dirPath)
return
}
files, err := filepath.Glob(filepath.Join(dirPath, "*.txt"))
if err != nil {
fmt.Printf("Error reading directory: %v\n", err)
return
}
mdFiles, err := filepath.Glob(filepath.Join(dirPath, "*.md"))
if err != nil {
fmt.Printf("Error reading directory: %v\n", err)
return
}
files = append(files, mdFiles...)
processed := 0
failed := 0
for _, file := range files {
if processFile(file) {
processed++
} else {
failed++
}
// Rate limiting - avoid overwhelming the API
time.Sleep(1 * time.Second)
}
fmt.Printf("\n📊 Batch processing complete:\n")
fmt.Printf(" ✅ Processed: %d files\n", processed)
fmt.Printf(" ❌ Failed: %d files\n", failed)
}
// Helper to get environment variables
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
// Initialize loads environment variables and validates configuration
func init() {
// Load environment variables from .env file if it exists
_ = godotenv.Load()
// Get API key from environment
apiKey = getEnv("GLOO_API_KEY", "")
// Validate that the API key is provided
if apiKey == "" {
fmt.Println("Error: GLOO_API_KEY must be set")
fmt.Println("Either:")
fmt.Println("1. Create a .env file with your credentials:")
fmt.Println(" GLOO_API_KEY=your_api_key_here")
fmt.Println("2. Export it as an environment variable:")
fmt.Println(" export GLOO_API_KEY=\"your_api_key_here\"")
os.Exit(1)
}
}
// --- Main Execution ---
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage:")
fmt.Println(" go run main.go watch # Monitor directory for new files")
fmt.Println(" go run main.go batch # Process all files in directory")
fmt.Println(" go run main.go single # Process single file")
os.Exit(1)
}
command := os.Args[1]
switch command {
case "watch":
if len(os.Args) > 2 {
if err := watchDirectory(os.Args[2]); err != nil {
fmt.Printf("Error watching directory: %v\n", err)
}
} else {
fmt.Println("Please specify a directory to watch")
}
case "batch":
if len(os.Args) > 2 {
batchProcessDirectory(os.Args[2])
} else {
fmt.Println("Please specify a directory to process")
}
case "single":
if len(os.Args) > 2 {
processFile(os.Args[2])
} else {
fmt.Println("Please specify a file to process")
}
default:
fmt.Println("Invalid command. Use watch, batch, or single")
}
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.github.cdimascio.dotenv.Dotenv;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.*;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
// --- Main Application Class ---
public class Main {
public static void main(String[] args) {
if (!RealtimeIngestion.validateCredentials()) {
System.exit(1);
}
if (args.length < 1) {
System.out.println("Usage:");
System.out.println(" java Main watch # Monitor directory for new files");
System.out.println(" java Main batch # Process all files in directory");
System.out.println(" java Main single # Process single file");
System.exit(1);
}
RealtimeIngestion ingestion = new RealtimeIngestion();
String command = args[0];
try {
switch (command) {
case "watch":
if (args.length > 1) {
ingestion.watchDirectory(args[1]);
} else {
System.out.println("Please specify a directory to watch");
}
break;
case "batch":
if (args.length > 1) {
ingestion.batchProcessDirectory(args[1]);
} else {
System.out.println("Please specify a directory to process");
}
break;
case "single":
if (args.length > 1) {
ingestion.processFile(args[1]);
} else {
System.out.println("Please specify a file to process");
}
break;
default:
System.out.println("Invalid command. Use watch, batch, or single");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// --- Realtime Ingestion Class ---
class RealtimeIngestion {
// Load environment variables from .env file if it exists
private static final Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
private static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
private static final String API_URL = "https://platform.ai.gloo.com/ingestion/v1/real_time_upload";
private static final String PUBLISHER_ID = "b6ada63e-f1ce-4a16-80da-9864f58c5bd7"; // Replace with your publisher ID
private final HttpClient httpClient = HttpClient.newHttpClient();
private final Gson gson = new Gson();
// Validate that credentials are provided
public static boolean validateCredentials() {
if ("YOUR_API_KEY".equals(API_KEY) || API_KEY == null || API_KEY.trim().isEmpty()) {
System.err.println("Error: GLOO_API_KEY must be set");
System.err.println("Create a .env file with your credentials:");
System.err.println("GLOO_API_KEY=your_api_key_here");
return false;
}
return true;
}
// --- Data Classes ---
static class ContentData {
String content;
String publisherId;
String item_title;
String[] author;
String publication_date;
String type;
String pub_type;
String[] item_tags;
boolean evergreen;
String[] drm;
public ContentData(String content, String publisherId, String itemTitle) {
this.content = content;
this.publisherId = publisherId;
this.item_title = itemTitle;
this.author = new String[]{"Automated Ingestion"};
this.publication_date = LocalDate.now().toString();
this.type = "Article";
this.pub_type = "technical";
this.item_tags = new String[]{"automated", "ingestion"};
this.evergreen = true;
this.drm = new String[]{"aspen", "kallm"};
}
}
static class ApiResponse {
boolean success;
String message;
String task_id;
String batch_id;
Object processing_details;
}
// --- API Methods ---
public ApiResponse uploadContent(ContentData contentData) throws IOException, InterruptedException {
String jsonPayload = gson.toJson(contentData);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("API call failed: " + response.body());
}
return gson.fromJson(response.body(), ApiResponse.class);
}
public boolean processFile(String filePath) {
try {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
System.out.println("❌ File does not exist: " + filePath);
return false;
}
String content = Files.readString(path);
String filename = path.getFileName().toString();
String title = filename.replaceAll("\\.(txt|md)$", "")
.replaceAll("[_-]", " ")
.replaceAll("\\b\\w", m -> m.toUpperCase());
ContentData contentData = new ContentData(content, PUBLISHER_ID, title);
ApiResponse result = uploadContent(contentData);
System.out.println("✅ Successfully uploaded: " + title);
System.out.println(" Response: " + result.message);
return true;
} catch (Exception e) {
System.out.println("❌ Failed to process " + filePath + ": " + e.getMessage());
return false;
}
}
public void watchDirectory(String watchDir) throws IOException, InterruptedException {
Path dir = Paths.get(watchDir);
if (!Files.exists(dir)) {
Files.createDirectories(dir);
System.out.println("Created watch directory: " + watchDir);
}
System.out.println("🔍 Monitoring directory: " + watchDir);
System.out.println(" Supported file types: .txt, .md");
System.out.println(" Press Ctrl+C to stop");
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path createdFile = dir.resolve((Path) event.context());
String fileName = createdFile.toString();
if (fileName.endsWith(".txt") || fileName.endsWith(".md")) {
System.out.println("📄 New file detected: " + createdFile);
// Small delay to ensure file write is complete
Thread.sleep(1000);
processFile(createdFile.toString());
}
}
}
key.reset();
}
}
public void batchProcessDirectory(String dirPath) {
Path dir = Paths.get(dirPath);
if (!Files.exists(dir)) {
System.out.println("Directory does not exist: " + dirPath);
return;
}
int processed = 0;
int failed = 0;
try (Stream files = Files.walk(dir, 1)) {
List supportedFiles = files
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt") || p.toString().endsWith(".md"))
.toList();
for (Path file : supportedFiles) {
if (processFile(file.toString())) {
processed++;
} else {
failed++;
}
// Rate limiting - avoid overwhelming the API
Thread.sleep(1000);
}
} catch (Exception e) {
System.out.println("Error processing directory: " + e.getMessage());
}
System.out.println("\n📊 Batch processing complete:");
System.out.println(" ✅ Processed: " + processed + " files");
System.out.println(" ❌ Failed: " + failed + " files");
}
}
```
***
## Testing Your Implementation
To test any of the complete examples:
### 1. Environment Setup
Create a `.env` file with your Gloo AI credentials:
```
GLOO_API_KEY=your_api_key_here
```
### 2. Install Dependencies
Each language has specific setup requirements - check the README files in the sandbox examples.
### 3. Test Single File Upload
```bash theme={null}
# Create a test file
echo "This is a test article about automated content ingestion." > test_content.txt
# Upload the file
python main.py single test_content.txt
```
### 4. Test Directory Monitoring
```bash theme={null}
# Start monitoring a directory
python main.py watch ./watch_folder
# In another terminal, add files to the directory
echo "New content article" > ./watch_folder/new_article.txt
```
### 5. Test Batch Processing
```bash theme={null}
# Create multiple test files
mkdir batch_test
echo "Article 1 content" > batch_test/article1.txt
echo "Article 2 content" > batch_test/article2.md
# Process all files
python main.py batch ./batch_test
```
The system will:
* Authenticate using your API key
* Extract metadata from filenames and content
* Upload content with proper error handling
* Display success/failure status for each operation
* Provide structured JSON responses from the API
***
## Production Deployment Considerations
### Error Handling
* **Authentication**: API key validation with proper error reporting
* **Rate Limiting**: Built-in delays between API calls
* **Network Issues**: Retry logic with configurable timeouts
* **File Processing**: Validation and error recovery
### Monitoring
* **Logging**: Structured logs for all operations
* **Metrics**: Success rates, processing times, error counts
* **Alerting**: Failed upload notifications
* **Health Checks**: System status monitoring
### Scaling
* **Concurrent Processing**: Parallel file processing capabilities
* **Queue Management**: Async processing for high-volume scenarios
* **Resource Management**: Memory and CPU optimization
* **Load Balancing**: Multiple instance coordination
***
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript,
TypeScript, Python, PHP, Go, Java) with setup instructions.
## Next Steps
Now that you have a working content ingestion pipeline, consider exploring:
1. **[Search API](/api-guides/search)** - Query your ingested content
2. **[Chat Integration](/tutorials/chat)** - Use ingested content in conversations
3. **[Content Management](/studio/manage-content)** - Organize and categorize content
4. **Advanced Metadata** - Rich content classification and tagging
5. **[Custom Pipelines](/best-practices/ingestion-pattern)** - The source-agnostic pattern for integrating with a CMS, API, database, file library, or RSS feed
This tutorial provides the foundation for building sophisticated content ingestion workflows that can scale from simple file uploads to enterprise-grade automated pipelines.
# Building Custom Recommendations
Source: https://docs.gloo.com/tutorials/recommendations
Learn how to surface relevant resources using the Gloo AI Recommendations API.
This tutorial shows you how to build a smart resource recommender using the Gloo AI Recommendations API. You'll learn to surface publisher-scoped content recommendations, add snippet previews for richer displays, and discover resources from across the Gloo affiliate publisher network.
Unlike keyword search, the Recommendations API ranks content by semantic relevance to a user's query, making it ideal for "You might also like" panels, related-resource sections, and cross-publisher content discovery.
The Recommendations API returns **ranked content items** from your publisher's collection or the Gloo affiliate network. It is optimized for recommendation UI patterns, not open-ended search.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* Your Tenant (publisher) name from [Organizations](/studio/manage-organizations) in Studio
* Content uploaded to the Data Engine (see [Upload Files Tutorial](/tutorials/upload-files))
* **Authentication setup** — Complete the [Authentication Tutorial](/tutorials/authentication) first
Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java). Includes a proxy server and browser-based frontend for each language.
Setup and testing instructions are provided later.
The code snippets in this tutorial are **simplified and self-contained** —
designed for readability and easy copy-paste. The [cookbook
examples](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/recommendations)
use a modular architecture plus production niceties. Both implement the same
APIs and patterns.
***
## Understanding the Recommendations API
Three endpoints cover the main recommendation use cases:
| Endpoint | Purpose |
| - | - |
| `POST /ai/v1/data/items/recommendations/base` | Publisher-scoped ranked items — metadata only |
| `POST /ai/v1/data/items/recommendations/verbose` | Publisher-scoped ranked items — includes full snippet text |
| `POST /ai/v1/data/affiliates/referenced-items` | Cross-publisher discovery from the Gloo affiliate network |
### Publisher Endpoints (base & verbose)
Both publisher endpoints share the same required parameters:
| Parameter | Description |
| - | - |
| `query` | The topic or question to match content against |
| `item_count` | Number of items to return (1–50) |
| `certainty_threshold` | Minimum relevance score (0–1). Default: `0.75` |
| `collection` | Always `"GlooProd"` |
| `tenant` | Your publisher (tenant) name |
### Affiliate Endpoint
The affiliate endpoint searches across the full Gloo publisher network so no `collection` or `tenant` is required:
| Parameter | Description |
| - | - |
| `query` | The topic or question to match content against |
| `item_count` | Number of items to return (1–50) |
| `certainty_threshold` | Minimum relevance score. Default: `0.75` |
### Response Structure
Publisher endpoints return an array of items. Each item includes a `uuids` array with the matched snippet and its relevance metadata:
```json theme={null}
[
{
"item_id": "abc123",
"item_title": "Finding True Happiness",
"author": ["John Smith"],
"item_url": "https://example.com/finding-true-happiness",
"uuids": [
{
"uuid": "snippet-uuid",
"ai_title": "The Path to Contentment",
"ai_subtitle": "A practical guide",
"item_summary": "Explores the foundations of lasting happiness...",
"certainty": 0.89,
"snippet": "Full snippet text here (verbose endpoint only)"
}
]
}
]
```
Key fields:
* **`uuids[0].certainty`** — Relevance score (0–1, higher = more relevant)
* **`uuids[0].ai_title`** — AI-generated section title for the matched snippet
* **`uuids[0].item_summary`** — AI-generated summary of the item
* **`uuids[0].snippet`** — Full snippet text (**verbose endpoint only**)
The affiliate endpoint returns a flatter structure without `uuids`, adding `tradition` and `item_subtitle` fields instead.
***
## Step 1: Publisher Recommendations
Let's fetch ranked content from your publisher's collection. The base endpoint returns metadata only with no snippet text, making it ideal for clean list UIs.
```python Python theme={null}
import requests, os, sys
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
TENANT = os.getenv("GLOO_TENANT", "your-tenant-name")
COLLECTION = os.getenv("GLOO_COLLECTION", "GlooProd")
BASE_URL = "https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base"
def get_recommendations(query, item_count=5):
payload = {
"query": query,
"item_count": item_count,
"certainty_threshold": 0.75,
"collection": COLLECTION,
"tenant": TENANT,
}
r = requests.post(
BASE_URL,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json=payload,
timeout=60,
)
r.raise_for_status()
return r.json()
query = sys.argv[1] if len(sys.argv) > 1 else "How do I deal with anxiety?"
item_count = int(sys.argv[2]) if len(sys.argv) > 2 else 5
items = get_recommendations(query, item_count)
print(f"Found {len(items)} item(s):\n")
for i, item in enumerate(items, 1):
print(f"--- Item {i} ---")
print(f"Title: {item.get('item_title', 'N/A')}")
if item.get("author"):
print(f"Author: {', '.join(item['author'])}")
if item.get("uuids"):
top = item["uuids"][0]
print(f"Relevance: {top.get('certainty', 0):.0%}")
if top.get("ai_title"): print(f"Section: {top['ai_title']}")
if top.get("item_summary"): print(f"Summary: {top['item_summary']}")
print()
```
```javascript JavaScript theme={null}
const axios = require('axios');
require('dotenv').config();
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const COLLECTION = process.env.GLOO_COLLECTION || 'GlooProd';
const BASE_URL = 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base';
async function getRecommendations(query, itemCount = 5) {
const { data } = await axios.post(
BASE_URL,
{ query, item_count: itemCount, certainty_threshold: 0.75, collection: COLLECTION, tenant: TENANT },
{ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
);
return data;
}
(async () => {
const query = process.argv[2] || 'How do I deal with anxiety?';
const itemCount = parseInt(process.argv[3]) || 5;
const items = await getRecommendations(query, itemCount);
console.log(`Found ${items.length} item(s):\n`);
items.forEach((item, i) => {
console.log(`--- Item ${i + 1} ---`);
console.log(`Title: ${item.item_title || 'N/A'}`);
if (item.author?.length) console.log(`Author: ${item.author.join(', ')}`);
const top = item.uuids?.[0];
if (top) {
console.log(`Relevance: ${Math.round(top.certainty * 100)}%`);
if (top.ai_title) console.log(`Section: ${top.ai_title}`);
if (top.item_summary) console.log(`Summary: ${top.item_summary}`);
}
console.log();
});
})();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as dotenv from 'dotenv';
dotenv.config();
const API_KEY = process.env.GLOO_API_KEY ?? 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT ?? 'your-tenant-name';
const COLLECTION = process.env.GLOO_COLLECTION ?? 'GlooProd';
const BASE_URL = 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base';
interface SnippetUUID {
ai_title: string; item_summary: string; certainty: number;
}
interface RecommendationItem {
item_title: string; author: string[]; item_url: string; uuids: SnippetUUID[];
}
async function getRecommendations(query: string, itemCount = 5): Promise {
const { data } = await axios.post(
BASE_URL,
{ query, item_count: itemCount, certainty_threshold: 0.75, collection: COLLECTION, tenant: TENANT },
{ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
);
return data;
}
(async () => {
const query = process.argv[2] ?? 'How do I deal with anxiety?';
const itemCount = parseInt(process.argv[3] ?? '5');
const items = await getRecommendations(query, itemCount);
console.log(`Found ${items.length} item(s):\n`);
items.forEach((item, i) => {
console.log(`--- Item ${i + 1} ---`);
console.log(`Title: ${item.item_title ?? 'N/A'}`);
if (item.author?.length) console.log(`Author: ${item.author.join(', ')}`);
const top = item.uuids?.[0];
if (top) {
console.log(`Relevance: ${Math.round(top.certainty * 100)}%`);
if (top.ai_title) console.log(`Section: ${top.ai_title}`);
if (top.item_summary) console.log(`Summary: ${top.item_summary}`);
}
console.log();
});
})();
```
```php PHP theme={null}
safeLoad();
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$TENANT = $_ENV['GLOO_TENANT'] ?? 'your-tenant-name';
$COLLECTION = $_ENV['GLOO_COLLECTION'] ?? 'GlooProd';
$BASE_URL = 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base';
function getRecommendations(string $query, int $itemCount = 5): array {
global $COLLECTION, $TENANT, $BASE_URL, $API_KEY;
$ch = curl_init($BASE_URL);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'query' => $query, 'item_count' => $itemCount,
'certainty_threshold' => 0.75,
'collection' => $COLLECTION, 'tenant' => $TENANT,
]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $API_KEY,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 60,
]);
$result = curl_exec($ch); curl_close($ch);
return json_decode($result, true);
}
$query = $argv[1] ?? 'How do I deal with anxiety?';
$itemCount = (int)($argv[2] ?? 5);
$items = getRecommendations($query, $itemCount);
echo "Found " . count($items) . " item(s):\n\n";
foreach ($items as $i => $item) {
echo "--- Item " . ($i + 1) . " ---\n";
echo "Title: " . ($item['item_title'] ?? 'N/A') . "\n";
if (!empty($item['author'])) echo "Author: " . implode(', ', $item['author']) . "\n";
if (!empty($item['uuids'])) {
$top = $item['uuids'][0];
echo "Relevance: " . round($top['certainty'] * 100) . "%\n";
if (!empty($top['ai_title'])) echo "Section: " . $top['ai_title'] . "\n";
if (!empty($top['item_summary'])) echo "Summary: " . $top['item_summary'] . "\n";
}
echo "\n";
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
var (
apiKey, tenant, collection string
baseURL = "https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base"
)
type RecommendationsRequest struct {
Query string `json:"query"`
ItemCount int `json:"item_count"`
CertaintyThreshold float64 `json:"certainty_threshold"`
Collection string `json:"collection"`
Tenant string `json:"tenant"`
}
type SnippetUUID struct {
AITitle string `json:"ai_title"`
ItemSummary string `json:"item_summary"`
Certainty float64 `json:"certainty"`
}
type RecommendationItem struct {
ItemTitle string `json:"item_title"`
Author []string `json:"author"`
ItemURL string `json:"item_url"`
UUIDs []SnippetUUID `json:"uuids"`
}
func getRecommendations(query string, itemCount int) []RecommendationItem {
payload, _ := json.Marshal(RecommendationsRequest{
Query: query, ItemCount: itemCount, CertaintyThreshold: 0.75,
Collection: collection, Tenant: tenant,
})
req, _ := http.NewRequest("POST", baseURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var items []RecommendationItem
json.Unmarshal(body, &items)
return items
}
func main() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
tenant = os.Getenv("GLOO_TENANT")
collection = os.Getenv("GLOO_COLLECTION")
if collection == "" { collection = "GlooProd" }
query := "How do I deal with anxiety?"
itemCount := 5
if len(os.Args) > 1 { query = os.Args[1] }
if len(os.Args) > 2 { itemCount, _ = strconv.Atoi(os.Args[2]) }
items := getRecommendations(query, itemCount)
fmt.Printf("Found %d item(s):\n\n", len(items))
for i, item := range items {
fmt.Printf("--- Item %d ---\n", i+1)
fmt.Printf("Title: %s\n", item.ItemTitle)
if len(item.Author) > 0 {
fmt.Printf("Author: %s\n", strings.Join(item.Author, ", "))
}
if len(item.UUIDs) > 0 {
top := item.UUIDs[0]
fmt.Printf("Relevance: %.0f%%\n", top.Certainty*100)
if top.AITitle != "" { fmt.Printf("Section: %s\n", top.AITitle) }
if top.ItemSummary != "" { fmt.Printf("Summary: %s\n", top.ItemSummary) }
}
fmt.Println()
}
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.List;
public class Recommendations {
static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
static final String TENANT = dotenv.get("GLOO_TENANT", "your-tenant-name");
static final String COLLECTION = dotenv.get("GLOO_COLLECTION", "GlooProd");
static final String BASE_URL = "https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base";
static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)).build();
static final Gson gson = new Gson();
static class Req { String query; int item_count; double certainty_threshold;
String collection; String tenant; }
static class UUID { String ai_title; String item_summary; double certainty; }
static class Item { String item_title; List author; List uuids; }
static List- getRecommendations(String query, int itemCount) throws Exception {
Req payload = new Req();
payload.query = query; payload.item_count = itemCount;
payload.certainty_threshold = 0.75;
payload.collection = COLLECTION; payload.tenant = TENANT;
HttpResponse resp = http.send(
HttpRequest.newBuilder().uri(URI.create(BASE_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.timeout(Duration.ofSeconds(60)).build(),
HttpResponse.BodyHandlers.ofString());
return gson.fromJson(resp.body(), new TypeToken
>() {}.getType());
}
public static void main(String[] args) throws Exception {
String query = args.length > 0 ? args[0] : "How do I deal with anxiety?";
int itemCount = args.length > 1 ? Integer.parseInt(args[1]) : 5;
List- items = getRecommendations(query, itemCount);
System.out.printf("Found %d item(s):%n%n", items.size());
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
System.out.printf("--- Item %d ---%n", i + 1);
System.out.printf("Title: %s%n", item.item_title != null ? item.item_title : "N/A");
if (item.author != null && !item.author.isEmpty())
System.out.printf("Author: %s%n", String.join(", ", item.author));
if (item.uuids != null && !item.uuids.isEmpty()) {
UUID top = item.uuids.get(0);
System.out.printf("Relevance: %.0f%%%n", top.certainty * 100);
if (top.ai_title != null) System.out.printf("Section: %s%n", top.ai_title);
if (top.item_summary != null) System.out.printf("Summary: %s%n", top.item_summary);
}
System.out.println();
}
}
}
```
### What You'll See
A successful call returns up to 5 items with metadata such as the title, summary, and relevance score:
```
Found 3 item(s):
--- Item 1 ---
Title: Finding True Happiness
Author: John Smith
Relevance: 89%
Section: The Path to Contentment
Summary: This article explores the foundations of lasting happiness from a biblical perspective.
--- Item 2 ---
Title: Overcoming Anxiety and Fear
Author: Jane Doe
Relevance: 84%
Section: Trusting God in Difficult Times
Summary: Practical guidance on releasing anxiety through prayer and scripture.
--- Item 3 ---
Title: Peace That Passes Understanding
Author: John Smith
Relevance: 81%
Section: Philippians 4 Applied
Summary: A study of Philippians 4:6-7 and its application to modern anxiety.
```
Fields like `Summary` and `Section` may not appear for every item — they are AI-generated metadata that depends on how content was processed during ingestion. Older or minimally processed content may return only `Title`, `Author`, and `Relevance`.
### Run the Cookbook Example
The cookbook includes a ready-to-run basic search script for each language:
```bash Python theme={null}
cd recommendations/python
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python recommend_base.py "How do I deal with anxiety?"
python recommend_base.py "parenting teenagers" 3
```
```bash JavaScript theme={null}
cd recommendations/javascript
npm install
node recommend-base.js "How do I deal with anxiety?"
node recommend-base.js "parenting teenagers" 3
```
```bash TypeScript theme={null}
cd recommendations/typescript
npm install
npx ts-node recommend-base.ts "How do I deal with anxiety?"
npx ts-node recommend-base.ts "parenting teenagers" 3
```
```bash PHP theme={null}
cd recommendations/php
composer install
php recommend_base.php "How do I deal with anxiety?"
php recommend_base.php "parenting teenagers" 3
```
```bash Go theme={null}
cd recommendations/go
go mod tidy
go run . base "How do I deal with anxiety?"
go run . base "parenting teenagers" 3
```
```bash Java theme={null}
cd recommendations/java
mvn compile -q
mvn -q exec:java -Dexec.args='base "How do I deal with anxiety?"'
mvn -q exec:java -Dexec.args='base "parenting teenagers" 3'
```
**Getting no results?** Work through these in order:
1. Test your query in the [Search Playground](/studio/playground-search) — if nothing shows there, your content may not be indexed yet.
2. Try a different query that you know returns results in the Playground.
3. If the Playground returns results but your code doesn't, lower `certainty_threshold` from `0.75` to `0.5` to match the Playground's default.
***
## Step 2: Recommendations with Snippet Previews
The verbose endpoint adds full snippet text to each result. This is ideal for content preview cards where you want to show a passage alongside the title.
The only change from Step 1 is the endpoint URL and reading `uuids[0].snippet` from the response.
```python Python theme={null}
import requests, os, sys
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
TENANT = os.getenv("GLOO_TENANT", "your-tenant-name")
COLLECTION = os.getenv("GLOO_COLLECTION", "GlooProd")
VERBOSE_URL = "https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose"
def get_verbose(query, item_count=5):
r = requests.post(
VERBOSE_URL,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={
"query": query, "item_count": item_count, "certainty_threshold": 0.75,
"collection": COLLECTION, "tenant": TENANT,
},
timeout=60,
)
r.raise_for_status()
return r.json()
query = sys.argv[1] if len(sys.argv) > 1 else "How do I deal with anxiety?"
item_count = int(sys.argv[2]) if len(sys.argv) > 2 else 5
items = get_verbose(query, item_count)
print(f"Found {len(items)} item(s):\n")
for i, item in enumerate(items, 1):
print(f"--- Item {i} ---")
print(f"Title: {item.get('item_title', 'N/A')}")
if item.get("author"):
print(f"Author: {', '.join(item['author'])}")
if item.get("uuids"):
top = item["uuids"][0]
print(f"Relevance: {top.get('certainty', 0):.0%}")
if top.get("ai_title"): print(f"Section: {top['ai_title']}")
if top.get("item_summary"): print(f"Summary: {top['item_summary']}")
snippet = top.get("snippet", "")
if snippet:
print(f"Preview: \"{snippet[:200]}{'...' if len(snippet) > 200 else ''}\"")
print()
```
```javascript JavaScript theme={null}
const axios = require('axios');
require('dotenv').config();
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const COLLECTION = process.env.GLOO_COLLECTION || 'GlooProd';
const VERBOSE_URL = 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose';
async function getVerbose(query, itemCount = 5) {
const { data } = await axios.post(
VERBOSE_URL,
{ query, item_count: itemCount, certainty_threshold: 0.75, collection: COLLECTION, tenant: TENANT },
{ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
);
return data;
}
(async () => {
const query = process.argv[2] || 'How do I deal with anxiety?';
const itemCount = parseInt(process.argv[3]) || 5;
const items = await getVerbose(query, itemCount);
console.log(`Found ${items.length} item(s):\n`);
items.forEach((item, i) => {
console.log(`--- Item ${i + 1} ---`);
console.log(`Title: ${item.item_title || 'N/A'}`);
if (item.author?.length) console.log(`Author: ${item.author.join(', ')}`);
const top = item.uuids?.[0];
if (top) {
console.log(`Relevance: ${Math.round(top.certainty * 100)}%`);
if (top.ai_title) console.log(`Section: ${top.ai_title}`);
if (top.item_summary) console.log(`Summary: ${top.item_summary}`);
if (top.snippet) {
const preview = top.snippet.length > 200 ? top.snippet.slice(0, 200) + '...' : top.snippet;
console.log(`Preview: "${preview}"`);
}
}
console.log();
});
})();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as dotenv from 'dotenv';
dotenv.config();
const API_KEY = process.env.GLOO_API_KEY ?? 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT ?? 'your-tenant-name';
const COLLECTION = process.env.GLOO_COLLECTION ?? 'GlooProd';
const VERBOSE_URL = 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose';
interface SnippetUUID {
ai_title: string; item_summary: string; certainty: number; snippet?: string;
}
interface RecommendationItem {
item_title: string; author: string[]; uuids: SnippetUUID[];
}
async function getVerbose(query: string, itemCount = 5): Promise {
const { data } = await axios.post(
VERBOSE_URL,
{ query, item_count: itemCount, certainty_threshold: 0.75, collection: COLLECTION, tenant: TENANT },
{ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
);
return data;
}
(async () => {
const query = process.argv[2] ?? 'How do I deal with anxiety?';
const itemCount = parseInt(process.argv[3] ?? '5');
const items = await getVerbose(query, itemCount);
console.log(`Found ${items.length} item(s):\n`);
items.forEach((item, i) => {
console.log(`--- Item ${i + 1} ---`);
console.log(`Title: ${item.item_title ?? 'N/A'}`);
if (item.author?.length) console.log(`Author: ${item.author.join(', ')}`);
const top = item.uuids?.[0];
if (top) {
console.log(`Relevance: ${Math.round(top.certainty * 100)}%`);
if (top.ai_title) console.log(`Section: ${top.ai_title}`);
if (top.item_summary) console.log(`Summary: ${top.item_summary}`);
if (top.snippet) {
const preview = top.snippet.length > 200 ? top.snippet.slice(0, 200) + '...' : top.snippet;
console.log(`Preview: "${preview}"`);
}
}
console.log();
});
})();
```
```php PHP theme={null}
safeLoad();
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$TENANT = $_ENV['GLOO_TENANT'] ?? 'your-tenant-name';
$COLLECTION = $_ENV['GLOO_COLLECTION'] ?? 'GlooProd';
$VERBOSE_URL = 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose';
function getVerbose(string $query, int $itemCount = 5): array {
global $COLLECTION, $TENANT, $VERBOSE_URL, $API_KEY;
$ch = curl_init($VERBOSE_URL);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'query' => $query, 'item_count' => $itemCount,
'certainty_threshold' => 0.75,
'collection' => $COLLECTION, 'tenant' => $TENANT,
]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $API_KEY,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 60,
]);
$result = curl_exec($ch); curl_close($ch);
return json_decode($result, true);
}
$query = $argv[1] ?? 'How do I deal with anxiety?';
$itemCount = (int)($argv[2] ?? 5);
$items = getVerbose($query, $itemCount);
echo "Found " . count($items) . " item(s):\n\n";
foreach ($items as $i => $item) {
echo "--- Item " . ($i + 1) . " ---\n";
echo "Title: " . ($item['item_title'] ?? 'N/A') . "\n";
if (!empty($item['author'])) echo "Author: " . implode(', ', $item['author']) . "\n";
if (!empty($item['uuids'])) {
$top = $item['uuids'][0];
echo "Relevance: " . round($top['certainty'] * 100) . "%\n";
if (!empty($top['ai_title'])) echo "Section: " . $top['ai_title'] . "\n";
if (!empty($top['item_summary'])) echo "Summary: " . $top['item_summary'] . "\n";
if (!empty($top['snippet'])) {
$preview = mb_substr($top['snippet'], 0, 200);
if (mb_strlen($top['snippet']) > 200) $preview .= '...';
echo "Preview: \"$preview\"\n";
}
}
echo "\n";
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
var (
apiKey, tenant, collection string
verboseURL = "https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose"
)
type RecommendationsRequest struct {
Query string `json:"query"`
ItemCount int `json:"item_count"`
CertaintyThreshold float64 `json:"certainty_threshold"`
Collection string `json:"collection"`
Tenant string `json:"tenant"`
}
type SnippetUUID struct {
AITitle string `json:"ai_title"`
ItemSummary string `json:"item_summary"`
Certainty float64 `json:"certainty"`
Snippet string `json:"snippet"`
}
type RecommendationItem struct {
ItemTitle string `json:"item_title"`
Author []string `json:"author"`
UUIDs []SnippetUUID `json:"uuids"`
}
func getVerbose(query string, itemCount int) []RecommendationItem {
payload, _ := json.Marshal(RecommendationsRequest{
Query: query, ItemCount: itemCount, CertaintyThreshold: 0.75,
Collection: collection, Tenant: tenant,
})
req, _ := http.NewRequest("POST", verboseURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var items []RecommendationItem
json.Unmarshal(body, &items)
return items
}
func main() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
tenant = os.Getenv("GLOO_TENANT")
collection = os.Getenv("GLOO_COLLECTION")
if collection == "" { collection = "GlooProd" }
query := "How do I deal with anxiety?"
itemCount := 5
if len(os.Args) > 1 { query = os.Args[1] }
if len(os.Args) > 2 { itemCount, _ = strconv.Atoi(os.Args[2]) }
items := getVerbose(query, itemCount)
fmt.Printf("Found %d item(s):\n\n", len(items))
for i, item := range items {
fmt.Printf("--- Item %d ---\n", i+1)
fmt.Printf("Title: %s\n", item.ItemTitle)
if len(item.Author) > 0 {
fmt.Printf("Author: %s\n", strings.Join(item.Author, ", "))
}
if len(item.UUIDs) > 0 {
top := item.UUIDs[0]
fmt.Printf("Relevance: %.0f%%\n", top.Certainty*100)
if top.AITitle != "" { fmt.Printf("Section: %s\n", top.AITitle) }
if top.ItemSummary != "" { fmt.Printf("Summary: %s\n", top.ItemSummary) }
if top.Snippet != "" {
preview := top.Snippet
if len(preview) > 200 { preview = preview[:200] + "..." }
fmt.Printf("Preview: %q\n", preview)
}
}
fmt.Println()
}
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.List;
public class RecommendationsVerbose {
static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
static final String TENANT = dotenv.get("GLOO_TENANT", "your-tenant-name");
static final String COLLECTION = dotenv.get("GLOO_COLLECTION", "GlooProd");
static final String VERBOSE_URL = "https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose";
static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)).build();
static final Gson gson = new Gson();
static class Req { String query; int item_count; double certainty_threshold;
String collection; String tenant; }
static class UUID { String ai_title; String item_summary; double certainty; String snippet; }
static class Item { String item_title; List author; List uuids; }
static List- getVerbose(String query, int itemCount) throws Exception {
Req payload = new Req();
payload.query = query; payload.item_count = itemCount;
payload.certainty_threshold = 0.75;
payload.collection = COLLECTION; payload.tenant = TENANT;
HttpResponse resp = http.send(
HttpRequest.newBuilder().uri(URI.create(VERBOSE_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.timeout(Duration.ofSeconds(60)).build(),
HttpResponse.BodyHandlers.ofString());
return gson.fromJson(resp.body(), new TypeToken
>() {}.getType());
}
public static void main(String[] args) throws Exception {
String query = args.length > 0 ? args[0] : "How do I deal with anxiety?";
int itemCount = args.length > 1 ? Integer.parseInt(args[1]) : 5;
List- items = getVerbose(query, itemCount);
System.out.printf("Found %d item(s):%n%n", items.size());
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
System.out.printf("--- Item %d ---%n", i + 1);
System.out.printf("Title: %s%n", item.item_title != null ? item.item_title : "N/A");
if (item.author != null && !item.author.isEmpty())
System.out.printf("Author: %s%n", String.join(", ", item.author));
if (item.uuids != null && !item.uuids.isEmpty()) {
UUID top = item.uuids.get(0);
System.out.printf("Relevance: %.0f%%%n", top.certainty * 100);
if (top.ai_title != null) System.out.printf("Section: %s%n", top.ai_title);
if (top.item_summary != null) System.out.printf("Summary: %s%n", top.item_summary);
if (top.snippet != null && !top.snippet.isEmpty()) {
String preview = top.snippet.length() > 200
? top.snippet.substring(0, 200) + "..." : top.snippet;
System.out.printf("Preview: \"%s\"%n", preview);
}
}
System.out.println();
}
}
}
```
### What You'll See
Same as Step 1, with a `Preview` line added for each item:
```
--- Item 1 ---
Title: Finding True Happiness
Author: John Smith
Relevance: 89%
Section: The Path to Contentment
Summary: This article explores the foundations of lasting happiness from a biblical perspective.
Preview: "True happiness is not found in circumstances but in relationship. The Beatitudes
describe a person who is blessed not because of what they have, but because of who..."
```
### Run the Cookbook Example
```bash Python theme={null}
python recommend_verbose.py "How do I deal with anxiety?"
python recommend_verbose.py "parenting teenagers" 3
```
```bash JavaScript theme={null}
node recommend-verbose.js "How do I deal with anxiety?"
node recommend-verbose.js "parenting teenagers" 3
```
```bash TypeScript theme={null}
npx ts-node recommend-verbose.ts "How do I deal with anxiety?"
npx ts-node recommend-verbose.ts "parenting teenagers" 3
```
```bash PHP theme={null}
php recommend_verbose.php "How do I deal with anxiety?"
php recommend_verbose.php "parenting teenagers" 3
```
```bash Go theme={null}
go run . verbose "How do I deal with anxiety?"
go run . verbose "parenting teenagers" 3
```
```bash Java theme={null}
mvn -q exec:java -Dexec.args='verbose "How do I deal with anxiety?"'
mvn -q exec:java -Dexec.args='verbose "parenting teenagers" 3'
```
***
## Step 3: Affiliate Network Discovery
The affiliate endpoint surfaces content from across the entire Gloo publisher network and not just your own collection. Use this to power "Explore More" sections that introduce users to resources from other publishers.
Two key differences from the publisher endpoints:
* **No `collection` or `tenant`** in the request — the search spans the full affiliate network
* **Flat response structure** — items have `tradition` and `item_subtitle` fields instead of `uuids`
```python Python theme={null}
import requests, os, sys
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
AFFILIATES_URL = "https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items"
def get_affiliates(query, item_count=5):
r = requests.post(
AFFILIATES_URL,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={"query": query, "item_count": item_count, "certainty_threshold": 0.75},
timeout=60,
)
r.raise_for_status()
return r.json()
query = sys.argv[1] if len(sys.argv) > 1 else "How do I deal with anxiety?"
item_count = int(sys.argv[2]) if len(sys.argv) > 2 else 5
items = get_affiliates(query, item_count)
print(f"Found {len(items)} item(s) from across the affiliate network:\n")
for i, item in enumerate(items, 1):
print(f"--- Item {i} ---")
print(f"Title: {item.get('item_title', 'N/A')}")
if item.get("author"):
print(f"Author: {', '.join(item['author'])}")
if item.get("tradition"):
print(f"Tradition: {item['tradition']}")
if item.get("item_subtitle"):
print(f"Subtitle: {item['item_subtitle']}")
if item.get("item_url"):
print(f"URL: {item['item_url']}")
print()
```
```javascript JavaScript theme={null}
const axios = require('axios');
require('dotenv').config();
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const AFFILIATES_URL = 'https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items';
async function getAffiliates(query, itemCount = 5) {
const { data } = await axios.post(
AFFILIATES_URL,
{ query, item_count: itemCount, certainty_threshold: 0.75 },
{ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
);
return data;
}
(async () => {
const query = process.argv[2] || 'How do I deal with anxiety?';
const itemCount = parseInt(process.argv[3]) || 5;
const items = await getAffiliates(query, itemCount);
console.log(`Found ${items.length} item(s) from across the affiliate network:\n`);
items.forEach((item, i) => {
console.log(`--- Item ${i + 1} ---`);
console.log(`Title: ${item.item_title || 'N/A'}`);
if (item.author?.length) console.log(`Author: ${item.author.join(', ')}`);
if (item.tradition) console.log(`Tradition: ${item.tradition}`);
if (item.item_subtitle) console.log(`Subtitle: ${item.item_subtitle}`);
if (item.item_url) console.log(`URL: ${item.item_url}`);
console.log();
});
})();
```
```typescript TypeScript theme={null}
import axios from 'axios';
import * as dotenv from 'dotenv';
dotenv.config();
const API_KEY = process.env.GLOO_API_KEY ?? 'YOUR_API_KEY';
const AFFILIATES_URL = 'https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items';
interface AffiliateItem {
item_title: string; item_subtitle?: string; author: string[];
tradition?: string; item_url?: string;
}
async function getAffiliates(query: string, itemCount = 5): Promise {
const { data } = await axios.post(
AFFILIATES_URL,
{ query, item_count: itemCount, certainty_threshold: 0.75 },
{ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }
);
return data;
}
(async () => {
const query = process.argv[2] ?? 'How do I deal with anxiety?';
const itemCount = parseInt(process.argv[3] ?? '5');
const items = await getAffiliates(query, itemCount);
console.log(`Found ${items.length} item(s) from across the affiliate network:\n`);
items.forEach((item, i) => {
console.log(`--- Item ${i + 1} ---`);
console.log(`Title: ${item.item_title ?? 'N/A'}`);
if (item.author?.length) console.log(`Author: ${item.author.join(', ')}`);
if (item.tradition) console.log(`Tradition: ${item.tradition}`);
if (item.item_subtitle) console.log(`Subtitle: ${item.item_subtitle}`);
if (item.item_url) console.log(`URL: ${item.item_url}`);
console.log();
});
})();
```
```php PHP theme={null}
safeLoad();
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$AFFILIATES_URL = 'https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items';
function getAffiliates(string $query, int $itemCount = 5): array {
global $AFFILIATES_URL, $API_KEY;
$ch = curl_init($AFFILIATES_URL);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'query' => $query, 'item_count' => $itemCount,
'certainty_threshold' => 0.75,
]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $API_KEY,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 60,
]);
$result = curl_exec($ch); curl_close($ch);
return json_decode($result, true);
}
$query = $argv[1] ?? 'How do I deal with anxiety?';
$itemCount = (int)($argv[2] ?? 5);
$items = getAffiliates($query, $itemCount);
echo "Found " . count($items) . " item(s) from across the affiliate network:\n\n";
foreach ($items as $i => $item) {
echo "--- Item " . ($i + 1) . " ---\n";
echo "Title: " . ($item['item_title'] ?? 'N/A') . "\n";
if (!empty($item['author'])) echo "Author: " . implode(', ', $item['author']) . "\n";
if (!empty($item['tradition'])) echo "Tradition: " . $item['tradition'] . "\n";
if (!empty($item['item_subtitle'])) echo "Subtitle: " . $item['item_subtitle'] . "\n";
if (!empty($item['item_url'])) echo "URL: " . $item['item_url'] . "\n";
echo "\n";
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
var (
apiKey string
affiliatesURL = "https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items"
)
type AffiliatesRequest struct {
Query string `json:"query"`
ItemCount int `json:"item_count"`
CertaintyThreshold float64 `json:"certainty_threshold"`
}
type AffiliateItem struct {
ItemTitle string `json:"item_title"`
ItemSubtitle string `json:"item_subtitle"`
Author []string `json:"author"`
Tradition string `json:"tradition"`
ItemURL string `json:"item_url"`
}
func getAffiliates(query string, itemCount int) []AffiliateItem {
payload, _ := json.Marshal(AffiliatesRequest{
Query: query, ItemCount: itemCount, CertaintyThreshold: 0.75,
})
req, _ := http.NewRequest("POST", affiliatesURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var items []AffiliateItem
json.Unmarshal(body, &items)
return items
}
func main() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
query := "How do I deal with anxiety?"
itemCount := 5
if len(os.Args) > 1 { query = os.Args[1] }
if len(os.Args) > 2 { itemCount, _ = strconv.Atoi(os.Args[2]) }
items := getAffiliates(query, itemCount)
fmt.Printf("Found %d item(s) from across the affiliate network:\n\n", len(items))
for i, item := range items {
fmt.Printf("--- Item %d ---\n", i+1)
fmt.Printf("Title: %s\n", item.ItemTitle)
if len(item.Author) > 0 { fmt.Printf("Author: %s\n", strings.Join(item.Author, ", ")) }
if item.Tradition != "" { fmt.Printf("Tradition: %s\n", item.Tradition) }
if item.ItemSubtitle != "" { fmt.Printf("Subtitle: %s\n", item.ItemSubtitle) }
if item.ItemURL != "" { fmt.Printf("URL: %s\n", item.ItemURL) }
fmt.Println()
}
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.List;
public class RecommendationsAffiliates {
static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
static final String AFFILIATES_URL = "https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items";
static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)).build();
static final Gson gson = new Gson();
static class Req { String query; int item_count; double certainty_threshold; }
static class Item {
String item_title; String item_subtitle; List author;
String tradition; String item_url;
}
static List- getAffiliates(String query, int itemCount) throws Exception {
Req payload = new Req();
payload.query = query; payload.item_count = itemCount;
payload.certainty_threshold = 0.75;
HttpResponse resp = http.send(
HttpRequest.newBuilder().uri(URI.create(AFFILIATES_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.timeout(Duration.ofSeconds(60)).build(),
HttpResponse.BodyHandlers.ofString());
return gson.fromJson(resp.body(), new TypeToken
>() {}.getType());
}
public static void main(String[] args) throws Exception {
String query = args.length > 0 ? args[0] : "How do I deal with anxiety?";
int itemCount = args.length > 1 ? Integer.parseInt(args[1]) : 5;
List- items = getAffiliates(query, itemCount);
System.out.printf("Found %d item(s) from across the affiliate network:%n%n", items.size());
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
System.out.printf("--- Item %d ---%n", i + 1);
System.out.printf("Title: %s%n", item.item_title != null ? item.item_title : "N/A");
if (item.author != null && !item.author.isEmpty())
System.out.printf("Author: %s%n", String.join(", ", item.author));
if (item.tradition != null && !item.tradition.isEmpty())
System.out.printf("Tradition: %s%n", item.tradition);
if (item.item_subtitle != null && !item.item_subtitle.isEmpty())
System.out.printf("Subtitle: %s%n", item.item_subtitle);
if (item.item_url != null && !item.item_url.isEmpty())
System.out.printf("URL: %s%n", item.item_url);
System.out.println();
}
}
}
```
### What You'll See
```
Found 3 item(s) from across the affiliate network:
--- Item 1 ---
Title: Anxiety and the Christian Life
Author: Crossway Publishers
Tradition: Evangelical
Subtitle: Finding Peace in a Worried World
URL: https://crossway.org/books/anxiety-and-the-christian-life
--- Item 2 ---
Title: Cast All Your Anxiety on Him
Author: Desiring God
Tradition: Reformed
URL: https://www.desiringgod.org/articles/cast-all-your-anxiety-on-him
--- Item 3 ---
Title: Freedom from Anxiety
Author: Focus on the Family
Tradition: Evangelical
URL: https://www.focusonthefamily.com/faith/freedom-from-anxiety
```
### Run the Cookbook Example
You can try out the script in the cookbook to search the affiliate network.
```bash Python theme={null}
python recommend_affiliates.py "How do I deal with anxiety?"
python recommend_affiliates.py "parenting teenagers" 3
```
```bash JavaScript theme={null}
node recommend-affiliates.js "How do I deal with anxiety?"
node recommend-affiliates.js "parenting teenagers" 3
```
```bash TypeScript theme={null}
npx ts-node recommend-affiliates.ts "How do I deal with anxiety?"
npx ts-node recommend-affiliates.ts "parenting teenagers" 3
```
```bash PHP theme={null}
php recommend_affiliates.php "How do I deal with anxiety?"
php recommend_affiliates.php "parenting teenagers" 3
```
```bash Go theme={null}
go run . affiliates "How do I deal with anxiety?"
go run . affiliates "parenting teenagers" 3
```
```bash Java theme={null}
mvn -q exec:java -Dexec.args='affiliates "How do I deal with anxiety?"'
mvn -q exec:java -Dexec.args='affiliates "parenting teenagers" 3'
```
***
## Try It: Frontend Example
The cookbook includes a browser-based frontend that connects to a proxy server, letting you visualize all three recommendation modes. The proxy server keeps your credentials secure on the server side.
### Architecture
```
Browser (HTML/JS) → Proxy Server (localhost:3000) → Gloo AI APIs
```
The proxy server exposes three endpoints:
* `POST /api/recommendations/base` — Publisher recommendations (metadata only)
* `POST /api/recommendations/verbose` — Publisher recommendations (with snippet previews)
* `POST /api/recommendations/affiliates` — Affiliate network discovery
### Start the Proxy Server
Each language includes a proxy server. Start one:
```bash Python theme={null}
cd recommendations/python
source venv/bin/activate
python server.py
```
```bash JavaScript theme={null}
cd recommendations/javascript
node server.js
```
```bash TypeScript theme={null}
cd recommendations/typescript
npx ts-node server.ts
```
```bash PHP theme={null}
cd recommendations/php
php -S localhost:3000 server.php
```
```bash Go theme={null}
cd recommendations/go
go run . server
```
```bash Java theme={null}
cd recommendations/java
mvn -q exec:java -Dexec.args='server'
```
Then open `http://localhost:3000` in your browser.
### Publisher Recommendations
Enter a query, choose how many items to return, and optionally enable **Verbose mode** to show snippet previews alongside each result:
### Affiliate Network Discovery
The **Explore More** panel fires automatically alongside your publisher results, surfacing related content from across the Gloo network:
The frontend is language-agnostic — the same HTML/JS works with any language's
proxy server. This is one approach; customize for your branding and framework.
***
## Complete Working Examples
Clone or browse the complete working examples for all 6 languages (JavaScript,
TypeScript, Python, PHP, Go, Java) with setup instructions, proxy servers, and
a browser-based frontend.
### What's Included
Each language implementation provides:
* **`auth`** — Shared API key configuration
* **`config`** — Centralized configuration (URLs, env vars)
* **`recommend_base`** — Publisher recommendations CLI (metadata only)
* **`recommend_verbose`** — Publisher recommendations CLI (with snippet text)
* **`recommend_affiliates`** — Affiliate network discovery CLI
* **`server`** — Proxy server exposing REST endpoints for the frontend
***
## Troubleshooting
### No Results Returned
* **`certainty_threshold` too strict**: The default `0.75` filters out lower-confidence matches. Lower it to `0.5` to broaden results.
* **Content not indexed**: Test the same query in the [Search Playground](/studio/playground-search). If content appears there, your API parameters are correct.
* **Wrong tenant**: Results are scoped to your publisher. Verify the tenant name matches your publisher in [Organizations](/studio/manage-organizations).
### 403 Forbidden
* You can only access your own publisher's content via the publisher endpoints.
* Verify your API key is correct and has not been revoked.
* The affiliate endpoint has broader access — a 403 there indicates an API key issue.
### Affiliate Endpoint Returns Empty
* Not all content is available in the affiliate network. Publishers must opt in.
* Try a broader or different query to confirm the endpoint is working.
### Authentication Errors
* If your API key is rejected, verify it is correct and has not been revoked (see [Authentication Tutorial](/tutorials/authentication)).
* Ensure you're using `Bearer {api_key}` in the Authorization header.
***
## Next Steps
Endpoint docs for recommendation results that return item metadata only.
Endpoint docs for recommendation results that include snippet previews.
Explore cross-publisher affiliate discovery for related content.
Add more content to your Data Engine for richer recommendations.
# Using the Guarded Responses API
Source: https://docs.gloo.com/tutorials/responses
Build with the Gloo AI Responses API: guardrails by default, values-aligned traditions, SSE streaming, and vision — with examples in six languages.
This guide shows how to use the Gloo AI Responses API. You'll build four working examples: a basic guarded response with a theological tradition, instructions with multi-turn input, streaming, and vision (image input).
**Why Responses?** The Responses API is the recommended way to build on Gloo AI. It uses the OpenAI-compatible Responses request shape and is **guarded by default** — every request runs the same guardrails, values-aligned `tradition` responses, and output moderation that power [Completions V2](/api-guides/completions-v2), with no extra configuration.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first
## Understanding the Responses API
All requests go to a single guarded endpoint:
**POST** `/ai/v2/guarded/responses`
### Key Request Fields
| Parameter | Description |
| - | - |
| `model` | The exact model ID. The Responses API is pinned-model only — no auto-routing or `model_family`. |
| `input` | A plain string, or an array of `{role, content}` items for multi-turn conversations. |
| `instructions` | Optional system-level guidance. Replaces the Completions `system` role message. |
| `tradition` | Optional theological tradition for values-aligned answers (e.g. `"evangelical"`). |
| `stream` | Set `true` to receive the response as Server-Sent Events. |
| `max_output_tokens` | Optional cap on response length. |
Answers come back as a typed `output[]` array. See [Example 1's "Understanding the Response"](#understanding-the-response) for the full shape and how to extract the text.
### Guardrail Behavior
**Values-aligned static answers arrive as normal messages.** When guardrails intervene with a curated, tradition-appropriate answer, it comes back as a regular `message` item in `output[]` — identical shape to a model answer. Your client needs no special handling; just extract the text as usual.
**Hard blocks return 403.** Requests that guardrails reject outright return an HTTP `403`:
```json theme={null}
{
"detail": {
"message": "Your request was rejected as it violates content policies.",
"type": "content_policy_violation",
"code": "content_policy_violation",
"retryable": false
}
}
```
A 403 is not a transient failure — don't blindly retry. Adjust the request content instead.
Model text output also passes through Gloo's output-moderation layer before it reaches you — for the full pipeline, see [Guardrails and values](/api-guides/responses#guardrails-and-values) in the Responses API Guide.
See the [Responses API Guide](/api-guides/responses) for complete endpoint documentation.
## Moving from Completions
If you already use [Completions V2](/api-guides/completions-v2), the migration is mostly a rename:
| Completions V2 | Responses API |
| :- | :- |
| `POST /ai/v2/guarded/chat/completions` | `POST /ai/v2/guarded/responses` |
| `messages` | `input` |
| `system` role message | `instructions` |
| `max_tokens` | `max_output_tokens` |
| `choices[].message.content` (string) | `output[]` (typed items) |
| `tradition` | `tradition` (unchanged) |
Auto-routing and `model_family` selection remain [Completions V2](/api-guides/completions-v2) features. The Responses API is pinned-model only — you specify an exact `model` on every request.
***
## Example 1: Basic Guarded Response with Tradition
Send a plain-string `input` and a `tradition` to get a values-aligned answer:
```python Python theme={null}
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
def make_basic_response(message, tradition="evangelical"):
"""Example 1: Basic guarded response with a theological tradition."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "gloo-anthropic-claude-sonnet-4.6",
"input": message,
"tradition": tradition,
"max_output_tokens": 256,
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
result = make_basic_response("How can our small group support a grieving member?")
print(result)
```
```javascript JavaScript theme={null}
require("dotenv").config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function makeBasicResponse(message, tradition = "evangelical") {
const payload = {
model: "gloo-anthropic-claude-sonnet-4.6",
input: message,
tradition: tradition,
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
return response.json();
}
makeBasicResponse("How can our small group support a grieving member?").then(console.log);
```
```typescript TypeScript theme={null}
import { config } from "dotenv";
config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function makeBasicResponse(message: string, tradition: string = "evangelical"): Promise {
const payload = {
model: "gloo-anthropic-claude-sonnet-4.6",
input: message,
tradition: tradition,
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
return response.json();
}
makeBasicResponse("How can our small group support a grieving member?").then(console.log);
```
```php PHP theme={null}
safeLoad();
$apiKey = $_ENV['GLOO_API_KEY'] ?? getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY';
$apiUrl = 'https://platform.ai.gloo.com/ai/v2/guarded/responses';
function makeBasicResponse(string $message, string $tradition = 'evangelical'): array {
global $apiKey, $apiUrl;
$payload = json_encode([
'model' => 'gloo-anthropic-claude-sonnet-4.6',
'input' => $message,
'tradition' => $tradition,
'max_output_tokens' => 256,
]);
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
]);
$body = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException('API request failed: ' . $error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('API request failed with status ' . $status . ': ' . $body);
}
return json_decode($body, true);
}
print_r(makeBasicResponse('How can our small group support a grieving member?'));
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const apiURL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
func makeBasicResponse(message, tradition string) (map[string]interface{}, error) {
payload := map[string]interface{}{
"model": "gloo-anthropic-claude-sonnet-4.6",
"input": message,
"tradition": tradition,
"max_output_tokens": 256,
}
body, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+os.Getenv("GLOO_API_KEY"))
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
json.Unmarshal(respBody, &result)
return result, nil
}
func main() {
result, err := makeBasicResponse("How can our small group support a grieving member?", "evangelical")
if err != nil {
fmt.Println("Error:", err)
return
}
out, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(out))
}
```
```java Java theme={null}
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.Map;
public class BasicResponse {
private static final String API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY");
private static final Gson gson = new Gson();
public static Map makeBasicResponse(String message, String tradition) throws Exception {
Map payload = new HashMap<>();
payload.put("model", "gloo-anthropic-claude-sonnet-4.6");
payload.put("input", message);
payload.put("tradition", tradition);
payload.put("max_output_tokens", 256);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.build();
HttpResponse response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("API request failed with status " + response.statusCode() + ": " + response.body());
}
return gson.fromJson(response.body(), Map.class);
}
public static void main(String[] args) throws Exception {
Map result = makeBasicResponse(
"How can our small group support a grieving member?", "evangelical");
System.out.println(gson.toJson(result));
}
}
```
### Understanding the Response
The answer comes back as a typed `output[]` array — a `message` item whose `content[]` holds `output_text` parts:
```json theme={null}
{
"id": "resp_...",
"object": "response",
"model": "gloo-anthropic-claude-sonnet-4.6",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{ "type": "output_text", "text": "Grief support starts with presence..." }
]
}
],
"usage": { "input_tokens": 24, "output_tokens": 38, "total_tokens": 62 }
}
```
To extract the text, walk `output[]` for `message` items and join their `output_text` parts — the cookbook samples do exactly this in an `extractText` helper.
### What You'll See
Running the full sample prints one block per example, then the final banner:
```text theme={null}
=== Gloo AI Guarded Responses API Test ===
Example 1: Basic Guarded Response (tradition=evangelical)
Testing: How can our small group support a grieving member?
Model used: gloo-anthropic-claude-sonnet-4.6
Response: I'm so sorry your group is walking through this loss. A few ways to sup...
Usage: {"input_tokens": 24, "output_tokens": 38, "total_tokens": 62}
✓ Example 1 passed
```
***
## Example 2: Instructions + Multi-Turn Input
Use `instructions` for system-level guidance and pass the conversation as an `input` array. The history alternates user → assistant → user (three turns), so the model continues the conversation with full context:
```python Python theme={null}
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
def make_instructions_response(instructions, input_items):
"""Example 2: Instructions plus multi-turn input array (alternating user/assistant roles)."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "gloo-anthropic-claude-sonnet-4.6",
"instructions": instructions,
"input": input_items,
"max_output_tokens": 256,
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
result = make_instructions_response(
"You are a compassionate pastoral assistant. Keep answers brief and warm.",
[
{
"role": "user",
"content": "I've been asked to lead a grief support group at church. Where do I start?",
},
{
"role": "assistant",
"content": "That's a meaningful calling. Start with prayerful preparation — ask God to prepare your own heart before you prepare the room.",
},
{
"role": "user",
"content": "What should I do in the very first meeting?",
},
],
)
print(result)
```
```javascript JavaScript theme={null}
require("dotenv").config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function makeInstructionsResponse(instructions, inputItems) {
const payload = {
model: "gloo-anthropic-claude-sonnet-4.6",
instructions: instructions,
input: inputItems,
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
return response.json();
}
makeInstructionsResponse(
"You are a compassionate pastoral assistant. Keep answers brief and warm.",
[
{
role: "user",
content: "I've been asked to lead a grief support group at church. Where do I start?",
},
{
role: "assistant",
content: "That's a meaningful calling. Start with prayerful preparation — ask God to prepare your own heart before you prepare the room.",
},
{
role: "user",
content: "What should I do in the very first meeting?",
},
]
).then(console.log);
```
```typescript TypeScript theme={null}
import { config } from "dotenv";
config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function makeInstructionsResponse(instructions: string, inputItems: any[]): Promise {
const payload = {
model: "gloo-anthropic-claude-sonnet-4.6",
instructions: instructions,
input: inputItems,
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
return response.json();
}
makeInstructionsResponse(
"You are a compassionate pastoral assistant. Keep answers brief and warm.",
[
{
role: "user",
content: "I've been asked to lead a grief support group at church. Where do I start?",
},
{
role: "assistant",
content: "That's a meaningful calling. Start with prayerful preparation — ask God to prepare your own heart before you prepare the room.",
},
{
role: "user",
content: "What should I do in the very first meeting?",
},
]
).then(console.log);
```
```php PHP theme={null}
safeLoad();
$apiKey = $_ENV['GLOO_API_KEY'] ?? getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY';
$apiUrl = 'https://platform.ai.gloo.com/ai/v2/guarded/responses';
function makeInstructionsResponse(string $instructions, array $inputItems): array {
global $apiKey, $apiUrl;
$payload = json_encode([
'model' => 'gloo-anthropic-claude-sonnet-4.6',
'instructions' => $instructions,
'input' => $inputItems,
'max_output_tokens' => 256,
]);
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
]);
$body = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException('API request failed: ' . $error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('API request failed with status ' . $status . ': ' . $body);
}
return json_decode($body, true);
}
print_r(makeInstructionsResponse(
'You are a compassionate pastoral assistant. Keep answers brief and warm.',
[
['role' => 'user', 'content' => "I've been asked to lead a grief support group at church. Where do I start?"],
['role' => 'assistant', 'content' => "That's a meaningful calling. Start with prayerful preparation — ask God to prepare your own heart before you prepare the room."],
['role' => 'user', 'content' => 'What should I do in the very first meeting?'],
]
));
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const apiURL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
func makeInstructionsResponse(instructions string, inputItems []map[string]interface{}) (map[string]interface{}, error) {
payload := map[string]interface{}{
"model": "gloo-anthropic-claude-sonnet-4.6",
"instructions": instructions,
"input": inputItems,
"max_output_tokens": 256,
}
body, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+os.Getenv("GLOO_API_KEY"))
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
json.Unmarshal(respBody, &result)
return result, nil
}
func main() {
result, err := makeInstructionsResponse(
"You are a compassionate pastoral assistant. Keep answers brief and warm.",
[]map[string]interface{}{
{"role": "user", "content": "I've been asked to lead a grief support group at church. Where do I start?"},
{"role": "assistant", "content": "That's a meaningful calling. Start with prayerful preparation — ask God to prepare your own heart before you prepare the room."},
{"role": "user", "content": "What should I do in the very first meeting?"},
},
)
if err != nil {
fmt.Println("Error:", err)
return
}
out, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(out))
}
```
```java Java theme={null}
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class InstructionsResponse {
private static final String API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY");
private static final Gson gson = new Gson();
public static Map makeInstructionsResponse(String instructions, List> inputItems) throws Exception {
Map payload = new HashMap<>();
payload.put("model", "gloo-anthropic-claude-sonnet-4.6");
payload.put("instructions", instructions);
payload.put("input", inputItems);
payload.put("max_output_tokens", 256);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.build();
HttpResponse response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("API request failed with status " + response.statusCode() + ": " + response.body());
}
return gson.fromJson(response.body(), Map.class);
}
public static void main(String[] args) throws Exception {
Map result = makeInstructionsResponse(
"You are a compassionate pastoral assistant. Keep answers brief and warm.",
List.of(
Map.of("role", "user", "content", "I've been asked to lead a grief support group at church. Where do I start?"),
Map.of("role", "assistant", "content", "That's a meaningful calling. Start with prayerful preparation — ask God to prepare your own heart before you prepare the room."),
Map.of("role", "user", "content", "What should I do in the very first meeting?")
));
System.out.println(gson.toJson(result));
}
}
```
### What You'll See
The reply arrives as the same `message` item in `output[]` shown in Example 1, continuing the conversation.
Running the full sample prints the second block:
```text theme={null}
Example 2: Instructions + Multi-turn Input
Testing: three-turn conversation (user → assistant → user) with pastoral-care instructions
Model used: gloo-anthropic-claude-sonnet-4.6
Response: Start with introductions and a simple opening question. Invite each member to share...
Usage: {"input_tokens": 87, "output_tokens": 41, "total_tokens": 128}
✓ Example 2 passed
```
***
## Example 3: Streaming (SSE)
Set `"stream": true` to receive the response as Server-Sent Events.
```python Python theme={null}
import json
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
def stream_response(message):
"""Example 3: Streaming via SSE; returns (accumulated_text, usage)."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "gloo-anthropic-claude-sonnet-4.6",
"input": message,
"stream": True,
"max_output_tokens": 256,
}
response = requests.post(API_URL, headers=headers, json=payload, stream=True)
response.raise_for_status()
text_parts = []
done_parts = []
usage = None
event = None
# Each data: payload is the flattened event object itself; lifecycle
# events are ignored, and response.completed carries usage but no output[].
for line in response.iter_lines(decode_unicode=True):
if line is None:
continue
if line.startswith("event:"):
event = line[len("event:"):].strip()
continue
if not line.startswith("data:"):
continue
data = json.loads(line[len("data:"):].strip())
if event == "response.output_text.delta":
delta = data.get("delta", "")
print(delta, end="", flush=True)
text_parts.append(delta)
elif event == "response.output_text.done":
done_parts.append(data.get("text", ""))
elif event == "response.completed":
final = data.get("response") or data
usage = final.get("usage")
elif event is not None and event.startswith("response."):
pass # lifecycle events — ignore
else:
raise RuntimeError(f"Stream terminated by event '{event}': {data}")
event = None
print()
text = "".join(text_parts) or "".join(done_parts)
return text, usage
text, usage = stream_response("Write a one-paragraph prayer for a new season of ministry.")
print(f"Streamed text ({len(text)} chars), usage: {usage}")
```
```javascript JavaScript theme={null}
require("dotenv").config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function streamResponse(message) {
const payload = {
model: "gloo-anthropic-claude-sonnet-4.6",
input: message,
stream: true,
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
const textParts = [];
const doneParts = [];
let usage = null;
// Each data: payload is the flattened event object itself; lifecycle
// events are ignored, and response.completed carries usage but no output[].
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let event = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? ""; // keep the incomplete last line
for (const rawLine of lines) {
const line = rawLine.replace(/\r$/, "");
if (line.startsWith("event:")) {
event = line.slice("event:".length).trim();
continue;
}
if (!line.startsWith("data:")) continue;
const data = JSON.parse(line.slice("data:".length).trim());
if (event === "response.output_text.delta") {
const delta = data.delta || "";
process.stdout.write(delta);
textParts.push(delta);
} else if (event === "response.output_text.done") {
doneParts.push(data.text || "");
} else if (event === "response.completed") {
const final = data.response || data;
usage = final.usage || null;
} else if (event !== null && event.startsWith("response.")) {
// lifecycle events — ignore
} else {
throw new Error(`Stream terminated by event '${event}': ${JSON.stringify(data)}`);
}
event = null;
}
}
process.stdout.write("\n");
return { text: textParts.join("") || doneParts.join(""), usage };
}
streamResponse("Write a one-paragraph prayer for a new season of ministry.").then(
({ text, usage }) => console.log(`Streamed text (${text.length} chars), usage: ${JSON.stringify(usage)}`)
);
```
```typescript TypeScript theme={null}
import { config } from "dotenv";
config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function streamResponse(message: string): Promise<{ text: string; usage: any }> {
const payload = {
model: "gloo-anthropic-claude-sonnet-4.6",
input: message,
stream: true,
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
const textParts: string[] = [];
const doneParts: string[] = [];
let usage: any = null;
// Each data: payload is the flattened event object itself; lifecycle
// events are ignored, and response.completed carries usage but no output[].
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
let event: string | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? ""; // keep the incomplete last line
for (const rawLine of lines) {
const line = rawLine.replace(/\r$/, "");
if (line.startsWith("event:")) {
event = line.slice("event:".length).trim();
continue;
}
if (!line.startsWith("data:")) continue;
const data = JSON.parse(line.slice("data:".length).trim()) as Record;
if (event === "response.output_text.delta") {
const delta = (data.delta as string) || "";
process.stdout.write(delta);
textParts.push(delta);
} else if (event === "response.output_text.done") {
doneParts.push((data.text as string) || "");
} else if (event === "response.completed") {
const final = (data.response as any) || data;
usage = final.usage || null;
} else if (event !== null && event.startsWith("response.")) {
// lifecycle events — ignore
} else {
throw new Error(`Stream terminated by event '${event}': ${JSON.stringify(data)}`);
}
event = null;
}
}
process.stdout.write("\n");
return { text: textParts.join("") || doneParts.join(""), usage };
}
streamResponse("Write a one-paragraph prayer for a new season of ministry.").then(
({ text, usage }) => console.log(`Streamed text (${text.length} chars), usage: ${JSON.stringify(usage)}`)
);
```
```php PHP theme={null}
safeLoad();
$apiKey = $_ENV['GLOO_API_KEY'] ?? getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY';
$apiUrl = 'https://platform.ai.gloo.com/ai/v2/guarded/responses';
/**
* Process one raw SSE event block (event:/data: lines), mutating $state.
*/
function processEvent(string $rawEvent, array &$state): void {
$event = null;
$data = null;
foreach (preg_split('/\r?\n/', $rawEvent) as $line) {
if (str_starts_with($line, 'event:')) {
$event = trim(substr($line, strlen('event:')));
} elseif (str_starts_with($line, 'data:')) {
$data = json_decode(trim(substr($line, strlen('data:'))), true);
}
}
if ($event === 'response.output_text.delta') {
$delta = $data['delta'] ?? '';
echo $delta;
$state['text_parts'][] = $delta;
} elseif ($event === 'response.output_text.done') {
$state['done_parts'][] = $data['text'] ?? '';
} elseif ($event === 'response.completed') {
// Flattened payload: usage is at the top level; no output[] array.
$final = $data['response'] ?? $data;
$state['usage'] = $final['usage'] ?? null;
} elseif ($event !== null && str_starts_with($event, 'response.')) {
// lifecycle events — ignore
} else {
throw new RuntimeException("Stream terminated by event '{$event}': " . json_encode($data));
}
}
function streamResponse(string $message): array {
global $apiKey, $apiUrl;
$payload = json_encode([
'model' => 'gloo-anthropic-claude-sonnet-4.6',
'input' => $message,
'stream' => true,
'max_output_tokens' => 256,
]);
$state = ['buffer' => '', 'text_parts' => [], 'done_parts' => [], 'usage' => null];
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: text/event-stream',
],
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use (&$state): int {
$state['buffer'] .= $chunk;
// SSE events are separated by blank lines; process every complete event.
while (($pos = strpos($state['buffer'], "\n\n")) !== false) {
$rawEvent = substr($state['buffer'], 0, $pos);
$state['buffer'] = substr($state['buffer'], $pos + 2);
processEvent($rawEvent, $state);
}
return strlen($chunk);
},
]);
curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException('API request failed: ' . $error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('API request failed with status ' . $status);
}
// Flush any final event not terminated by a blank line.
if (trim($state['buffer']) !== '') {
processEvent($state['buffer'], $state);
}
echo PHP_EOL;
$text = implode('', $state['text_parts']) ?: implode('', $state['done_parts']);
return [$text, $state['usage']];
}
[$text, $usage] = streamResponse('Write a one-paragraph prayer for a new season of ministry.');
echo 'Streamed text (' . mb_strlen($text) . " chars), usage: " . json_encode($usage) . "\n";
?>
```
```go Go theme={null}
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)
const apiURL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
// streamResponse streams via SSE; returns accumulated text and usage.
//
// Each data: payload is the flattened event object itself; lifecycle
// events are ignored, and response.completed carries usage but no output[].
func streamResponse(message string) (string, map[string]interface{}, error) {
payload := map[string]interface{}{
"model": "gloo-anthropic-claude-sonnet-4.6",
"input": message,
"stream": true,
"max_output_tokens": 256,
}
body, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
if err != nil {
return "", nil, err
}
req.Header.Add("Authorization", "Bearer "+os.Getenv("GLOO_API_KEY"))
req.Header.Add("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return "", nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
var textParts, doneParts []string
var usage map[string]interface{}
event := ""
scanner := bufio.NewScanner(resp.Body)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[len("event:"):])
continue
case !strings.HasPrefix(line, "data:"):
continue
}
var data map[string]interface{}
if err := json.Unmarshal([]byte(strings.TrimSpace(line[len("data:"):])), &data); err != nil {
return "", nil, fmt.Errorf("failed to parse SSE data: %w", err)
}
switch {
case event == "response.output_text.delta":
delta, _ := data["delta"].(string)
fmt.Print(delta)
textParts = append(textParts, delta)
case event == "response.output_text.done":
text, _ := data["text"].(string)
doneParts = append(doneParts, text)
case event == "response.completed":
final, ok := data["response"].(map[string]interface{})
if !ok {
final = data // flattened payload
}
if u, ok := final["usage"].(map[string]interface{}); ok {
usage = u
}
case strings.HasPrefix(event, "response."):
// lifecycle events — ignore
default:
dataJSON, _ := json.Marshal(data)
return "", nil, fmt.Errorf("stream terminated by event '%s': %s", event, string(dataJSON))
}
event = ""
}
fmt.Println()
text := strings.Join(textParts, "")
if text == "" {
text = strings.Join(doneParts, "")
}
return text, usage, nil
}
func main() {
text, usage, err := streamResponse("Write a one-paragraph prayer for a new season of ministry.")
if err != nil {
fmt.Println("Error:", err)
return
}
usageJSON, _ := json.Marshal(usage)
fmt.Printf("Streamed text (%d chars), usage: %s\n", len([]rune(text)), string(usageJSON))
}
```
```java Java theme={null}
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class StreamResponse {
private static final String API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY");
private static final Gson gson = new Gson();
public static Map streamResponse(String message) throws Exception {
Map payload = new HashMap<>();
payload.put("model", "gloo-anthropic-claude-sonnet-4.6");
payload.put("input", message);
payload.put("stream", true);
payload.put("max_output_tokens", 256);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.build();
HttpResponse response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
String body = new String(response.body().readAllBytes(), StandardCharsets.UTF_8);
throw new IOException("API request failed with status " + response.statusCode() + ": " + body);
}
StringBuilder textBuilder = new StringBuilder();
StringBuilder doneBuilder = new StringBuilder();
JsonElement usage = null;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body(), StandardCharsets.UTF_8))) {
String event = null;
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("event:")) {
event = line.substring("event:".length()).trim();
continue;
}
if (!line.startsWith("data:")) {
continue;
}
JsonObject data = JsonParser.parseString(line.substring("data:".length()).trim()).getAsJsonObject();
if ("response.output_text.delta".equals(event)) {
String delta = data.has("delta") && !data.get("delta").isJsonNull()
? data.get("delta").getAsString() : "";
System.out.print(delta);
textBuilder.append(delta);
} else if ("response.output_text.done".equals(event)) {
String text = data.has("text") && !data.get("text").isJsonNull()
? data.get("text").getAsString() : "";
doneBuilder.append(text);
} else if ("response.completed".equals(event)) {
// Flattened payload: usage is at the top level; no output[] array.
JsonObject finalResponse = data.has("response") && data.get("response").isJsonObject()
? data.getAsJsonObject("response")
: data;
usage = finalResponse.get("usage");
} else if (event != null && event.startsWith("response.")) {
// lifecycle events — ignore
} else {
throw new IOException("Stream terminated by event '" + event + "': " + gson.toJson(data));
}
event = null;
}
}
System.out.println();
String text = textBuilder.length() > 0 ? textBuilder.toString() : doneBuilder.toString();
Map result = new HashMap<>();
result.put("text", text);
result.put("usage", usage);
return result;
}
public static void main(String[] args) throws Exception {
Map result = streamResponse("Write a one-paragraph prayer for a new season of ministry.");
System.out.println("Streamed text (" + ((String) result.get("text")).length()
+ " chars), usage: " + gson.toJson(result.get("usage")));
}
}
```
### Understanding the Response
When you set `"stream": true`, the API response is an SSE stream. Here are the key event types, and what to do with each:
| Event Type | When | What to do |
| - | - | - |
| `response.created` | Once — stream start | Nothing required — the stream is open. |
| `response.output_text.delta` | Many — as tokens are generated | Print or buffer each `delta` as it arrives. |
| `response.output_text.done` | Once per `message` item | Optionally capture `text` — the item's complete text. |
| `response.completed` | Once — stream end | Read `usage`, then close the stream. |
| `response.in_progress`
`response.output_item.added` / `done`
`response.content_part.added` / `done` | Interleaved throughout | Ignore — they're purely structural. |
| `error.*` (or any event you don't recognize) | Terminal — ends the stream | Treat as an error: close the stream and surface the message. |
On the wire, each event is an `event:` line followed by a `data:` line:
```text theme={null}
event: response.created
data: {"type":"response.created","id":"resp_a1b2c3","object":"response",...}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_...","delta":"Grief "}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_...","text":"Grief support starts..."}
event: response.completed
data: {"type":"response.completed","id":"resp_a1b2c3",...,"usage":{"input_tokens":24,"output_tokens":38,"total_tokens":62}}
```
The live API sends **flattened payloads**: each `data:` line is the event object itself (its `type` matches the `event:` line), with no nested `response` wrapper. The `response.completed` payload carries `usage` at the top level and does **not** include the `output[]` array — the full text arrives via `response.output_text.done`. The snippets below accept both the flattened shape and the nested shape so they work with either.
### What You'll See
When you run the full sample, the deltas print live as they arrive. After the stream completes, the runner prints the model, the streamed-text length with a preview, and the usage from `response.completed`:
```text theme={null}
Example 3: Streaming (SSE)
Testing: Write a one-paragraph prayer for a new season of ministry.
Lord, as this new season begins, we ask for Your guidance... (deltas print live as they arrive)
Model used: gloo-anthropic-claude-sonnet-4.6
Streamed text (273 chars): Lord, as this new season begins, we ask for Your guidance...
Usage: {"input_tokens": 21, "output_tokens": 74, "total_tokens": 95}
✓ Example 3 passed
```
Streams can fail after the response has started — a dropped connection mid-stream means partial output. See [Handling Streaming Failures](/best-practices/completions-streaming-failures) for retry, continuation, and partial-output guidance.
***
## Example 4: Vision (Image Input)
Vision uses the same endpoint — pass typed content parts (`input_text` + `input_image`) in the `content` of an input item. Use a vision-capable model such as `gloo-google-gemini-3.1-pro`:
```python Python theme={null}
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
def make_vision_response(image_url, question):
"""Example 4: Vision — image input via typed content parts."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": "gloo-google-gemini-3.1-pro",
"input": [
{
"role": "user",
"content": [
{"type": "input_text", "text": question},
{"type": "input_image", "image_url": image_url},
],
}
],
"max_output_tokens": 256,
}
response = requests.post(API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
result = make_vision_response(
"https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
"What animal is in this image?",
)
print(result)
```
```javascript JavaScript theme={null}
require("dotenv").config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function makeVisionResponse(imageUrl, question) {
const payload = {
model: "gloo-google-gemini-3.1-pro",
input: [
{
role: "user",
content: [
{ type: "input_text", text: question },
{ type: "input_image", image_url: imageUrl },
],
},
],
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
return response.json();
}
makeVisionResponse(
"https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
"What animal is in this image?"
).then(console.log);
```
```typescript TypeScript theme={null}
import { config } from "dotenv";
config();
const API_KEY = process.env.GLOO_API_KEY || "YOUR_API_KEY";
const API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
async function makeVisionResponse(imageUrl: string, question: string): Promise {
const payload = {
model: "gloo-google-gemini-3.1-pro",
input: [
{
role: "user",
content: [
{ type: "input_text", text: question },
{ type: "input_image", image_url: imageUrl },
],
},
],
max_output_tokens: 256,
};
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}: ${await response.text()}`);
}
return response.json();
}
makeVisionResponse(
"https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
"What animal is in this image?"
).then(console.log);
```
```php PHP theme={null}
safeLoad();
$apiKey = $_ENV['GLOO_API_KEY'] ?? getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY';
$apiUrl = 'https://platform.ai.gloo.com/ai/v2/guarded/responses';
function makeVisionResponse(string $imageUrl, string $question): array {
global $apiKey, $apiUrl;
$payload = json_encode([
'model' => 'gloo-google-gemini-3.1-pro',
'input' => [
[
'role' => 'user',
'content' => [
['type' => 'input_text', 'text' => $question],
['type' => 'input_image', 'image_url' => $imageUrl],
],
],
],
'max_output_tokens' => 256,
]);
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
]);
$body = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException('API request failed: ' . $error);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException('API request failed with status ' . $status . ': ' . $body);
}
return json_decode($body, true);
}
print_r(makeVisionResponse(
'https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg',
'What animal is in this image?'
));
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const apiURL = "https://platform.ai.gloo.com/ai/v2/guarded/responses"
func makeVisionResponse(imageURL, question string) (map[string]interface{}, error) {
payload := map[string]interface{}{
"model": "gloo-google-gemini-3.1-pro",
"input": []map[string]interface{}{
{
"role": "user",
"content": []map[string]interface{}{
{"type": "input_text", "text": question},
{"type": "input_image", "image_url": imageURL},
},
},
},
"max_output_tokens": 256,
}
body, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+os.Getenv("GLOO_API_KEY"))
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]interface{}
json.Unmarshal(respBody, &result)
return result, nil
}
func main() {
result, err := makeVisionResponse(
"https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
"What animal is in this image?",
)
if err != nil {
fmt.Println("Error:", err)
return
}
out, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(out))
}
```
```java Java theme={null}
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class VisionResponse {
private static final String API_URL = "https://platform.ai.gloo.com/ai/v2/guarded/responses";
private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY");
private static final Gson gson = new Gson();
public static Map makeVisionResponse(String imageUrl, String question) throws Exception {
Map textPart = new HashMap<>();
textPart.put("type", "input_text");
textPart.put("text", question);
Map imagePart = new HashMap<>();
imagePart.put("type", "input_image");
imagePart.put("image_url", imageUrl);
Map inputItem = new HashMap<>();
inputItem.put("role", "user");
inputItem.put("content", List.of(textPart, imagePart));
Map payload = new HashMap<>();
payload.put("model", "gloo-google-gemini-3.1-pro");
payload.put("input", List.of(inputItem));
payload.put("max_output_tokens", 256);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.build();
HttpResponse response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("API request failed with status " + response.statusCode() + ": " + response.body());
}
return gson.fromJson(response.body(), Map.class);
}
public static void main(String[] args) throws Exception {
Map result = makeVisionResponse(
"https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
"What animal is in this image?");
System.out.println(gson.toJson(result));
}
}
```
### What You'll See
The reply arrives as a normal `message` item in `output[]` — for the cat photo above, the answer identifies the animal:
```json theme={null}
{
"id": "resp_...",
"object": "response",
"model": "gloo-google-gemini-3.1-pro",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{ "type": "output_text", "text": "The animal in this image is a cat." }
]
}
],
"usage": { "input_tokens": 275, "output_tokens": 9, "total_tokens": 284 }
}
```
Running the full sample prints the fourth block:
```text theme={null}
Example 4: Vision (Image Input)
Testing: https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg
Model used: gloo-google-gemini-3.1-pro
Response: The animal in this image is a cat.
Usage: {"input_tokens": 275, "output_tokens": 9, "total_tokens": 284}
✓ Example 4 passed
```
***
## Complete Example
Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions.
### What's Included
Each language implementation provides:
* **`auth`** — Shared API key configuration
* **`config`** — Centralized configuration (endpoint, env vars)
* **Runner per example** — Basic guarded response, instructions + multi-turn, streaming, and vision, plus a final banner
* **Env helper** — Ignores unset `.env` placeholders
* **`post` helper** — Shared request helper that annotates 403s with "(guardrails hard-blocked this request)"
* **`extractText` helper** — Reusable helper for pulling text out of `output[]`
Set up your credentials first:
```bash theme={null}
# Create a .env file in the language directory (or export for Go/Java)
GLOO_API_KEY=your_actual_api_key_here
```
Then run per language:
```bash Python theme={null}
cd responses/python
pip install -r requirements.txt
python main.py
```
```bash JavaScript theme={null}
cd responses/javascript
npm install && npm start
```
```bash TypeScript theme={null}
cd responses/typescript
npm install && npx tsx index.ts
```
```bash PHP theme={null}
cd responses/php
composer install && php index.php
```
```bash Go theme={null}
cd responses/go
go mod tidy && go run main.go
```
```bash Java theme={null}
cd responses/java
mvn compile && mvn exec:java
```
Each runner prints a block per example (model used, first \~100 characters of the response, token usage, and a ✓ pass line), then the final banner:
```text theme={null}
=== All Responses API tests passed! ===
```
The snippets in this tutorial are simplified to be self-contained. The cookbook samples are structured the same way but add robustness: an env helper that ignores unset `.env` placeholders, a shared `post` helper that annotates 403s with "(guardrails hard-blocked this request)", and a reusable `extractText` helper for pulling text out of `output[]`. The prompts, field names, and endpoint are identical.
***
## Troubleshooting
**`Please set your GLOO_API_KEY environment variable`**
: Your `.env` file is missing or doesn't contain `GLOO_API_KEY`. Create one in the language's directory (see [Complete Example](#complete-example)), or export the variable in your shell for Go and Java.
**`API request failed with status 403`**
: Guardrails hard-blocked the request. The 403 body includes a `content_policy_violation` detail — rephrase the request rather than retrying. See [Guardrail behavior](#guardrail-behavior).
**Vision request returns 400 / `INVALID_REQUEST`**
: The model isn't vision-capable. Vision requires a multimodal model like `gloo-google-gemini-3.1-pro` — sending image parts to a text-only model (e.g. `gloo-anthropic-claude-sonnet-4.6`) fails. Check [Supported Models](/api-guides/supported-models) for capabilities.
**Vision request fails on image fetch**
: The API fetches `image_url` server-side, so the URL must be publicly reachable. Test it in a browser or `curl -I `. Private or expiring URLs fail; use a public URL or a base64 `data:` URI.
**Stream parses nothing, or every event looks unknown**
: Remember the payloads are **flattened** — the `data:` line is the event object itself, not `{ "response": {...} }`. Match on the `event:` line, handle `response.output_text.delta`/`done` and `response.completed`, and ignore other `response.*` lifecycle events. Treating unknown events as terminal (and closing the stream) is the safe default.
***
## Next Steps
Now that you understand the Responses API, explore:
1. **[Responses API Guide](/api-guides/responses)** - Full API documentation
2. **[Tool Use](/api-guides/tool-use)** - Function calling with Responses
3. **[Grounded Responses](/api-guides/grounded-responses)** - RAG with source attribution
4. **[Direct Responses](/api-guides/direct-responses)** - Unguarded direct endpoint
5. **[Completions V2 Guide](/api-guides/completions-v2)** - Auto-routing and `model_family` selection
6. **[Supported Model IDs](/api-guides/supported-models)** - All available models
# Building Custom Search with the Search API
Source: https://docs.gloo.com/tutorials/search
Learn how to build your own search experience using the Gloo AI Search API.
This tutorial shows you how to build custom search functionality using the Gloo AI Search API. You'll learn to authenticate, perform semantic search, work with rich results, and optionally combine search with Completions V2 for Retrieval Augmented Generation (RAG).
The Search API gives you full control over how search works in your application — from the query to the UI. Whether you're building a knowledge base, a chatbot, or a content discovery experience, this tutorial covers the backend patterns you need.
The **Discovery Widget** in Gloo AI Studio provides a quick, embeddable search
experience. This tutorial shows you how to build equivalent functionality
using the Search API directly, giving you full control over branding, UI, and
integration patterns.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* Your Tenant (publisher) name from [Organizations](/studio/manage-organizations) in Studio. Tenant names are normalized (spaces stripped) — see [Find Your Tenant](#understanding-the-search-api) below, or pull the exact string from `GET /ai/v1/data/tenant/names`.
* Content uploaded to the Data Engine (see [Upload Files Tutorial](/tutorials/upload-files))
* **Authentication setup** — Complete the [Authentication Tutorial](/tutorials/authentication) first
Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java). Includes a proxy server and browser-based frontend for each language.
Setup and testing instructions are provided later.
The code snippets in this tutorial are **simplified and self-contained** —
designed for readability and easy copy-paste. The [cookbook
examples](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/search-tutorial)
use a modular architecture plus production niceties. Both implement the same
APIs and patterns.
***
## Understanding the Search API
The Search API provides AI-powered semantic search across your ingested content. Unlike keyword search, semantic search understands the *meaning* behind queries — so a search for "secrets to a happy marriage" will find content about "rules for keeping a marriage healthy" even without exact word matches.
**Endpoint:** `POST /ai/v1/data/search`
### Key Features
* **Semantic Search**: Near-text search that understands meaning, not just keywords
* **Rich Metadata**: AI-generated summaries, biblical analysis, content classifications
* **Snippet Extraction**: Pre-chunked content ready for display or RAG
* **Relevance Scoring**: Distance, certainty, and score metrics for ranking
### Required Parameters
| Parameter | Description |
| - | - |
| `query` | The search query string |
| `collection` | Always `"GlooProd"` |
| `tenant` | Your publisher (tenant) name |
| `limit` | Number of results to return (10-100 recommended) |
**Tenant names are normalized.** Spaces are stripped from the publisher name to
form the tenant identifier — for example, `Example Publisher Name` becomes
`ExamplePublisherName`. Passing the raw publisher name with spaces can cause a
`400 "invalid tenant"` error. To get the exact string, call
`GET /ai/v1/data/tenant/names` and use the value returned.
### Optional Parameters
| Parameter | Type | Description |
| - | - | - |
| `certainty` | float (0-1) | Minimum relevance threshold. The [Search Playground](/studio/playground-search) defaults to `0.5`. We recommend starting with `0.5` and adjusting as needed. |
**Important:** The API's default `certainty` is `0.75` when omitted, which is
stricter than the Playground's `0.5`. If you're getting no results, add
`"certainty": 0.5` to your request to match Playground behavior.
### Response Structure
Each result in the `data` array contains:
```json theme={null}
{
"uuid": "unique-result-id",
"metadata": {
"distance": 0.396,
"certainty": 0.802,
"score": 0.0
},
"properties": {
"item_title": "Finding True Happiness",
"type": "Article",
"author": ["Author Name"],
"snippet": "Content text...",
"summaries": { ... },
"biblical_analysis": { ... }
},
"collection": "GlooProd"
}
```
Key fields:
* **`metadata.certainty`** — Relevance score (0-1, higher = more relevant)
* **`properties.snippet`** — Content chunk text, ideal for display or RAG context
* **`properties.summaries`** — AI-generated summaries in multiple styles
* **`properties.biblical_analysis`** — Bible references, concepts, and lessons (if applicable)
***
## Test in the Playground First
Before writing code, test your queries in the [Search Playground](/studio/playground-search) to verify your content is indexed and understand the response structure.
1. Navigate to **Playground** in [Gloo AI Studio](https://studio.ai.gloo.com)
2. Select the **Search** tab
3. Choose your publisher from the dropdown
4. Enter a query and review the results
**Try it now:** Search for a topic covered in your uploaded content. The
Playground displays each result with its title, snippet text, and AI-generated
insights.
***
## Step 1: Basic Search
Let's make a search request with proper authentication. This is the foundation for everything that follows.
Each implementation below makes the search request and displays results with titles, types, authors, and relevance scores.
```python Python theme={null}
#!/usr/bin/env python3
"""Basic search using the Gloo AI Search API."""
import requests
import os
import sys
import time
from dotenv import load_dotenv
load_dotenv()
# Configuration
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
TENANT = os.getenv("GLOO_TENANT", "your-tenant-name")
SEARCH_URL = "https://platform.ai.gloo.com/ai/v1/data/search"
# --- Search ---
def search(query, limit=10):
"""Perform a semantic search query."""
token = API_KEY
payload = {
"query": query,
"collection": "GlooProd",
"tenant": TENANT,
"limit": limit,
"certainty": 0.5
}
response = requests.post(
SEARCH_URL,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
},
json=payload,
timeout=60
)
response.raise_for_status()
return response.json()
# --- Run ---
query = sys.argv[1] if len(sys.argv) > 1 else "How can I know my purpose?"
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
print(f"Searching for: '{query}'")
print(f"Limit: {limit} results\n")
results = search(query, limit)
if not results.get('data'):
print("No results found.")
else:
print(f"Found {len(results['data'])} results:\n")
for i, result in enumerate(results['data'], 1):
props = result.get('properties', {})
meta = result.get('metadata', {})
print(f"--- Result {i} ---")
print(f"Title: {props.get('item_title', 'N/A')}")
print(f"Type: {props.get('type', 'N/A')}")
print(f"Author: {', '.join(props.get('author', ['N/A']))}")
print(f"Relevance Score: {meta.get('certainty', 0):.4f}")
snippet = props.get('snippet', '')
if snippet:
print(f"Snippet: {snippet[:200]}...")
print()
```
```javascript JavaScript theme={null}
/**
* Basic search using the Gloo AI Search API.
*/
const axios = require('axios');
require('dotenv').config();
// Configuration
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/v1/data/search';
// --- Search ---
async function search(query, limit = 10) {
const token = API_KEY;
const response = await axios.post(
SEARCH_URL,
{
query,
collection: 'GlooProd',
tenant: TENANT,
limit,
certainty: 0.5,
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: 60000,
},
);
return response.data;
}
// --- Run ---
(async () => {
const query = process.argv[2] || 'How can I know my purpose?';
const limit = parseInt(process.argv[3] || '10', 10);
console.log(`Searching for: '${query}'`);
console.log(`Limit: ${limit} results\n`);
const results = await search(query, limit);
if (!results.data || results.data.length === 0) {
console.log('No results found.');
return;
}
console.log(`Found ${results.data.length} results:\n`);
results.data.forEach((result, i) => {
const props = result.properties || {};
const metadata = result.metadata || {};
console.log(`--- Result ${i + 1} ---`);
console.log(`Title: ${props.item_title || 'N/A'}`);
console.log(`Type: ${props.type || 'N/A'}`);
console.log(`Author: ${(props.author || ['N/A']).join(', ')}`);
console.log(`Relevance Score: ${(metadata.certainty || 0).toFixed(4)}`);
if (props.snippet) {
console.log(`Snippet: ${props.snippet.substring(0, 200)}...`);
}
console.log();
});
})();
```
```typescript TypeScript theme={null}
/**
* Basic search using the Gloo AI Search API.
*/
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
// Configuration
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/v1/data/search';
// --- Types ---
interface SearchResult {
uuid: string;
metadata: { distance: number; certainty: number; score: number };
properties: {
item_title: string;
type: string;
author: string[];
snippet: string;
};
collection: string;
}
interface SearchResponse {
data: SearchResult[];
intent: number;
}
// --- Search ---
async function search(
query: string,
limit: number = 10,
): Promise {
const token = API_KEY;
const response = await axios.post(
SEARCH_URL,
{
query,
collection: 'GlooProd',
tenant: TENANT,
limit,
certainty: 0.5,
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: 60000,
},
);
return response.data;
}
// --- Run ---
(async () => {
const query = process.argv[2] || 'How can I know my purpose?';
const limit = parseInt(process.argv[3] || '10', 10);
console.log(`Searching for: '${query}'`);
console.log(`Limit: ${limit} results\n`);
const results = await search(query, limit);
if (!results.data || results.data.length === 0) {
console.log('No results found.');
} else {
console.log(`Found ${results.data.length} results:\n`);
results.data.forEach((result, i) => {
console.log(`--- Result ${i + 1} ---`);
console.log(`Title: ${result.properties.item_title || 'N/A'}`);
console.log(`Type: ${result.properties.type || 'N/A'}`);
console.log(`Author: ${(result.properties.author || ['N/A']).join(', ')}`);
console.log(
`Relevance Score: ${(result.metadata.certainty || 0).toFixed(4)}`,
);
if (result.properties.snippet) {
console.log(`Snippet: ${result.properties.snippet.substring(0, 200)}...`);
}
console.log();
});
}
})();
```
```php PHP theme={null}
safeLoad();
// Configuration
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$TENANT = $_ENV['GLOO_TENANT'] ?? 'your-tenant-name';
$SEARCH_URL = 'https://platform.ai.gloo.com/ai/v1/data/search';
// --- Search ---
function search(string $query, int $limit = 10): array {
global $API_KEY, $TENANT, $SEARCH_URL;
$token = $API_KEY;
$payload = json_encode([
'query' => $query,
'collection' => 'GlooProd',
'tenant' => $TENANT,
'limit' => $limit,
'certainty' => 0.5,
]);
$ch = curl_init($SEARCH_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
// --- Run ---
$query = $argv[1] ?? 'How can I know my purpose?';
$limit = isset($argv[2]) ? (int)$argv[2] : 10;
echo "Searching for: '$query'\n";
echo "Limit: $limit results\n\n";
$results = search($query, $limit);
if (empty($results['data'])) {
echo "No results found.\n";
} else {
echo "Found " . count($results['data']) . " results:\n\n";
foreach ($results['data'] as $i => $result) {
$props = $result['properties'] ?? [];
$meta = $result['metadata'] ?? [];
echo "--- Result " . ($i + 1) . " ---\n";
echo "Title: " . ($props['item_title'] ?? 'N/A') . "\n";
echo "Type: " . ($props['type'] ?? 'N/A') . "\n";
echo "Author: " . implode(', ', $props['author'] ?? ['N/A']) . "\n";
echo "Relevance Score: " . number_format($meta['certainty'] ?? 0, 4) . "\n";
if (!empty($props['snippet'])) {
echo "Snippet: " . substr($props['snippet'], 0, 200) . "...\n";
}
echo "\n";
}
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
)
// Configuration loaded from .env
var (
apiKey, tenant string
searchURL = "https://platform.ai.gloo.com/ai/v1/data/search"
)
// --- Types ---
type SearchRequest struct {
Query string `json:"query"`
Collection string `json:"collection"`
Tenant string `json:"tenant"`
Limit int `json:"limit"`
Certainty float64 `json:"certainty"`
}
type SearchResult struct {
UUID string `json:"uuid"`
Metadata struct {
Certainty float64 `json:"certainty"`
} `json:"metadata"`
Properties struct {
ItemTitle string `json:"item_title"`
Type string `json:"type"`
Author []string `json:"author"`
Snippet string `json:"snippet"`
} `json:"properties"`
}
type SearchResponse struct {
Data []SearchResult `json:"data"`
}
// --- Search ---
func search(query string, limit int) (*SearchResponse, error) {
token := apiKey
payload, _ := json.Marshal(SearchRequest{
Query: query, Collection: "GlooProd",
Tenant: tenant, Limit: limit, Certainty: 0.5,
})
req, _ := http.NewRequest("POST", searchURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("search failed: %d %s", resp.StatusCode, string(body))
}
var result SearchResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode failed: %w", err)
}
return &result, nil
}
// --- Run ---
func main() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
tenant = os.Getenv("GLOO_TENANT")
query := "How can I know my purpose?"
limit := 10
if len(os.Args) > 2 { query = os.Args[2] }
if len(os.Args) > 3 { limit, _ = strconv.Atoi(os.Args[3]) }
fmt.Printf("Searching for: '%s'\nLimit: %d results\n\n", query, limit)
results, err := search(query, limit)
if err != nil {
fmt.Fprintf(os.Stderr, "Search failed: %v\n", err)
os.Exit(1)
}
if len(results.Data) == 0 {
fmt.Println("No results found.")
return
}
fmt.Printf("Found %d results:\n\n", len(results.Data))
for i, r := range results.Data {
fmt.Printf("--- Result %d ---\n", i+1)
fmt.Printf("Title: %s\n", r.Properties.ItemTitle)
fmt.Printf("Type: %s\n", r.Properties.Type)
fmt.Printf("Author: %s\n", strings.Join(r.Properties.Author, ", "))
fmt.Printf("Relevance Score: %.4f\n", r.Metadata.Certainty)
snippet := r.Properties.Snippet
if len(snippet) > 200 { snippet = snippet[:200] }
if snippet != "" { fmt.Printf("Snippet: %s...\n", snippet) }
fmt.Println()
}
}
```
```java Java theme={null}
import com.google.gson.Gson;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;
public class SearchExample {
static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
static final String TENANT = dotenv.get("GLOO_TENANT");
static final String SEARCH_URL = "https://platform.ai.gloo.com/ai/v1/data/search";
static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)).build();
static final Gson gson = new Gson();
// --- Search ---
static Map search(String query, int limit) throws Exception {
String token = API_KEY;
Map payload = Map.of(
"query", query, "collection", "GlooProd",
"tenant", TENANT, "limit", limit, "certainty", 0.5);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(SEARCH_URL))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.timeout(Duration.ofSeconds(60))
.build();
HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(resp.body(), Map.class);
}
// --- Run ---
public static void main(String[] args) throws Exception {
String query = args.length > 0 ? args[0] : "How can I know my purpose?";
int limit = args.length > 1 ? Integer.parseInt(args[1]) : 10;
System.out.printf("Searching for: '%s'%nLimit: %d results%n%n", query, limit);
Map results = search(query, limit);
List data = (List) results.get("data");
if (data == null || data.isEmpty()) {
System.out.println("No results found.");
return;
}
System.out.printf("Found %d results:%n%n", data.size());
for (int i = 0; i < data.size(); i++) {
Map props = (Map) data.get(i).get("properties");
Map meta = (Map) data.get(i).get("metadata");
System.out.printf("--- Result %d ---%n", i + 1);
System.out.printf("Title: %s%n", props.get("item_title"));
System.out.printf("Type: %s%n", props.get("type"));
System.out.printf("Author: %s%n",
String.join(", ", (List) props.getOrDefault("author", List.of("N/A"))));
System.out.printf("Relevance Score: %.4f%n", (Double) meta.get("certainty"));
String snippet = (String) props.getOrDefault("snippet", "");
if (!snippet.isEmpty()) {
System.out.printf("Snippet: %s...%n",
snippet.length() > 200 ? snippet.substring(0, 200) : snippet);
}
System.out.println();
}
}
}
```
### What You'll See
A successful search returns results with titles, types, and relevance scores:
```
Searching for: 'How can I know my purpose?'
Limit: 10 results
Found 9 results:
--- Result 1 ---
Title: Finding True Happiness
Type: Article
Author: Automated Ingestion
Relevance Score: 0.7920
Snippet: # Finding True Happiness: A Christian Perspective In a world obsessed...
--- Result 2 ---
Title: Finding True Happiness
Type: Article
Author: Automated Ingestion
Relevance Score: 0.7700
Snippet: ## The Beatitudes: God's Blueprint for Blessedness Jesus outlined the path...
```
### Key Points
* **Collection** must always be `"GlooProd"`
* **Tenant** scopes results to your publisher's content only
* **`certainty: 0.5`** matches the Playground default — adjust as needed
* Request time increases non-linearly with larger `limit` values
### Run the Cookbook Example
The cookbook includes a ready-to-run basic search script for each language:
```bash Python theme={null}
cd search-tutorial/python
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
python search_basic.py "How can I know my purpose?" 5
```
```bash JavaScript theme={null}
cd search-tutorial/javascript
npm install
node search-basic.js "How can I know my purpose?" 5
```
```bash TypeScript theme={null}
cd search-tutorial/typescript
npm install
npx ts-node search-basic.ts "How can I know my purpose?" 5
```
```bash PHP theme={null}
cd search-tutorial/php
composer install
php search_basic.php "How can I know my purpose?" 5
```
```bash Go theme={null}
cd search-tutorial/go
go mod download
go run . search "How can I know my purpose?" 5
```
```bash Java theme={null}
cd search-tutorial/java
mvn clean install -q
mvn exec:java -Dexec.args='search "How can I know my purpose?" 5'
```
***
## Step 2: Search + RAG with Completions V2
Search results become even more powerful when used as context for AI-generated responses. This is **Retrieval Augmented Generation (RAG)** — search for relevant content, then generate an answer grounded in that content.
**Two approaches to RAG with Gloo AI:**
* **Search API + Completions V2** (this section): Full control over context, prompts, and formatting
* **[Grounded Completions](/tutorials/completions-grounded)**: Single API call, simpler but less control
Both retrieve identical content. The difference is who controls how that content is presented to the LLM.
### The RAG Workflow
1. **Search** — Query the Search API for relevant content
2. **Extract** — Pull snippets from results
3. **Format** — Build context for the LLM
4. **Generate** — Call Completions V2 with the context
5. **Return** — Deliver the response with source citations
```python Python theme={null}
#!/usr/bin/env python3
"""Search + RAG using the Gloo AI Search API and Completions V2."""
import requests
import os
import sys
import time
from dotenv import load_dotenv
load_dotenv()
# Configuration
API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY")
TENANT = os.getenv("GLOO_TENANT", "your-tenant-name")
SEARCH_URL = "https://platform.ai.gloo.com/ai/v1/data/search"
COMPLETIONS_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
# --- Step 1: Search ---
def search(query, limit=5):
token = API_KEY
response = requests.post(SEARCH_URL, headers={
"Authorization": f"Bearer {token}", "Content-Type": "application/json"
}, json={
"query": query, "collection": "GlooProd",
"tenant": TENANT, "limit": limit, "certainty": 0.5
}, timeout=60)
response.raise_for_status()
return response.json()
# --- Step 2: Extract Snippets ---
def extract_snippets(results, max_snippets=5, max_chars=500):
snippets = []
for result in results.get("data", [])[:max_snippets]:
props = result.get("properties", {})
snippets.append({
"text": props.get("snippet", "")[:max_chars],
"title": props.get("item_title", "N/A"),
"type": props.get("type", "N/A"),
})
return snippets
# --- Step 3: Format Context ---
def format_context(snippets):
parts = []
for i, s in enumerate(snippets, 1):
parts.append(f"[Source {i}: {s['title']} ({s['type']})]\n{s['text']}\n")
return "\n---\n".join(parts)
# --- Step 4: Generate Response ---
def generate_with_context(query, context):
token = API_KEY
payload = {
"messages": [
{"role": "system", "content":
"You are a helpful assistant. Answer the user's question based on the "
"provided context. If the context doesn't contain relevant information, "
"say so honestly."},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"}
],
"auto_routing": True,
"max_tokens": 3000
}
response = requests.post(COMPLETIONS_URL, headers={
"Authorization": f"Bearer {token}", "Content-Type": "application/json"
}, json=payload, timeout=60)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
# --- Run Complete RAG Flow ---
query = sys.argv[2] if len(sys.argv) > 2 else "How can I know my purpose?"
limit = int(sys.argv[3]) if len(sys.argv) > 3 else 5
print(f"RAG Search for: '{query}'\n")
print("Step 1: Searching for relevant content...")
results = search(query, limit)
print(f"Found {len(results.get('data', []))} results\n")
print("Step 2: Extracting snippets...")
snippets = extract_snippets(results)
context = format_context(snippets)
print(f"Extracted {len(snippets)} snippets\n")
print("Step 3: Generating response with context...\n")
response = generate_with_context(query, context)
print("=== Generated Response ===")
print(response)
print("\n=== Sources Used ===")
for s in snippets:
print(f"- {s['title']} ({s['type']})")
```
```javascript JavaScript theme={null}
/**
* Search + RAG using the Gloo AI Search API and Completions V2.
*/
const axios = require('axios');
require('dotenv').config();
// Configuration
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/v1/data/search';
const COMPLETIONS_URL = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions';
// --- Step 1: Search ---
async function search(query, limit = 5) {
const token = API_KEY;
const response = await axios.post(
SEARCH_URL,
{
query,
collection: 'GlooProd',
tenant: TENANT,
limit,
certainty: 0.5,
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: 60000,
},
);
return response.data;
}
// --- Step 2: Extract Snippets ---
function extractSnippets(results, maxSnippets = 5, maxChars = 500) {
return (results.data || []).slice(0, maxSnippets).map((r) => ({
text: (r.properties.snippet || '').substring(0, maxChars),
title: r.properties.item_title || 'N/A',
type: r.properties.type || 'N/A',
}));
}
// --- Step 3: Format Context ---
function formatContext(snippets) {
return snippets
.map((s, i) => `[Source ${i + 1}: ${s.title} (${s.type})]\n${s.text}\n`)
.join('\n---\n');
}
// --- Step 4: Generate Response ---
async function generateWithContext(query, context) {
const token = API_KEY;
const response = await axios.post(
COMPLETIONS_URL,
{
messages: [
{
role: 'system',
content:
"You are a helpful assistant. Answer the user's question based on the " +
"provided context. If the context doesn't contain relevant information, " +
'say so honestly.',
},
{ role: 'user', content: `Context:\n${context}\n\nQuestion: ${query}` },
],
auto_routing: true,
max_tokens: 3000,
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: 60000,
},
);
return response.data.choices[0].message.content;
}
// --- Run Complete RAG Flow ---
(async () => {
const query = process.argv[3] || 'How can I know my purpose?';
const limit = parseInt(process.argv[4] || '5', 10);
console.log(`RAG Search for: '${query}'\n`);
console.log('Step 1: Searching for relevant content...');
const results = await search(query, limit);
console.log(`Found ${results.data?.length || 0} results\n`);
console.log('Step 2: Extracting snippets...');
const snippets = extractSnippets(results);
const context = formatContext(snippets);
console.log(`Extracted ${snippets.length} snippets\n`);
console.log('Step 3: Generating response with context...\n');
const response = await generateWithContext(query, context);
console.log('=== Generated Response ===');
console.log(response);
console.log('\n=== Sources Used ===');
snippets.forEach((s) => console.log(`- ${s.title} (${s.type})`));
})();
```
```typescript TypeScript theme={null}
/**
* Search + RAG using the Gloo AI Search API and Completions V2.
*/
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
// Configuration
const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/v1/data/search';
const COMPLETIONS_URL = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions';
// --- Step 1: Search ---
interface SearchResult {
metadata: { certainty: number };
properties: { item_title: string; type: string; snippet: string };
}
async function search(query: string, limit = 5) {
const token = API_KEY;
const response = await axios.post(
SEARCH_URL,
{
query,
collection: 'GlooProd',
tenant: TENANT,
limit,
certainty: 0.5,
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: 60000,
},
);
return response.data as { data: SearchResult[] };
}
// --- Steps 2-3: Extract & Format ---
interface Snippet {
text: string;
title: string;
type: string;
}
function extractSnippets(
results: { data: SearchResult[] },
max = 5,
maxChars = 500,
): Snippet[] {
return (results.data || []).slice(0, max).map((r) => ({
text: (r.properties.snippet || '').substring(0, maxChars),
title: r.properties.item_title || 'N/A',
type: r.properties.type || 'N/A',
}));
}
function formatContext(snippets: Snippet[]): string {
return snippets
.map((s, i) => `[Source ${i + 1}: ${s.title} (${s.type})]\n${s.text}\n`)
.join('\n---\n');
}
// --- Step 4: Generate Response ---
async function generateWithContext(
query: string,
context: string,
): Promise {
const token = API_KEY;
const response = await axios.post(
COMPLETIONS_URL,
{
messages: [
{
role: 'system',
content:
"You are a helpful assistant. Answer the user's question based on the " +
"provided context. If the context doesn't contain relevant information, " +
'say so honestly.',
},
{ role: 'user', content: `Context:\n${context}\n\nQuestion: ${query}` },
],
auto_routing: true,
max_tokens: 3000,
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
timeout: 60000,
},
);
return response.data.choices[0].message.content;
}
// --- Run Complete RAG Flow ---
(async () => {
const query = process.argv[3] || 'How can I know my purpose?';
const limit = parseInt(process.argv[4] || '5', 10);
console.log(`RAG Search for: '${query}'\n`);
console.log('Step 1: Searching for relevant content...');
const results = await search(query, limit);
console.log(`Found ${results.data?.length || 0} results\n`);
console.log('Step 2: Extracting snippets...');
const snippets = extractSnippets(results);
const context = formatContext(snippets);
console.log(`Extracted ${snippets.length} snippets\n`);
console.log('Step 3: Generating response with context...\n');
const answer = await generateWithContext(query, context);
console.log('=== Generated Response ===');
console.log(answer);
console.log('\n=== Sources Used ===');
snippets.forEach((s) => console.log(`- ${s.title} (${s.type})`));
})();
```
```php PHP theme={null}
safeLoad();
// Configuration
$API_KEY = $_ENV['GLOO_API_KEY'] ?? 'YOUR_API_KEY';
$TENANT = $_ENV['GLOO_TENANT'] ?? 'your-tenant-name';
$SEARCH_URL = 'https://platform.ai.gloo.com/ai/v1/data/search';
$COMPLETIONS_URL = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions';
// --- Step 1: Search ---
function search(string $query, int $limit = 5): array {
global $API_KEY, $TENANT, $SEARCH_URL;
$token = $API_KEY;
$ch = curl_init($SEARCH_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'query' => $query, 'collection' => 'GlooProd',
'tenant' => $TENANT, 'limit' => $limit, 'certainty' => 0.5,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token, 'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
// --- Steps 2-3: Extract & Format ---
function extractSnippets(array $results, int $max = 5, int $maxChars = 500): array {
$snippets = [];
foreach (array_slice($results['data'] ?? [], 0, $max) as $result) {
$props = $result['properties'] ?? [];
$snippets[] = [
'text' => substr($props['snippet'] ?? '', 0, $maxChars),
'title' => $props['item_title'] ?? 'N/A',
'type' => $props['type'] ?? 'N/A',
];
}
return $snippets;
}
function formatContext(array $snippets): string {
$parts = [];
foreach ($snippets as $i => $s) {
$num = $i + 1;
$parts[] = "[Source $num: {$s['title']} ({$s['type']})]\n{$s['text']}\n";
}
return implode("\n---\n", $parts);
}
// --- Step 4: Generate Response ---
function generateWithContext(string $query, string $context): string {
global $API_KEY, $COMPLETIONS_URL;
$token = $API_KEY;
$ch = curl_init($COMPLETIONS_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'messages' => [
['role' => 'system', 'content' =>
'You are a helpful assistant. Answer the user\'s question based on the '
. 'provided context. If the context doesn\'t contain relevant information, '
. 'say so honestly.'],
['role' => 'user', 'content' => "Context:\n$context\n\nQuestion: $query"],
],
'auto_routing' => true,
'max_tokens' => 3000,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token, 'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$result = curl_exec($ch);
curl_close($ch);
$data = json_decode($result, true);
return $data['choices'][0]['message']['content'] ?? '';
}
// --- Run Complete RAG Flow ---
$query = $argv[2] ?? 'How can I know my purpose?';
$limit = isset($argv[3]) ? (int)$argv[3] : 5;
echo "RAG Search for: '$query'\n\n";
echo "Step 1: Searching for relevant content...\n";
$results = search($query, $limit);
echo "Found " . count($results['data'] ?? []) . " results\n\n";
echo "Step 2: Extracting snippets...\n";
$snippets = extractSnippets($results);
$context = formatContext($snippets);
echo "Extracted " . count($snippets) . " snippets\n\n";
echo "Step 3: Generating response with context...\n\n";
$response = generateWithContext($query, $context);
echo "=== Generated Response ===\n";
echo $response . "\n";
echo "\n=== Sources Used ===\n";
foreach ($snippets as $s) {
echo "- {$s['title']} ({$s['type']})\n";
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
)
var (
apiKey, tenant string
searchURL = "https://platform.ai.gloo.com/ai/v1/data/search"
completionsURL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"
)
type Snippet struct{ Text, Title, Type string }
func search(query string, limit int) map[string]interface{} {
token := apiKey
payload, _ := json.Marshal(map[string]interface{}{
"query": query, "collection": "GlooProd",
"tenant": tenant, "limit": limit, "certainty": 0.5,
})
req, _ := http.NewRequest("POST", searchURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "Search request failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Fprintf(os.Stderr, "Search decode failed: %v\n", err)
os.Exit(1)
}
return result
}
func extractSnippets(results map[string]interface{}, max, maxChars int) []Snippet {
data, _ := results["data"].([]interface{})
var snippets []Snippet
for i, item := range data {
if i >= max { break }
r := item.(map[string]interface{})
props := r["properties"].(map[string]interface{})
text, _ := props["snippet"].(string)
if len(text) > maxChars { text = text[:maxChars] }
snippets = append(snippets, Snippet{
Text: text, Title: fmt.Sprint(props["item_title"]), Type: fmt.Sprint(props["type"]),
})
}
return snippets
}
func formatContext(snippets []Snippet) string {
var parts []string
for i, s := range snippets {
parts = append(parts, fmt.Sprintf("[Source %d: %s (%s)]\n%s\n", i+1, s.Title, s.Type, s.Text))
}
result := ""
for i, p := range parts {
if i > 0 { result += "\n---\n" }
result += p
}
return result
}
func generateWithContext(query, context string) string {
token := apiKey
payload, _ := json.Marshal(map[string]interface{}{
"messages": []map[string]string{
{"role": "system", "content": "You are a helpful assistant. Answer the user's question based on the provided context. If the context doesn't contain relevant information, say so honestly."},
{"role": "user", "content": fmt.Sprintf("Context:\n%s\n\nQuestion: %s", context, query)},
},
"auto_routing": true, "max_tokens": 3000,
})
req, _ := http.NewRequest("POST", completionsURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "Completions request failed: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
fmt.Fprintf(os.Stderr, "Completions decode failed: %v\n", err)
os.Exit(1)
}
choices := result["choices"].([]interface{})
msg := choices[0].(map[string]interface{})["message"].(map[string]interface{})
return msg["content"].(string)
}
func main() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
tenant = os.Getenv("GLOO_TENANT")
query := "How can I know my purpose?"
limit := 5
if len(os.Args) > 2 { query = os.Args[2] }
if len(os.Args) > 3 { limit, _ = strconv.Atoi(os.Args[3]) }
fmt.Printf("RAG Search for: '%s'\n\n", query)
fmt.Println("Step 1: Searching for relevant content...")
results := search(query, limit)
data, _ := results["data"].([]interface{})
fmt.Printf("Found %d results\n\n", len(data))
fmt.Println("Step 2: Extracting snippets...")
snippets := extractSnippets(results, 5, 500)
context := formatContext(snippets)
fmt.Printf("Extracted %d snippets\n\n", len(snippets))
fmt.Println("Step 3: Generating response with context...\n")
response := generateWithContext(query, context)
fmt.Println("=== Generated Response ===")
fmt.Println(response)
fmt.Println("\n=== Sources Used ===")
for _, s := range snippets { fmt.Printf("- %s (%s)\n", s.Title, s.Type) }
}
```
```java Java theme={null}
import com.google.gson.Gson;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;
public class RAGSearchExample {
static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
static final String API_KEY = dotenv.get("GLOO_API_KEY", "YOUR_API_KEY");
static final String TENANT = dotenv.get("GLOO_TENANT");
static final String SEARCH_URL = "https://platform.ai.gloo.com/ai/v1/data/search";
static final String COMPLETIONS_URL = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions";
static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)).build();
static final Gson gson = new Gson();
static Map search(String query, int limit) throws Exception {
String token = API_KEY;
Map payload = Map.of(
"query", query, "collection", "GlooProd",
"tenant", TENANT, "limit", limit, "certainty", 0.5);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(SEARCH_URL))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.timeout(Duration.ofSeconds(60)).build();
HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(resp.body(), Map.class);
}
static List> extractSnippets(Map results, int max, int maxChars) {
List data = (List) results.get("data");
if (data == null) return List.of();
List> snippets = new ArrayList<>();
for (int i = 0; i < Math.min(data.size(), max); i++) {
Map props = (Map) data.get(i).get("properties");
String text = (String) props.getOrDefault("snippet", "");
if (text.length() > maxChars) text = text.substring(0, maxChars);
snippets.add(Map.of(
"text", text,
"title", (String) props.getOrDefault("item_title", "N/A"),
"type", (String) props.getOrDefault("type", "N/A")));
}
return snippets;
}
static String formatContext(List> snippets) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < snippets.size(); i++) {
if (i > 0) sb.append("\n---\n");
Map s = snippets.get(i);
sb.append(String.format("[Source %d: %s (%s)]\n%s\n", i+1, s.get("title"), s.get("type"), s.get("text")));
}
return sb.toString();
}
static String generateWithContext(String query, String context) throws Exception {
String token = API_KEY;
Map payload = Map.of(
"messages", List.of(
Map.of("role", "system", "content",
"You are a helpful assistant. Answer the user's question based on the " +
"provided context. If the context doesn't contain relevant information, " +
"say so honestly."),
Map.of("role", "user", "content",
"Context:\n" + context + "\n\nQuestion: " + query)),
"auto_routing", true, "max_tokens", 3000);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(COMPLETIONS_URL))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
.timeout(Duration.ofSeconds(60)).build();
HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
Map result = gson.fromJson(resp.body(), Map.class);
List choices = (List) result.get("choices");
Map msg = (Map) choices.get(0).get("message");
return (String) msg.get("content");
}
public static void main(String[] args) throws Exception {
String query = args.length > 0 ? args[0] : "How can I know my purpose?";
int limit = args.length > 1 ? Integer.parseInt(args[1]) : 5;
System.out.printf("RAG Search for: '%s'%n%n", query);
System.out.println("Step 1: Searching for relevant content...");
Map results = search(query, limit);
List data = (List) results.get("data");
System.out.printf("Found %d results%n%n", data != null ? data.size() : 0);
System.out.println("Step 2: Extracting snippets...");
var snippets = extractSnippets(results, 5, 500);
String context = formatContext(snippets);
System.out.printf("Extracted %d snippets%n%n", snippets.size());
System.out.println("Step 3: Generating response with context...\n");
String response = generateWithContext(query, context);
System.out.println("=== Generated Response ===");
System.out.println(response);
System.out.println("\n=== Sources Used ===");
for (var s : snippets) {
System.out.printf("- %s (%s)%n", s.get("title"), s.get("type"));
}
}
}
```
### What You'll See
The RAG flow searches, extracts context, then generates an AI response grounded in your content:
```
RAG Search for: 'How can I know my purpose?'
Step 1: Searching for relevant content...
Found 5 results
Step 2: Extracting snippets...
Extracted 5 snippets
Step 3: Generating response with context...
=== Generated Response ===
Based on the provided articles, finding purpose involves orienting your life
toward God and cultivating specific spiritual qualities. Here are a few key ideas:
- **Relationship with God:** True happiness and purpose are found in your
relationship with God and aligning your life with His will. (Sources 3, 5)
- **The Beatitudes:** Jesus provided a "blueprint for blessedness" in the
Beatitudes (Matthew 5:3-12). (Sources 1, 5)
=== Sources Used ===
- Finding True Happiness (Article)
- Finding True Happiness (Article)
- Beatitudes True Happiness (Article)
```
### Run the Cookbook Example
```bash Python theme={null}
python search_advanced.py rag "How can I know my purpose?" 5
```
```bash JavaScript theme={null}
node search-advanced.js rag "How can I know my purpose?" 5
```
```bash TypeScript theme={null}
npx ts-node search-advanced.ts rag "How can I know my purpose?" 5
```
```bash PHP theme={null}
php search_advanced.php rag "How can I know my purpose?" 5
```
```bash Go theme={null}
go run . rag "How can I know my purpose?" 5
```
```bash Java theme={null}
mvn exec:java -Dexec.args='rag "How can I know my purpose?" 5'
```
### Key Concepts
* **`auto_routing: true`** — Lets Gloo AI automatically select the best model
* **System prompt** — Customize to match your use case (tone, format, domain rules)
* **Context formatting** — Source labels help the LLM cite correctly
* **Token budget** — Keep context concise. 3-5 snippets of \~500 chars each works well
### Search + Completions V2 vs Grounded Completions
| | Search + Completions V2 | Grounded Completions |
| - | - | - |
| **Control** | Full control over context, prompts, ordering | Gloo handles context automatically |
| **Complexity** | More code, more flexibility | Single API call |
| **Custom prompts** | Yes — any system prompt | Limited customization |
| **Context formatting** | You control structure and ordering | Gloo optimizes automatically |
| **Best for** | Custom UX, domain-specific needs | Quick prototyping, standard Q\&A |
Both approaches use the same underlying search. Start with [Grounded
Completions](/tutorials/completions-grounded) if you want simplicity, then
switch to Search + Completions V2 when you need more control.
***
## Try It: Frontend Example
The cookbook includes a browser-based frontend that connects to a proxy server, giving you a visual way to test both search and RAG. The proxy server keeps your credentials secure on the server side.
### Architecture
```
Browser (HTML/JS) → Proxy Server (localhost:3000) → Gloo AI APIs
```
The proxy server exposes two endpoints:
* `GET /api/search?q=&limit=` — Basic search
* `POST /api/search/rag` — Search + RAG with Completions V2
### Start the Proxy Server
Each language includes a proxy server. Start one:
```bash Python theme={null}
cd search-tutorial/python
source venv/bin/activate
python server.py
```
```bash JavaScript theme={null}
cd search-tutorial/javascript
node server.js
```
```bash TypeScript theme={null}
cd search-tutorial/typescript
npx ts-node server.ts
```
```bash PHP theme={null}
cd search-tutorial/php
php -S localhost:3000 server.php
```
```bash Go theme={null}
cd search-tutorial/go
go run . server
```
```bash Java theme={null}
cd search-tutorial/java
mvn exec:java -Dexec.args="server"
```
Then open `http://localhost:3000` in your browser.
### Search Results
Enter a query and click **Search** to see results with titles, content types, and relevance scores:
### AI-Powered Answers (RAG)
Click **Ask AI** to send the same query through the RAG pipeline. The AI generates a response grounded in your search results, with sources listed:
The frontend is language-agnostic — the same HTML/JS works with any language's
proxy server. This is one approach; customize for your branding and framework.
***
## Complete Working Examples
Clone or browse the complete working examples for all 6 languages (JavaScript,
TypeScript, Python, PHP, Go, Java) with setup instructions, proxy servers, and
a browser-based frontend.
### What's Included
Each language implementation provides:
* **`auth`** — Shared API key configuration
* **`config`** — Centralized configuration (URLs, env vars, RAG settings)
* **`search_basic`** — Basic search (CLI script)
* **`search_advanced`** — Advanced search + RAG helpers (CLI script)
* **`server`** — Proxy server exposing REST endpoints for the frontend
***
## Troubleshooting
### No Results Returned
* **Missing `certainty`**: Add `"certainty": 0.5` to your payload. The API defaults to `0.75` when omitted, which may be too strict.
* **Content not indexed**: Test in the [Search Playground](/studio/playground-search) first. If results appear there but not via API, check your `tenant` name.
* **Wrong tenant**: Results are scoped to your publisher. Verify the tenant name matches your publisher in [Organizations](/studio/manage-organizations). Remember that tenant names are **normalized** — spaces are stripped (e.g. `Example Publisher Name` → `ExamplePublisherName`). If you're unsure of the exact string, pull it from `GET /ai/v1/data/tenant/names`.
### 403 Forbidden
* You can only access your own publisher's content. Other tenant names return 403.
* A 403 may also mean your API key lacks the gateway permission for the search route (this is a separate permission from grounded completions). Contact support to provision access.
* Verify your API key is correct and has not been revoked.
### Slow Responses
* Reduce the `limit` parameter. Request time rises non-linearly with larger result sets.
* Start with `limit=10` and increase only as needed.
### Authentication Errors
* If your API key is rejected, verify it is correct and has not been revoked (see [Authentication Tutorial](/tutorials/authentication)).
* Ensure you're using `Bearer {api_key}` in the Authorization header.
### Empty RAG Responses
* Verify search returns results before calling Completions V2.
* Use `auto_routing: true` instead of specifying a model name.
***
## Next Steps
Simpler RAG approach — single API call with automatic context management.
Add more content to your Data Engine for richer search results.
Full control over LLM interactions with custom prompts and parameters.
Complete endpoint documentation with request/response schemas.
# Upload Files to Data Engine
Source: https://docs.gloo.com/tutorials/upload-files
Learn how to upload files to the Gloo AI Data Engine for processing and AI-powered search.
This recipe shows how to upload files directly to the Gloo AI Data Engine using the Upload Files API. You'll learn to upload single files, batch multiple files, associate custom IDs, and add metadata to your content.
**Upload once. Search instantly. Build faster.**
The Upload Files API lets you send any file directly to Gloo for real-time processing and indexing. Within minutes, your content becomes searchable, context-aware, and ready for AI-driven interactions.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* Your Publisher ID from your Gloo AI Studio account
* **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first
The Upload Files API requires an API key for authentication. If you haven't set up authentication yet, follow the [Authentication Tutorial](/tutorials/authentication).
***
## Understanding the Upload Files API
The Upload Files API allows you to upload documents that get processed and made available for search and AI interaction. The primary endpoint is:
**POST** `/ingestion/v2/files`
### Key Features
* **Direct File Upload**: Send files via multipart/form-data
* **Multi-File Support**: Upload multiple files in a single request
* **Automatic Processing**: Files are parsed, indexed, and made searchable
* **Format Detection**: Automatic detection and processing of various file types
* **Duplicate Detection**: Identifies and reports duplicate content
### Required Fields
The Upload Files API requires two form fields:
* `publisher_id`: Your publisher ID from Gloo AI Studio
* `files`: The file(s) to upload (use the field name `files`, not `file`)
### Supported File Types
* PDF documents
* Microsoft Word (.doc, .docx)
* Plain text (.txt)
* Markdown (.md)
* And more
***
## Step 1: Basic Single File Upload
Let's start with uploading a single file. This demonstrates the core API call with proper authentication.
```bash Shell theme={null}
curl -X POST https://platform.ai.gloo.com/ingestion/v2/files \
-H "Authorization: Bearer $GLOO_API_KEY" \
-F "publisher_id=$GLOO_PUBLISHER_ID" \
-F "files=@/path/to/your/document.pdf"
```
```python Python theme={null}
import requests
import os
from dotenv import load_dotenv
load_dotenv()
# Get API key (see Authentication tutorial)
API_KEY = os.getenv("GLOO_API_KEY")
PUBLISHER_ID = os.getenv("GLOO_PUBLISHER_ID")
API_URL = "https://platform.ai.gloo.com/ingestion/v2/files"
def upload_single_file(file_path):
"""Upload a single file to the Data Engine."""
headers = {
"Authorization": f"Bearer {API_KEY}"
}
with open(file_path, 'rb') as f:
files = {"files": (os.path.basename(file_path), f)}
data = {"publisher_id": PUBLISHER_ID}
response = requests.post(API_URL, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
# Example usage
if __name__ == "__main__":
result = upload_single_file("/path/to/your/document.pdf")
print(f"Upload result: {result}")
```
```javascript JavaScript theme={null}
const fs = require('fs');
const path = require('path');
const FormData = require('form-data');
const axios = require('axios');
require('dotenv').config();
const API_KEY = process.env.GLOO_API_KEY;
const PUBLISHER_ID = process.env.GLOO_PUBLISHER_ID;
const API_URL = "https://platform.ai.gloo.com/ingestion/v2/files";
async function uploadSingleFile(filePath) {
const formData = new FormData();
formData.append('files', fs.createReadStream(filePath));
formData.append('publisher_id', PUBLISHER_ID);
const response = await axios.post(API_URL, formData, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
}
});
return response.data;
}
// Example usage
uploadSingleFile('/path/to/your/document.pdf')
.then(result => console.log('Upload result:', result))
.catch(error => console.error('Upload failed:', error.message));
```
```typescript TypeScript theme={null}
import * as fs from 'fs';
import * as path from 'path';
import FormData from 'form-data';
import axios from 'axios';
import * as dotenv from 'dotenv';
dotenv.config();
const API_KEY = process.env.GLOO_API_KEY;
const PUBLISHER_ID = process.env.GLOO_PUBLISHER_ID;
const API_URL = "https://platform.ai.gloo.com/ingestion/v2/files";
interface UploadResponse {
success: boolean;
message: string;
ingesting: string[];
duplicates: string[];
}
async function uploadSingleFile(filePath: string): Promise {
const formData = new FormData();
formData.append('files', fs.createReadStream(filePath));
formData.append('publisher_id', PUBLISHER_ID);
const response = await axios.post(API_URL, formData, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
}
});
return response.data;
}
// Example usage
uploadSingleFile('/path/to/your/document.pdf')
.then(result => console.log('Upload result:', result))
.catch(error => console.error('Upload failed:', error.message));
```
```php PHP theme={null}
load();
$API_KEY = $_ENV['GLOO_API_KEY'] ?? null;
$PUBLISHER_ID = $_ENV['GLOO_PUBLISHER_ID'] ?? null;
$API_URL = 'https://platform.ai.gloo.com/ingestion/v2/files';
if (!$API_KEY) {
die("Error: GLOO_API_KEY environment variable is required\n");
}
function uploadSingleFile(string $filePath): array {
global $API_KEY, $PUBLISHER_ID, $API_URL;
$ch = curl_init();
$postFields = [
'files' => new CURLFile($filePath, mime_content_type($filePath), basename($filePath)),
'publisher_id' => $PUBLISHER_ID
];
curl_setopt($ch, CURLOPT_URL, $API_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $API_KEY
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception('Upload failed: ' . curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
// Example usage
$result = uploadSingleFile('/path/to/your/document.pdf');
echo "Upload result: " . json_encode($result, JSON_PRETTY_PRINT) . "\n";
?>
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"github.com/joho/godotenv"
)
var (
apiKey string
publisherID string
apiURL = "https://platform.ai.gloo.com/ingestion/v2/files"
)
type UploadResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Ingesting []string `json:"ingesting"`
Duplicates []string `json:"duplicates"`
}
func init() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
publisherID = os.Getenv("GLOO_PUBLISHER_ID")
if apiKey == "" {
fmt.Println("Error: GLOO_API_KEY environment variable is required")
os.Exit(1)
}
}
func uploadSingleFile(filePath string) (*UploadResponse, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("files", filepath.Base(filePath))
if err != nil {
return nil, err
}
if _, err := io.Copy(part, file); err != nil {
return nil, err
}
// Add publisher_id field
if err := writer.WriteField("publisher_id", publisherID); err != nil {
return nil, err
}
writer.Close()
req, err := http.NewRequest("POST", apiURL, &body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("upload failed: %s", string(respBody))
}
var result UploadResponse
json.Unmarshal(respBody, &result)
return &result, nil
}
func main() {
result, err := uploadSingleFile("/path/to/your/document.pdf")
if err != nil {
fmt.Printf("Upload failed: %v\n", err)
return
}
fmt.Printf("Upload result: %+v\n", result)
}
```
```java Java theme={null}
import io.github.cdimascio.dotenv.Dotenv;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
public class FileUploader {
private static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
private static final String API_KEY = dotenv.get("GLOO_API_KEY");
private static final String PUBLISHER_ID = dotenv.get("GLOO_PUBLISHER_ID");
private static final String API_URL = "https://platform.ai.gloo.com/ingestion/v2/files";
private static final HttpClient httpClient = HttpClient.newHttpClient();
public static String uploadSingleFile(String filePath) throws IOException, InterruptedException {
if (API_KEY == null || API_KEY.isEmpty()) {
throw new IllegalStateException("GLOO_API_KEY environment variable is required");
}
Path path = Path.of(filePath);
String boundary = UUID.randomUUID().toString();
byte[] body = buildMultipartBody(boundary, path);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new IOException("Upload failed: " + response.body());
}
return response.body();
}
private static byte[] buildMultipartBody(String boundary, Path filePath) throws IOException {
String fileName = filePath.getFileName().toString();
byte[] fileContent = Files.readAllBytes(filePath);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Add publisher_id field
out.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
out.write("Content-Disposition: form-data; name=\"publisher_id\"\r\n\r\n".getBytes(StandardCharsets.UTF_8));
out.write((PUBLISHER_ID + "\r\n").getBytes(StandardCharsets.UTF_8));
// Add file field
out.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
out.write(("Content-Disposition: form-data; name=\"files\"; filename=\"" + fileName + "\"\r\n").getBytes(StandardCharsets.UTF_8));
out.write("Content-Type: application/octet-stream\r\n\r\n".getBytes(StandardCharsets.UTF_8));
out.write(fileContent);
out.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
return out.toByteArray();
}
public static void main(String[] args) {
try {
String result = uploadSingleFile("/path/to/your/document.pdf");
System.out.println("Upload result: " + result);
} catch (Exception e) {
System.err.println("Upload failed: " + e.getMessage());
}
}
}
```
### Expected Response
```json theme={null}
{
"success": true,
"message": "File processing started in background.",
"ingesting": [
"c999008e-de60-495c-8c9f-6a4b59cdb04b"
],
"duplicates": []
}
```
***
## Step 2: Multi-File Upload
You can upload multiple files in a single request for better efficiency.
You can mix file types freely - the endpoint automatically detects and processes each format.
```bash Shell theme={null}
curl -X POST https://platform.ai.gloo.com/ingestion/v2/files \
-H "Authorization: Bearer $GLOO_API_KEY" \
-F "publisher_id=$GLOO_PUBLISHER_ID" \
-F "files=@/path/to/document1.pdf" \
-F "files=@/path/to/document2.pdf" \
-F "files=@/path/to/document3.docx"
```
```python Python theme={null}
def upload_multiple_files(file_paths):
"""Upload multiple files to the Data Engine."""
headers = {
"Authorization": f"Bearer {API_KEY}"
}
files = []
open_files = []
try:
for file_path in file_paths:
f = open(file_path, 'rb')
open_files.append(f)
files.append(('files', (os.path.basename(file_path), f)))
data = {"publisher_id": PUBLISHER_ID}
response = requests.post(API_URL, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
finally:
for f in open_files:
f.close()
# Example usage
if __name__ == "__main__":
files_to_upload = [
"/path/to/document1.pdf",
"/path/to/document2.pdf",
"/path/to/document3.docx"
]
result = upload_multiple_files(files_to_upload)
print(f"Uploaded {len(result.get('ingesting', []))} files")
if result.get('duplicates'):
print(f"Duplicates found: {len(result['duplicates'])}")
```
```javascript JavaScript theme={null}
async function uploadMultipleFiles(filePaths) {
const formData = new FormData();
for (const filePath of filePaths) {
formData.append('files', fs.createReadStream(filePath));
}
formData.append('publisher_id', PUBLISHER_ID);
const response = await axios.post(API_URL, formData, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
}
});
return response.data;
}
// Example usage
const filesToUpload = [
'/path/to/document1.pdf',
'/path/to/document2.pdf',
'/path/to/document3.docx'
];
uploadMultipleFiles(filesToUpload)
.then(result => {
console.log(`Uploaded ${result.ingesting?.length || 0} files`);
if (result.duplicates?.length) {
console.log(`Duplicates found: ${result.duplicates.length}`);
}
})
.catch(error => console.error('Upload failed:', error.message));
```
```typescript TypeScript theme={null}
async function uploadMultipleFiles(filePaths: string[]): Promise {
const formData = new FormData();
for (const filePath of filePaths) {
formData.append('files', fs.createReadStream(filePath));
}
formData.append('publisher_id', PUBLISHER_ID);
const response = await axios.post(API_URL, formData, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
}
});
return response.data;
}
// Example usage
const filesToUpload = [
'/path/to/document1.pdf',
'/path/to/document2.pdf',
'/path/to/document3.docx'
];
uploadMultipleFiles(filesToUpload)
.then(result => {
console.log(`Uploaded ${result.ingesting?.length || 0} files`);
if (result.duplicates?.length) {
console.log(`Duplicates found: ${result.duplicates.length}`);
}
})
.catch(error => console.error('Upload failed:', error.message));
```
```php PHP theme={null}
function uploadMultipleFiles(array $filePaths): array {
global $API_KEY, $PUBLISHER_ID, $API_URL;
$ch = curl_init();
$postFields = ['publisher_id' => $PUBLISHER_ID];
foreach ($filePaths as $index => $filePath) {
$postFields["files[$index]"] = new CURLFile(
$filePath,
mime_content_type($filePath),
basename($filePath)
);
}
curl_setopt($ch, CURLOPT_URL, $API_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $API_KEY
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception('Upload failed: ' . curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
// Example usage
$filesToUpload = [
'/path/to/document1.pdf',
'/path/to/document2.pdf',
'/path/to/document3.docx'
];
$result = uploadMultipleFiles($filesToUpload);
echo "Uploaded " . count($result['ingesting'] ?? []) . " files\n";
if (!empty($result['duplicates'])) {
echo "Duplicates found: " . count($result['duplicates']) . "\n";
}
```
```go Go theme={null}
func uploadMultipleFiles(filePaths []string) (*UploadResponse, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for _, filePath := range filePaths {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
part, err := writer.CreateFormFile("files", filepath.Base(filePath))
if err != nil {
file.Close()
return nil, err
}
if _, err := io.Copy(part, file); err != nil {
file.Close()
return nil, err
}
file.Close()
}
// Add publisher_id field
if err := writer.WriteField("publisher_id", publisherID); err != nil {
return nil, err
}
writer.Close()
req, err := http.NewRequest("POST", apiURL, &body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("upload failed: %s", string(respBody))
}
var result UploadResponse
json.Unmarshal(respBody, &result)
return &result, nil
}
func main() {
filesToUpload := []string{
"/path/to/document1.pdf",
"/path/to/document2.pdf",
"/path/to/document3.docx",
}
result, err := uploadMultipleFiles(filesToUpload)
if err != nil {
fmt.Printf("Upload failed: %v\n", err)
return
}
fmt.Printf("Uploaded %d files\n", len(result.Ingesting))
if len(result.Duplicates) > 0 {
fmt.Printf("Duplicates found: %d\n", len(result.Duplicates))
}
}
```
```java Java theme={null}
public static String uploadMultipleFiles(String[] filePaths) throws IOException, InterruptedException {
if (API_KEY == null || API_KEY.isEmpty()) {
throw new IllegalStateException("GLOO_API_KEY environment variable is required");
}
String boundary = UUID.randomUUID().toString();
StringBuilder body = new StringBuilder();
// Add publisher_id field
body.append("--").append(boundary).append("\r\n");
body.append("Content-Disposition: form-data; name=\"publisher_id\"\r\n\r\n");
body.append(PUBLISHER_ID).append("\r\n");
for (String filePath : filePaths) {
Path path = Path.of(filePath);
String fileName = path.getFileName().toString();
byte[] fileContent = Files.readAllBytes(path);
body.append("--").append(boundary).append("\r\n");
body.append("Content-Disposition: form-data; name=\"files\"; filename=\"")
.append(fileName).append("\"\r\n");
body.append("Content-Type: application/octet-stream\r\n\r\n");
body.append(new String(fileContent));
body.append("\r\n");
}
body.append("--").append(boundary).append("--\r\n");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new IOException("Upload failed: " + response.body());
}
return response.body();
}
public static void main(String[] args) {
try {
String[] filesToUpload = {
"/path/to/document1.pdf",
"/path/to/document2.pdf",
"/path/to/document3.docx"
};
String result = uploadMultipleFiles(filesToUpload);
System.out.println("Upload result: " + result);
} catch (Exception e) {
System.err.println("Upload failed: " + e.getMessage());
}
}
```
### Expected Response
```json theme={null}
{
"success": true,
"message": "File processing started in background.",
"ingesting": [
"c999008e-de60-495c-8c9f-6a4b59cdb04b",
"b10e85d8-243d-46e2-9504-d93874a9ebcb",
"b45058a8-2f8c-4a88-8aba-7adb4afcd38d"
],
"duplicates": []
}
```
***
## Step 3: Using Producer ID
The `producer_id` query parameter lets you associate your internal ID with an uploaded file. This is useful for tracking and updating content from your system.
If the `producer_id` field is supplied with a multi-file upload, the ID will be ignored. Producer IDs must have a one-to-one relationship with a file.
```bash Shell theme={null}
curl -X POST "https://platform.ai.gloo.com/ingestion/v2/files?producer_id=my-internal-id-12345" \
-H "Authorization: Bearer $GLOO_API_KEY" \
-F "publisher_id=$GLOO_PUBLISHER_ID" \
-F "files=@/path/to/your/document.pdf"
```
```python Python theme={null}
def upload_file_with_producer_id(file_path, producer_id):
"""Upload a file with a custom producer ID."""
headers = {
"Authorization": f"Bearer {API_KEY}"
}
params = {
"producer_id": producer_id
}
with open(file_path, 'rb') as f:
files = {"files": (os.path.basename(file_path), f)}
data = {"publisher_id": PUBLISHER_ID}
response = requests.post(API_URL, headers=headers, files=files, data=data, params=params)
response.raise_for_status()
return response.json()
# Example usage
if __name__ == "__main__":
result = upload_file_with_producer_id(
"/path/to/document.pdf",
"my-internal-id-12345"
)
print(f"Upload result: {result}")
```
```javascript JavaScript theme={null}
async function uploadFileWithProducerId(filePath, producerId) {
const formData = new FormData();
formData.append('files', fs.createReadStream(filePath));
formData.append('publisher_id', PUBLISHER_ID);
const response = await axios.post(API_URL, formData, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
},
params: {
producer_id: producerId
}
});
return response.data;
}
// Example usage
uploadFileWithProducerId('/path/to/document.pdf', 'my-internal-id-12345')
.then(result => console.log('Upload result:', result))
.catch(error => console.error('Upload failed:', error.message));
```
```typescript TypeScript theme={null}
async function uploadFileWithProducerId(filePath: string, producerId: string): Promise {
const formData = new FormData();
formData.append('files', fs.createReadStream(filePath));
formData.append('publisher_id', PUBLISHER_ID);
const response = await axios.post(API_URL, formData, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
},
params: {
producer_id: producerId
}
});
return response.data;
}
// Example usage
uploadFileWithProducerId('/path/to/document.pdf', 'my-internal-id-12345')
.then(result => console.log('Upload result:', result))
.catch(error => console.error('Upload failed:', error.message));
```
```php PHP theme={null}
function uploadFileWithProducerId(string $filePath, string $producerId): array {
global $API_KEY, $PUBLISHER_ID, $API_URL;
$url = $API_URL . '?' . http_build_query(['producer_id' => $producerId]);
$ch = curl_init();
$postFields = [
'files' => new CURLFile($filePath, mime_content_type($filePath), basename($filePath)),
'publisher_id' => $PUBLISHER_ID
];
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $API_KEY
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception('Upload failed: ' . curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
// Example usage
$result = uploadFileWithProducerId('/path/to/document.pdf', 'my-internal-id-12345');
echo "Upload result: " . json_encode($result, JSON_PRETTY_PRINT) . "\n";
```
```go Go theme={null}
func uploadFileWithProducerId(filePath, producerId string) (*UploadResponse, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("files", filepath.Base(filePath))
if err != nil {
return nil, err
}
if _, err := io.Copy(part, file); err != nil {
return nil, err
}
// Add publisher_id field
if err := writer.WriteField("publisher_id", publisherID); err != nil {
return nil, err
}
writer.Close()
url := fmt.Sprintf("%s?producer_id=%s", apiURL, producerId)
req, err := http.NewRequest("POST", url, &body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("upload failed: %s", string(respBody))
}
var result UploadResponse
json.Unmarshal(respBody, &result)
return &result, nil
}
func main() {
result, err := uploadFileWithProducerId("/path/to/document.pdf", "my-internal-id-12345")
if err != nil {
fmt.Printf("Upload failed: %v\n", err)
return
}
fmt.Printf("Upload result: %+v\n", result)
}
```
```java Java theme={null}
public static String uploadFileWithProducerId(String filePath, String producerId)
throws IOException, InterruptedException {
if (API_KEY == null || API_KEY.isEmpty()) {
throw new IllegalStateException("GLOO_API_KEY environment variable is required");
}
Path path = Path.of(filePath);
String boundary = UUID.randomUUID().toString();
String body = buildMultipartBodyWithPublisher(boundary, path);
String url = API_URL + "?producer_id=" + producerId;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new IOException("Upload failed: " + response.body());
}
return response.body();
}
// Note: buildMultipartBodyWithPublisher includes publisher_id and uses "files" field name
// See the complete example in the sandbox code for full implementation
public static void main(String[] args) {
try {
String result = uploadFileWithProducerId("/path/to/document.pdf", "my-internal-id-12345");
System.out.println("Upload result: " + result);
} catch (Exception e) {
System.err.println("Upload failed: " + e.getMessage());
}
}
```
***
## Step 4: Adding Metadata to Uploaded Content
After uploading files, you can add or update metadata using the Update Item Metadata endpoint. Identify content by either Gloo's `item_id` (returned from the upload) or your `producer_id`.
Only the fields you include are updated; omitted fields remain unchanged.
```bash Shell theme={null}
curl -X POST https://platform.ai.gloo.com/engine/v2/item \
-H "Authorization: Bearer $GLOO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"publisher_id": "your-publisher-id",
"item_id": "c999008e-de60-495c-8c9f-6a4b59cdb04b",
"item_title": "Document Title",
"item_subtitle": "A brief subtitle",
"author": ["John Doe", "Jane Smith"],
"publication_date": "2025-01-15",
"item_tags": ["category1", "category2"]
}'
```
```python Python theme={null}
METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item"
def update_item_metadata(item_id=None, producer_id=None, publisher_id=None, **metadata):
"""Update metadata for an uploaded item."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
data = {
"publisher_id": publisher_id
}
if item_id:
data["item_id"] = item_id
if producer_id:
data["producer_id"] = producer_id
data.update(metadata)
response = requests.post(METADATA_URL, headers=headers, json=data)
response.raise_for_status()
return response.json()
# Example usage
if __name__ == "__main__":
result = update_item_metadata(
item_id="c999008e-de60-495c-8c9f-6a4b59cdb04b",
publisher_id="your-publisher-id",
item_title="Document Title",
item_subtitle="A brief subtitle",
author=["John Doe", "Jane Smith"],
publication_date="2025-01-15",
item_tags=["category1", "category2"]
)
print(f"Metadata update result: {result}")
```
```javascript JavaScript theme={null}
const METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item";
async function updateItemMetadata({ itemId, producerId, publisherId, ...metadata }) {
const data = {
publisher_id: publisherId,
...(itemId && { item_id: itemId }),
...(producerId && { producer_id: producerId }),
...metadata
};
const response = await axios.post(METADATA_URL, data, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
return response.data;
}
// Example usage
updateItemMetadata({
itemId: 'c999008e-de60-495c-8c9f-6a4b59cdb04b',
publisherId: 'your-publisher-id',
item_title: 'Document Title',
item_subtitle: 'A brief subtitle',
author: ['John Doe', 'Jane Smith'],
publication_date: '2025-01-15',
item_tags: ['category1', 'category2']
})
.then(result => console.log('Metadata update result:', result))
.catch(error => console.error('Update failed:', error.message));
```
```typescript TypeScript theme={null}
const METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item";
interface ItemMetadata {
itemId?: string;
producerId?: string;
publisherId: string;
item_title?: string;
item_subtitle?: string;
author?: string[];
publication_date?: string;
item_tags?: string[];
item_summary?: string;
item_url?: string;
item_image?: string;
}
interface MetadataResponse {
success: boolean;
message: string;
}
async function updateItemMetadata(metadata: ItemMetadata): Promise {
const { itemId, producerId, publisherId, ...rest } = metadata;
const data = {
publisher_id: publisherId,
...(itemId && { item_id: itemId }),
...(producerId && { producer_id: producerId }),
...rest
};
const response = await axios.post(METADATA_URL, data, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
return response.data;
}
// Example usage
updateItemMetadata({
itemId: 'c999008e-de60-495c-8c9f-6a4b59cdb04b',
publisherId: 'your-publisher-id',
item_title: 'Document Title',
item_subtitle: 'A brief subtitle',
author: ['John Doe', 'Jane Smith'],
publication_date: '2025-01-15',
item_tags: ['category1', 'category2']
})
.then(result => console.log('Metadata update result:', result))
.catch(error => console.error('Update failed:', error.message));
```
```php PHP theme={null}
$METADATA_URL = 'https://platform.ai.gloo.com/engine/v2/item';
function updateItemMetadata(array $params): array {
global $API_KEY, $METADATA_URL;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $METADATA_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $API_KEY,
'Content-Type: application/json'
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception('Update failed: ' . curl_error($ch));
}
curl_close($ch);
return json_decode($result, true);
}
// Example usage
$result = updateItemMetadata([
'publisher_id' => 'your-publisher-id',
'item_id' => 'c999008e-de60-495c-8c9f-6a4b59cdb04b',
'item_title' => 'Document Title',
'item_subtitle' => 'A brief subtitle',
'author' => ['John Doe', 'Jane Smith'],
'publication_date' => '2025-01-15',
'item_tags' => ['category1', 'category2']
]);
echo "Metadata update result: " . json_encode($result, JSON_PRETTY_PRINT) . "\n";
```
```go Go theme={null}
var metadataURL = "https://platform.ai.gloo.com/engine/v2/item"
type ItemMetadata struct {
PublisherID string `json:"publisher_id"`
ItemID string `json:"item_id,omitempty"`
ProducerID string `json:"producer_id,omitempty"`
ItemTitle string `json:"item_title,omitempty"`
ItemSubtitle string `json:"item_subtitle,omitempty"`
Author []string `json:"author,omitempty"`
PublicationDate string `json:"publication_date,omitempty"`
ItemTags []string `json:"item_tags,omitempty"`
}
type MetadataResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func updateItemMetadata(metadata ItemMetadata) (*MetadataResponse, error) {
jsonData, err := json.Marshal(metadata)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", metadataURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("update failed: %s", string(respBody))
}
var result MetadataResponse
json.Unmarshal(respBody, &result)
return &result, nil
}
func main() {
result, err := updateItemMetadata(ItemMetadata{
PublisherID: "your-publisher-id",
ItemID: "c999008e-de60-495c-8c9f-6a4b59cdb04b",
ItemTitle: "Document Title",
ItemSubtitle: "A brief subtitle",
Author: []string{"John Doe", "Jane Smith"},
PublicationDate: "2025-01-15",
ItemTags: []string{"category1", "category2"},
})
if err != nil {
fmt.Printf("Update failed: %v\n", err)
return
}
fmt.Printf("Metadata update result: %+v\n", result)
}
```
```java Java theme={null}
private static final String METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item";
public static String updateItemMetadata(String jsonPayload) throws IOException, InterruptedException {
if (API_KEY == null || API_KEY.isEmpty()) {
throw new IllegalStateException("GLOO_API_KEY environment variable is required");
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(METADATA_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new IOException("Update failed: " + response.body());
}
return response.body();
}
public static void main(String[] args) {
try {
String payload = """
{
"publisher_id": "your-publisher-id",
"item_id": "c999008e-de60-495c-8c9f-6a4b59cdb04b",
"item_title": "Document Title",
"item_subtitle": "A brief subtitle",
"author": ["John Doe", "Jane Smith"],
"publication_date": "2025-01-15",
"item_tags": ["category1", "category2"]
}
""";
String result = updateItemMetadata(payload);
System.out.println("Metadata update result: " + result);
} catch (Exception e) {
System.err.println("Update failed: " + e.getMessage());
}
}
```
### Available Metadata Fields
| Field | Type | Description |
| - | - | - |
| `publisher_id` | string | **Required.** Your publisher ID |
| `item_id` | string | Gloo's item ID (from upload response) |
| `producer_id` | string | Your internal ID |
| `item_title` | string | Document title |
| `item_subtitle` | string | Document subtitle |
| `file_name` | string | Original filename |
| `publication_date` | string | Publication date (YYYY-MM-DD) |
| `item_image` | string | Image URL |
| `item_url` | string | Source URL |
| `item_summary` | string | Brief summary |
| `author` | string\[] | List of authors |
| `item_tags` | string\[] | Categorization tags |
If you provide **both** `item_id` and `producer_id`, the service will **prefer `item_id`** when loading the target item. This allows for the `producer_id` to be updated.
***
## Step 5: Verifying Your Upload
After uploading content, you can verify it in Gloo AI Studio:
1. Log in to [Gloo AI Studio](https://studio.ai.gloo.com/)
2. Navigate to the **Data Engine** section from the main sidebar
3. Click on **Your Data**
You'll see your uploaded files with their processing status and metadata.
***
## Complete Example
Here's a complete example that combines authentication, file upload, and metadata update:
```python Python theme={null}
"""
Complete file upload workflow for Gloo AI Data Engine.
"""
import requests
import time
import os
from dotenv import load_dotenv
load_dotenv()
# Configuration
API_KEY = os.getenv("GLOO_API_KEY")
PUBLISHER_ID = os.getenv("GLOO_PUBLISHER_ID")
UPLOAD_URL = "https://platform.ai.gloo.com/ingestion/v2/files"
METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item"
def upload_file(file_path, producer_id=None):
"""Upload a file to the Data Engine."""
headers = {"Authorization": f"Bearer {API_KEY}"}
params = {"producer_id": producer_id} if producer_id else {}
with open(file_path, 'rb') as f:
files = {"files": (os.path.basename(file_path), f)}
data = {"publisher_id": PUBLISHER_ID}
response = requests.post(UPLOAD_URL, headers=headers, files=files, data=data, params=params)
response.raise_for_status()
return response.json()
def update_metadata(item_id, **metadata):
"""Update metadata for an uploaded item."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
data = {"publisher_id": PUBLISHER_ID, "item_id": item_id, **metadata}
response = requests.post(METADATA_URL, headers=headers, json=data)
response.raise_for_status()
return response.json()
def main():
"""Main workflow: upload file and add metadata."""
file_path = "/path/to/your/document.pdf"
# Step 1: Upload the file
print(f"Uploading {file_path}...")
upload_result = upload_file(file_path, producer_id="my-doc-001")
print(f"Upload result: {upload_result}")
if upload_result.get('ingesting'):
item_id = upload_result['ingesting'][0]
# Step 2: Add metadata
print(f"Adding metadata to item {item_id}...")
metadata_result = update_metadata(
item_id,
item_title="My Document Title",
author=["Author Name"],
publication_date="2025-01-15",
item_tags=["documentation", "api"]
)
print(f"Metadata result: {metadata_result}")
if __name__ == "__main__":
main()
```
```javascript JavaScript theme={null}
/**
* Complete file upload workflow for Gloo AI Data Engine.
*/
const fs = require('fs');
const path = require('path');
const FormData = require('form-data');
const axios = require('axios');
require('dotenv').config();
// Configuration
const API_KEY = process.env.GLOO_API_KEY;
const PUBLISHER_ID = process.env.GLOO_PUBLISHER_ID;
if (!API_KEY) {
console.error("Error: GLOO_API_KEY environment variable is required");
process.exit(1);
}
const UPLOAD_URL = "https://platform.ai.gloo.com/ingestion/v2/files";
const METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item";
async function uploadFile(filePath, producerId = null) {
const formData = new FormData();
formData.append('files', fs.createReadStream(filePath));
formData.append('publisher_id', PUBLISHER_ID);
const config = {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
}
};
if (producerId) {
config.params = { producer_id: producerId };
}
const response = await axios.post(UPLOAD_URL, formData, config);
return response.data;
}
async function updateMetadata(itemId, metadata) {
const data = {
publisher_id: PUBLISHER_ID,
item_id: itemId,
...metadata
};
const response = await axios.post(METADATA_URL, data, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
return response.data;
}
async function main() {
const filePath = '/path/to/your/document.pdf';
try {
// Step 1: Upload the file
console.log(`Uploading ${filePath}...`);
const uploadResult = await uploadFile(filePath, 'my-doc-001');
console.log('Upload result:', uploadResult);
if (uploadResult.ingesting?.length > 0) {
const itemId = uploadResult.ingesting[0];
// Step 2: Add metadata
console.log(`Adding metadata to item ${itemId}...`);
const metadataResult = await updateMetadata(itemId, {
item_title: 'My Document Title',
author: ['Author Name'],
publication_date: '2025-01-15',
item_tags: ['documentation', 'api']
});
console.log('Metadata result:', metadataResult);
}
} catch (error) {
console.error('Error:', error.response?.data || error.message);
}
}
main();
```
```typescript TypeScript theme={null}
/**
* Complete file upload workflow for Gloo AI Data Engine.
*/
import * as fs from 'fs';
import * as path from 'path';
import FormData from 'form-data';
import axios from 'axios';
import * as dotenv from 'dotenv';
dotenv.config();
// Configuration
const API_KEY = process.env.GLOO_API_KEY!;
const PUBLISHER_ID = process.env.GLOO_PUBLISHER_ID!;
if (!API_KEY) {
console.error("Error: GLOO_API_KEY environment variable is required");
process.exit(1);
}
const UPLOAD_URL = "https://platform.ai.gloo.com/ingestion/v2/files";
const METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item";
// Types
interface UploadResponse {
success: boolean;
message: string;
ingesting: string[];
duplicates: string[];
}
interface MetadataResponse {
success: boolean;
message: string;
}
async function uploadFile(filePath: string, producerId?: string): Promise {
const formData = new FormData();
formData.append('files', fs.createReadStream(filePath));
formData.append('publisher_id', PUBLISHER_ID);
const config: any = {
headers: {
'Authorization': `Bearer ${API_KEY}`,
...formData.getHeaders()
}
};
if (producerId) {
config.params = { producer_id: producerId };
}
const response = await axios.post(UPLOAD_URL, formData, config);
return response.data;
}
async function updateMetadata(itemId: string, metadata: Record): Promise {
const data = {
publisher_id: PUBLISHER_ID,
item_id: itemId,
...metadata
};
const response = await axios.post(METADATA_URL, data, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
return response.data;
}
async function main(): Promise {
const filePath = '/path/to/your/document.pdf';
try {
// Step 1: Upload the file
console.log(`Uploading ${filePath}...`);
const uploadResult = await uploadFile(filePath, 'my-doc-001');
console.log('Upload result:', uploadResult);
if (uploadResult.ingesting?.length > 0) {
const itemId = uploadResult.ingesting[0];
// Step 2: Add metadata
console.log(`Adding metadata to item ${itemId}...`);
const metadataResult = await updateMetadata(itemId, {
item_title: 'My Document Title',
author: ['Author Name'],
publication_date: '2025-01-15',
item_tags: ['documentation', 'api']
});
console.log('Metadata result:', metadataResult);
}
} catch (error: any) {
console.error('Error:', error.response?.data || error.message);
}
}
main();
```
```php PHP theme={null}
safeLoad();
// Configuration
$API_KEY = $_ENV['GLOO_API_KEY'] ?? '';
$PUBLISHER_ID = $_ENV['GLOO_PUBLISHER_ID'] ?? '';
if (empty($API_KEY)) {
die("Error: GLOO_API_KEY environment variable is required\n");
}
$UPLOAD_URL = 'https://platform.ai.gloo.com/ingestion/v2/files';
$METADATA_URL = 'https://platform.ai.gloo.com/engine/v2/item';
function uploadFile(string $filePath, ?string $producerId = null): array {
global $API_KEY, $UPLOAD_URL, $PUBLISHER_ID;
$url = $UPLOAD_URL;
if ($producerId) {
$url .= '?' . http_build_query(['producer_id' => $producerId]);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'files' => new CURLFile($filePath, mime_content_type($filePath), basename($filePath)),
'publisher_id' => $PUBLISHER_ID
]);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $API_KEY]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
function updateMetadata(string $itemId, array $metadata): array {
global $API_KEY, $METADATA_URL, $PUBLISHER_ID;
$data = array_merge(['publisher_id' => $PUBLISHER_ID, 'item_id' => $itemId], $metadata);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $METADATA_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $API_KEY,
'Content-Type: application/json'
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
// Main workflow
$filePath = '/path/to/your/document.pdf';
echo "Uploading $filePath...\n";
$uploadResult = uploadFile($filePath, 'my-doc-001');
echo "Upload result: " . json_encode($uploadResult) . "\n";
if (!empty($uploadResult['ingesting'])) {
$itemId = $uploadResult['ingesting'][0];
echo "Adding metadata to item $itemId...\n";
$metadataResult = updateMetadata($itemId, [
'item_title' => 'My Document Title',
'author' => ['Author Name'],
'publication_date' => '2025-01-15',
'item_tags' => ['documentation', 'api']
]);
echo "Metadata result: " . json_encode($metadataResult) . "\n";
}
?>
```
```go Go theme={null}
// Complete file upload workflow for Gloo AI Data Engine.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"github.com/joho/godotenv"
)
// Configuration
var (
apiKey string
publisherID string
uploadURL = "https://platform.ai.gloo.com/ingestion/v2/files"
metadataURL = "https://platform.ai.gloo.com/engine/v2/item"
)
// Types
type UploadResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Ingesting []string `json:"ingesting"`
Duplicates []string `json:"duplicates"`
}
type MetadataResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func init() {
godotenv.Load()
apiKey = os.Getenv("GLOO_API_KEY")
publisherID = os.Getenv("GLOO_PUBLISHER_ID")
if apiKey == "" {
fmt.Println("Error: GLOO_API_KEY environment variable is required")
os.Exit(1)
}
}
func uploadFile(filePath string, producerID string) (*UploadResponse, error) {
file, _ := os.Open(filePath)
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, _ := writer.CreateFormFile("files", filepath.Base(filePath))
io.Copy(part, file)
writer.WriteField("publisher_id", publisherID)
writer.Close()
targetURL := uploadURL
if producerID != "" {
u, _ := url.Parse(uploadURL)
q := u.Query()
q.Set("producer_id", producerID)
u.RawQuery = q.Encode()
targetURL = u.String()
}
req, _ := http.NewRequest("POST", targetURL, &body)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result UploadResponse
json.Unmarshal(respBody, &result)
return &result, nil
}
func updateMetadata(itemID string, metadata map[string]interface{}) (*MetadataResponse, error) {
metadata["publisher_id"] = publisherID
metadata["item_id"] = itemID
jsonData, _ := json.Marshal(metadata)
req, _ := http.NewRequest("POST", metadataURL, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result MetadataResponse
json.Unmarshal(respBody, &result)
return &result, nil
}
func main() {
filePath := "/path/to/your/document.pdf"
fmt.Printf("Uploading %s...\n", filePath)
uploadResult, _ := uploadFile(filePath, "my-doc-001")
fmt.Printf("Upload result: %+v\n", uploadResult)
if len(uploadResult.Ingesting) > 0 {
itemID := uploadResult.Ingesting[0]
fmt.Printf("Adding metadata to item %s...\n", itemID)
metadataResult, _ := updateMetadata(itemID, map[string]interface{}{
"item_title": "My Document Title",
"author": []string{"Author Name"},
"publication_date": "2025-01-15",
"item_tags": []string{"documentation", "api"},
})
fmt.Printf("Metadata result: %+v\n", metadataResult)
}
}
```
```java Java theme={null}
/**
* Complete file upload workflow for Gloo AI Data Engine.
*/
package com.gloo.tutorial.uploadfiles;
import io.github.cdimascio.dotenv.Dotenv;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
public class Main {
private static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
private static final String API_KEY = dotenv.get("GLOO_API_KEY");
private static final String PUBLISHER_ID = dotenv.get("GLOO_PUBLISHER_ID");
private static final String UPLOAD_URL = "https://platform.ai.gloo.com/ingestion/v2/files";
private static final String METADATA_URL = "https://platform.ai.gloo.com/engine/v2/item";
private static final HttpClient httpClient = HttpClient.newHttpClient();
private static final Gson gson = new Gson();
private static Map uploadFile(String filePath, String producerId) throws Exception {
if (API_KEY == null || API_KEY.isEmpty()) {
throw new IllegalStateException("GLOO_API_KEY environment variable is required");
}
Path path = Path.of(filePath);
String boundary = UUID.randomUUID().toString();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8), true);
// Add publisher_id
writer.append("--").append(boundary).append("\r\n");
writer.append("Content-Disposition: form-data; name=\"publisher_id\"\r\n\r\n");
writer.append(PUBLISHER_ID).append("\r\n");
// Add file
writer.append("--").append(boundary).append("\r\n");
writer.append("Content-Disposition: form-data; name=\"files\"; filename=\"")
.append(path.getFileName().toString()).append("\"\r\n");
writer.append("Content-Type: application/octet-stream\r\n\r\n");
writer.flush();
baos.write(Files.readAllBytes(path));
writer.append("\r\n--").append(boundary).append("--\r\n");
writer.flush();
String url = producerId != null ? UPLOAD_URL + "?producer_id=" + producerId : UPLOAD_URL;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(baos.toByteArray()))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(response.body(), Map.class);
}
private static Map updateMetadata(String itemId, Map metadata) throws Exception {
if (API_KEY == null || API_KEY.isEmpty()) {
throw new IllegalStateException("GLOO_API_KEY environment variable is required");
}
JsonObject data = new JsonObject();
data.addProperty("publisher_id", PUBLISHER_ID);
data.addProperty("item_id", itemId);
for (Map.Entry entry : metadata.entrySet()) {
if (entry.getValue() instanceof String) {
data.addProperty(entry.getKey(), (String) entry.getValue());
} else if (entry.getValue() instanceof List) {
data.add(entry.getKey(), gson.toJsonTree(entry.getValue()));
}
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(METADATA_URL))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(data)))
.build();
HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return gson.fromJson(response.body(), Map.class);
}
public static void main(String[] args) {
try {
String filePath = "/path/to/your/document.pdf";
System.out.println("Uploading " + filePath + "...");
Map uploadResult = uploadFile(filePath, "my-doc-001");
System.out.println("Upload result: " + uploadResult);
List ingesting = (List) uploadResult.get("ingesting");
if (ingesting != null && !ingesting.isEmpty()) {
String itemId = ingesting.get(0);
System.out.println("Adding metadata to item " + itemId + "...");
Map metadata = new HashMap<>();
metadata.put("item_title", "My Document Title");
metadata.put("author", List.of("Author Name"));
metadata.put("publication_date", "2025-01-15");
metadata.put("item_tags", List.of("documentation", "api"));
Map metadataResult = updateMetadata(itemId, metadata);
System.out.println("Metadata result: " + metadataResult);
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}
```
***
## Troubleshooting
### Error: 401 Unauthorized
**Cause**: API key is incorrect or has been revoked.
**Solution**: Verify your API key is correct and has not been revoked.
### Error: 403 Forbidden
**Cause**: Insufficient permissions for the publisher.
**Solution**: Verify you have access to the specified publisher in Gloo AI Studio.
### Error: 413 Request Entity Too Large
**Cause**: File size exceeds the limit.
**Solution**: Check the [API limits](/api-reference/general/limits) and consider splitting large files.
### Error: Duplicate detected
**Cause**: A file with identical content already exists.
**Solution**: This is informational - the file won't be processed again. Check the `duplicates` array in the response.
***
## Working Code Sample
Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions.
## Next Steps
Now that you can upload files to the Data Engine, explore:
1. **[Search API](/api-guides/search)** - Query your uploaded content
2. **[Chat Integration](/tutorials/chat)** - Use uploaded content in conversations
3. **[Manage Content](/api-guides/manage-content)** - Manage and update your content
4. **[Upload Files API](/api-reference/ingestion/v2)** - For additional API information
5. **[Update Item Metadata API](/api-reference/ingestion/v2)** - For additional API information
# AI-Assisted Development
Source: https://docs.gloo.com/tutorials/vibe-coding
Using AI coding assistants to quickly build applications with Gloo AI.
These days, developers and non-developers are building apps using AI coding assistants like Claude Code, Codex, Lovable, Base44, Antigravity, and Replit Agent. The development flow at times can be described as "vibe coding" which is a term Andrej Karpathy coined in early 2025. Vibe coding is attractive for various reasons:
* You can prototype, iterate and create full-stack applications in minutes instead of hours, days, or even weeks.
* You don't need deep expertise on UI/UX, databases, APIs, and so on.
* You can focus on what you want your app to do rather than integration details.
Technical expertise is very helpful when you run into errors or need to scale your application for production environments. The right prompting techniques can help you avoid many common pitfalls.
This tutorial teaches you how to use these tools to build apps with Gloo AI. You'll craft effective prompts and learn best practices for building production-ready apps.
## Prerequisites
Before starting, ensure you have:
* A Gloo AI Studio account
* Your API key from the [API Credentials page](/studio/manage-api-credentials)
* Access to an AI coding assistant (Bolt.new, Lovable, Replit Agent, Claude Code, Codex, etc.)
* Basic understanding of web applications (frontend + backend)
> **Note:** If you prefer to code manually, start with our [Authentication Tutorial](/tutorials/authentication) and other hands-on tutorials.
***
## How This Tutorial Works
This tutorial is different from traditional code tutorials. Instead of showing you code to copy, we'll show you **prompts** that generate the code for you.
**The process:**
1. Choose your use case (chat app, tool use, etc.)
2. Copy the prompt from this tutorial
3. Paste into your AI coding assistant (Bolt.new, Lovable, Replit Agent, Claude Code, Codex, etc.)
4. Generate the app
5. Test and deploy the generated app
**What makes a good prompt:**
* Clear objective (what you're building)
* Specific architecture (React, Vue, serverless, etc.)
* Complete authentication pattern (API key as Bearer token)
* Exact API endpoints and request/response formats
* UI/UX requirements (mobile-responsive, loading states, etc.)
Each example in this tutorial follows this structure, so you can adapt them to your needs.
***
## Step 1: Understanding Prompt Structure
Before we dive into specific examples, let's understand what makes an effective prompt for building Gloo AI applications.
Every effective prompt has five components:
### 1. **Clear Objective**
Start with a single sentence describing what you're building.
* ❌ Bad: "Make a chat thing"
* ✅ Good: "Build a chat application that connects to Gloo AI with streaming responses"
### 2. **Architecture Requirements**
Specify your tech stack so the AI generates appropriate code.
* Frontend: React, Vue, Next.js, vanilla JavaScript
* Backend: Node.js/Express, serverless functions, backend functions, Python/Flask
* Important: Always separate frontend and backend for secure authentication
### 3. **Authentication Flow**
Always include instructions to ensure API key authentication is implemented securely. The key? Never expose credentials in frontend code. The figure below shows the flow for a properly authenticated Gloo AI API call.
Every prompt you create must include instructions to ensure this, for example:
```
Use API key authentication:
1. Backend reads GLOO_API_KEY from environment variables
2. Use Authorization: Bearer {GLOO_API_KEY} for all API calls
3. NEVER expose GLOO_API_KEY to frontend
```
### 4. **API Interaction Details**
Be specific about:
* What the frontend sends to your backend
* What your backend sends to Gloo AI (exact endpoint, headers, body)
* What your backend returns to the frontend
### 5. **UI/UX Requirements**
Describe the user experience:
* Visual layout (chat bubbles, forms, cards)
* Interactive elements (buttons, inputs, dropdowns)
* Feedback mechanisms (loading states, errors, success messages)
* Responsive design needs
**Practice:** Before using the examples below, try writing a one-sentence objective for an app you want to build. This helps you choose the right example to start from.
***
## Step 2: Choose Your Starting Point
Select an example prompt from the three examples in this section. We've prepared these tested prompts for common use cases. Start with Example 1 (Chat Application) if you're new to this approach. Each example includes:
* When to use this pattern
* Which platforms we've tested on
* The complete prompt ready to copy
### Example 1: Chat Application (with Streaming Support)
**What you'll get:** A chat app with real-time, typewriter-style AI responses
**When to use this:**
* Want immediate feedback as AI generates responses
* Need to handle long responses gracefully
* Want users to see progress in real-time
**Key concept:** Streaming shows response chunks as they're generated instead of waiting for the complete response.
**Tested on:** Antigravity, Bolt.new, Claude Code, Codex, Lovable, Replit Agent, v0.dev
**The Prompt:**
Copy the entire prompt below and enter it into your coding assistant of choice:
````markdown theme={null}
# Gloo AI Streaming Chat
Build a chat app with real-time streaming responses from Gloo AI.
API key authentication in backend; frontend receives SSE stream.
**Auth:**
Use API key authentication:
1. Backend reads GLOO_API_KEY from environment variables
2. Use Authorization: Bearer {GLOO_API_KEY} for all API calls
3. NEVER expose GLOO_API_KEY to frontend
**Streaming flow:**
Frontend initiates:
POST /api/chat/stream
Body: { "message": "..."}
Backend calls Gloo with streaming:
POST https://platform.ai.gloo.com/ai/v2/guarded/chat/completions
Authorization: Bearer {GLOO_API_KEY}
Content-Type: application/json
Body:
```json
{
"messages": [
{
"role": "user",
"content": "User's message"
}
],
"auto_routing": true,
"stream": true
}
```
Backend forwards SSE to frontend:
- Parse Gloo's streaming response chunks
- Forward as Server-Sent Events
- Accumulate full message
- Send suggestions at end
**UI Requirements:**
- Typewriter effect for AI responses
- Cancel stream button
- Smooth markdown rendering as chunks arrive
- Show typing indicator
````
All five components of creating an effective prompt are shown in the prompt:
| Component | Where in Prompt | Example Text |
| - | - | - |
| **1. Clear Objective** | First sentence | "Build a chat app with real-time streaming responses from Gloo AI." |
| **2. Architecture Requirements** | Second sentence | "API key authentication in backend; frontend receives SSE stream." |
| **3. Authentication Flow** | "Auth:" section | "Use API key authentication:
1. Backend reads GLOO\_API\_KEY from env and sends it as a Bearer token..." |
| **4. API Interaction Details** | "Streaming flow:" section | Frontend initiates:
`POST /api/chat/stream`
Backend calls Gloo AI:
`POST https://.../ai/v2/guarded/chat/completions`
Backend forwards SSE chunks to frontend |
| **5. UI/UX Requirements** | "UI Requirements:" section | "- Typewriter effect for AI responses..." |
Notice how each component is clearly separated and specific. The AI coding assistant knows exactly what to build, how to structure it, how to authenticate, which APIs to call, and what the UI should look like.
***
### Example 2: Completions with Tool Use
**What you'll get:** An application that returns structured, predictable JSON responses.
**When to use this:**
* You need consistent data formats (not free-form text)
* Building forms, assessments, or data extraction tools
* Need machine-readable output for downstream processing
* Want type-safe responses with validation
**Key concept:** Tool use forces the AI to follow a JSON schema you define, perfect for applications that need predictable outputs.
**Tested on:** Antigravity, Bolt.new, Claude Code, Codex, Lovable
**Example Use Cases:**
* Personal growth plans
* Recipe generation with structured ingredients
* Data extraction from text into structured format
The prompt example shows a goal-setting assistant that provides a structured growth plan based on a user's goal.
**The Prompt:**
```markdown theme={null}
# Gloo AI Goal-Setting Assistant
Build a goal-setting assistant that takes a user’s goal and returns a
structured growth plan with actionable steps and timelines. Use Gloo
AI's completions with tool use feature to get structured, predictable
JSON responses.
**Auth:**
Use API key authentication:
1. Backend reads GLOO_API_KEY from environment variables
2. Use Authorization: Bearer {GLOO_API_KEY} for all API calls
3. NEVER expose GLOO_API_KEY to frontend
**API flow:**
Frontend sends:
{
"goal": "User's goal or question",
"toolType": "growth_plan" // or other structured output types
}
Backend calls Gloo with tool definition:
POST https://platform.ai.gloo.com/ai/v2/guarded/chat/completions
Authorization: Bearer {GLOO_API_KEY}
Content-Type: application/json
Body:
{
"auto_routing": true,
"messages": [
{
"role": "system",
"content": "You are a helpful assistant that creates personalized growth plans."
},
{
"role": "user",
"content": "I want to grow in my faith"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "create_growth_plan",
"description": "Generate a structured personal growth plan",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "A clear, inspiring title for the growth plan"
},
"areas": {
"type": "array",
"items": {
"type": "object",
"properties": {
"area": {
"type": "string",
"description": "Specific area of growth"
},
"actions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Concrete action steps"
}
},
"required": ["area", "actions"]
},
"description": "Different areas of growth with specific actions"
},
"timeline": {
"type": "string",
"description": "Suggested timeline for the plan"
}
},
"required": ["title", "areas", "timeline"]
}
}
}
],
"tool_choice": "required" // Force AI to use the tool
}
Response structure:
{
"id": "chatcmpl-abc123",
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": null,
"role": "assistant",
"tool_calls": [
{
"id": "call_xyz789",
"type": "function",
"function": {
"name": "create_growth_plan",
"arguments": "{\"title\":\"Faith Growth Journey\",\"areas\":[{\"area\":\"Daily Prayer\",\"actions\":[\"Set aside 15 minutes each morning\",\"Use a prayer journal\"]},{\"area\":\"Scripture Study\",\"actions\":[\"Read one chapter daily\",\"Join a Bible study group\"]}],\"timeline\":\"30 days\"}"
}
}
]
}
}
],
"created": 1755118870,
"model": "gloo-anthropic-claude-sonnet-4.5",
"object": "chat.completion",
"usage": {
"completion_tokens": 391,
"prompt_tokens": 1631,
"total_tokens": 2022
}
}
Backend parses and returns to frontend:
{
"title": "Faith Growth Journey",
"areas": [
{
"area": "Daily Prayer",
"actions": [
"Set aside 15 minutes each morning",
"Use a prayer journal"
]
},
{
"area": "Scripture Study",
"actions": [
"Read one chapter daily",
"Join a Bible study group"
]
}
],
"timeline": "30 days"
}
**UI Requirements:**
- Input form for user's goal/question
- Display structured output in formatted cards/sections
- Show loading state during API call
- Handle and display schema validation errors
- Option to regenerate with different parameters
- Export structured data as JSON or CSV
```
Try spotting where the 5 components to crafting a good prompt can be found in the prompt above. **Hint**: Look at Example 1's breakdown.
### Example 3: Building AI Overviews
**What you'll get:** An AI Overview feature that retrieves relevant snippets and generates a grounded answer with inline citations.
This example uses item recommendations functionality that requires you to have a Pro or Enterprise plan as well as content uploaded to your content library.
**When to use this:**
* You want a search-first experience with synthesized answers
* You need source-aware responses with explicit citation mapping
* You want a UI pattern similar to AI overview cards
**Key concept:** Retrieve relevant snippets first, then ground a Completions v2 response only in those sources.
**Tested on:** Antigravity, Bolt.new, Claude Code, Codex, Lovable, Replit Agent
**The Prompt:**
````markdown theme={null}
# Gloo AI Overview
Build an AI Overview feature that takes a user's search query, retrieves relevant faith-based content snippets, and generates a synthesized AI answer with inline source citations.
- Frontend (ex: React)
- Backend (ex: serverless functions or Node.js/Express)
**Auth:**
Use API key authentication:
1. Backend reads GLOO_API_KEY from environment variables
2. Use Authorization: Bearer {GLOO_API_KEY} for all API calls
3. NEVER expose GLOO_API_KEY to frontend
**API flow:**
Frontend sends:
```json
{
"query": "What is my purpose in life?"
}
```
Step 1 — Backend fetches relevant sources from Gloo:
```
POST https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose
Authorization: Bearer {GLOO_API_KEY}
Content-Type: application/json
Body:
{
"query": "What is my purpose in life?",
"collection": "GlooProd",
"tenant": "{your_tenant}",
"publishers": ["{your_publisher}"],
"max_snippet_count_overall": 5,
"certainty_threshold": 0.5
}
Response structure:
[
{
"item_id": "...",
"item_title": "Finding True Happiness",
"item_url": "",
"uuids": [
{
"uuid": "...",
"snippet": "# Finding True Happiness...",
"certainty": 0.703
}
]
}
]
Note: item_url may be empty or contain a link.
```
From the response, extract up to 5 sources. For each source, capture:
- item_title — used for citation tooltip
- item_url — used for citation link
- The first snippet from uuids[] — used as context for the AI
Step 2 — Backend generates a grounded AI answer:
```
POST https://platform.ai.gloo.com/ai/v2/guarded/chat/completions
Authorization: Bearer {GLOO_API_KEY}
Content-Type: application/json
Body:
{
"messages": [
{
"role": "system",
"content": "You are a helpful assistant. Answer the user's question using ONLY the provided sources. Insert a citation marker like [1], [2], etc. inline immediately after each sentence or claim that draws from a source. Use only the sources given. Be concise."
},
{
"role": "user",
"content": "Sources:\\n[1] {item_title_1}: {snippet_1}\\n[2] {item_title_2}: {snippet_2}\\n...\\n\\nQuestion: {query}"
}
],
"auto_routing": true
}
```
Backend returns to frontend:
```json
{
"answer": "The key to happiness, from a Christian perspective, is found in a relationship with God. [1] God's grace is unconditional. [2] This lasting joy is rooted in understanding one's identity as a beloved child of God. [3]",
"sources": [
{
"index": 1,
"title": "Finding True Happiness",
"url": "https://..."
},
{
"index": 2,
"title": "God's Unconditional Grace",
"url": "https://..."
},
{
"index": 3,
"title": "Finding Joy",
"url": "https://..."
}
]
}
```
**UI Requirements:**
- Display a search bar at the top with a clear (×) and submit button
- Below the search bar, show an AI Overview card with:
- A header row with a star/sparkle icon, the label "AI Overview"
- Render the answer text with each [N] marker replaced by a URL link icon
- Each link icon should be hoverable — on hover, show a tooltip with the source title and a link to url
- Show a loading skeleton while waiting for the response
- Display a disclaimer below the answer: "AI-generated answer. Please verify critical facts."
- Show an error banner if either API call fails
````
***
## Step 3: Generate Your Application
After copying a prompt to your AI coding assistant, follow these steps:
### 1. Set Up Your Gloo AI Credentials
* Enter your Gloo AI credentials if prompted during code generation. This typically applies to coding assistants that generate code online such as Lovable and Replit Agent.
* Enter your credentials as instructed after code generation. This typically applies to assistants that create code locally such as Claude Code and Codex.
Get your credentials from [Gloo AI Studio](https://studio.ai.gloo.com/) > API Credentials.
### 2. Enter Your Publisher and Tenant (Example 3 Only)
If you're using Example 3 (Building AI Overviews), you'll need to provide two additional values before your app will work:
* **Publisher** — name of your publisher
* **Tenant** — tenant identifier
These values are used in the recommendations API call.
Find these values in [Gloo AI Studio](https://studio.ai.gloo.com/):
1. Navigate to the “Manage Organizations” page.
2. In your organization’s row, click on “View Publishers”.
3. Note the publisher name.
4. The tenant identifier is derived from the publisher name by removing any spaces. For example, if your publisher name is "Faith Matters," the tenant identifier would be "FaithMatters."
Enter the two values as environment variables or when prompted by your AI coding assistant.
### 3. Review the Generated Code
* Check that API key authentication is in the backend only
* Verify environment variables are properly configured
* Ensure error handling is present for 401, 429, 500 errors
* Confirm the API key is loaded from environment variables
### 4. Start the Application
Online coding tools will automatically start your app in a Preview tab.
For locally generated code:
* Follow instructions (typically in a README) to install dependencies
* Start the backend and frontend servers
***
## Step 4: Test Your Application
Once your app is running, check that it's working well.
### Example 1: Chat Application (with Streaming Support)
Enter a message and ensure the results are streamed in. You should see something like this:
Because these coding models are generative, the app UI will vary with each run — even using identical prompts. However, the overall functionality should consistently match your specifications.
### Example 2: Completions with Tool Use
Enter a test goal, for example "I want to grow in faith". You should see something similar to the following:
### Example 3: Building AI Overviews
Enter a search query (for example, "What is my purpose in life?"). Verify that:
* The UI shows a loading skeleton while waiting
* The answer includes inline citation markers rendered as link icons
* Hovering each link icon shows the source title and link
You should see something similar to the following:
### Iterate if Needed
If something isn't right, use follow-up prompts:
* "Add error handling for network timeouts"
* "Make the UI mobile-responsive"
* "Add a loading skeleton instead of a spinner"
* "Change the color scheme to match our brand"
***
## Step 5: Understanding Advanced Patterns
As you build more complex applications, you'll want to understand these patterns. You can ask your AI coding assistant to implement these by adding them to your prompts. You can also use them in a follow-up prompt.
### Pattern 1: API Key Loading
```markdown theme={null}
Implement API key loading:
- Load GLOO_API_KEY once at startup from environment variables
- Reuse the same key for every API call
- Never log the API key
- Keep the key only in backend memory
```
**Short version:** "Load GLOO\_API\_KEY from environment variables and reuse it for every request"
***
### Pattern 2: Comprehensive Error Handling
```markdown theme={null}
Implement comprehensive error handling:
401 Unauthorized:
- API key invalid, missing, or revoked
- Verify GLOO_API_KEY is set correctly
- Return error to user (do not retry — a static key won't become valid)
429 Rate Limited:
- Implement exponential backoff (1s, 2s, 4s, 8s)
- Show user-friendly message about temporary limit
- Queue requests if possible
- Log rate limit occurrences
500 Server Error:
- Show user-friendly error message
- Log full error details for debugging
- Retry once after 2 seconds
- If retry fails, fail gracefully
Network Errors:
- Retry with exponential backoff
- Set maximum retry attempts (3-5)
- Timeout after 30 seconds
- Show connection error message
```
**Short version:** "Implement comprehensive error handling with automatic retry for 401 and exponential backoff for 429"
***
### Pattern 3: Client-Side Rate Limiting
```markdown theme={null}
Implement client-side rate limiting:
- Track requests per user in Redis or memory
- Implement sliding window (e.g., 100 requests/hour per user)
- Return 429 with retry-after header when limit hit
- Show rate limit status in UI (e.g., "47/100 requests remaining")
- Queue requests when approaching limit
- Allow burst requests with token bucket algorithm
```
**Short version:** "Implement sliding window rate limiting with 100 requests per hour per user"
**Practice:** Try adding one of these patterns to Example 1 (Chat Application) by appending the instruction to the prompt or using it in a follow-up prompt.
***
## Platform-Specific Optimizations
Different AI coding assistants have different strengths. Here are a few pointers to get the best results from each:
### **Base44 / Bolt.new / Lovable / Replit Agent / v0.dev**
* Use assistants like ChatGPT to create a detailed project requirements document (PRD) first. Review and split into phases for implementation.
* Upload screenshots or sketches instead of describing layouts.
* Stick to the tool's default stack to avoid friction.
* Use automatically created checkpoints to revert to a previous version if needed.
* Iterate in small steps rather than requesting complete rewrites.
### **Claude Code / Codex**
* Go into plan mode before coding.
* Break projects into short, reviewable steps or components.
* Review results often, request refinements after each pass.
* Start fresh sessions or limit irrelevant history to reduce confusion.
* Be clear about what "done" means (ex: passing unit tests, zero lint errors, responsive design).
### **Antigravity**
* Define a high-level goal (e.g., "Implement search bar") instead of small tasks.
* Utilize Agent Manager for parallelizing and overseeing tasks.
* Request Artifacts like Screenshots or Browser Recordings for verification.
***
## Troubleshooting
Here are some common issues you may run into with these examples and in general when using AI coding assistants to build with Gloo AI.
* **503 errors when backend calls API**:
* Verify the endpoint being used in the code.
* Provide the coding assistant with links to relevant resources, such as the API reference or related tutorial documentation.
* **500 errors calling completions with tool use**:
* Verify the endpoint being used in the code.
* Verify that the model being used supports tool use.
* Try the API call directly in [the playground](/api-reference/studio/post-completions).
***
## What You've Learned
Congratulations! You now know how to effectively use AI assistants to build with Gloo AI. You learned how to structure effective prompts, properly secure your API calls, then tested these out on some example use cases.
## Next Steps
* Check out the [API documentation](https://docs.gloo.com/api-reference/general/overview) for complete API reference
* Review [other tutorials](https://docs.gloo.com/tutorials/overview) where you can work directly with code.
**Give us feedback**
We would love to hear from you as well:
* Found a better way to structure a prompt? Share it with us.
* Generated an innovative application? We'd love to see it.
This is a new way to learn. Please let us know:
* Which prompts worked best for you?
* Which AI coding assistant you prefer?
* What examples you'd like to see added?