# AI Overview Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/ai-overview Introduces the fundamentals of artificial intelligence and how Gloo applies modern AI responsibly to empower our partners and customers. # What Is All This AI Stuff? Welcome! At Gloo AI, we believe everyone deserves to understand how modern artificial intelligence works—without needing a PhD or vibe coding your own app. If you’re just getting started, we will walk you through the core concepts you’ll hear about all the time in AI conversations. These are the foundational ideas behind everything from ChatGPT to image generators and smart search engines. ## Artificial Intelligence (AI) **What it means:** AI is the broad field of teaching machines to do things that typically require human intelligence—like understanding language, recognizing images, making decisions, or learning from data. **Analogy:** Think of AI as the umbrella term. It's like saying “sports”—under that, you have soccer, tennis, basketball. In AI, those subfields include things like machine learning and computer vision. **How it shows up in Gloo:** Gloo uses AI to help organizations deliver personalized, accurate, and aligned experiences using their own content. AI powers search, assists staff with Chat for Teams, and enhances content understanding in the Data Engine and Studio. ## Machine Learning (ML) **What it means:** Machine Learning is a branch of AI where computers learn patterns from data instead of being explicitly programmed. Instead of writing rules, we feed the machine examples, and it figures out how to make predictions or decisions. **Example:** You don’t teach a machine every rule for recognizing cats in photos. You just show it 10,000 pictures labeled “cat” and “not cat.” It learns the patterns that typically define a cat. ## Deep Learning **What it means:** Deep Learning is a type of machine learning that uses multi-layered “neural networks” to learn complex patterns—especially good for things like language, images, and audio. **Analogy:** If machine learning is like learning to recognize apples by color and shape, deep learning figures it out by examining thousands of layers of features—like stem angle, skin texture, or reflection—without being told what those features are. ## Neural Network **What it means:** A neural network is a system of algorithms inspired by the way human brains process information. It’s made up of interconnected “neurons” that process and pass along signals. **Visualize it:** Picture rows of dots connected by lines. Each dot processes a tiny piece of information and passes it along. The final output is the decision or prediction the system makes. ## Transformer **What it means:** Transformers are a special type of neural network architecture that revolutionized how machines handle sequences—like words in a sentence. They're the core architecture behind most modern language models. **Why it matters:** Before transformers, AI struggled with long text or understanding meaning across multiple sentences. Transformers fixed that by focusing attention on important parts of input, not just the most recent word. **Famous example:** ChatGPT and nearly every powerful text model today is built on transformer architecture. ## Diffusion **What it means:** Diffusion is a technique used in image and video generation. The model starts with pure noise and gradually “denoises” it into a coherent image based on a prompt. **Analogy:** Imagine taking a photo and gradually adding static until it’s just noise. Diffusion models learn how to reverse that process—starting with noise and ending with an image of, say, “a dog riding a skateboard in space.” ## Model **What it means:** A model is the result of training an AI system. It’s a learned set of rules and parameters that can take an input (like a sentence or image) and return an output (like a summary or classification). **How to think about it:** If data is the fuel, and algorithms are the engine, then the model is the car you build from both. It’s what you deploy when you're ready for your AI to start working in the real world. **How it shows up in Gloo:** Gloo does not train new models. Instead, Gloo integrates frontier and foundation models and adds alignment layers, safeguards, and rights management so organizations can safely apply AI to their own content. ## Large Language Model (LLM) **What it means:** An LLM is a type of AI trained on huge amounts of text to understand, generate, and interact using natural language. **How it works:** It doesn’t “know” facts in the human sense. Instead, it’s learned patterns in language—how likely one word is to follow another—and uses that to respond in surprisingly smart ways. **Example:** GPT-4, Claude, Gemini, and LLaMA are all examples of LLMs. **How it shows up in Gloo:** LLMs power many of Gloo's key capabilities, including search, content question answering, metadata enrichment, and aligned generative responses in Chat for Teams. ## Multimodal AI **What it means:** Multimodal AI can understand and generate more than one kind of input—like text, images, audio, or video—all within the same system. **Use case:** You can upload a picture and ask, “What’s happening in this photo?” and the AI can understand and respond. Or you might give it a chart and ask it to write a summary. **How it shows up in Gloo:** Gloo supports multimodal ingestion by allowing organizations to upload documents such as PDFs, transcripts, and scanned pages. The platform converts these into usable, searchable content for RAG. ## Artificial General Intelligence (AGI) **What it means:** AGI is a hypothetical form of AI that can perform any intellectual task a human can. It’s still far off and the subject of much debate. **Why it matters:** Right now, AI is narrow—it does specific things well (like answering questions or generating images). AGI would be flexible, able to learn new tasks on the fly like a person. *** *Next Up: How AI Models Learn* In the next section, we’ll walk through how models are trained, fine-tuned, and made useful—answering the question: **"How do AI models learn, improve, and adapt after they’re built?"** # AI Training & Learning Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/ai-training-and-learning Explains how AI models learn, improve, and adapt through training, fine-tuning, and alignment processes used within Gloo’s trusted ecosystem. # How Do AI Models Learn and Improve? In the last section, we introduced the big-picture building blocks of modern AI. Now let’s look at how these models actually learn and evolve. At Gloo AI, we often describe this as the model's life cycle from raw data to a working system that continues to improve over time. These concepts explain what happens **before**, **during**, and **after** a model is trained, and how organizations fine-tune them to be safer, more helpful, or more domain-specific. ## Pre-training **What it means:** Pre-training is the first major phase of model development. It’s when a model is fed massive amounts of data (like books, websites, and code) so it can learn general patterns in language, images, or other types of input. **What to know:** This process doesn’t teach the model facts. It teaches it *how language or imagery behaves* so it can later predict and generate content. **How it shows up in Gloo:** While Gloo hasn’t launched any pre-trained models to date, keep an eye out for news about Project Genesis! ## Post-training **What it means:** Post-training includes all the steps that happen **after** pre-training like refining the model for safety, quality, or specific use cases. **Why it matters:** Post-training turns a general-purpose model into a useful product. It might involve adding safety rules, formatting guidelines, or behavior alignment. ## Fine-tuning **What it means:** Fine-tuning is when a pre-trained model is retrained on a smaller, curated dataset to specialize in a topic, task, or tone. **Use case:** If you wanted a model to work only with legal documents or theological texts, you’d fine-tune it on relevant examples to get better performance in that domain. ## Instruction Tuning **What it means:** Instruction tuning teaches the model how to follow human instructions more reliably. It’s done by showing examples of clear instructions and good responses. **Why it helps:** This is what makes models like ChatGPT feel responsive and helpful. It teaches models not just to predict, but to follow your intent. ## Reinforcement Learning from Human Feedback (RLHF) **What it means:** RLHF is a method where humans rate different model outputs, and the model learns to prioritize responses that align better with human preferences. **Analogy:** It’s like giving the model a thumbs up or down for each answer, and letting it learn what types of replies people actually want to see. ## Alignment **What it means:** Alignment is the process of making sure a model’s behavior stays consistent with human values, safety guidelines, and ethical boundaries. **Why it matters:** A powerful model that isn't aligned could give unsafe, biased, or manipulative responses. Alignment helps ensure models behave as intended even in unexpected situations. **How it shows up in Gloo:** Alignment is central to Gloo’s approach. Gloo applies custom system instructions, safety layers, theological constraints, and allowed content boundaries to ensure responses reflect the values of each organization and never contradict their content. The Data Engine and Studio ensure answers are grounded in approved material. ## Distillation **What it means:** Distillation is the process of compressing a large model into a smaller, faster version while keeping most of the same abilities. **Use case:** Companies often use distillation to deploy smaller versions of powerful models on phones, websites, or embedded devices where resources are limited. ## Transfer Learning **What it means:** Transfer learning means applying knowledge learned in one context to another. A model trained on general language can adapt to specialized tasks without starting from scratch. **Example:** You could take a model trained on internet text and quickly adapt it to answer medical questions without retraining the whole system. **How it shows up in Gloo:** Gloo uses retrieval instead of training the model on new data. By combining a pre-trained LLM with your uploaded content via the Data Engine, Gloo achieves the benefits of transfer learning without modifying the model. This means faster updates, safer usage, and no risk of leaking proprietary content. *** *Next Up: How We Talk to Models* In the next section, we’ll answer: **“How do we give AI models instructions, and how do they turn those prompts into useful answers?”** # Application Patterns Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/application-patterns Explores advanced AI patterns such as agents, orchestration, and guardrails that power real workflows in Gloo’s platform. # How Do Models Do More Than Just Answer Questions? So far, we’ve looked at how models are trained, how you prompt them, and how they retrieve helpful context. Now let’s explore the next level of capability: what happens when models begin to act, plan, or chain together multiple steps. This section covers **application patterns**—ways AI systems are built to move beyond answering a single prompt. These patterns allow models to assist with real workflows, reason across steps, use tools, and even operate semi-autonomously. ## Agent **What it means:** An agent is an AI system that can take actions on your behalf by observing, planning, and executing steps toward a goal you define for it. **Analogy:** If a single prompt is like asking a question, an agent is like assigning a task. Agents decide what steps to take and in what order to complete something more complex. **Use case:** You ask an agent to "prepare a report on our website traffic." It might search your data, summarize the results, and send you a document—all without another prompt. **How it shows up in Gloo:** Gloo uses agent patterns inside Chat for Teams and Studio. These allow the system to retrieve documents, summarize content, classify materials, and generate structured outputs without the user needing to prompt every step. ## Autonomous Agent **What it means:** An autonomous agent can operate over longer periods without user input. It can keep track of goals, retry failed steps, and decide what to do next on its own. **Why it matters:** Autonomous agents move AI from reactive to proactive. Instead of waiting for instructions, they can keep working in the background toward a goal. **Caution:** Autonomy introduces risk. These systems need clear constraints, guardrails, and oversight to prevent unwanted behaviors. **How it shows up in Gloo:** Gloo does not currently offer autonomous agents that act without user oversight. Instead, Gloo emphasizes controlled, predictable workflows where the model operates only within the organization's content, rights boundaries, and safety constraints. ## Tool Use (or Function Calling) **What it means:** Tool Use lets the AI call specific functions or APIs during a conversation. These tools do things the model itself cannot do—like look up real-time data, send messages, or update a calendar. **Example:** If a user asks, "What’s the weather in Kansas City right now?" the model can call a weather API to get the answer. It didn’t guess—it used a tool. **Why it matters:** Tool use bridges the gap between AI and software systems. It makes AI incredibly more useful, more accurate, and more connected. **How it shows up in Gloo:** Gloo uses function calling when users or applications ask questions, the system automatically retrieves relevant documents, enriches content, or performs structured tasks using tools vetted for or designed specifically for the flourishing ecosystem. ## Orchestration **What it means:** Orchestration is the coordination layer that decides what the model (or agent) should do next. It might involve chaining multiple tools together, switching between models, or managing memory across steps. **Analogy:** Think of orchestration as a conductor managing a band of agents, models, and tools—making sure each one comes in at the right time and in the right order. **Use case:** In a customer service flow, orchestration might route one request to a document summarizer, another to a live agent, and another to a scheduling assistant—all automatically. **How it shows up in Gloo:** Gloo orchestrates multiple backend steps such as chunking documents, running embeddings, performing semantic search, and assembling grounded responses. This orchestration ensures each response is aligned, accurate, and sourced from the organization's content and content to which they have access rights. ## Chain of Thought (CoT) **What it means:** Chain of Thought refers to the model reasoning step-by-step rather than jumping to a final answer immediately. It is often prompted with intermediate steps to improve accuracy. **Why it helps:** Just like people solve problems by breaking them into steps, models can be encouraged to “think aloud” before answering. This improves reliability for complex tasks like math or logic. ## Memory **What it means:** Memory refers to the model’s ability to retain relevant information between interactions. Unlike the context window (which is temporary), memory can persist across sessions. **Use case:** A chatbot with memory might remember your name, preferences, or past questions—even days later. **Note:** Memory is often implemented using external storage (like a database), not inside the model itself. **How it shows up in Gloo:** Gloo Chat for Teams uses memory to constantly improve responses for its users. Also, organizational memory is handled through the Data Engine, which stores content, metadata, and past documents that the AI can reference when generating grounded responses. ## Guardrails **What it means:** Guardrails are safety rules or constraints placed around AI behavior. They define what the model can and cannot do, say, or access. **Examples:** * Blocking certain topics * Preventing sensitive data from being generated * Requiring multiple confirmations before triggering actions **Why it matters:** As AI gets more powerful, especially in agent or autonomous settings, guardrails help maintain safety and trust. **How it shows up in Gloo:** Guardrails are absolutlely central to Gloo's design. The platform uses system instructions, theological alignment, rights enforcement, safety filters, and content grounding to ensure any model used behaves consistently with the values and boundaries of each organization. Gloo doesn’t just use safe models, it makes any model you choose to use for your use case safer. *** *Next Up: What Are the Risks and Challenges of AI, and How Can We Use It Responsibly?* In the next section, we’ll answer: **“What limitations, risks, and safety concerns come with modern AI, and how do we address them with care and responsibility?”** # Challenges & Risks Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/challenges-and-risks Highlights key AI risks including bias, hallucinations, and misalignment and shows how Gloo’s design safeguards protect users and organizations. # What Are the Real Risks with AI? AI offers powerful capabilities, but it also introduces risks that need to be understood and managed. AI systems learn patterns from data, not meaning or truth, which means they can behave in unpredictable or harmful ways if these risks are not addressed. By understanding these challenges, we can design safer, more trustworthy AI systems that support people rather than confuse or mislead them. In this section, we introduce the most common risks in modern AI development and explain why they matter in real-world applications. ## Hallucinations **What it means:** A hallucination occurs when an AI generates incorrect, fabricated, or misleading information while sounding confident. **Why it matters:** Hallucinations can spread false information and cause users to rely on inaccurate outputs, especially in contexts involving education, leadership, counseling, or faith. **How it shows up in Gloo:** Gloo reduces hallucinations by grounding every answer in an organization’s verified content through the Data Engine. Instead of relying on model memory or guesses, Gloo retrieves real documents and uses them as the authoritative source for generated responses. The current state of AI means that its not currently impossible to reduce Hallucinations to zero. However, Gloo is actively working to make sure those Hallucinations are effectively eliminated specifically when it comes to Safety, Biblical Accuracy, and Theological Alignment. ## Bias **What it means:** Bias occurs when an AI reflects unfair patterns, stereotypes, or imbalanced representation found in its training data. **Why it matters:** If not monitored, AI systems can unintentionally reinforce harmful assumptions or favor certain groups over others. **How it shows up in Gloo:** Gloo uses curated organizational content and constantly improving guardrails to minimize the influence of broad internet training data. By restricting responses to an organization's own materials and theological boundaries, Gloo reduces unwanted bias in partner applications and experiences. ## Toxicity **What it means:** Toxicity refers to outputs that are rude, offensive, harmful, or inappropriate. **Why it matters:** Toxic or harmful content can make AI unsafe in community environments and may violate organizational or platform guidelines. **How it shows up in Gloo:** Gloo applies safety filters, system prompts, and response shaping to prevent toxic or harmful outputs. Since responses are often grounded in approved content, the risk of inappropriate language or tone is significantly reduced. ## Model Collapse **What it means:** Model collapse happens when models begin training on data generated by other models instead of human-created data, causing quality to degrade over time. **Why it matters:** Over-reliance on AI-generated data reduces originality, accuracy, and reliability in future models. **How it shows up in Gloo:** Gloo doesn’t use user content to retrain or fine tune the underlying models. This prevents AI generated material from re entering the training loop. ## Data Poisoning **What it means:** Data poisoning occurs when harmful or misleading data is intentionally inserted into training sets to manipulate how the AI behaves. **Why it matters:** If poisoned data influences training, it can cause models to behave incorrectly, unpredictably, or in ways that are vulnerable to exploitation. **How it shows up in Gloo:** Because Gloo does not use just any uploaded content for model training, poisoned or adversarial data cannot alter model behavior. Content ingestion pipelines also include structural processing and enrichment steps that help identify malformed or suspicious inputs. Any training is done only at the express consent of the content owners and is highly curated by both the partners and Gloo toward a specific goal. ## Prompt Injection **What it means:** Prompt injection is when a user manipulates the model through crafted input to override system instructions or cause unintended behavior. **Why it matters:** Prompt injection can lead to leaks of internal instructions, bypassing of guardrails, or harmful model actions. **How it shows up in Gloo:** Bad actors will always exist where technology meets humans. Gloo uses multiple layers of protection against prompt injection, including controlled system prompts, behavior guardrails, strict function calling, and content grounding. The model is prevented from executing unsafe instructions or overriding alignment rules. At Gloo these protections are always evolving and adapting to improve and address new attack vectors. ## Misalignment **What it means:** Misalignment refers to a gap between what the AI is designed to do and what it actually does in real interactions. **Why it matters:** If a model’s behavior does not match organizational values or user intent, it can harm trust and produce outcomes that conflict with the mission or goals. **How it shows up in Gloo:** Gloo’s alignment system is constantly evolving to ensure responses match each organization’s theology, values, and content boundaries. The Data Engine, Studio’s rights and curation tools, and Chat for Teams system instructions work together to ensure the model behaves predictably and mission aligned. *** *Next Up: What Types of AI Models Exist, and Why Do They Work Differently?* In the next section, we will answer: **“What distinguishes foundation models, frontier models, open-weight models, multilingual models, and instruction-tuned models, and when do you use each one?”** # Data & Memory Structures Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/data-and-memory Describes how AI systems represent, store, and retrieve information using embeddings, vectors, and knowledge bases like those in the Gloo Data Engine. # How Does AI Store and “Know” Things? AI models do not "know" things in the traditional human sense. They do not access a database of facts. Instead, they learn patterns. But for more advanced tasks, like answering questions based on your company’s documents or personalizing responses based on a user’s profile, models need a way to access external information. That is where vector-based systems come in. In this section, we explain how data is converted into AI-readable formats, stored, and retrieved in real time using embeddings, vectors, and vector stores. These are critical to many modern AI applications, especially tools that combine retrieval with generation. ## Embedding **What it means:** An embedding is a numerical representation of a word, sentence, or document. It captures the meaning and context of the text in a form that an AI model can use for comparison. **Analogy:** Think of an embedding like a coordinate in space that represents the idea behind a piece of text. Words or phrases with similar meanings are close together in this space. Words like “Gas Station” and “Convenience Store” are not identical but are similar enough to be grouped together conceptually. **Why it matters:** Embeddings make it possible to compare concepts based on **meaning** rather than just exact words. **How it shows up in Gloo:** Gloo automatically converts uploaded content such as sermons, PDFs, curriculum, and articles into embeddings. These embeddings allow the Data Engine to understand meaning rather than just keywords, enabling accurate retrieval for Chat for Teams and Studio. ## Vector **What it means:** A vector is the actual list of numbers that make up an embedding. It is what gets stored and compared in AI systems. **Example:** A short sentence like “I feel happy” might turn into a vector like \[0.12, -0.45, 0.88, ...]. These numbers don’t mean much on their own, but together they allow the system to measure similarity with other sentences. **How it shows up in Gloo:** Every piece of content processed by the Data Engine becomes a vector. These vectors are used internally to compare concepts, match user questions to relevant content, and power high quality search and RAG responses. ## Vector Store (or Vector Database) **What it means:** A vector store is a special kind of database that stores embeddings (vectors) and can quickly find the ones that are most similar to a new query. **Use case:** Let’s say you ask, “What’s our return policy?” The system turns your question into a vector, searches the database for similar content, and retrieves the relevant documents. The model can then generate a response based on those results. **Popular tools:** Pinecone, Weaviate, FAISS, and Qdrant are examples of vector databases commonly used with AI systems. **How it shows up in Gloo:** Gloo maintains a secure, rights respecting vector store (with a continually improving feature set) that organizes an organization's content. This vector store allows the system to retrieve the most relevant passages, scriptures, or policy documents when users ask questions in Chat for Teams or Studio. ## Semantic Search **What it means:** Semantic search is the process of searching based on meaning, not keywords. It uses embeddings and vector similarity instead of traditional keyword matching. **Example:** If you search “how do I get a refund,” the system might return a document titled “return policy” even though the exact word “refund” isn’t used. **How it shows up in Gloo:** All Gloo search and retrieval is semantic. The platform matches user questions with content based on meaning, not exact keywords. This makes it possible for staff and volunteers to ask natural questions and still receive accurate, grounded answers, and for developers to make valuable experiences for their users. ## Retrieval-Augmented Generation (RAG) **What it means:** RAG is a technique where a model retrieves specific relevant documents before generating a response. It combines search and generation in one system. **Why it matters:** RAG allows models to stay up-to-date or domain-specific without retraining. The model brings in outside context, then writes its answer based on that. **Use case:** In a church outreach setting, RAG could allow an AI tool to answer questions using curated content from your organization’s internal materials, even if that content wasn’t part of the model’s original training data. For example this could allow your site to have a search feature that could return specific snippets of past sermons or answer a volunteer’s question about specific policies. **How it shows up in Gloo:** RAG is at the core of many of Gloo's platform offerings. When a user asks a question, the system retrieves the most relevant documents from the organization's content, then generates a response grounded in that material. This ensures alignment, accuracy, and trustworthiness without fine tuning. ## Knowledge Base **What it means:** A knowledge base is the collection of documents or facts that a system pulls from. When combined with embeddings and a vector store, it becomes searchable and usable by an AI. **Why it matters:** This is the human-curated layer of information that brings reliability, structure, and context to AI responses. **How it shows up in Gloo:** An organization's uploaded content becomes its private knowledge base inside Gloo. Studio helps manage this content with tools like enrichment, insights, and DRM. Chat for Teams uses the knowledge base to respond with accurate, organization specific information. ## Enrichment **What it means:** Enrichment refers to adding metadata, structure, or additional tags to documents before or after embedding. This helps improve retrieval and relevance. **Example:** Before storing a sermon transcript, you might add the speaker’s name, date, Bible passage, and topic as enrichment fields. When users ask questions, the AI can prioritize the most relevant enriched results. **How it shows up in Gloo:** Gloo enriches content as requested per customer by extracting topics, themes, scripture references, and metadata. These enriched fields improve retrieval accuracy and help structure content for ministry workflows. *** *Next Up: What Are Agents and How Do They Make AI More Capable?* In the next section, we’ll answer: **“What are AI agents, and how do they plan, act, and work across tasks like humans do?”** # Hardware & Infrastructure Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/hardware-and-infrastructure Covers the computing systems, chips, and processing units that enable Gloo to deliver fast, secure, and efficient AI experiences. # What Kind of Hardware Powers AI, and Why Does It Matter? AI models require immense computing power to operate. From the training phase—where models learn from huge datasets—to the inference phase—where they respond to your prompts in real time—the right hardware makes a big difference in performance, cost, and accessibility. In this section, we’ll look at the key types of chips and infrastructure that make AI run, from cloud data centers to devices in your pocket. All of these concepts apply to how AI shows up for you at Gloo, with the exception being that Gloo does not currently offer any on-device or offline AI solutions. ## Inference Engine **What it means:** An inference engine is the part of the AI system that runs the model and produces outputs in real time, based on your input. **Why it matters:** Inference is what happens when a model is already trained and ready to answer your questions, generate content, or complete tasks. Efficient inference determines how fast, accurate, and affordable those interactions are. ## AI Chips **What it means:** AI chips are computer processors designed specifically for the kinds of math operations AI models rely on—especially matrix multiplication. **Why it matters:** They’re faster and more power-efficient than general-purpose CPUs. AI chips make it possible to train and run models at scale without breaking the bank. ## Neural Processing Units (NPUs) **What it means:** NPUs are specialized processors built into some phones, tablets, or laptops to handle AI tasks locally. **Why it matters:** NPUs enable real-time AI features—like voice transcription, image enhancement, or translation—directly on your device, with better speed and privacy. ## Graphics Processing Units (GPUs) **What it means:** Originally designed for rendering graphics, GPUs are now the standard hardware for training and running AI models due to their ability to handle many operations in parallel. **Why it matters:** GPUs are the workhorses behind most large-scale AI systems. Companies like NVIDIA dominate this space. ## TPU (Tensor Processing Unit) **What it means:** A TPU is a custom chip developed by Google specifically for machine learning workloads. **Why it matters:** TPUs offer high performance for Google’s internal AI operations and are also available in its cloud platform, giving developers another powerful option for model training and inference. ## Edge AI **What it means:** Edge AI refers to running AI models on local devices—like smartphones, drones, or IoT devices—rather than sending data to the cloud. **Why it matters:** Edge AI reduces latency, improves privacy, and allows offline functionality. It’s useful for real-time tasks in healthcare, robotics, or smart home devices. ## On-device AI **What it means:** A subset of edge AI, on-device AI specifically refers to models that run directly on personal devices, often using NPUs. **Why it matters:** It allows AI features—like predictive text or facial recognition—to happen without internet access, improving speed and keeping sensitive data local. *** *Next Up: How Do We Know if an AI Model Is Good?* In the next section, we’ll answer: **“How do researchers measure model performance, and what makes one model better than another for a specific task?”** # Keep Going Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/keep-going Encourages continued exploration of Gloo’s AI ecosystem and provides guidance for advancing AI literacy, safety, and responsible innovation. # What Did You Learn, and What Can You Do With Gloo AI? You’ve just completed a full walkthrough of how AI works—from the core concepts to real-world applications, risks, and model types. You now understand how modern AI systems are built, used, evaluated, and deployed. But theory is only half the journey. The next step is putting what you’ve learned into practice—safely and effectively—through the Gloo AI platform. Whether you're a ministry leader, developer, content owner, or strategist, Gloo offers tools built to serve your mission with clarity, control, and confidence. ## What You Learned * **AI Models:** What they are, how they’re trained, and what makes foundation and frontier models different. * **Prompts and Generation:** How to guide an AI’s output using effective instructions. * **RAG & Search:** How retrieval-augmented generation connects external knowledge with AI generation. * **Agents:** How AI can plan and take multi-step action like a human collaborator. * **Hardware:** Why GPUs, NPUs, and other chips matter for performance and cost. * **Risk & Safety:** How to recognize issues like hallucinations, bias, or prompt injection—and why responsible use matters. * **Evaluation:** How to interpret AI performance using real-world metrics and benchmarks. This knowledge now gives you the tools to make smart, strategic decisions about when and how to use AI in your context. ## What You Can Do Next with Gloo AI ### Gloo Data Engine *Bring your content into the AI era—securely.* Gloo Data Engine lets your organization or publishing team **upload and securely manage your content** in ways that allow AI to interact with it. Whether it’s sermon transcripts, curriculum, devotionals, or training guides, Gloo Data Engine makes this content usable by AI tools without compromising safety, theology, or privacy. You can then power features like AI search, RAG-based assistants, and personalized content delivery—without needing to train your own model or risk sharing data externally. ### Gloo Studio *Control your content, protect your rights, and learn from your data.* Gloo Studio gives you the tools to **manage your organization’s content lifecycle**. That includes setting **Digital Rights Management (DRM)** policies, tracking how your content is being used, and seeing **insights into what’s resonating** with your audience. Studio connects your people and your content with the controls you need to do it responsibly. Studio is where theology, attribution, and analytics meet—so your content works for you, not against you. ### Chat for Teams *Give your staff a safe, powerful AI assistant.* Chat for Teams is a **theologically-aligned AI chat tool** designed for use inside churches, nonprofits, and Christian organizations. It uses RAG and Gloo’s Data Engine to ensure your staff are getting **accurate, biblically grounded, and context-aware** responses—not just general internet knowledge. Use it for sermon prep, HR questions, volunteer onboarding, or ministry FAQs—whatever your team needs to get done faster and more faithfully. ## Want to See It in Action? The best way to understand Gloo AI is to try it. Whether you’re just starting to explore AI or you’re ready to scale a solution across your network, our team can walk you through how to: * Load your content safely * Launch a chat assistant * Monitor usage and engagement * Stay aligned with your values and mission Let us know where you want to start. We’re ready when you are. # Metrics & Evaluation Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/metrics-and-evaluation Explains how AI performance is measured with benchmarks, speed metrics, and Gloo’s proprietary Flourishing AI Benchmarks for human well-being. # How Do We Know if an AI Model Is Good? Not all AI models are created equal. Some are fast but imprecise, others are accurate but slow or expensive to run. To decide which models are worth using—and how to improve them—AI developers and researchers use a range of tests, scores, and real-world indicators. In this section, we explain the most common methods for evaluating AI performance. These include standardized academic benchmarks, speed metrics, and human feedback. Together, they help teams ensure models are not just powerful, but also useful and safe. ## Benchmarks (e.g., MMLU, HellaSwag, TruthfulQA) **What it means:** Benchmarks are standardized sets of test questions used to measure and compare how well different models perform across specific types of tasks or reasoning. **Examples:** * **MMLU:** Evaluates knowledge and reasoning across 57 academic and professional subjects. * **HellaSwag:** Tests the model’s ability to apply common sense and continue sentences in plausible ways. * **TruthfulQA:** Measures whether a model produces accurate and truthful responses. **Why it matters:** Benchmarks let researchers compare different models in a consistent way and track improvements over time. **How it shows up in Gloo:** Gloo reviews external academic benchmarks when selecting foundation and frontier models, but we go further by developing our own Flourishing AI Benchmarks. These proprietary evaluations measure how well a model supports the dimensions of human flourishing such as emotional health, relational strength, purpose, and resilience. Early results show that Gloo aligned models score significantly higher on these measures than off the shelf systems. ## Latency **What it means:** Latency is the time it takes for an AI model to start responding after a prompt is submitted. **Why it matters:** Low latency feels fast and responsive. High latency can make even a smart model frustrating to use in real-time applications. **How it shows up in Gloo:** Responsive interactions are essential for ministry and organizational workflows. Gloo monitors latency across each model provider we integrate and routes requests to the fastest reliable option. This helps Chat for Teams feel immediate, especially when retrieving larger documents from the Data Engine. ## Throughput **What it means:** Throughput measures how many prompts a model can handle per second when running at scale. **Why it matters:** Higher throughput means the system can serve more users at once—critical for apps that rely on AI for live chat, search, or customer service. **How it shows up in Gloo:** High throughput is important for partners who rely on Gloo for large scale search, enrichment, or chat workloads. Gloo manages throughput behind the scenes by autoscaling infrastructure when necessary, ensuring consistent performance regardless of volume. ## Tokens per Second (TPS) **What it means:** TPS tells you how quickly a model can generate text. AI models process and output content in units called tokens (which are usually chunks of words or syllables). **Example:** If a model runs at 50 TPS, it can generate a 250-token reply (about 2–3 paragraphs) in 5 seconds. **Why it matters:** Faster TPS leads to better user experiences, especially for tools that generate long or complex responses. **How it shows up in Gloo:** Gloo tracks TPS to ensure that longer answers, such as sermon summaries, and policy explanations generate quickly during live use. Faster TPS creates a smoother experience in Chat for Teams and helps Studio users iterate on content without waiting for slow responses. ## BLEU / ROUGE / F1 Score **What it means:** These are automated scores that measure how closely a model’s outputs match expected answers. * **BLEU:** Used mostly in translation tasks. * **ROUGE:** Commonly used to evaluate summarization. * **F1 Score:** Balances precision (how much of the output is correct) with recall (how much of the correct output was included). **Why it matters:** These scores provide fast, repeatable ways to evaluate accuracy, especially during model development. **How it shows up in Gloo:** Traditional accuracy metrics matter most for enrichment and document processing tasks. When Gloo extracts topics, classifies documents, or creates metadata, automated scoring methods help validate that enrichment is consistent and high quality across an organization's full content library. ## Human Eval **What it means:** Human evaluation is when people judge model outputs based on qualities like helpfulness, clarity, and tone. **Why it matters:** Humans can catch things machines can’t—like whether an answer is polite, well-structured, or socially appropriate. Human eval is especially important for AI used in education, counseling, or creative writing. **How it shows up in Gloo:** Human evaluation plays a major role in shaping Gloo’s alignment system. Our team of Safety reviewers assess model outputs to ensure responses are pastorally appropriate, theologically sound, and supportive of flourishing. These evaluations continuously inform Gloo’s safety instructions, grounding strategy, and model routing. *** *Next Up: Review and What’s Next* In the next section, we’ll answer: **“What have you learned and what can I do next with Gloo and AI?”** # Model Types & Categories Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/model-types-and-categories Breaks down the major types of AI models—foundation, frontier, open, and closed—and explains which Gloo uses for safe, scalable applications. # What Are the Different Types of AI Models? Not all AI models are built or used the same way. Some are massive and general-purpose, while others are lightweight or tuned for specific tasks or audiences. Understanding the **types and categories of models** is key to knowing what a given AI can or cannot do—and whether it’s the right tool for your use case. In this section, we’ll walk through the main ways people classify models: by how they’re trained, how they’re released, what languages they support, and how they’re tuned. ## Foundation Model **What it means:** A foundation model is a large, general-purpose AI model trained on vast amounts of text, code, or images. It serves as the base for many downstream applications. **Why it matters:** These models are typically trained once, then fine-tuned or adapted for specific uses—making them the “foundation” for a whole ecosystem of tools. GPT-4 and Claude are examples. **How it shows up in Gloo:** Gloo integrates trusted foundation models from leading providers and layers organizational alignment, rights protection, and retrieval on top of them. These models provide the core language understanding that powers Chat for Teams, enrichment, semantic search, and Studio workflows. ## Frontier Model **What it means:** A frontier model is a cutting-edge model representing the newest and most capable tier of AI performance. **Why it matters:** These models tend to push the boundaries of intelligence, safety, and scale. They’re often more powerful but also riskier if not carefully aligned. **How it shows up in Gloo:** Gloo routes you to frontier models when higher reasoning, deeper retrieval, or more complex text generation is needed. Frontier models offer stronger performance, and Gloo applies guardrails and theological alignment to ensure they operate safely in ministry and organizational contexts. ## Open-Weight Model **What it means:** An open-weight model has its training parameters publicly available. Anyone can download it, run it, or fine-tune it. **Why it matters:** This promotes transparency, experimentation, and decentralized innovation. Models like Meta’s LLaMA or Mistral fall into this category. **How it shows up in Gloo:** Gloo offers options of open weight models for certain classification, enrichment, or lightweight tasks where transparency or on device execution is helpful. Regardless of model source, Gloo wraps each model in the same alignment and rights protections. ## Closed Model **What it means:** Closed models are proprietary, with their weights, training data, and internal design kept private. **Why it matters:** While often highly capable, closed models like OpenAI’s GPT-4 can’t be audited or modified by the public. You only access them via API. **How it shows up in Gloo:** Depending on the need, some Gloo experiences rely on closed, high performing models such as GPT class or Claude class systems. These models connect through APIs and are always combined with content grounding, organizational alignment, and controlled system prompts. ## Multilingual Model **What it means:** These models are trained to understand and generate content in multiple languages. **Why it matters:** Multilingual support makes models globally useful and more inclusive. However, performance can vary across languages. **How it shows up in Gloo:** With Gloo you benefit from multilingual model capabilities, allowing you to interpret and enrich documents in multiple languages. Chat for Teams can also answer questions in a variety of languages as long as the organization's content is available for grounding. ## Instruction-Tuned Model **What it means:** This is a model fine-tuned to follow human instructions, usually using examples of prompt-response pairs. **Why it matters:** Instruction-tuning helps models understand tasks framed as natural language instructions—making them easier to interact with. **How it shows up in Gloo:** Instruction tuned models form the backbone of Gloo’s conversational experiences. Gloo builds additional instruction layers on top to ensure that responses reflect organizational voice, safety requirements, and theological boundaries while still delivering helpful, actionable outputs. *** *Next Up: What Kind of Hardware Powers AI, and Why Does It Matter?* In the next section, we’ll answer: **“What hardware does AI depend on, and how do specialized chips and infrastructure affect cost and performance?”** # Prompting & Generation Source: https://docs.gloo.com/ai-learning-center/gloo-ai-101/prompting-and-generation Teaches how to communicate effectively with AI through prompts, system instructions, and response generation to achieve accurate results in Gloo tools. # How Do I Tell AI What I Need It To Do? So now we know what AI models are and how they’re trained. But how do we actually use them? This section covers how we interact with models through prompts, how generation works behind the scenes, and what affects the quality of a model’s response. At Gloo AI, we believe one of the most empowering things you can learn is how to write clear prompts and understand what influences model behavior. ## Prompt **What it means:** A prompt is the input or question you give to an AI model. It tells the model what to generate or how to respond. **Examples:** * “Summarize this paragraph.” * “Write a prayer for a new mother.” * “What are 5 ways a church could use data in outreach?” **Why it matters:** The quality of the prompt heavily influences the quality of the output. Prompting well is often called “prompt engineering.” **How it shows up in Gloo:** Prompts are used throughout Gloo products, especially in Chat for Teams. When staff ask questions, write messages, or request summaries, the prompt determines how the model retrieves and uses their organization's content through the Data Engine. ## System Prompt **What it means:** A system prompt is a special instruction given to the model to set its overall behavior, tone, or rules **before** it interacts with a user. **Use case:** A system prompt might say: “You are a friendly assistant who always answers concisely and avoids strong opinions.” This tells the model how to behave for the entire session. **Note:** Users don’t always see the system prompt, but it often shapes how the model responds. **How it shows up in Gloo:** Gloo uses carefully designed system prompts to enforce organizational voice, theological alignment, safety expectations, and content boundaries. These system instructions guide model behavior consistently, even when users ask open-ended questions. ## Prompt Engineering **What it means:** Prompt engineering is the skill of designing prompts to get better, more accurate, or more useful responses from an AI model. **Why it’s useful:** Sometimes a vague prompt gives a vague answer. Prompt engineering helps you guide the model clearly by setting expectations in the prompt. **Example:** Instead of saying “Explain photosynthesis,” a better prompt might be “Explain photosynthesis in simple terms for a middle school science student.” **How it shows up in Gloo:** Prompt engineering helps organizations get the most out of Chat for Teams and our APIs. Clear prompts improve retrieval accuracy, content grounding, and response alignment, especially when working with complex or sensitive ministry topics. ## Inference **What it means:** Inference is the process the model goes through to generate a response based on your prompt. It happens after training, during actual usage. **Analogy:** Training is like studying for an exam. Inference is answering the question during the test. **Why it matters:** Inference speed and accuracy affect how useful the model is in real-time settings like chat apps or search tools. **How it shows up in Gloo:** Inference happens every time users interact with Gloo AI. The system retrieves relevant content from the Data Engine and generates aligned responses in real time. Fast inference ensures smooth chat experiences and accurate document enrichment. ## Tokens **What it means:** Tokens are chunks of text the model reads or generates. They can be as small as a character or as large as a word. **Example:** The sentence “Hello there!” is about 3 tokens: “Hello,” “there,” and “!” **Why it matters:** Models have token limits. Longer prompts or responses use more tokens. Most models process somewhere between 2,000 and 100,000 tokens depending on their size. **How it shows up in Gloo:** Token usage impacts request size and model behavior within the Gloo API. When documents are uploaded, they are chunked into token sized sections for embedding. ## Context Window **What it means:** The context window is the total number of tokens a model can “remember” at once. It includes both your prompt and the model’s reply. **Analogy:** Think of it like a whiteboard. If you write too much, older stuff gets erased to make room. Bigger context windows mean the model can reference more of the conversation. **How it shows up in Gloo:** Gloo uses models with large context windows to allow better grounding in an organization's uploaded content. This improves RAG accuracy, ensures longer documents can be analyzed, and helps maintain conversation continuity in Chat for Teams. ## Temperature **What it means:** Temperature is a setting that controls how creative or focused a model’s response is. **How it works:** * Lower temperature (like 0.2): More focused and predictable answers * Higher temperature (like 0.9): More random and creative responses **Use case:** Use a low temperature for fact summaries. Use a higher one for brainstorming. **How it shows up in Gloo:** Temperature is preset by Gloo for most experiences to balance creativity and safety. ## Top-k / Top-p Sampling **What it means:** These are technical settings that influence which words the model chooses from when generating text. They help balance randomness and coherence. **Quick comparison:** * **Top-k** looks at the top K likely words and picks from them * **Top-p** looks at the smallest number of words whose probabilities add up to P (like 90 percent), then chooses from those **Why it matters:** You usually don’t have to change these yourself, but understanding them can explain why a model sometimes repeats itself or surprises you. **How it shows up in Gloo:** Gloo manages these sampling settings internally to ensure stable, predictable behavior. By tuning these parameters, Gloo reduces repetition and maintains the accuracy and reliability that champion partners and developers expect. ## Stop Sequence **What it means:** A stop sequence is a specific signal that tells the model to stop generating text. **Use case:** You might set “###” as a stop sequence so the model stops there and doesn’t keep talking after a section. **How it shows up in Gloo:** Gloo uses stop sequences behind the scenes to structure outputs, enforce formatting rules, and keep responses within safe boundaries. This helps ensure that generated content does not run on too long or produce unintended sections. *** *Next Up: How AI Uses and Stores Knowledge* In the next section, we’ll answer: **“What are vectors, embeddings, and retrieval systems, and how do they help AI remember or reason?”** # Putting It All Together Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/all-together Synthesize all prompt engineering techniques into a practical daily framework with templates, checklists, and real-world applications. # Lesson 10: Putting It All Together ## You Made It. Now Let's Make It Stick. Remember when you first opened an AI chat interface and thought, "Okay... what do I even say to this thing?" Look how far you've come. Over nine lessons, you've built a complete toolkit: * **Lesson 1:** The conversation mindset * **Lesson 2:** How AI actually works (prediction, tokens, context windows) * **Lesson 3:** Task-Context-Format framework * **Lesson 4:** The four types of context * **Lesson 5:** Few-shot examples * **Lesson 6:** Role assignment * **Lesson 7:** Chain of thought reasoning * **Lesson 8:** Effective iteration * **Lesson 9:** Troubleshooting and diagnosis Now let's turn this knowledge into a practical system you can use every single day. *** ## The Prompt Engineering Checklist Before we dive into real-world examples, let's crystallize everything into a simple checklist. Think of this as your pre-flight routine before launching any important prompt. ### Your 5-Point Prompt Check **1. What's the task?** Be crystal clear about what you're asking for. "Help me with this email" is fuzzy. "Write a professional but warm email declining a meeting request" is precise. **2. What context does the AI need?** Remember: AI can't read your mind. Ask yourself, "If I were explaining this to a smart new colleague, what background would they need?" Include the audience, purpose, constraints, and any relevant details. **3. Would an example help?** For anything involving a specific style, format, or tone, showing is often better than telling. Even one good example can dramatically improve results. **4. Should I assign a role?** Sometimes the fastest path to a great response is telling the AI who to "be." Need critical feedback? Ask for a devil's advocate. Want something explained simply? Ask for a patient teacher. **5. Is this complex enough to need step-by-step thinking?** For anything involving analysis, reasoning, or multiple considerations, ask the AI to think through it step by step. This single technique prevents more errors than almost anything else. ### The Quick Version For everyday prompts, you can boil this down to three questions: * **What** do I want? (Task) * **Why** and for **whom**? (Context) * **How** should it look? (Format/Examples) Nail these three, and you'll be ahead of 90% of people using AI. *** ## Building Prompts for Real-World Tasks Theory is great, but let's get practical. Here's how to apply everything you've learned to the tasks you probably face regularly. ### Writing Emails **The common mistake:** > "Write an email to my boss about the project delay." **The prompt engineer's approach:** > "I need to email my boss about a two-week delay on our product launch. The delay is due to unexpected technical issues with our payment system (not anyone's fault, just complex problems that surfaced during testing). My boss values directness and solutions over excuses. Write a professional email that acknowledges the delay, briefly explains why, and proposes a new timeline with specific next steps. Keep it under 200 words." **Why it works:** You've given the AI the what (email about delay), the why (technical issues), the who (a boss who values directness), and the how (professional, solution-focused, concise). *** ### Summarizing Documents **The common mistake:** > "Summarize this article." **The prompt engineer's approach:** > "Summarize this article for someone who has 2 minutes to decide whether to read the full thing. Include: the main argument, 2-3 key supporting points, and any surprising findings or conclusions. Format as bullet points. If the article makes recommendations, list those separately at the end." **Why it works:** You've specified the audience (busy decision-maker), the purpose (decide whether to read more), and the format (bullet points with specific sections). *** ### Brainstorming Ideas **The common mistake:** > "Give me ideas for team building activities." **The prompt engineer's approach:** > "I'm planning a team building activity for a group of 12 people. We're a remote team meeting in person for the first time at a conference in Chicago. We have a 2-hour window on Thursday evening, budget of around \$500, and we have team members who don't drink alcohol. Some people are introverted and hate forced icebreakers. > > Give me 5 creative ideas that would help us actually get to know each other without feeling awkward. For each idea, briefly explain why it might work for our specific situation." **Why it works:** You've loaded the prompt with specific constraints and context. The AI can now generate ideas tailored to your actual situation rather than generic suggestions you'd find in any Google search. *** ### Analyzing Information **The common mistake:** > "What do you think of this business plan?" **The prompt engineer's approach:** > "Act as an experienced venture capital investor who has seen hundreds of business plans. Review the attached business plan and provide analysis in three parts: > > 1. STRENGTHS: What's compelling about this opportunity? > 2. CONCERNS: What questions or red flags would you raise? > 3. MISSING PIECES: What information would you want before making a decision? > > Be direct and specific. I'd rather hear honest concerns now than polished encouragement." **Why it works:** You've assigned a knowledgeable role, requested structured output, and explicitly asked for honest feedback (which helps overcome AI's tendency toward generic positivity). *** ## Try It Yourself: The Transformation Exercise Let's practice turning weak prompts into strong ones. For each prompt below, identify what's missing and rewrite it using the techniques you've learned. ### Exercise 1: The Vague Request **Original:** "Help me prepare for my interview." **What's missing?** (Think about it before reading on...) The AI doesn't know: What job? What company? What stage of the interview process? What are you worried about? What's your background? **Your turn:** Rewrite this prompt to include the relevant context. *** ### Exercise 2: The Formatless Ask **Original:** "Tell me about climate change." **What's missing?** No audience, no purpose, no format, no constraints. This could produce a 10-page essay or a single sentence. **Your turn:** Rewrite this for a specific situation. Maybe you're preparing a 5-minute presentation for high school students, or you need talking points for a skeptical relative, or you're writing a newsletter for your company. *** ### Exercise 3: The Skipped Steps **Original:** "Should I take this job offer?" **What's missing?** The AI has no information about you, the offer, your priorities, or your alternatives. And for a complex decision like this, you probably want it to think through multiple angles. **Your turn:** Rewrite this to give the AI what it needs to actually help you think through the decision. *** ## Creating Your Personal Prompt Templates Here's a secret that experienced prompt engineers know: you don't need to craft every prompt from scratch. Once you find prompts that work well for tasks you do regularly, save them as templates. ### How to Build Your Template Library **Step 1: Notice patterns** What types of tasks do you use AI for repeatedly? Emails? Meeting summaries? Research? First drafts? Feedback on your writing? **Step 2: Create your first templates** Take your best prompts for recurring tasks and turn them into fill-in-the-blank templates. Here are some starters: *** **Email Template:** ``` Write a [TONE] email to [RECIPIENT/RELATIONSHIP] about [TOPIC]. Context: [RELEVANT BACKGROUND] The email should: - [KEY POINT 1] - [KEY POINT 2] - [DESIRED OUTCOME/CALL TO ACTION] Keep it [LENGTH] and match the tone of someone who [TONE DESCRIPTION]. ``` *** **Feedback Request Template:** ``` Act as a [RELEVANT EXPERT ROLE]. Review the following [DOCUMENT TYPE] and provide feedback on: 1. [ASPECT 1] 2. [ASPECT 2] 3. [ASPECT 3] Be direct and specific. Highlight both strengths and areas for improvement. [PASTE YOUR CONTENT HERE] ``` *** **Learning/Explanation Template:** ``` Explain [TOPIC] to someone who [AUDIENCE DESCRIPTION]. Use [ANALOGY TYPE] analogies where helpful. Break it down into [NUMBER] main concepts, and for each one: - Explain what it is in simple terms - Give a concrete example - Explain why it matters Avoid jargon unless you define it first. ``` *** **Decision-Making Template:** ``` Help me think through a decision about [DECISION]. Here's the context: - My situation: [BACKGROUND] - My options: [LIST OPTIONS] - What I care about most: [PRIORITIES] - My concerns: [WORRIES/CONSTRAINTS] Walk through this step by step. Consider the pros and cons of each option, potential risks I might not be seeing, and questions I should ask myself before deciding. ``` *** **Step 3: Refine over time** Every time you use a template and get great results, note what worked. When something falls flat, figure out why and update your template. Your library will get better the more you use it. *** ## Common Pitfalls (One Last Time) As you head out into the world with your new skills, watch out for these traps: **The "It Worked Once" Trap** Just because a prompt worked perfectly one time doesn't mean it will always work, especially if you're using different AI tools or the model has been updated. Stay flexible and be ready to iterate. **The "More Is Always Better" Trap** Yes, context matters. But there's such a thing as too much. If you dump your entire life story into every prompt, the AI might lose focus on what actually matters. Include relevant context, not all context. **The "Set It and Forget It" Trap** Your prompt templates are starting points, not final destinations. The best prompts evolve as you learn what works, as your needs change, and as AI tools improve. **The "AI Said It, Must Be True" Trap** You've learned to troubleshoot and verify. Don't forget that now that you're more confident. Even with perfect prompts, AI can still make mistakes. Your judgment matters. *** ## Level Up: Your Capstone Challenge You've made it to the final challenge of this course. Here's your mission: **Create a "Prompt Engineering Playbook" for your own life.** 1. **Identify 3-5 tasks** you do regularly where AI could help (work tasks, personal projects, whatever fits your life) 2. **For each task, create a polished prompt template** using the techniques from this course 3. **Test each template** at least twice, noting what works and what needs adjustment 4. **Refine and save your templates** somewhere you'll actually use them This isn't just an exercise: it's the beginning of a practical tool that will keep paying dividends. The more you use and refine these templates, the more AI becomes a genuine extension of your capabilities. *** ## Where to Go From Here Congratulations. Seriously. You've done something most people never bother to do: you've actually learned how to communicate effectively with AI. That's a skill that will only become more valuable. But learning doesn't stop here. Here are some ways to keep growing: ### Keep Practicing The single best way to improve is to use what you've learned. Every prompt is practice. Every iteration teaches you something. Make prompting part of your regular workflow. ### Stay Curious AI is evolving fast. New models bring new capabilities (and sometimes new quirks). Follow the updates from major AI providers, not to chase every shiny feature, but to understand how your tools are changing. ### Explore Different Tools If you've mainly used one AI assistant, try others. Different models have different strengths. Claude tends to be great at nuanced analysis and writing. GPT excels at certain technical tasks. Experimenting helps you understand what's possible and match tools to tasks. ### Learn From the Community There are vibrant communities of people sharing prompting techniques, templates, and discoveries. Reddit forums, Discord servers, newsletters: find the sources that match your interests and skill level. ### Teach Someone Else One of the best ways to solidify your own understanding is to explain it to someone else. When a colleague or friend asks how to get better results from AI, you now have a lot to share. *** ## Conclusion: You've Got This Think back to that first tentative prompt you ever typed. Compare it to what you know now: * You understand that AI predicts based on patterns, and you can shape those predictions with clear instructions * You know how to provide context that eliminates guesswork * You can show the AI exactly what you want through examples * You can assign roles to tap into different expertise and perspectives * You know when to ask for step-by-step reasoning * You're comfortable iterating and refining until you get what you need * And when things go wrong, you can diagnose the problem and fix it That's not just theoretical knowledge. That's a genuine toolkit. The truth is, prompt engineering isn't really about AI at all. It's about clear thinking and clear communication. When you learn to articulate what you want, why you want it, and how it should look, that skill transfers everywhere. To emails. To meetings. To any situation where you need to convey your ideas clearly. AI is a powerful amplifier. And now you know how to point it in the right direction. So go use what you've learned. Start small if you want: a better email, a clearer summary, a more thorough brainstorm. But start. Because the gap between knowing these techniques and benefiting from them is simply practice. You've got the toolkit. You've got the checklist. You've got a clear path forward. Now go make something great. *** *Thank you for completing the Prompt Engineering 102 course. We hope these skills serve you well, and that AI becomes a trusted tool in your work and life.* # The Anatomy of a Great Prompt Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/anatomy-of-a-prompt Master the three pillars of effective prompts: Task, Context, and Format, plus learn how specificity and constraints improve your results. # Lesson 3: The Anatomy of a Great Prompt ## What Makes Some Prompts Work Like Magic? You've learned that AI is a conversation partner that predicts based on patterns. Now let's get practical: what actually makes a prompt work? Think about giving directions to a new employee. "Handle the client situation" could mean anything from sending an email to scheduling a meeting to issuing a refund. But "Send a friendly follow-up email to the client confirming their order shipped today" gives them exactly what they need to help you. AI works the same way. Vague instructions force guessing. Clear instructions let AI focus on actually delivering what you need. The good news? Writing great prompts isn't some mystical skill. It's a learnable craft with clear principles that build directly on the conversation mindset from Lesson 1 and your understanding of how AI predicts from Lesson 2. *** ## Core Concepts ### The Three Pillars: Task, Context, and Format Think of every great prompt as a three-legged stool. Remove any leg, and the whole thing gets wobbly. These three elements work together to give AI everything it needs to help you: **1. Task: What do you want the AI to do?** This is your verb, your action, your mission statement. Be specific about the actual work you need done. * Weak: "Help me with this email." * Strong: "Rewrite this email to be more professional while keeping the friendly tone." **2. Context: What background does the AI need?** AI doesn't know your situation, your audience, or your constraints unless you tell it. Context fills in the blanks. * Weak: "Write a welcome message." * Strong: "Write a welcome message for new volunteers at a food bank, most of whom are first-time volunteers in their 20s and 30s." **3. Format: How should the response look?** Do you want bullet points or paragraphs? A formal report or a casual summary? Three sentences or three pages? Tell the AI what shape the answer should take. * Weak: "Explain project management." * Strong: "Explain project management in 5 bullet points that a high school student could understand." When you nail all three, something almost magical happens: the AI stops guessing and starts delivering exactly what you need. *** ### Being Specific vs. Being Vague Let's play a game. Below are two prompts asking for essentially the same thing. Which one do you think will get better results? **Prompt A:** > "Write something about team communication." **Prompt B:** > "Write a 200-word article for managers explaining three specific ways to improve team communication in remote work environments. Use a conversational but professional tone." It's not even close, right? Here's what Prompt A might produce: A generic essay about communication that could apply to anyone, anywhere, covering everything from body language to email etiquette. It might be technically correct but completely unhelpful for your actual needs. Here's what Prompt B produces: A focused, practical article that speaks directly to managers, addresses remote work specifically, and delivers exactly three actionable tips in about 200 words. **The specificity spectrum looks like this:** | Vague | Specific | | - | - | | "Write about dogs" | "Write a 300-word guide for first-time dog owners on the three most important things to know about house training a puppy" | | "Help me with my resume" | "Review my resume for a marketing manager position and suggest improvements to make my accomplishments more quantifiable" | | "Explain economics" | "Explain inflation to a 10-year-old using an analogy about their allowance and candy prices" | Notice the pattern? Specific prompts include WHO the audience is, WHAT the scope is, HOW it should be presented, and sometimes even WHY you need it. *** ### The Power of Constraints: Telling AI What NOT to Do Here's a counterintuitive truth: sometimes the best way to get what you want is to tell AI what you don't want. Constraints are like guardrails on a mountain road. They don't slow you down; they keep you from driving off a cliff. **Examples of powerful constraints:** * "Do NOT use jargon or technical terms" * "Avoid cliches like 'think outside the box' or 'at the end of the day'" * "Don't include any information about pricing" * "Skip the introduction and get straight to the actionable advice" * "Do not make up statistics; only include claims you can support" **Why constraints work so well:** AI is trained to be helpful, which sometimes means it over-delivers. It might add flowery introductions when you want directness. It might use industry buzzwords when you need plain language. It might include disclaimers when you just want the answer. Constraints solve this by drawing clear boundaries. They're like telling a friend, "I need advice, but please don't suggest I talk to my ex." You're not being rude; you're being clear about what kind of help you actually need. **A prompt transformation:** Before: > "Write a product description for our new running shoes." After: > "Write a product description for our new running shoes. Keep it under 100 words. Focus on comfort and durability, not style. Avoid superlatives like 'best' or 'amazing.' Don't mention competitor brands." That second prompt will produce something you can actually use, with far less back-and-forth. *** ### Prompt Length: When More is Better and When Less is More "Should my prompts be long or short?" The honest answer: it depends. But here's a framework to help you decide. **Shorter prompts work when:** * The task is simple and self-explanatory * You're brainstorming and want variety * You're having a back-and-forth conversation and can clarify as you go * The context is already established from earlier in the conversation **Example of a good short prompt:** > "Give me 10 names for a bakery that specializes in sourdough." This works because the task is clear, the context is embedded (bakery + sourdough), and the format is implied (a list of names). **Longer prompts work when:** * The task is complex or multi-step * You need a specific output format * The context requires explanation * You want to avoid multiple rounds of revision * Precision matters more than speed **Example of a good long prompt:** > "I'm a nonprofit director preparing a board presentation about our volunteer program. Write an executive summary (250-300 words) covering: 1) volunteer growth over the past year, 2) key challenges we've faced, and 3) our goals for next year. The tone should be optimistic but honest. Board members are busy professionals who appreciate data and clear takeaways. Assume I'll fill in the specific numbers later; use \[X] as a placeholder." This prompt is longer, yes, but every word earns its place. There's no fluff; it's all useful instruction. **The golden rule:** Your prompt should be as long as it needs to be and no longer. A 50-word prompt that gives clear, complete instructions beats a 200-word prompt that rambles. But a thorough 200-word prompt that prevents three rounds of revision beats a 50-word prompt that leaves the AI guessing. *** ## Try It Yourself Ready to put this into practice? Here are three exercises you can do right now with any AI assistant. ### Exercise 1: The Three Pillars Check Take this vague prompt and rewrite it using Task, Context, and Format: **Original:** "Help me with my presentation." **Your rewrite should include:** * What specific help do you need? (Task) * What's the presentation about, and who's the audience? (Context) * What form should the help take? (Format) *Hint: A great rewrite might look like: "Create an outline for a 10-minute presentation about our team's Q3 results. The audience is company executives who care most about revenue growth and customer retention. Format it as bullet points with suggested talking times for each section."* ### Exercise 2: The Specificity Upgrade Transform these vague prompts into specific ones: 1. "Write a social media post." Your specific version: ***\_\_***\_ 2. "Explain machine learning." Your specific version: ***\_\_***\_ 3. "Give me recipe ideas." Your specific version: ***\_\_***\_ ### Exercise 3: Add Constraints Take this prompt and add at least three constraints that would make the output more useful: **Original:** "Write a thank-you email to a donor." **Your version with constraints:** ***\_\_***\_ *Think about: length, tone, what to include, what to avoid, format preferences.* *** ## Common Pitfalls Even with the best intentions, there are a few traps that trip people up. Here's how to avoid them: ### Pitfall 1: The Kitchen Sink Prompt **The problem:** Cramming so many requirements into one prompt that the AI gets confused or drops some of them. **What it looks like:** > "Write a blog post about productivity that's funny but professional, includes statistics, tells a personal story, is SEO-optimized for the keyword 'time management,' has a compelling headline, includes a call to action, is between 800-1000 words, uses the inverted pyramid structure, avoids passive voice, and mentions our product naturally without being salesy." **The fix:** If you have this many requirements, break them into steps. First, ask for an outline. Review it. Then ask for a draft. Then ask for specific revisions. Complex outputs often require a conversation, not a single prompt. ### Pitfall 2: Assuming the AI Knows Your Context **The problem:** Leaving out crucial information because it feels obvious to you. **What it looks like:** > "Write a follow-up email about the meeting." The AI doesn't know: What meeting? With whom? What was discussed? What's the relationship? What's the goal of the follow-up? **The fix:** Pretend you're briefing a smart new colleague who just joined today. They're capable, but they don't know your history, your relationships, or your unspoken assumptions. ### Pitfall 3: Format Amnesia **The problem:** Getting a response in the wrong format and then struggling to reshape it. **What it looks like:** You wanted bullet points but got a wall of paragraphs. You wanted a brief answer but got an essay. You wanted plain language but got academic prose. **The fix:** State your format preferences upfront. "Use bullet points." "Keep this under 100 words." "Write like you're explaining to a friend over coffee." AI is remarkably good at matching formats; you just have to ask. ### Pitfall 4: The Vague Feedback Loop **The problem:** Responding to an unsatisfactory output with equally vague feedback. **What it looks like:** * You: "Write a product description." * AI: \[Writes something] * You: "That's not quite right." The AI can't improve without knowing what specifically missed the mark. **The fix:** Be specific about what needs to change. "The tone is too formal; make it more playful." "Focus more on the health benefits and less on the ingredients." "Shorten the first paragraph and expand the third." Precise feedback leads to precise improvements. *** ## Level Up Here's a challenge to test your new skills: **The Scenario:** You work for a small bookstore and need to write a monthly newsletter email. Your audience is loyal customers who appreciate book recommendations but don't want to feel marketed to. **Your Mission:** Write a single prompt that would generate the first draft of this newsletter. Your prompt should: * Include all three pillars (Task, Context, Format) * Be specific about the tone and audience * Include at least two constraints * Specify the approximate length Take 5 minutes to craft your prompt before moving on. Then actually use it with an AI assistant and see what you get! **Bonus challenge:** If the first output isn't quite right, write a specific follow-up prompt that would improve it, using what you learned about avoiding the vague feedback loop. *** ## Key Takeaway Great prompts aren't magic; they're clear communication. The Task-Context-Format framework gives you a reliable structure: specify what you want done, provide the background AI needs, and describe how the output should look. Add constraints to keep AI focused, and match your prompt length to the complexity of the request. *** ## What's Next Now that you understand the anatomy of a great prompt, we're going to dive deep into one of the three pillars: context. In **Lesson 4: Context is King**, we'll explore why AI can't read your mind (and what to do about it), learn the "pretend you're explaining to a new coworker" test, and discover exactly what kinds of context lead to dramatically better results. Because here's the truth: most prompt problems aren't about unclear tasks or wrong formats. They're about missing context. And once you master the art of providing the right background information, you'll wonder how you ever got by without it. See you in the next lesson! # The Art of Iteration Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/art-of-iteration Learn to refine, clarify, and redirect AI responses through effective follow-up prompts and know when to iterate versus start fresh. # Lesson 8: The Art of Iteration ## Why Your First Prompt Is Just the Beginning Here's a secret that might make you feel better: even the people who seem like AI wizards rarely nail it on the first try. That perfect response you saw someone share online? It probably took them three, five, maybe ten attempts to get there. They just didn't show you the messy middle. You've now learned the core techniques: Task-Context-Format, the four context types, few-shot examples, role assignment, and chain of thought. But here's the truth: even with all these tools, your first prompt is rarely your best. And that's completely fine. The real skill isn't writing perfect prompts; it's knowing how to quickly improve them when they miss the mark. ## The Sculptor's Mindset Imagine you're watching a sculptor at work. They don't sit down, make one perfect chisel strike, and reveal a masterpiece. They chip away, step back, squint, chip some more, rotate the stone, and keep refining. The final statue emerges through dozens of small adjustments, not one brilliant flash of genius. Working with AI is exactly the same. Your first prompt is the rough block of marble. Iteration is the chisel. So let's learn how to sculpt. *** ## Core Concepts ### Why Your First Prompt Is Rarely Your Best Think about the last time you explained something complicated to a friend. Did they understand perfectly the first time? Probably not. You likely had to: * Rephrase when they looked confused * Add an example when the concept didn't land * Back up and fill in context you'd assumed they had Prompting AI works the same way. Your first prompt is an educated guess at what will work. The AI's response tells you whether that guess was right, and if not, what might need adjusting. Here's the mindset shift: **a "failed" prompt isn't a failure. It's information.** It's the AI showing you, "Hey, I interpreted your request this way. Is that what you wanted?" Often the answer is "not quite," and that's totally fine. The goal isn't to eliminate iteration. The goal is to get good at it. ### The Feedback Loop: Refine, Clarify, Redirect Every response from AI gives you something to work with. Think of prompting as a feedback loop with three main moves: **1. Refine:** The response is close but not quite right * "That's good, but make it shorter." * "Can you make this more formal?" * "I like points 2 and 4. Expand on those and drop the others." **2. Clarify:** The AI misunderstood something * "When I said 'marketing copy,' I meant for social media, not email." * "I should have mentioned: this is for teenagers, not adults." * "I'm asking about the process, not the history." **3. Redirect:** The response went somewhere you don't need * "Let's focus just on the budget section." * "Actually, forget the technical details. I need this explained simply." * "That's not quite the angle I need. Can you approach this from \[different perspective]?" The magic is that you don't have to re-explain everything each time. The AI remembers the conversation. You can build on what's already there. ### Useful Follow-Up Phrases That Actually Work Here's a toolkit of follow-up phrases you can steal. These are conversation-continuers, ways to nudge the AI in the right direction without starting over: **When you need adjustments to tone or style:** * "Make this more casual/formal/friendly/professional." * "Rewrite this as if you're talking to a \[specific audience]." * "This is too salesy. Tone it down." **When length is the issue:** * "Cut this in half." * "Give me just the key points, no more than 3 bullet points." * "This needs to be longer. Add more detail to each section." **When you want to zoom in:** * "Expand on the second point." * "I like where you're going with \[specific part]. Tell me more about that." * "Let's dive deeper into the part about..." **When something's missing:** * "You didn't address \[specific thing]. Can you add that?" * "Include an example for each point." * "Add some specific numbers or statistics." **When you want alternatives:** * "Give me three different versions of this." * "What's another way to approach this?" * "That's one option. What else could work?" **When it's just not right:** * "That's not quite what I meant. Let me try explaining differently: \[new explanation]." * "Ignore that last response and try this instead..." * "Let's start this section over. Here's what I actually need..." Notice how these phrases are conversational, not robotic. You're talking to the AI like a collaborator, not issuing commands to a machine. ### Knowing When to Start Fresh vs. Keep Refining Sometimes iteration works beautifully. Other times, you realize you're just polishing a rock when you actually wanted a diamond. Here's how to tell the difference: **Keep iterating when:** * The AI understood the core task but the execution needs tweaking * You can clearly see what adjustment would help * Each response is getting closer to what you want * You're making progress with each exchange **Start fresh when:** * The AI fundamentally misunderstood your goal * You've realized you were asking for the wrong thing entirely * The conversation has gone so far down a wrong path that corrections would be confusing * You've learned enough from the failed attempts to write a much better initial prompt Here's a good rule of thumb: **if your next message would be longer than your original prompt, you might be better off starting over with a clearer initial request.** Starting fresh isn't admitting defeat. It's using what you learned. Often your second attempt will be dramatically better because you now understand how the AI interpreted your first try. *** ## Try It Yourself ### Exercise 1: The Intentional Iteration Start with this deliberately vague prompt: > "Write something about productivity." See what the AI generates. Then practice iteration by sending follow-up messages to shape it into something useful. Try to get to a final result you'd actually use through a series of refinements. Track your conversation. How many exchanges did it take? What follow-up phrases worked best? ### Exercise 2: The Rescue Mission Give the AI this prompt: > "Explain blockchain." You'll likely get a technical explanation. Now practice redirecting. Through follow-up messages alone (don't start over), transform this into: * An explanation for a 10-year-old * A 2-sentence summary * An analogy using a library * A list of why someone might actually care ### Exercise 3: The A/B Test Ask for something and get a response. Then start a completely new conversation and ask for the same thing, but apply what you learned in the first attempt to write a better initial prompt. Compare: Which approach got you to a good result faster? The iterative conversation or the improved single prompt? (Spoiler: The answer will depend on the task. Both approaches have their place.) *** ## Common Pitfalls ### Pitfall 1: Giving Up After One "Bad" Response One response that misses the mark doesn't mean you're bad at prompting or the AI is broken. It means you're at the beginning of a normal process. The question isn't "why didn't this work?" but rather "what does this response tell me about how to adjust?" ### Pitfall 2: Making Corrections Too Vague Saying "That's not right" or "Try again" doesn't give the AI much to work with. Be specific about what's off. Instead of "That's not good," try "The tone is too formal for my audience" or "I need this to focus more on practical steps, not theory." ### Pitfall 3: Piling On Too Many Changes at Once If you ask for six different adjustments in one message, it's hard to tell which changes helped and which hurt. When in doubt, make one or two changes at a time so you can see what's working. ### Pitfall 4: Never Starting Fresh Sometimes people get stubborn and keep iterating on a doomed conversation out of sunk-cost thinking. If you've gone back and forth five times and you're still nowhere near what you need, it's probably faster to start over with a new approach. ### Pitfall 5: Always Starting Fresh The opposite problem: abandoning ship at the first imperfect response. Give iteration a chance before you reset. Often you're just one or two follow-ups away from something great. *** ## Level Up: The Three-Turn Challenge Here's a challenge to sharpen your iteration skills: Pick any task you genuinely need done (an email, a summary, a creative piece, whatever). Your goal: get to a response you'd actually use in exactly three turns. * **Turn 1:** Your initial prompt (make it reasonable, not intentionally vague) * **Turn 2:** One follow-up refinement based on the first response * **Turn 3:** One more refinement to polish it The constraint of three turns forces you to be intentional about each message. You can't rely on endless back-and-forth, so you have to make each exchange count. Try this challenge three times with different tasks. You'll notice your initial prompts getting better and your follow-up instincts getting sharper. *** ## Key Takeaway Expert prompters aren't people who write perfect prompts. They're people who iterate quickly and effectively. They treat the first response as a starting point, not a final answer. They know how to read what the AI gave them, identify what needs to change, and make targeted adjustments.\* *So next time your first prompt doesn't land perfectly, don't get frustrated. Get curious. Ask yourself: what did the AI understand, what did it miss, and what's my next move?* *** ## What's Next Sometimes, despite your best iterating efforts, things go sideways. The AI misunderstands you completely, gives you something way too long or absurdly short, confidently states something incorrect, or refuses to help with something perfectly reasonable. In **Lesson 9: Troubleshooting (When AI Goes Wrong)**, we'll build your diagnostic toolkit. You'll learn to identify what's actually causing problems and how to fix them. Because even the best prompters hit walls, they just know how to get around them. # Context is King Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/context-is-king Learn how to provide the right background information to AI, including the four types of context that matter most and how to structure it effectively. # Lesson 4: Context is King ## The Missing Ingredient In Lesson 3, you learned the Task-Context-Format framework. Now we're going to dive deep into the most commonly neglected pillar: context. Here's a scenario. You tell a colleague, "Can you handle the Johnson situation?" If they've been in every meeting with you for the past month, they know exactly what you mean. But if they just transferred from another department? They're lost. Every AI conversation starts fresh. It doesn't know your job, your project, your preferences, or what happened five minutes ago in a different chat. When you skip context, you're asking AI to help with "the Johnson situation" when it has no idea who Johnson is. The good news? Once you understand what context AI actually needs, providing it becomes second nature. *** ## Core Concepts ### Why AI Can't Read Your Mind (And What To Do About It) Let's get one thing straight: AI is incredibly powerful, but it's not psychic. When you start a conversation, the AI knows only what you tell it in that moment. It doesn't have access to: * Your previous conversations (unless you're in the same chat thread) * Your company's internal knowledge * Your personal preferences * The project you've been working on all week * The email chain that prompted your question Think of it this way: every prompt is like meeting someone for the first time. They're smart, helpful, and eager to assist, but they're starting from zero. If you want them to help effectively, you need to bring them up to speed. **The fix?** Tell the AI what it needs to know. Be the narrator of your own situation. *** ### The "New Coworker" Test Here's a mental trick that will instantly improve your prompts: **Before you hit enter, ask yourself: "Would a brand new coworker understand what I'm asking?"** Imagine someone just started at your job today. They're smart, they're motivated, but they don't know the internal lingo, the backstory, or the unspoken expectations. If you walked up to their desk and said exactly what you're about to type to the AI, would they be able to help you? If the answer is "probably not," then you need to add more context. Let's see this in action: **Without context (confusing for a new coworker):** > "Can you fix the intro?" **With context (now they can actually help):** > "I'm writing a fundraising email for our nonprofit's annual campaign. The intro feels too formal and we want it to sound warm and personal. Can you suggest a more conversational opening that still communicates urgency?" See the difference? The second prompt tells the AI: * What you're working on (fundraising email) * Who it's for (nonprofit, annual campaign) * What the problem is (too formal) * What you want instead (warm, personal, conversational) * Any constraints (still needs to communicate urgency) That's not extra work. That's giving the AI everything it needs to actually help you. *** ### The Four Types of Context That Matter Most Not all context is created equal. When you're deciding what to include, focus on these four categories: #### 1. Audience Who is this for? A CEO reads differently than a teenager. A technical expert needs different language than a complete beginner. **Examples:** * "This is for small business owners who aren't tech-savvy." * "My audience is parents of middle schoolers." * "I'm presenting to our executive team who has limited time." #### 2. Purpose What are you trying to accomplish? Information? Persuasion? Entertainment? Problem-solving? **Examples:** * "I need to convince my manager to approve this budget." * "This should help customers troubleshoot on their own before calling support." * "I want to make a complex topic feel approachable and interesting." #### 3. Tone How should it sound? Professional? Casual? Urgent? Playful? The same information can be delivered in wildly different ways. **Examples:** * "Keep it professional but not stuffy." * "This should feel like advice from a trusted friend." * "Match the tone of our other marketing materials, which is upbeat and encouraging." #### 4. Constraints What are the boundaries? Length limits, things to avoid, required elements, formatting needs? **Examples:** * "Keep it under 200 words." * "Don't mention competitors by name." * "Must include a call-to-action at the end." * "Format as bullet points, not paragraphs." You don't always need all four, but asking yourself "Have I covered audience, purpose, tone, and constraints?" is a great checklist before you send a prompt. *** ### Structuring Context So AI Doesn't Get Lost Here's something that trips up a lot of people: they dump a huge wall of text at the AI and expect it to sort everything out perfectly. Sometimes it works. Often, it doesn't. AI processes information in order, and it can get confused when context is scattered, contradictory, or buried in the middle of a long prompt. The solution? Structure your context clearly. **A simple structure that works:** ``` BACKGROUND: [What the AI needs to know about the situation] TASK: [What you want the AI to do] REQUIREMENTS: [Any specific constraints or preferences] ``` Let's see this in practice: **Unstructured (harder to follow):** > "So I have this blog post I wrote last week about productivity tips and my editor said it's too long and also we need to make sure it appeals to remote workers specifically, not just anyone, and can you help me shorten it while keeping the main points and maybe make it sound a bit more energetic? Oh and it should be under 800 words. Here's the post: \[text]" **Structured (much clearer):** > "BACKGROUND: I wrote a blog post about productivity tips. My editor says it's too long and wants it to appeal specifically to remote workers. > > TASK: Help me shorten this post while keeping the main points intact. > > REQUIREMENTS: > > * Target audience: remote workers > * Tone: energetic and engaging > * Length: under 800 words > > Here's the current post: > \[text]" The content is the same, but the structured version is dramatically easier for both you and the AI to work with. **Pro tip:** You don't have to use labels like "BACKGROUND" and "TASK." The key is putting related information together and separating different types of instructions clearly. Even simple line breaks and logical ordering help a lot. *** ### The Goldilocks Zone: Not Too Little, Not Too Much Is there such a thing as too much context? Actually, yes. If you write a 2,000-word preamble before asking a simple question, the AI might lose focus on what you actually need. Worse, it might start hallucinating connections that aren't there. The goal is **relevant** context. Ask yourself: "Does this information help the AI give me a better answer?" If yes, include it. If not, leave it out. **Too little context:** > "Write an apology email." (Apologizing for what? To whom? How serious is this?) **Too much context:** > "Last Tuesday at 3:47 PM, I was in a meeting with Sarah, who works in accounting, and she has two kids and just got back from vacation in Florida, and during the meeting I accidentally interrupted her while she was presenting Q3 numbers, which reminded me of something my grandmother used to say about patience, and anyway I need to write an apology email..." (Most of this isn't relevant. The AI doesn't need to know about Florida or your grandmother.) **Just right:** > "I accidentally interrupted a colleague during her presentation in a team meeting. I want to send a brief, sincere apology email. Keep it professional but warm, and not overly dramatic since it was a minor incident." *** ## Try It Yourself Time to practice! Here are some exercises to build your context-giving muscles. ### Exercise 1: The Context Upgrade Take this vague prompt and rewrite it with proper context: **Original:** "Help me write a message to my team." Before rewriting, ask yourself: * What kind of message? (announcement, update, request, celebration?) * What's it about? * How should it sound? * How long should it be? Write your upgraded version, then try both prompts and compare the results. ### Exercise 2: The New Coworker Test Think of a task you actually need help with right now. Write your prompt, then read it out loud as if you're explaining it to someone who just started working with you today. * Did you use any jargon they wouldn't know? * Did you assume knowledge they wouldn't have? * Is there backstory they'd need? Revise accordingly, then test it. ### Exercise 3: The Four-Part Checklist Pick any writing task (an email, a social media post, a summary) and before prompting, explicitly write out: 1. **Audience:** Who is this for? 2. **Purpose:** What should it accomplish? 3. **Tone:** How should it sound? 4. **Constraints:** Any rules or limits? Then incorporate all four into your prompt and see how the output improves. *** ## Common Pitfalls ### Pitfall 1: The "You Know What I Mean" Assumption **What it looks like:** "Make it better" or "Fix this" without explaining what "better" means to you. **Why it fails:** "Better" could mean shorter, longer, funnier, more formal, more detailed, simpler... the AI will guess, and it might guess wrong. **The fix:** Be specific. "Make it more concise" or "add more technical detail" or "adjust the tone to sound more confident." ### Pitfall 2: Assuming the AI Remembers **What it looks like:** Referencing something from a previous conversation that happened in a different chat session. **Why it fails:** Each conversation typically starts fresh. The AI doesn't remember that last week you told it you work in healthcare or that you prefer bullet points. **The fix:** Re-state important context at the start of each new conversation. Yes, it feels repetitive to you, but the AI genuinely doesn't know. ### Pitfall 3: Burying the Important Stuff **What it looks like:** Writing three paragraphs of background before mentioning what you actually need, or putting critical constraints at the very end. **Why it fails:** The AI processes your prompt in order. Important details can get lost in the shuffle. **The fix:** Lead with the most important information. State your task clearly near the beginning, then provide supporting context. ### Pitfall 4: Contradictory Context **What it looks like:** "Make it short and comprehensive" or "Keep it casual but very professional." **Why it fails:** The AI will try to satisfy both requirements and usually end up doing neither well. **The fix:** Think through what you really want. If there's a tension (short but detailed), acknowledge it: "Keep it concise, but make sure to cover X, Y, and Z specifically." *** ## Level Up: The Context Challenge Here's a prompt that's missing critical context: > "Write a paragraph about dogs." Your challenge: Rewrite this prompt three different times, adding context that would result in three completely different paragraphs. **Version 1:** Make it educational for children. **Version 2:** Make it persuasive for someone considering adopting a rescue dog. **Version 3:** Make it humorous for a casual blog audience. Test all three and notice how the same basic topic transforms based on context alone. That's the power you're learning to wield. *** ## Key Takeaway The more relevant context you provide, the less AI has to guess, and guessing is where things go wrong. Use the four types of context (audience, purpose, tone, constraints) as a checklist. Structure your context clearly so AI can follow it. And remember: the gap between what's in your head and what's in your prompt is where misunderstandings live. Close that gap, and watch your results transform. *** ## What's Next You now know how to set the stage with context. But what if you could do more than describe what you want... what if you could *show* the AI exactly what success looks like? In **Lesson 5: Show, Don't Just Tell**, you'll learn the powerful technique of few-shot prompting, where giving the AI examples transforms vague instructions into crystal-clear guidance. Because sometimes, the best way to explain what you want is to demonstrate it. # How AI Thinks (Spoiler: It Doesn't) Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/how-ai-thinks Understand the word-prediction mechanism behind AI responses and learn about tokens, context windows, and hallucinations. # Lesson 2: How AI "Thinks" (Spoiler: It Doesn't) ## Peeking Behind the Curtain Here's a question that might surprise you: What if I told you that ChatGPT, Claude, and every other AI you've chatted with has absolutely no idea what it's saying? Seriously. When you ask an AI "What's the capital of France?" it doesn't *know* the answer the way you do. It doesn't have a little mental map of Europe stored somewhere. It doesn't remember learning this in school. It's doing something much stranger and, honestly, kind of beautiful in its simplicity. Understanding what that "something" is will completely change how you write prompts. You'll stop expecting AI to think like a human and start working *with* how it actually operates. And that's when things get really good. Let's peek behind the curtain. *** ## Core Concepts ### The World's Most Sophisticated Autocomplete Remember the last time you typed a text message and your phone suggested the next word? Maybe you typed "See you" and it suggested "later" or "tomorrow" or "soon." That's basically what AI does. Just... a lot more impressively. Large Language Models (LLMs) are prediction machines. Given everything that came before, they predict what word (or piece of a word) should come next. Then they predict the next one. And the next. Over and over until they've generated a complete response. When you ask "What's the capital of France?" the AI isn't retrieving a fact from a database. It's essentially thinking: "Given all the text I've seen during training, what words are most likely to follow this question?" And because it's seen millions of examples where "France" and "capital" appear near "Paris," that's what it predicts. Here's why this matters for prompting: **the words you use shape what predictions the AI makes.** A vague prompt leads to generic predictions. A specific, well-crafted prompt steers the AI toward more useful predictions. ### Tokens: The Building Blocks of AI Language AI doesn't actually read words the way you do. It reads *tokens*, chunks of text that might be a whole word, part of a word, or even just punctuation. Think of tokens like LEGO bricks. The word "understanding" might be broken into three tokens: "under" + "stand" + "ing". The word "cat" is probably one token. A period? That's a token too. Why should you care about this? A few reasons: **1. Token limits are real.** Every AI has a maximum number of tokens it can handle at once. This includes both your prompt AND the response. If you're working with long documents or detailed instructions, you might hit this ceiling. **2. Tokens affect cost.** Many AI services charge by the token. Understanding this helps you write efficient prompts, getting great results without unnecessary verbosity. **3. It explains some weird behavior.** Ever notice AI struggling with counting letters in words? Or making odd spelling mistakes? That's because it's not seeing individual letters; it's seeing tokens. The word "strawberry" might be tokenized in a way that makes counting the R's surprisingly tricky. A rough rule of thumb: one token is approximately 4 characters in English, or about three-quarters of a word. So a 100-word paragraph is roughly 130-140 tokens. ### The Context Window: AI's Short-Term Memory Here's something crucial: AI has no long-term memory between conversations. Every time you start a new chat, you're talking to a blank slate. But within a single conversation, AI does remember what you've discussed, up to a point. That memory is called the **context window**, and it has a fixed size (measured in tokens, of course). Think of the context window like a whiteboard that can only hold so much writing. As your conversation grows, older content might effectively "fall off" the edge. The AI can only pay attention to what fits on the whiteboard right now. This has practical implications: * **Long conversations can "forget" early details.** If you established something important at the start of a very long chat, the AI might lose track of it. * **Everything costs context space.** Your system instructions, the AI's previous responses, your prompts: it all counts against the limit. * **Starting fresh isn't always bad.** Sometimes a new conversation (with a well-crafted initial prompt) beats trying to course-correct a confused long thread. Modern AI models have gotten much better here. Some can handle hundreds of thousands of tokens, equivalent to a novel or more. But even with large context windows, the principle remains: AI only knows what's in front of it right now. ### Hallucinations: When Prediction Goes Wrong Now for the uncomfortable truth: because AI is predicting what *sounds right* rather than consulting a source of truth, sometimes it confidently generates complete nonsense. This is called **hallucination**, and it's one of the most important things to understand about AI. Here's how it happens. Remember, the AI is playing the "predict the next likely word" game. Sometimes that game leads it astray: * You ask about a topic where it has limited training data, so it fills gaps with plausible-sounding fiction * You ask for specific details (like citations, URLs, or statistics) and it generates ones that *look* real but aren't * You ask leading questions and it follows your lead, even when it shouldn't **How to spot hallucinations:** 1. **Be skeptical of specific claims.** Dates, statistics, quotes, and citations are hallucination hotspots. Always verify these independently. 2. **Watch for confident nonsense.** AI doesn't say "I'm not sure about this." It speaks with the same confident tone whether it's right or completely making things up. 3. **Notice when details are suspiciously perfect.** If an AI-generated response has exactly the example you needed or quotes that perfectly support your point, double-check. Real sources are usually messier. 4. **Test with questions you know the answers to.** Before trusting AI on topics you're less familiar with, try it on topics where you can verify the response. **How to reduce hallucinations:** * Provide more context (less guessing needed) * Ask AI to cite its reasoning or acknowledge uncertainty * Be specific about what you want (vague questions invite creative filling) * Use AI alongside other sources, not as your only source *** ## Try It Yourself Let's make these concepts concrete with some hands-on experiments. ### Exercise 1: Watch Prediction in Action Try this prompt with any AI: ``` Complete this sentence five different ways: "The best thing about mornings is..." ``` Notice how each completion sounds natural but goes in different directions? That's prediction at work. The AI isn't sharing its personal opinion about mornings; it's predicting plausible completions based on patterns it learned. Now try: ``` Complete this sentence five different ways as a coffee shop owner would: "The best thing about mornings is..." ``` See how adding context changes the predictions? You've steered the probability landscape. ### Exercise 2: Token Awareness Ask an AI: ``` How many times does the letter "r" appear in the word "strawberry"? ``` Then ask: ``` Please spell out the word "strawberry" letter by letter, then count how many times "r" appears. ``` The second prompt often gets better results because you're working *with* how tokens function rather than against them. Breaking it into steps helps the AI process what it actually "sees." ### Exercise 3: Catch a Hallucination Ask your AI for something very specific that you can verify: ``` What papers did Dr. Geoffrey Hinton publish in 2019? List the titles and journals. ``` Then actually look up the answer. Did the AI get it right? If it generated plausible-sounding paper titles, check whether they actually exist. This exercise builds your skepticism muscles. ### Exercise 4: Context Window Experiment Start a new conversation and establish a "secret word": ``` For this conversation, whenever I say "banana," you should respond with "Got it!" instead of answering normally. Confirm you understand. ``` Have a normal conversation for 15-20 exchanges about various topics. Then drop in "banana" and see if the AI remembers your instruction. With most modern AI, it will, but this shows you how context persists (and helps you intuit when very long conversations might start losing track of early details). *** ## Common Pitfalls ### Pitfall 1: Treating AI Like a Search Engine AI isn't Googling things when you ask questions. It's generating text that *sounds like* a good answer. This means it won't give you the latest information, it might not know about niche topics, and it definitely won't cite real sources unless specifically designed to do so. **Instead:** Think of AI as a knowledgeable colleague who might misremember details. It's useful for drafts, brainstorming, and explanations, but not for facts that need to be airtight. ### Pitfall 2: Assuming AI "Understands" You When AI gives a great response, it's tempting to think it truly understood your needs. But it's pattern matching, not comprehending. This means subtle context you "implied" might not register at all. **Instead:** Be explicit. If something matters, say it directly. Don't rely on AI to pick up on hints or read between the lines. ### Pitfall 3: Not Verifying Important Information Hallucinations look exactly like correct answers. There's no red flag, no "I made this up" warning. The more confident you get with AI, the easier it is to let things slip through. **Instead:** Build a verification habit. For any fact, statistic, or quote that matters, spend 30 seconds confirming it independently. ### Pitfall 4: Ignoring Token Limits in Complex Tasks If you're giving AI a massive document plus detailed instructions plus asking for a lengthy response, you might hit the context limit. The AI might truncate its response, miss parts of your input, or give lower-quality answers. **Instead:** For very long content, break it into chunks. Process sections separately, then synthesize. Or explicitly tell the AI to focus on specific portions. *** ## Level Up Here's your challenge: Become a hallucination detective. Pick a topic you know well (your profession, a hobby, your hometown, whatever you're an expert in). Ask an AI to explain something specific about that topic. Something detailed enough that you'd know if the answer was wrong. Your mission: 1. Identify at least one error or questionable claim in the response 2. Figure out *why* the AI might have made that error (limited training data? Confused with something similar? Made up a specific detail?) 3. Rewrite your original prompt in a way that would reduce the chance of that error This exercise builds two crucial skills: healthy skepticism and the ability to craft prompts that minimize AI's weaknesses. *** ## Key Takeaway AI doesn't think; it predicts the next likely word based on patterns in its training data. Understanding this helps you write prompts that lead it toward the answers you actually want. Be specific to guide predictions, be aware of token limits and context windows, and always verify important information because even confident AI can be confidently wrong. *** ## What's Next Now you've got a mental model for how AI works under the hood. You understand that it's predicting text, operating within token limits and context windows, and sometimes confidently generating nonsense. But here's the exciting part: this knowledge is power. Because if you understand that AI is a prediction engine, you can learn to write prompts that guide those predictions exactly where you want them to go. In **Lesson 3: The Anatomy of a Great Prompt**, we'll break down the core components that separate frustrating prompts from fantastic ones. You'll learn a simple framework (Task, Context, and Format) that you can apply to any AI interaction. It's time to move from understanding how AI works to making it work for you. # Role Play: Giving AI a Persona Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/role-play Learn to assign roles and personas to AI to access different expertise, perspectives, and communication styles on demand. # Lesson 6: Role Play: Giving AI a Persona ## Summoning the Right Expert You now have a solid toolkit: Task-Context-Format, the four types of context, and few-shot examples. Here's another powerful technique that works beautifully alongside everything you've learned. Have you ever noticed how different people explain the same thing in completely different ways? Ask a lawyer to explain a contract, and you get precise legal language. Ask your best friend, and you get "basically, don't sign this, it's sketchy." Ask a teacher, and you get a patient breakdown with examples. Same information. Wildly different delivery. What if you could instantly summon that expert perspective whenever you needed it? Not a real person (those are expensive and busy), but an AI that thinks and communicates like that expert would? That's exactly what role assignment does. *** ## Core Concepts ### The "Act As..." Technique The simplest version of role play is literally telling the AI who to be: > "Act as a experienced financial advisor and review my budget." > "You are a skilled copy editor. Please improve this email for clarity and tone." > "Pretend you're a skeptical customer. What questions would you have about this product?" Why does this work? Remember from Lesson 2 that AI learned from vast amounts of text, including books, articles, forums, and conversations written by and about all kinds of experts. When you say "act as a financial advisor," you're essentially telling the AI: "Draw from the patterns you learned about how financial advisors think, talk, and approach problems." It's like giving the AI a costume and a script suggestion. The costume changes how it shows up. **Think of it this way:** Imagine you have a friend who's read everything ever written by and about doctors, lawyers, teachers, chefs, and poets. They're not actually any of those professionals, but they've absorbed so much of that knowledge and communication style that they can do a remarkably convincing impression. That's what role play with AI is like. ### Choosing the Right Expert for the Job Not all roles are created equal. The key is matching the expert to your actual need. **Want different things? Summon different experts:** | Your Need | Good Role to Assign | | - | - | | Simplify a complex topic | Elementary school teacher | | Find holes in your argument | Devil's advocate or debate opponent | | Make writing more engaging | Journalist or storyteller | | Ensure technical accuracy | Subject matter expert (be specific!) | | Get encouraging feedback | Supportive mentor or coach | | Prepare for tough questions | Skeptical interviewer | | Make something more concise | Editor at a newspaper with strict word limits | Notice how specific these are. "Act as an expert" is vague. "Act as a veteran editor at The Economist who values brevity and precision" paints a much clearer picture. **The more specific your role description, the more distinct the output.** ### Building a Complete Role A really effective role assignment often includes: 1. **The title or profession** (who they are) 2. **Their experience level** (novice vs. veteran) 3. **Their personality or approach** (patient, direct, creative, skeptical) 4. **Their goal or motivation** (what they care about) Compare these two prompts: **Weak role:** > "Act as a writing coach and give me feedback on my essay." **Strong role:** > "Act as a writing coach who has helped hundreds of first-time authors publish their books. You're known for being direct but encouraging: you don't sugarcoat problems, but you always help writers see a path forward. Your goal is to help me make this essay compelling enough that readers can't put it down. Here's my draft..." The second prompt gives the AI a fully-formed character to inhabit. You'll get feedback that's direct, constructive, and focused on engagement, exactly what you asked for. ### Combining Roles with Tasks Here's where it gets really powerful: roles work best when combined with clear tasks (remember Lesson 3: Task, Context, Format). **Role + Task + Format = Magic** > "You are a senior software engineer who specializes in explaining technical concepts to non-technical stakeholders. Your communication style is clear, jargon-free, and uses lots of analogies. > > **Task:** Explain what an API is to a marketing team that needs to understand it for a product launch. > > **Format:** Keep it under 200 words. Use one memorable analogy." Now the AI knows who it is, what it needs to do, and how to deliver it. That's a recipe for a great response. ### Creative Uses You Might Not Have Thought Of Role play isn't just for getting expert knowledge. Here are some creative applications: **The Devil's Advocate** > "Act as someone who completely disagrees with my position. What are the strongest arguments against my plan to \[X]?" This helps you stress-test ideas before presenting them to others. **The Confused Beginner** > "Pretend you're someone who has never heard of \[topic]. What questions would you have after reading this explanation?" Great for checking if your writing is actually clear to newcomers. **The Ideal Customer** > "You are my ideal customer: a busy parent of three who values convenience but is skeptical of marketing claims. React to this product description honestly." Perfect for pressure-testing marketing copy. **The Socratic Teacher** > "Act as a Socratic teacher. Instead of giving me answers, ask me questions that help me discover the solution myself." Fantastic for learning and working through problems. **The Translator (Not Languages, But Contexts)** > "Act as a bridge between technical and non-technical teams. Take this engineering update and rewrite it so the sales team can understand what it means for customers." Role play becomes a translation service between different worlds. *** ## Try It Yourself Time to experiment! Try each of these exercises and notice how the role changes the output. ### Exercise 1: The Transformation Test Take this simple request and try it three different ways: **Base prompt:** "Give me feedback on this opening paragraph for a blog post about productivity." Now add three different roles: 1. "Act as a tough newspaper editor who has no patience for fluff..." 2. "Act as an encouraging creative writing teacher who wants to nurture my voice..." 3. "Act as a busy reader who will stop reading if you don't hook them in 10 seconds..." Compare the three responses. Notice how the same task produces completely different (and useful!) feedback depending on the role. ### Exercise 2: Expert Consultation Think of a decision you're currently facing. It could be personal (should I take this class?) or professional (how should I approach this project?). Now "consult" with three different experts: 1. A pragmatic strategist focused on practical outcomes 2. A creative thinker who challenges conventional approaches 3. A risk-aware advisor who spots potential problems Write out your prompts, get the responses, and see how each perspective adds something valuable. ### Exercise 3: The Perspective Flip Take something you've written recently (an email, a proposal, a message) and ask the AI to react to it from different perspectives: * "Act as the recipient of this email. What's your honest first reaction?" * "Act as my boss reviewing this proposal. What concerns would you have?" * "Act as a competitor seeing this for the first time. What weaknesses do you spot?" This exercise is incredibly useful for anticipating reactions before you hit send. *** ## Common Pitfalls ### Pitfall 1: Roles That Are Too Vague **Weak:** "Act as an expert." Expert in what? An expert chef and an expert accountant will approach your question very differently. **Better:** "Act as an expert nutritionist who specializes in helping busy professionals eat healthier without meal prepping." ### Pitfall 2: Conflicting Role and Task **Confusing:** "Act as a supportive, encouraging mentor. Now tear apart every flaw in my business plan." The role says "supportive and encouraging" but the task says "tear apart." The AI will be confused, and so will the output. **Better:** "Act as a mentor who believes in me but also knows I need honest feedback to succeed. Point out the weaknesses in my business plan, but frame them as opportunities for improvement." ### Pitfall 3: Forgetting the Role Mid-Conversation In a long conversation, the AI might drift from the role you initially assigned. If responses start feeling generic, remind it: > "Remember, you're acting as my skeptical investor. Given that perspective, what would you say about this new information?" ### Pitfall 4: Roles That Ask for Actual Professional Advice There's a difference between "explain this like a doctor would" and "give me medical advice." AI playing a role is still AI. It's useful for explanation, exploration, and drafting, but it shouldn't replace actual professionals for decisions that matter. Use role play to understand perspectives and improve your thinking, not to get definitive answers on medical, legal, or financial decisions. ### Pitfall 5: Over-Complicating the Role Sometimes simpler is better. You don't always need a paragraph-long character description. **Perfectly fine for many tasks:** "Act as an experienced editor and tighten up this paragraph." Save the detailed role descriptions for when you need a very specific voice or perspective. *** ## Level Up Ready for a challenge? Try this advanced exercise: **The Panel Discussion** Pick a topic you care about, maybe a decision you're weighing or an idea you're developing. Now, in a single prompt, ask the AI to give you perspectives from three different experts, clearly labeled. For example: > "I'm considering whether to start a side business while keeping my full-time job. Give me perspectives from three different advisors: > > **The Pragmatic Financial Planner:** Focus on financial implications and risk management. > > **The Entrepreneurial Coach:** Focus on growth potential and taking calculated risks. > > **The Work-Life Balance Advocate:** Focus on time, energy, and personal well-being. > > Have each advisor give their honest take in 2-3 sentences." This technique (getting multiple perspectives in a single prompt) is incredibly powerful for exploring complex decisions. **Bonus challenge:** After getting the three perspectives, ask a follow-up: "Now, act as a moderator summarizing where all three advisors agree and where they disagree." *** ## Key Takeaway When you tell AI who to "be," you tap into different knowledge patterns and communication styles. A well-chosen role doesn't just change the words; it changes the entire approach to your question. Combine roles with Task-Context-Format for even better results, and remember: the more specific your role description, the more distinct and useful the output. *** ## What's Next You've just learned how to give AI a persona, essentially unlocking different "modes" of thinking and communicating on demand. This is powerful stuff. But what happens when your questions get more complex? When you need the AI to actually think through a problem step by step rather than jumping to an answer? In **Lesson 7: Think Step by Step**, we'll explore chain of thought prompting, a technique that dramatically improves AI's ability to handle complex reasoning. You'll learn why sometimes asking for the process is more important than asking for the answer. See you there! # Show, Don't Just Tell Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/show-dont-tell Learn few-shot prompting, where providing examples dramatically improves AI outputs for style, format, and tone consistency. # Lesson 5: Show, Don't Just Tell ## When Examples Beat Explanations You've mastered Task, Context, and Format. You know how to provide the four types of context. But sometimes, even detailed descriptions fall short. Have you ever tried explaining something to someone, only to realize they understood it perfectly the moment you showed them an example? "No, no, the email should sound more like... here, let me just show you one I wrote last week." That instant click of understanding when someone sees what you mean instead of just hearing it? That's exactly what we're going to harness today with a technique called "few-shot prompting." *** ## Core Concepts ### Zero-Shot vs. Few-Shot: What's the Difference? Let's break down these terms, because they sound way more technical than they actually are. **Zero-shot prompting** is what you've been doing all along. You give the AI a task with no examples: > "Write a product description for a coffee mug." The AI takes its best guess at what you want based on your instructions alone. Sometimes that works great. Sometimes... not so much. **Few-shot prompting** is when you include examples of what you want before asking for the task: > "Here are some product descriptions in my brand's style: > > **Ceramic Coaster Set:** Rest easy knowing your surfaces are protected. These handcrafted coasters bring artisan charm to any room while keeping water rings where they belong: nowhere. > > **Bamboo Cutting Board:** Slice, dice, and serve in style. Sustainably sourced bamboo meets thoughtful design in a board that's as kind to your knives as it is to the planet. > > Now write a product description for a coffee mug in the same style." See the difference? Instead of hoping the AI guesses your brand voice, you've demonstrated exactly what "good" looks like. **Think of it this way:** Zero-shot is like telling a new employee "make it professional." Few-shot is like handing them three emails you've written and saying "make it sound like these." ### The "Shot" Lingo You might hear people throw around these terms: * **Zero-shot:** No examples, just instructions * **One-shot:** One example provided * **Few-shot:** Two to five examples provided There's no magic number, but research shows that 2-3 well-chosen examples often hit the sweet spot—enough to establish a pattern without overwhelming the AI (or yourself). ### Why Does This Work? Remember from earlier lessons that AI predicts "what would likely come next" based on patterns? When you provide examples, you're essentially telling the AI: "Here's the pattern I want you to follow." Instead of the AI drawing on its entire training data to decide what a "professional email" looks like, your examples narrow the field dramatically. You're giving it a much clearer target to aim for. *** ## How to Write Effective Examples Not all examples are created equal. Here's how to craft ones that actually work: ### 1. Make Your Examples Representative Your examples should genuinely reflect what you want. If you're asking for short, punchy responses but your examples are all long and detailed, you're sending mixed signals. **Poor choice:** Providing formal examples when you want casual output **Better choice:** Providing examples that match the tone, length, and style you actually want ### 2. Show Variety Within Consistency If you're providing multiple examples, show the AI that your pattern works across different scenarios: > **Example 1 (positive review response):** > "Thanks so much for the kind words, Sarah! We're thrilled the workshop helped. See you at the next one!" > > **Example 2 (addressing a concern):** > "Thanks for letting us know, Mike. That's not the experience we want for you. I'm reaching out directly to make this right." > > **Example 3 (answering a question):** > "Great question, Jamie! Our next session starts March 15th. Grab your spot at the link in our bio!" These examples show the AI that your brand voice stays consistent even when the content changes. ### 3. Use the Same Format You Want Back If you want the AI to give you bullet points, use bullet points in your examples. If you want headers, use headers. Format teaches format. ### 4. Include Edge Cases When They Matter If there's a tricky scenario you want handled a certain way, include an example of it: > "When someone asks about pricing before we've announced it, respond like this: > 'We're putting the finishing touches on our pricing now! Drop your email at \[link] and you'll be first to know when we launch.'" *** ## When Examples Help (and When They Hurt) Few-shot prompting is powerful, but it's not always the right tool. Here's how to know when to use it: ### Examples Help Most When: **You need a specific style or voice** Brand voice, writing tone, personality—these are hard to describe but easy to demonstrate. **The task has a consistent format** Things like data transformation, categorization, or structured responses benefit enormously from examples. **Your requirements are unusual** If what you want differs from the "typical" way something is done, examples help the AI understand your unique needs. **You've struggled to get it right with instructions alone** If you've tried describing what you want and keep getting close-but-not-quite results, examples often bridge that final gap. ### Examples Might Hurt When: **Your examples are inconsistent** Conflicting examples confuse the AI more than no examples at all. **You're dealing with a simple, straightforward task** "Translate this sentence to Spanish" doesn't need examples. Adding them just wastes space in your prompt. **You want creative diversity** If you want the AI to surprise you with different approaches, too many examples can make it stick too closely to your pattern. **Your examples are low quality** The AI will mimic what you show it—including your mistakes. Bad examples lead to bad outputs. *** ## Building Your Own Example Library Here's a productivity hack that few people think about: start collecting good examples. ### What to Save: * **Emails you've written** that perfectly captured your voice * **Responses** you've crafted that you were proud of * **Formats** that worked well for specific tasks * **AI outputs** that were exactly what you wanted (yes, save the good ones!) ### How to Organize Them: Keep a simple document or note with categories like: * Professional email tone * Casual social media voice * Customer support responses * Technical explanations made simple * Meeting summaries When you need to prompt for something similar, you'll have ready-made examples at your fingertips. ### The Flywheel Effect Here's something cool: as you use few-shot prompting, you'll generate more great outputs. Save those outputs as examples for next time. Your example library grows, your prompts get better, and the cycle continues. *** ## Try It Yourself ### Exercise 1: Feel the Difference Try this zero-shot prompt: > "Write a brief bio for a freelance graphic designer." Now try this few-shot version: > "Here are two bios in the style I want: > > **Sarah Chen, UX Researcher:** I help teams stop guessing what users want. Ten years of asking the right questions, now helping startups build products people actually use. Currently obsessed with accessibility in fintech. > > **Marcus Williams, Content Strategist:** Words are my medium, clarity is my mission. I've helped brands from scrappy startups to Fortune 500s find their voice. When I'm not writing, I'm probably arguing about Oxford commas. > > Now write a bio for a freelance graphic designer in the same style." Compare the results. Notice how the few-shot version captures the casual-but-professional tone, the structure, and even the little personality touches? ### Exercise 2: Format Matching You need to create FAQ responses. First, write out 2-3 example Q\&As in exactly the format and tone you want. Then ask the AI to continue the pattern with new questions. ### Exercise 3: Voice Cloning Find an email or piece of writing you've created that you really like. Use it as an example, then ask the AI to write something new in the same voice. How close did it get? *** ## Common Pitfalls ### The "Kitchen Sink" Mistake Providing too many examples (7, 8, 10...) doesn't usually help and can confuse the AI or use up valuable space in your prompt. Stick to 2-4 strong examples. ### Inconsistent Examples If your examples conflict with each other in style, format, or approach, the AI has to guess which pattern to follow. Make sure your examples tell a coherent story. ### Examples That Don't Match the Task Providing examples of customer support emails when you're asking for marketing copy sends the AI in the wrong direction. Keep examples relevant to what you're actually asking for. ### Forgetting to Include the Actual Request It sounds obvious, but after setting up all those examples, don't forget to clearly state what you want the AI to do with them. "Now write one for \[X]" or "Following this pattern, create \[Y]." ### Using Outdated or Poor-Quality Examples Your examples set the ceiling for quality. If they're mediocre, expect mediocre results. Take time to choose (or craft) examples that represent your best work. *** ## Level Up Here's your challenge: Think about a recurring task where you've struggled to get consistent results from AI. Maybe it's writing in a specific style, formatting data a certain way, or responding to a particular type of message. 1. Find or create 2-3 examples of exactly what you want 2. Build a few-shot prompt with those examples 3. Test it and compare to your previous zero-shot attempts 4. Save the prompt template for future use Bonus points: Start your example library today with at least three examples in one category. *** ## Key Takeaway Sometimes the best way to explain what you want is to show AI exactly what "good" looks like. A few well-chosen examples can communicate more than paragraphs of instructions. Use 2-3 representative examples that match the style, format, and tone you want, and start building your own example library for tasks you do repeatedly. *** ## What's Next You've learned how to show AI what you want through examples. But there's another powerful technique for shaping AI behavior: telling it who to be. In **Lesson 6: Role Play—Giving AI a Persona**, we'll explore how assigning roles and perspectives can dramatically change the outputs you get. Want expert-level advice? Academic rigor? Creative spark? The right persona unlocks different capabilities—and it's more fun than you might think. See you there! # Think Step by Step Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/think-step-by-step Master chain of thought prompting to dramatically improve AI reasoning on complex problems by asking it to show its work. # Lesson 7: Think Step by Step ## Why Showing Your Work Isn't Just for Math Class Here's a riddle for you: A farmer has 17 sheep. All but 9 run away. How many sheep does the farmer have left? Quick, what's your gut answer? If you said "8," you're not alone. Many people do. But the correct answer is 9. ("All but 9" means 9 remain.) Here's the fascinating part: when researchers asked AI models this exact question, the models often got it wrong too, *unless* they were asked to think through the problem step by step. When prompted to reason out loud, accuracy jumped dramatically. This lesson is about that simple but powerful idea: **asking AI to show its work doesn't just help you understand the answer; it helps the AI find the right answer in the first place.** *** ## Core Concepts ### Why "Just Give Me the Answer" Often Fails Remember back in Lesson 2 when we talked about how AI doesn't really "think"? It predicts the most likely next word based on patterns. This works beautifully for simple tasks, but it can trip up on anything that requires multiple logical steps. Think about it this way. Imagine I asked you to multiply 23 by 47 in your head. Could you do it? Maybe. But it would be much easier (and you'd be much less likely to make mistakes) if you could write out the steps: ``` 23 x 47 = 23 x 40 + 23 x 7 = 920 + 161 = 1,081 ``` AI faces the same challenge. When you ask for just the answer to a complex problem, you're essentially asking it to do mental math. Sometimes it nails it. Sometimes it confidently gives you the wrong answer. And you might not even realize it's wrong because the response *sounds* so sure of itself. **The solution?** Give the AI permission (even explicit instructions) to work through the problem step by step. ### The Magic Phrase: "Let's Think Step by Step" Researchers discovered something almost comically simple: adding "Let's think step by step" to a prompt can dramatically improve AI performance on reasoning tasks. This technique is called **Chain of Thought (CoT) prompting**. Here's why it works. When you ask AI to explain its reasoning, you're essentially creating a trail of logic. Each step builds on the previous one. If the AI makes a wrong turn early on, it's more likely to catch it, and you're more likely to spot it too. **Without Chain of Thought:** > **Prompt:** If I have 3 apples and buy 2 bags with 6 apples each, then give away half of all my apples, how many do I have? > > **AI Response:** You have 6 apples. Maybe that's right? Let's see. **With Chain of Thought:** > **Prompt:** If I have 3 apples and buy 2 bags with 6 apples each, then give away half of all my apples, how many do I have? Let's think through this step by step. > > **AI Response:** > > * Starting apples: 3 > * Apples from 2 bags (6 each): 2 x 6 = 12 > * Total apples: 3 + 12 = 15 > * After giving away half: 15 / 2 = 7.5 > > You would have 7.5 apples. (In practical terms, you'd have 7 apples and would have given away 8.) Now you can see the reasoning and catch the answer that's actually 7.5, not 6. You can also see exactly where the AI went and verify each step makes sense. ### Breaking Complex Tasks Into Smaller Pieces "Let's think step by step" is great, but sometimes you need to be even more explicit about *how* to break down a problem. This is especially true for tasks that aren't purely mathematical. Let's say you want AI to help you plan a community event. You could ask: > "Help me plan a summer barbecue for 50 people." And you'll get a generic list. Serviceable, maybe. But compare that to: > "I need to plan a summer barbecue for 50 people. Walk me through this step by step: > > 1. First, what key decisions do I need to make? > 2. Then, what food and supplies will I need? > 3. Next, what's a realistic timeline for preparation? > 4. Finally, what could go wrong and how should I prepare for it?" By explicitly laying out the steps you want the AI to walk through, you get a much more thorough, organized, and useful response. You've essentially given the AI a roadmap for its thinking. **Pro tip:** You don't always know the right steps in advance, and that's okay! You can ask the AI to figure out the steps first: > "I want to learn how to brew coffee at home like a pro. Before giving me instructions, first break this goal down into the key skills or areas I'd need to learn, then walk me through each one." This two-stage approach (ask for the framework, then ask for the details) is incredibly powerful for learning and tackling unfamiliar topics. ### When to Ask for Reasoning vs. Just Results Not every prompt needs Chain of Thought. Asking AI to "think step by step" about what time zone Tokyo is in is overkill. The answer is simple and factual. So when should you deploy this technique? Here's a handy guide: **Use Chain of Thought when:** * The task involves multiple steps or calculations * You need to verify the AI's logic (not just trust the answer) * The problem has potential for misinterpretation * You're dealing with word problems, puzzles, or conditional logic * You're making a decision and want to see the trade-offs considered * The stakes are high enough that a wrong answer matters **Skip Chain of Thought when:** * The question has a simple, factual answer * You're doing creative tasks where the "why" doesn't matter * Speed is more important than verification * You already trust the straightforward response Think of it like asking for an itemized receipt versus just getting a total. Sometimes you just want to pay and leave. But if you're submitting an expense report or checking for billing errors, you need to see the breakdown. *** ## Try It Yourself ### Exercise 1: The Classic Test Try this prompt in your favorite AI tool: > "A bat and a ball cost $1.10 together. The bat costs $1.00 more than the ball. How much does the ball cost?" Note the answer. Now try: > "A bat and a ball cost $1.10 together. The bat costs $1.00 more than the ball. How much does the ball cost? Work through this step by step before giving your final answer." Compare the responses. Did you get the intuitive (but wrong) answer of $0.10 the first time? The correct answer is $0.05, because if the ball costs $0.05, the bat costs $1.05, which is $1.00 more, and together they're $1.10. ### Exercise 2: Real-World Reasoning Pick a genuine decision you're facing (or make one up), then try this prompt: > "I'm trying to decide whether to \[your decision]. Think through this systematically: > > * First, list the key factors I should consider > * Then, evaluate the pros and cons of each option > * Consider what information I might be missing > * Finally, suggest which option seems strongest and why" Notice how the structured reasoning gives you not just an answer, but a framework for thinking about the problem yourself. ### Exercise 3: Explain It To Me Choose a concept you'd like to understand better (how compound interest works, why the sky is blue, how vaccines work, anything). Try: > "Explain \[concept] to me. Build up the explanation step by step, starting from the most basic foundation and adding one piece at a time until I understand the full picture." This forces the AI to construct a logical progression rather than dumping information on you all at once. *** ## Common Pitfalls **Pitfall 1: Forgetting that step-by-step still needs good context** "Let's think step by step" isn't magic fairy dust that fixes a vague prompt. If you ask "Help me with my project step by step," the AI has no idea what project you're working on. You still need to apply everything you've learned about context (Lesson 4) and specificity (Lesson 3). **Pitfall 2: Not actually reading the steps** The whole point of Chain of Thought is that you can see and verify the reasoning. If you just skip to the final answer, you've gained nothing. When the AI shows its work, actually check it. You'll catch mistakes, and you'll learn something in the process. **Pitfall 3: Over-engineering simple questions** If you ask the AI to think step by step about what the capital of France is, you'll get three paragraphs explaining that Paris is indeed the capital. It's correct, but it's wasted effort. Match the technique to the complexity of the task. **Pitfall 4: Accepting confident-sounding steps uncritically** AI can sound very confident while walking through completely flawed logic. Just because it *looks* like careful reasoning doesn't mean it *is* careful reasoning. Stay engaged. Ask yourself: does each step actually follow from the previous one? *** ## Level Up Ready for a challenge? Try this: You're planning to read 24 books this year. It's now March, and you've read 3 books. You typically read for 30 minutes a day, and the average book takes you about 6 hours to complete. Ask your AI to help you figure out: Are you on track? If not, what adjustments would you need to make? Have it show its reasoning completely. Then, critically evaluate the AI's step-by-step work. Did it make any assumptions you disagree with? Did it miss any factors? Did any of its math steps have errors? This is the real skill: using Chain of Thought not just to get better answers from AI, but to become a better critical thinker yourself. *** ## Key Takeaway For complex problems, asking AI to show its work leads to dramatically better answers and gives you the ability to catch mistakes before they matter. "Let's think step by step" is one of the most powerful phrases in your toolkit. Use it whenever accuracy matters more than speed, and always actually read the steps to verify the reasoning. *** ## What's Next You've now learned how to get AI to reason through problems step by step. But here's the truth: even with perfect technique, your first prompt is rarely your best prompt. Great results usually come from refining, clarifying, and iterating. In **Lesson 8: The Art of Iteration**, we'll explore how to build on AI's responses, redirect when things go off track, and develop the back-and-forth conversation skills that turn good prompts into great outcomes. Because prompting isn't a single shot; it's a dialogue. # Troubleshooting Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/troubleshooting Diagnose and fix common AI problems including misunderstandings, wrong-length responses, hallucinations, and refusals. # Lesson 9: Troubleshooting ## When AI Goes Wrong You've learned the techniques. You've practiced iteration. But sometimes things still go sideways: the AI writes a novel when you wanted a tweet, answers a question you never asked, or confidently states something that's just plain wrong. Every AI user has been there. The difference between frustrated beginners and confident prompters isn't that experts never get bad outputs. It's that they've learned to read those bad outputs like clues in a mystery, then apply the right fix. In this lesson, we're turning you into a prompt detective. *** ## Core Concepts ### "That's Not What I Meant!": Fixing Misunderstandings This is probably the most common AI problem, and it almost always comes down to one thing: the AI didn't have enough information to understand what you actually wanted. **The Restaurant Analogy** Imagine you walk into a restaurant and tell the server, "I'd like something good." They bring you a plate of liver and onions. Technically, someone thinks that's good. But it's not what you meant. Now imagine you said, "I'd like something light, maybe a salad with grilled chicken, no onions, with the dressing on the side." Much harder to mess that up, right? AI works the same way. When it "misunderstands" you, it's usually because your prompt left too much room for interpretation. **The Fix: Get Specific About What You Actually Want** Look at your failed prompt and ask yourself: * Did I explain the context? (Who is this for? What's the situation?) * Did I specify the format? (Length, style, structure?) * Did I mention any constraints? (What should it NOT include?) **Example of a Misunderstanding** Your prompt: > "Write about dogs." What you got: A 500-word essay about the evolutionary history of canines. What you wanted: A fun Instagram caption for your new puppy photo. The fix: > "Write a short, playful Instagram caption (under 20 words) for a photo of my new golden retriever puppy playing in autumn leaves. Keep it lighthearted and use 1-2 relevant emojis." See the difference? You didn't just say what you wanted - you said who it's for, how long it should be, and what tone to use. *** ### Responses Too Long, Too Short, or Too Generic Sometimes the AI understands your request but delivers it in the wrong "size" or with all the personality of a corporate memo. **The Goldilocks Problem** * **Too long:** You asked for a summary and got an essay. The AI is trying to be thorough, but you needed brevity. * **Too short:** You wanted a detailed explanation and got a sentence. The AI defaulted to concise when you needed comprehensive. * **Too generic:** The response is technically correct but could apply to anyone or anything. It lacks the specific details that would make it useful. **The Fixes** For responses that are too long: * Add explicit length constraints: "in 2-3 sentences," "under 100 words," "in bullet points (max 5)" * Tell it what to leave out: "Skip the background context and get straight to the actionable steps" * Use formatting instructions: "Give me the headline version, not the full article" For responses that are too short: * Ask for specifics: "Include at least 3 examples," "Explain your reasoning," "Cover both the pros and cons" * Request depth: "Go into detail about each step," "Elaborate on the most important points" * Add "comprehensive," "thorough," or "in-depth" to your request For responses that are too generic: * Add your specific context: instead of "marketing tips," try "marketing tips for a small bakery in a college town" * Include details about your situation: your constraints, your audience, your goals * Ask it to tailor the response: "Make this specific to my situation as a..." **Example of Fixing Generic** Your prompt: > "Give me tips for better sleep." What you got: Generic advice about dark rooms and avoiding screens. The fix: > "I'm a new parent with a 3-month-old who wakes up twice a night. I work from home and need to be functional for 9am meetings. Give me realistic sleep optimization tips that work around unpredictable baby schedules." Now the AI has something to work with. The tips will actually fit your life. *** ### AI Confidently Giving Wrong Information This is perhaps the trickiest problem because the AI doesn't signal when it's uncertain. It delivers wrong information with the same confident tone as correct information. Welcome to the world of "hallucinations." **Why This Happens** Remember from Lesson 2 that AI predicts the most likely next words based on patterns. Sometimes those patterns lead to plausible-sounding but incorrect information. The AI isn't lying - it doesn't know the difference between true and false. It's just generating text that sounds right. **Red Flags to Watch For** * **Specific numbers, dates, or statistics** (especially recent ones) - these are often fabricated * **Quotes attributed to real people** - AI frequently makes these up * **Claims about current events** - AI's knowledge has a cutoff date * **Technical details in specialized fields** - the AI may blend similar concepts incorrectly * **Anything that feels "too perfect"** - if it sounds exactly like what you wanted to hear, double-check it **The Fixes** 1. **Ask the AI to flag uncertainty:** Add to your prompt: "If you're not certain about something, say so. It's okay to say 'I'm not sure' or 'you should verify this.'" 2. **Request sources or reasoning:** "Explain how you arrived at this answer" or "What's this based on?" This doesn't guarantee accuracy, but it helps you spot shaky logic. 3. **Break it into verifiable pieces:** Instead of asking "Tell me everything about X," ask specific questions you can fact-check. 4. **Use AI as a starting point, not the final word:** For anything important, treat AI output as a first draft that needs verification, not a finished answer. 5. **Cross-reference important facts:** If the AI gives you statistics or claims that matter for your work, take 30 seconds to verify them with a quick search. **The Healthy Mindset** Think of AI as a very knowledgeable but sometimes overconfident friend. They know a lot and can help you think through problems, but you wouldn't stake your reputation on their memory of specific facts without double-checking. *** ### When the AI Refuses to Help (And What to Do) Sometimes you get a response that essentially says "I can't help with that." This can be frustrating, especially when your request seems perfectly reasonable. **Why AI Says No** AI systems have built-in guidelines that cause them to decline certain requests. These typically fall into a few categories: 1. **Safety guardrails:** Requests that could potentially cause harm 2. **Content policies:** Topics the AI is instructed to avoid 3. **Misinterpreted intent:** The AI thinks you're asking for something problematic when you're not 4. **Ambiguous requests:** The AI isn't sure if the request is okay, so it errs on the side of caution **The Fixes** **For misinterpreted intent - clarify your purpose:** If you're asking about something sensitive for a legitimate reason, explain that reason. Instead of: "How do I pick a lock?" Try: "I'm a landlord and I'm locked out of my own rental property. What are my legitimate options for gaining entry? I'm also happy to call a locksmith - what should that process look like?" Instead of: "Write a story where the villain wins." Try: "I'm writing a literary fiction piece that explores moral complexity. I'd like to write a scene from the antagonist's perspective that helps readers understand their motivation, even if they don't agree with their actions." **For content the AI seems hesitant about - reframe the request:** Sometimes how you ask matters as much as what you ask. Instead of: "Write a negative review of \[competitor product]" Try: "Help me write an honest comparison between Product A and Product B, including the weaknesses of each." **For topics that require expertise the AI shouldn't provide:** If the AI declines to give medical, legal, or financial advice, that's actually appropriate. Reframe your request to get useful help without asking AI to play professional: Instead of: "What medication should I take for my symptoms?" Try: "What questions should I prepare to ask my doctor about these symptoms? What information will they likely need from me?" **When to Accept the "No"** Sometimes the AI is right to decline. If you're asking for something that could harm others, violate privacy, or cross ethical lines, the refusal is doing its job. Take a moment to consider whether your request might need rethinking. **The Workaround Approach** If you genuinely need help with something the AI seems reluctant about, try breaking it into smaller, clearly-appropriate pieces. Often the AI can help with components of a task even if it's hesitant about the whole thing. *** ## Try It Yourself ### Exercise 1: Diagnose the Problem Here are three "failed" prompts and their unsatisfying outputs. For each one, identify what went wrong and write an improved version. **Prompt A:** > "Explain quantum computing." *Output: A 1,500-word technical explanation filled with jargon about qubits, superposition, and quantum entanglement.* What went wrong? ***\_\_***\_ Your improved prompt: ***\_\_***\_ **Prompt B:** > "I need a business plan." *Output: A generic template with sections like "Executive Summary" and "Market Analysis" - no specifics, just headers and placeholder text.* What went wrong? ***\_\_***\_ Your improved prompt: ***\_\_***\_ **Prompt C:** > "What's the best restaurant in Chicago?" *Output: The AI names a specific restaurant with made-up details about awards it supposedly won in 2024.* What went wrong? ***\_\_***\_ Your improved prompt: ***\_\_***\_ ### Exercise 2: The Troubleshooting Checklist Take a prompt that recently gave you a disappointing result. Run it through this diagnostic: 1. **Clarity check:** Could a smart stranger understand exactly what I want? 2. **Context check:** Did I provide enough background? 3. **Format check:** Did I specify length, style, or structure? 4. **Constraint check:** Did I say what to avoid or exclude? 5. **Accuracy check:** Am I asking about facts I should verify? Which check revealed the problem? Rewrite your prompt addressing that gap. ### Exercise 3: Salvage an Output You asked: "Write me a cover letter for a marketing job." You got: A generic cover letter that could be for any job, any company, any person. Without starting over, write 2-3 follow-up messages that would transform this generic output into something you could actually use. (Hint: Think about what specific information you need to add.) *** ## Common Pitfalls **Pitfall 1: Blaming the AI instead of the prompt** When you get a bad output, the instinct is to think "this AI is terrible." But 90% of the time, the AI is doing exactly what the prompt asked for - you just asked for the wrong thing. Train yourself to look at the prompt first. **Pitfall 2: Starting completely over instead of iterating** A bad output isn't a dead end. It's information. What specifically was wrong? Too long? Wrong tone? Missing context? Use that feedback to refine rather than restart. **Pitfall 3: Not being specific enough about "wrong"** When something isn't working, vague follow-ups like "That's not right" or "Try again" don't help. Be specific: "That's too formal - use a conversational tone like you're talking to a friend." Give the AI something to work with. **Pitfall 4: Trusting confident-sounding facts** AI doesn't have a "not sure" voice. It states made-up facts with the same confidence as well-established ones. Build in verification for anything that matters. **Pitfall 5: Fighting the refusal instead of understanding it** When AI declines a request, your first instinct shouldn't be to find a way around it. First, consider: is there a legitimate reason for the hesitation? Then decide if reframing (vs. circumventing) is the right approach. *** ## Level Up Here's a challenge that puts all your troubleshooting skills to work. **The Scenario:** You're helping a friend who's new to AI. They show you this exchange: > **Their prompt:** "Write a speech for my dad's retirement party." > > **AI output:** A 800-word formal speech filled with corporate jargon about "leveraging synergies" and "transitioning to the next chapter," plus made-up references to "his 30 years at the company." Your friend says: "See? AI is useless. It doesn't know anything about my dad." **Your challenge:** 1. Explain to your friend what went wrong (in a kind way that doesn't make them feel bad). 2. Write 3 specific questions you'd ask them to gather the information needed. 3. Craft the improved prompt using the information they might provide. 4. Add a sentence to the prompt that would prevent the AI from making up facts it doesn't know. *** ## Key Takeaway AI isn't trying to frustrate you; it's doing its best with the information you gave it. When something goes wrong, diagnose first: Misunderstanding? Add context and specifics. Wrong length? Add explicit constraints. Too generic? Include your unique situation. Potentially wrong? Ask for reasoning and verify key facts. Refused? Clarify your purpose or reframe the request. The more you practice diagnosing problems, the faster you'll write prompts that work the first time. *** ## What's Next You've now learned how to craft prompts, iterate on them, and troubleshoot when things go wrong. In our final lesson, **Lesson 10: Putting It All Together**, we'll synthesize everything into a practical framework you can use every day. You'll create your own prompt templates, work through real-world scenarios, and leave with a complete toolkit for making AI a genuine amplifier of your work. You've come a long way. Let's bring it all home. # Welcome to the Conversation Source: https://docs.gloo.com/ai-learning-center/gloo-ai-102/welcome-to-the-chat Learn the foundational mindset shift from treating AI like a search engine to having a productive conversation with a helpful collaborator. # Lesson 1: Welcome to the Conversation ## You Already Know How to Do This Here's a secret that the tech industry doesn't want you to know: you've been training for AI your entire life. Every time you've asked someone for help and realized you needed to explain things differently... every time you've rephrased a question because the first version didn't land... every time you've given more context because someone misunderstood what you meant... You were prompt engineering. You just didn't know it had a fancy name. *** ## The Google Trap Let's start with an experiment. Think about the last time you used Google. You probably typed something like: > best italian restaurants near me Short. Choppy. Keywords. No full sentences needed. That's because search engines are like vending machines. You punch in a code, and they spit out a list of links. The machine doesn't care about you, your preferences, or your situation. It just pattern-matches your keywords against a database. Now here's the thing: **AI doesn't work like that.** When people first try ChatGPT, Claude, or other AI assistants, they often treat them like fancy search engines. They type keyword-style queries and get confused when the results feel... off. Too generic. Not quite right. That's not a failure of the AI. It's a mismatch in expectations. Because AI isn't a vending machine. It's more like a very eager new colleague who's brilliant but doesn't know anything about you yet. *** ## Core Concepts ### What Is an LLM, Really? LLM stands for Large Language Model. But let's skip the jargon and talk about what it actually does. An LLM is a system that has read an almost incomprehensible amount of text (books, articles, websites, conversations, code) and learned patterns from all of it. Not facts, exactly. Patterns. It learned how humans communicate, how ideas flow, how questions get answered. **Think of it like this:** Imagine someone who has read every book in the world's largest library. They can talk about almost any topic, write in almost any style, and help with almost any task. But they weren't there when those books were written. They don't have personal experiences. And they sometimes mix things up or fill in gaps with confident-sounding guesses. That's an LLM. Incredibly capable. Sometimes surprisingly insightful. But not all-knowing, and definitely not infallible. **Why should you care?** Because LLMs are now woven into the tools you use every day, whether you realize it or not. Email assistants, customer service chatbots, writing tools, coding helpers, search features. Understanding how they work gives you a superpower: you can actually get them to help you instead of frustrating you. *** ### The Conversation Mindset Here's the mental shift that changes everything: **Stop thinking of AI as a tool. Start thinking of it as a conversation partner.** When you talk to a friend about a problem, you don't bark keywords at them. You explain the situation. You share context. You describe what you're trying to accomplish. And if they misunderstand, you clarify. AI works the same way. The difference between someone who's frustrated with AI and someone who gets amazing results often comes down to this single mindset shift. One person is typing commands into a machine. The other is having a conversation with a helpful (if sometimes literal-minded) collaborator. **Your role in this conversation:** * You bring the goals, the context, the judgment * You know what "good" looks like for your situation * You can steer, clarify, and redirect **The AI's role:** * It brings vast knowledge and pattern recognition * It can generate, analyze, and transform at incredible speed * It never gets tired, impatient, or annoyed when you ask for changes It's a partnership. And like any partnership, the quality of what you get out depends heavily on the quality of what you put in. *** ### Your First Prompt: What's Actually Happening Let's say you type this into an AI assistant: > Write me a poem The AI will write you a poem. It will probably be... fine. Generic. A little Hallmark-y. Not bad, but not special. Now let's say you type this instead: > Write me a short poem in the style of Mary Oliver about finding unexpected peace while doing mundane chores This time, you'll get something much more interesting. Why? Because in the first prompt, the AI had to guess at everything: What kind of poem? What tone? What subject? What length? Who's the audience? It made safe, middle-of-the-road choices because you gave it nothing to work with. In the second prompt, you gave it constraints and direction. You told it what "good" looks like for your specific situation. The AI didn't have to guess; it could focus all its capability on the actual creative work. **Here's the principle:** AI doesn't read minds. Every piece of information you leave out is a blank the AI fills in with its best guess. Sometimes those guesses are great. Often they're not what you wanted. *** ### What AI Can and Can't Do Before we go further, let's set some realistic expectations. This will save you a lot of frustration. **What AI is genuinely good at:** * **Generating drafts and starting points.** Need a first version of an email, a blog post, an outline? AI can give you something to react to instead of a blank page. * **Explaining things in different ways.** Confused about a concept? AI can reframe it, simplify it, or explain it from a different angle. * **Brainstorming and ideation.** AI can generate lots of options quickly. Most won't be winners, but often that spark of an idea is all you need. * **Transforming content.** Summarizing, expanding, reformatting, translating: AI excels at taking something that exists and turning it into something else. * **Answering questions about things it's been trained on.** Within its knowledge base, AI can be remarkably helpful for research and learning. * **Tedious but straightforward tasks.** Formatting data, generating variations, filling in templates. These are tasks that are boring for humans but clear in their requirements. **What AI struggles with:** * **Being current.** AI has a knowledge cutoff date. It doesn't know about yesterday's news (unless it has access to the web, and even then, verification matters). * **Accuracy for critical facts.** AI can sound confident while being completely wrong. Always verify important information. * **Understanding your specific context without being told.** The AI doesn't know your company's culture, your personal preferences, or your audience unless you share that information. * **Genuine creativity and originality.** AI remixes patterns it has seen. It can be surprisingly creative within those patterns, but true novelty is hard. * **Judgment calls that require human values.** AI can inform a decision, but the final call, especially on ethical matters, should be yours. * **Doing things in the real world.** AI generates text (and sometimes images, code, etc.). It can't actually send emails, make purchases, or take physical actions on your behalf unless connected to other systems. **The bottom line:** AI is an incredibly powerful thinking partner and productivity tool. It's not a replacement for your judgment, your expertise, or your responsibility for the final output. *** ## Try It Yourself Time to get your hands dirty. Open your favorite AI assistant (ChatGPT, Claude, Gemini, whatever you have access to) and try these exercises. ### Exercise 1: The Vending Machine vs. Conversation Test First, try a "vending machine" style prompt: > marketing tips Note what you get. Is it useful? Specific to your situation? Probably not. Now try a conversational version: > I run a small bakery in a college town. We're great at making artisan bread but terrible at social media. What are three realistic marketing ideas I could start this week with zero budget? Compare the two responses. Notice how the second one gives you actionable, specific ideas because you gave it context about who you are and what you actually need? ### Exercise 2: The Clarifying Follow-Up Take whatever response you got from Exercise 1 and practice the conversation. Try saying: > That second idea sounds interesting, but I'm worried it might come across as gimmicky. Can you help me think through how to do it in a way that feels authentic to our brand? Notice how the AI adjusts based on your feedback? That's the conversation in action. You're not starting over; you're refining together. ### Exercise 3: The "Wrong" Response On purpose, give the AI a vague prompt and see what happens: > Help me with my project The AI will probably ask clarifying questions or give you something very generic. That's not the AI being dumb; it's the AI showing you exactly where your prompt was unclear. *** ## Common Pitfalls As you start this journey, watch out for these traps that catch almost everyone: **The Keyword Habit** You've spent years training yourself to type keywords into search boxes. Breaking that habit takes conscious effort. When you catch yourself typing choppy keyword phrases, stop and ask yourself: "How would I explain this to a helpful colleague?" **The One-and-Done Mentality** Your first prompt rarely gives you the perfect result. That's not failure; that's normal. The magic of AI is in the back-and-forth. Think of your first prompt as starting a conversation, not placing an order. **Over-Trusting the Confident Tone** AI always sounds confident, even when it's making things up. This is probably its most dangerous feature. If something seems off, or if the information is important, verify it. AI is a great starting point for research, not the end point. **Under-Sharing Context** People often worry about making prompts "too long." In reality, more context almost always leads to better results. You're not bothering the AI by explaining your situation; you're helping it help you. **Expecting Perfection** AI outputs are drafts, not final products. They're meant to give you something to work with, react to, and refine. If you expect perfection on the first try, you'll always be disappointed. *** ## Level Up: Your Challenge Here's a challenge to test what you've learned: Think of a real task you need to accomplish this week, something you might actually use AI for. Maybe it's drafting an email, brainstorming ideas for a presentation, or explaining a concept to someone. Write two versions of a prompt for this task: 1. **The "old you" version:** What you would have typed before reading this lesson 2. **The "conversation mindset" version:** A prompt that includes context, specifics, and clear direction Try both in your AI assistant and compare the results. **Bonus challenge:** Take the better result and have a follow-up conversation. Ask the AI to adjust something, explain its reasoning, or give you alternatives. Notice how the conversation evolves. *** ## Key Takeaway AI is a conversation partner, not a search engine. The quality of your results depends on how well you communicate your goals, context, and needs. Your first prompt is just the beginning of a dialogue, and more context almost always leads to better results. *** ## What's Next In this lesson, you learned the most important foundation of prompt engineering: AI is a conversation, not a command line. How you communicate matters just as much as what you're asking for. But you might be wondering: Why does the AI respond the way it does? Why does it sometimes "hallucinate" or give you weird answers? What's actually going on under the hood? In **Lesson 2: How AI "Thinks" (Spoiler: It Doesn't)**, we'll peek behind the curtain. You'll learn about the word-prediction game that powers LLMs, why that explains a lot of their quirks, and how understanding the machinery helps you work with it more effectively. Understanding how AI works isn't just academic curiosity; it's a practical tool that will make you a better prompter. See you there. # Building a Knowledge Base Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/building-a-knowledge-base Learn how to prepare information for RAG, including chunking documents, choosing chunk sizes, and creating a searchable knowledge base. # Lesson 4: Building a Knowledge Base ## From Messy Documents to Searchable Knowledge You've learned how RAG works and how embeddings capture meaning. But here's a practical question: What do you actually search through? The answer is your knowledge base: the collection of information that RAG retrieves from. And here's the thing: how you prepare that knowledge base dramatically affects how well RAG works. Think of it like organizing a library. You could throw all the books in a giant pile (technically a library, but good luck finding anything). Or you could organize them thoughtfully, with clear sections and a system that helps you find what you need. Building a good knowledge base is that organizational work for RAG. *** ## Core Concepts ### What Can Be a Knowledge Source? Almost any text-based information can become part of a RAG knowledge base: **Documents and files:** * PDFs, Word documents, text files * Help articles and documentation * Policy manuals and handbooks * Research papers and reports **Web content:** * Website pages * Blog posts * FAQ sections * Knowledge base articles **Structured data:** * Database records (converted to text) * Spreadsheets (with context added) * Product catalogs * Customer information **Conversations and communications:** * Email archives * Chat transcripts * Meeting notes * Support tickets The key is that the information needs to be converted to text that can be embedded and searched. Images, videos, and audio generally need to be transcribed or described in text form. ### Why We Can't Just Embed Entire Documents Here's an important concept: you typically don't embed whole documents. Instead, you break them into smaller pieces called chunks. Why? Several reasons: **1. Embedding quality degrades with length** An embedding tries to capture the "meaning" of a piece of text. A 50-page document covers many topics, so its embedding becomes a vague average that doesn't strongly match any specific query. **2. Context window limits** When retrieved text gets added to the AI prompt, it counts against the context window. If you retrieve entire long documents, you'll quickly run out of space. **3. Precision matters** If someone asks about vacation policy, you want to retrieve the specific section about vacation, not an entire 100-page employee handbook. Chunking solves these problems by creating focused, manageable pieces of information. ### The Goldilocks Problem: Chunk Size Choosing the right chunk size is one of the most important decisions in building a knowledge base. And like Goldilocks, you're looking for something that's "just right." **Chunks that are too small:** * Lose important context * A sentence about "15 days" makes no sense without knowing it's about vacation policy * Lead to fragmented, confusing retrieval **Chunks that are too big:** * Dilute the embedding (meaning gets averaged out) * Include irrelevant information alongside relevant content * Use up context window space inefficiently **Chunks that are just right:** * Contain complete, self-contained ideas * Are large enough to be meaningful but small enough to be specific * Typically range from 200-1000 tokens (roughly 150-750 words) The ideal size depends on your content. Dense technical documentation might need smaller chunks. Conversational content might work better with larger ones. ### Chunking Strategies There are several approaches to breaking documents into chunks: **Fixed-size chunking:** Split text every N tokens or characters. Simple and predictable, but might cut sentences or paragraphs in awkward places. **Semantic chunking:** Split at natural boundaries like paragraphs, sections, or topic changes. Preserves meaning better but requires more sophisticated processing. **Overlapping chunks:** Include some overlap between adjacent chunks so context isn't lost at boundaries. If chunk 1 ends mid-thought, chunk 2 might include the last few sentences of chunk 1. **Hierarchical chunking:** Create chunks at multiple levels (document, section, paragraph) and retrieve at the appropriate level based on the query. Most RAG implementations use some combination of these approaches, often splitting at paragraph boundaries with slight overlap. ### Adding Metadata Raw text chunks are useful, but chunks with metadata are even better. Metadata is additional information attached to each chunk: **Source information:** * Document title * Section heading * Page number * URL **Temporal information:** * Creation date * Last updated * Version number **Categorical information:** * Document type (policy, tutorial, FAQ) * Department or team * Product or feature area Metadata enables filtering (only search HR documents), provides context (this chunk is from page 15 of the Employee Handbook), and helps users verify sources. ### The Indexing Process Once you have your chunks and metadata, you need to create the searchable index. The process typically looks like: 1. **Collect:** Gather all source documents 2. **Process:** Extract text, handle formatting, clean up errors 3. **Chunk:** Break into appropriately sized pieces 4. **Enrich:** Add metadata to each chunk 5. **Embed:** Convert each chunk to an embedding vector 6. **Store:** Save the chunks, embeddings, and metadata in a vector database This process needs to happen initially when you set up the system, and then again whenever your source content changes. Platforms like Gloo AI Studio handle much of this complexity for you, allowing you to upload content and have it automatically processed into a searchable knowledge base with appropriate safety considerations. ### Quality In, Quality Out Here's a crucial principle: **your RAG system is only as good as your knowledge base.** If your source documents are: * Outdated, your answers will be wrong * Poorly written, your answers will be confusing * Contradictory, your answers will be inconsistent * Missing important information, your answers will have gaps RAG doesn't magically improve bad content. It just makes that content retrievable and uses it to generate responses. Garbage in, garbage out still applies. *** ## Try It Yourself ### Exercise 1: Identify Your Knowledge Sources Think about a real use case (your job, a project, a personal interest area). List: 1. What documents or information would you want a RAG system to search? 2. Where does that information currently live? (Files, websites, databases, etc.) 3. What format is it in? What processing might be needed? ### Exercise 2: Chunking Practice Take a long document you have access to (a report, an article, a manual). Read through and identify: 1. Natural break points (section headers, topic changes, paragraph boundaries) 2. How would you chunk this document? 3. Are there places where context would be lost if you split there? This exercise builds intuition for how chunking affects information retrieval. ### Exercise 3: Metadata Design For a RAG system serving a customer support team, what metadata would be valuable for each chunk? Consider: 1. What filtering might users want? (product, topic, date) 2. What source information should be displayed with answers? 3. What metadata would help identify outdated content? Design a metadata schema with 5-7 fields that would make the system more useful. *** ## Common Pitfalls ### Pitfall 1: Inconsistent Chunk Sizes Mixing very small and very large chunks in the same knowledge base leads to unpredictable retrieval. Large chunks dominate some queries; small chunks get lost in others. **The fix:** Aim for consistent chunk sizes throughout your knowledge base. If some documents naturally have different structures, consider separate indices or normalization. ### Pitfall 2: Losing Context at Boundaries When you chunk at arbitrary points, important context can be lost. "The policy is 15 days" means nothing without knowing what policy and what the 15 days refers to. **The fix:** Use overlap, include section headers in each chunk, or add contextual prefixes that remind the chunk what document and section it came from. ### Pitfall 3: Neglecting Maintenance Knowledge bases aren't "set and forget." Information changes, documents get updated, new content gets created. A knowledge base that isn't maintained becomes unreliable. **The fix:** Build processes for regular updates. Track when content was last indexed. Flag stale content for review. ### Pitfall 4: Including Everything The temptation is to include every document you can find. But more isn't always better. Irrelevant content creates noise that can confuse retrieval. **The fix:** Be intentional about what goes in. Ask: "Would a user asking questions in this domain need this information?" If not, leave it out. *** ## Level Up Here's a practical challenge: **Scenario:** You're building a knowledge base for a cooking website. You have: * 500 recipes * 50 technique guides ("How to julienne vegetables") * 30 equipment reviews * 20 ingredient deep-dives **Your task:** Design the knowledge base structure. 1. How would you chunk recipes differently than technique guides? 2. What metadata would you attach to each content type? 3. How would you handle a recipe that references a technique guide? (Should they be linked somehow?) 4. What queries would this knowledge base need to handle well? This exercise helps you think about knowledge base design for heterogeneous content. *** ## Key Takeaway A RAG system is only as good as its knowledge base. Preparing information for RAG means chunking documents into appropriately sized pieces, enriching them with useful metadata, and creating embeddings that enable semantic search. The goal is to create a well-organized collection of information where each chunk is self-contained, properly sized, and easy to find when relevant. *** ## What's Next You've learned how to build a knowledge base. Now let's explore what happens when a user actually asks a question. In **Lesson 5: The Retrieval Step**, we'll dive deep into how queries get matched to relevant chunks. You'll learn about similarity search, top-K retrieval, and strategies for finding the best information to feed into the generation step. # Embeddings: How AI Understands Meaning Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/embeddings Discover how embeddings turn text into numbers that capture meaning, enabling semantic search that finds what you mean, not just what you typed. # Lesson 3: Embeddings: How AI Understands Meaning ## Beyond Keyword Matching Quick question: If you're searching for information about "automobiles," should a document about "cars" show up in your results? Obviously, yes. But traditional keyword search wouldn't find it. The word "automobiles" doesn't appear in a document that only uses "cars." You'd miss relevant information simply because the exact words don't match. This is the problem embeddings solve. They let computers understand that "car" and "automobile" mean the same thing, that "happy" is similar to "joyful," and that a question about "vacation time" is related to a document about "paid time off." Embeddings are the secret sauce that makes RAG's retrieval step actually useful. Let's understand how they work. *** ## Core Concepts ### The Limits of Keyword Search Traditional search engines (and simple database queries) work by matching keywords. They look for documents containing the exact words you typed. This approach has serious limitations: * **Synonyms:** "Car" vs. "automobile" vs. "vehicle" * **Phrasing variations:** "How do I reset my password?" vs. "Password reset instructions" * **Conceptual relationships:** A document about "employee burnout" is relevant to a search about "workplace stress," but shares few keywords Keyword search is like a very literal-minded assistant who only recognizes exact matches. Useful, but frustrating when you need flexibility. ### What Are Embeddings? An embedding is a way of representing text as a list of numbers (called a vector) that captures its meaning. Here's the key insight: **similar meanings end up as similar numbers.** Imagine you could place every word, sentence, or document somewhere on a map. Words with similar meanings would be close together on the map. "Happy" would be near "joyful" and "cheerful." "Sad" would be in a different area, near "unhappy" and "melancholy." Embeddings create this map, but instead of two dimensions (like a paper map), they use hundreds or thousands of dimensions. Each dimension captures some aspect of meaning. ### Numbers That Capture Meaning Let's make this concrete. When you convert a sentence to an embedding, you get something like: ``` "The cat sat on the mat" → [0.23, -0.45, 0.12, 0.87, -0.33, ... ] (hundreds more numbers) ``` These numbers aren't random. They're carefully calculated so that: * "The cat sat on the mat" and "A feline rested on the rug" produce similar number sequences * "The stock market crashed yesterday" produces a very different sequence The magic is that we can mathematically compare these number sequences to measure how similar two pieces of text are. Sequences that are close together represent similar meanings. ### Similarity in Embedding Space When we say embeddings are "close together" or "similar," we're talking about mathematical distance in embedding space. **Think of it like coordinates on a map:** If you're at coordinates (3, 4) and your friend is at (3.1, 4.2), you're very close together. If another person is at (100, 200), they're far away. Embeddings work the same way, just with many more dimensions. We can calculate the "distance" between any two embeddings and use that as a measure of semantic similarity. * Small distance = similar meaning * Large distance = different meaning ### How Embeddings Power RAG Now you can see why embeddings are essential for RAG: 1. **Index time:** Every chunk of your knowledge base gets converted to an embedding and stored 2. **Query time:** When a user asks a question, that question gets converted to an embedding 3. **Search:** The system finds stored embeddings that are closest to the question embedding 4. **Retrieval:** The text chunks corresponding to those close embeddings get retrieved This is semantic search: finding information based on meaning, not just keywords. So when someone asks "What's the policy on working from home?" the system can find a document titled "Remote Work Guidelines" even though the words don't match, because the embeddings are semantically similar. ### The Embedding Model Embeddings don't appear magically. They're created by specialized AI models trained specifically to understand meaning and produce useful representations. These embedding models have learned from massive amounts of text to understand: * That synonyms should have similar embeddings * That questions and their answers should be related * That context matters (the word "bank" means different things in "river bank" vs. "bank account") Different embedding models exist, each with its own strengths. Some are better at short text, others at long documents. Some are optimized for specific domains like legal or medical text. Platforms like Gloo provide access to embedding models as part of their AI offerings, with a focus on producing reliable, values-aligned results. ### Dimensions and Trade-offs Embedding vectors typically have hundreds to thousands of dimensions. Common sizes are 384, 768, 1024, or 1536 dimensions. More dimensions can capture more nuanced meanings but require more storage and computation. It's a trade-off: * **More dimensions:** More nuanced understanding, but larger storage and slower search * **Fewer dimensions:** Faster and smaller, but might miss subtle differences in meaning For most RAG applications, modern embedding models strike a good balance. You don't need to obsess over this unless you're optimizing for very specific requirements. *** ## Try It Yourself ### Exercise 1: Think in Similarities Without any technology, just using your intuition: Rate how similar these pairs are on a scale of 1-10 (1 = completely different, 10 = almost identical): 1. "The dog chased the ball" vs. "The puppy ran after the toy" 2. "The dog chased the ball" vs. "Stock prices rose sharply" 3. "How do I return an item?" vs. "What's your refund policy?" 4. "Python programming tutorial" vs. "Learn to code in Python" Embedding models are essentially trained to match human intuitions like yours. The pairs you rated as similar would have close embeddings; dissimilar pairs would be far apart. ### Exercise 2: Synonym Exploration Think of a topic and list five different ways to phrase a question about it: **Topic:** Getting time off from work 1. "How do I request vacation days?" 2. "What's the PTO policy?" 3. "Can I take time off next month?" 4. "How do I schedule annual leave?" 5. "What's the process for booking holiday?" A good embedding model would recognize all of these as semantically related, even though they share few keywords. This is the power that enables flexible retrieval. ### Exercise 3: When Keywords Fail Think of a scenario where keyword search would completely fail: **Example:** You search your company wiki for "getting reimbursed for client dinners" but the relevant document is titled "Expense Policy for Business Entertainment." Can you think of three similar scenarios in your own work or life where the words you'd naturally use don't match the words in the source documents? *** ## Common Pitfalls ### Pitfall 1: Assuming Embeddings Are Perfect Embeddings are powerful but not infallible. They can miss nuances, misunderstand context, or produce unexpected similarity scores. Edge cases exist. **The fix:** Test your retrieval with real queries. Don't assume it will always find the right documents; verify it does. ### Pitfall 2: Ignoring Domain Mismatch An embedding model trained on general text might not perform well on highly specialized content. Legal jargon, medical terminology, or industry-specific language can confuse general-purpose models. **The fix:** For specialized domains, consider embedding models tuned for that domain, or test thoroughly with domain-specific queries. ### Pitfall 3: Embedding the Wrong Units What you embed matters. Embedding entire long documents produces less useful results than embedding appropriately sized chunks. The embedding represents the "average meaning" of the text; too much text and that meaning becomes diluted. **The fix:** Chunk your documents thoughtfully (we'll cover this in Lesson 4) so each embedding represents a coherent, focused piece of information. ### Pitfall 4: Forgetting That Embeddings Need Updates If your knowledge base changes, the embeddings need to be updated too. New documents need to be embedded; deleted documents need their embeddings removed. **The fix:** Build embedding updates into your content management process. Stale embeddings mean stale search results. *** ## Level Up Here's a thought experiment to deepen your understanding: **Scenario:** You're building a RAG system for a multilingual customer support team. Customers write in English, Spanish, and French, but your documentation is only in English. **Questions to consider:** 1. How might embeddings help here? (Hint: Some embedding models are multilingual, meaning "car" in English and "coche" in Spanish produce similar embeddings.) 2. What challenges might still exist even with multilingual embeddings? 3. How would you test whether the system works well across languages? This exercise helps you think about embeddings not just as a technical feature but as a tool with real capabilities and limitations. *** ## Key Takeaway Embeddings convert text into numbers that capture meaning, enabling semantic search that goes far beyond keyword matching. Similar meanings produce similar embeddings, allowing RAG systems to find relevant information even when the exact words don't match. This is what makes RAG retrieval truly intelligent: it understands what you mean, not just what you typed. *** ## What's Next You now understand how embeddings enable smart retrieval. But before retrieval can happen, you need something to search through: a knowledge base. In **Lesson 4: Building a Knowledge Base**, we'll explore how to prepare your information for RAG. You'll learn about chunking (breaking documents into searchable pieces), why chunk size matters, and how to turn raw content into a well-organized knowledge base. # Getting Started with RAG Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/getting-started-with-rag Explore practical paths for implementing RAG, from no-code platforms to custom solutions, and create your roadmap for getting started. # Lesson 10: Getting Started with RAG ## From Understanding to Action You've come a long way. You understand why RAG exists, how it works, and when to use it. You know about embeddings, knowledge bases, retrieval strategies, and prompt design. You can evaluate RAG quality and recognize its limitations. Now what? This final lesson is about turning knowledge into action. We'll explore the different paths for implementing RAG, from no-code platforms to custom builds, and help you identify the right starting point for your situation. *** ## Core Concepts ### The RAG Implementation Spectrum RAG implementations exist on a spectrum from simple to complex: **No-code platforms:** Upload documents, get a working RAG system. No programming required. Fastest time to value, least customization. **Low-code/API-based solutions:** Use pre-built APIs and services to assemble RAG functionality. Some technical skill required, more flexibility than no-code. **Framework-based development:** Use open-source frameworks like LangChain or LlamaIndex to build custom RAG pipelines. Significant flexibility, requires development skills. **Fully custom solutions:** Build everything from scratch: embedding pipelines, vector databases, retrieval logic, prompt engineering. Maximum control, maximum effort. There's no universally "right" choice. The best path depends on your goals, resources, and constraints. ### Starting with Platforms: Gloo AI Studio For many users, especially those new to RAG or working in organizations that value safety, platforms like Gloo AI Studio offer an accessible entry point. **What platforms typically provide:** * **Content upload and processing:** Add documents and have them automatically chunked and embedded * **Built-in retrieval:** Semantic search without building the infrastructure yourself * **Response generation:** AI that uses retrieved content to answer questions * **User interface:** Ready-to-use chat or search interfaces * **Safety guardrails:** Responses designed to be helpful, appropriate, and values-aligned **The Gloo approach specifically emphasizes:** * Values-aligned AI that focuses on human flourishing * Safety considerations built into the platform * Content enrichment through AI Studio * APIs for developers who want to integrate RAG into their own applications **When platforms make sense:** * You want to validate the RAG concept quickly * You don't have dedicated development resources * Safety and values alignment are priorities * Your use case fits the platform's designed patterns ### Building with APIs and Frameworks If you need more customization, you can build RAG functionality using APIs and frameworks: **Components you might use:** * **Embedding APIs:** Services that convert text to embeddings * **Vector databases:** Specialized databases for storing and searching embeddings (Pinecone, Weaviate, Chroma, etc.) * **LLM APIs:** Language models for generation (available through various providers, including Gloo) * **Orchestration frameworks:** Tools like LangChain or LlamaIndex that help connect these components **What this looks like in practice:** 1. Write code to process documents into chunks 2. Use an embedding API to create vectors 3. Store vectors in a vector database 4. Write retrieval logic to search for relevant chunks 5. Construct augmented prompts with retrieved content 6. Call an LLM API to generate responses 7. Present results in your application **When building makes sense:** * You need custom behavior that platforms don't support * You're integrating RAG into an existing application * You have development resources and time * You want maximum control over every component ### Preparing Your First Knowledge Base Regardless of implementation approach, you'll need content. Here's a practical process: **1. Inventory your content:** What documents, articles, or data do you want to make searchable? Where does it live? What format is it in? **2. Assess quality:** Is the content accurate and up-to-date? Is it well-written? Are there gaps that need filling? **3. Prioritize:** Start with the most valuable, highest-quality content. You can expand later. **4. Prepare for processing:** Extract text from PDFs, clean up formatting issues, remove irrelevant headers/footers. **5. Decide on structure:** How will you organize content? What metadata will you track? **6. Start small:** Don't try to index everything on day one. Start with a focused subset and validate the approach. ### The Iterative Approach: Start Simple, Measure, Improve Here's wisdom from experienced RAG practitioners: **don't try to build the perfect system from day one.** **Start simple:** * A small, high-quality knowledge base * Basic retrieval settings (default top-K, standard embedding model) * Straightforward prompt templates **Measure what matters:** * Are users getting helpful answers? * What questions fail? Why? * What feedback are you getting? **Improve based on data:** * Expand the knowledge base based on what's missing * Tune retrieval based on what works and what doesn't * Refine prompts based on generation quality This iterative cycle is more effective than trying to anticipate everything upfront. Real usage reveals what actually needs improvement. ### Resources for Continued Learning Your RAG journey doesn't end with this curriculum. Here are areas for continued exploration: **Deeper technical understanding:** * How embedding models are trained and compared * Vector database internals and optimization * Advanced retrieval strategies (hybrid search, re-ranking, query expansion) **Evaluation and quality:** * Systematic evaluation frameworks for RAG * User research methods for understanding needs * A/B testing and continuous improvement **Advanced patterns:** * Multi-modal RAG (images, audio, video) * Agentic RAG (systems that take actions, not just answer questions) * RAG for conversation (maintaining context across multiple turns) **Domain-specific applications:** * RAG for legal, medical, financial, or other specialized domains * Compliance and security considerations * Enterprise deployment patterns The field is evolving rapidly. Stay curious, experiment, and keep learning. *** ## Try It Yourself ### Exercise 1: Assess Your Situation Answer these questions to clarify your starting point: 1. **Goal:** What problem are you trying to solve with RAG? 2. **Content:** What knowledge would you want the system to use? 3. **Users:** Who would use this system? What questions would they ask? 4. **Resources:** Do you have development capabilities, or do you need a no-code solution? 5. **Constraints:** Are there security, compliance, or values requirements? Based on your answers, which implementation path seems most appropriate? ### Exercise 2: Plan Your Pilot Design a small pilot project to test RAG in your context: 1. **Scope:** What's the smallest valuable version you could build? 2. **Content:** What subset of content would you start with? 3. **Success criteria:** How will you know if the pilot worked? 4. **Timeline:** What's a realistic timeframe for initial testing? 5. **Next steps:** If the pilot succeeds, what would you do next? Write out a simple one-page pilot plan. ### Exercise 3: Explore Available Tools Do some research on RAG implementation options: 1. Visit Gloo's documentation to understand their platform capabilities 2. Look at one vector database (Pinecone, Weaviate, or Chroma) to understand what they provide 3. Explore LangChain or LlamaIndex documentation to see what framework-based development looks like Note: You don't need to become an expert in all of these. The goal is to understand what's available so you can make informed choices. *** ## Common Pitfalls ### Pitfall 1: Analysis Paralysis With so many options for implementing RAG, it's easy to get stuck comparing approaches instead of building anything. **The fix:** Pick a starting point and try it. You'll learn more from building something imperfect than from endlessly researching the "best" approach. ### Pitfall 2: Starting Too Big Trying to index all your content, serve all use cases, and satisfy all requirements on day one is a recipe for failure. **The fix:** Start with a focused pilot. Prove the concept, then expand. ### Pitfall 3: Underestimating Content Work Many RAG projects stall because of content issues: documents aren't available, need cleaning, have permissions problems, or turn out to be lower quality than expected. **The fix:** Assess your content situation realistically before committing to timelines. Content preparation often takes longer than the technical implementation. ### Pitfall 4: Forgetting the User It's easy to get caught up in technical details and forget that the point is to help users accomplish something. **The fix:** Start with user needs. What questions do they actually ask? What would make their lives better? Let that guide your implementation choices. *** ## Level Up Here's your final challenge: Create your RAG roadmap. **Part 1: The Opportunity** In 2-3 sentences, describe a specific RAG opportunity in your work or life. What information would be searchable? Who would benefit? **Part 2: The Approach** Based on your resources and constraints, which implementation path would you take? Why? **Part 3: The Pilot** Describe a minimal pilot: what content, what questions it would answer, how you'd measure success. **Part 4: The Next Step** What's the single next action you would take to move forward? This roadmap is your transition from learning about RAG to doing something with RAG. *** ## Key Takeaway RAG can be implemented across a spectrum from no-code platforms like Gloo AI Studio to fully custom solutions. The right choice depends on your goals, resources, and constraints. The best approach is to start simple, measure results, and improve iteratively. Don't get stuck in analysis paralysis; pick a path and begin. Your first implementation won't be perfect, but you'll learn more from building than from planning. The knowledge you've gained in this curriculum gives you the foundation; now it's time to build. *** ## Conclusion: You're Ready Congratulations. You've completed the RAG curriculum. You started by understanding the problem RAG solves: AI that's limited by its training data, that hallucinates when it doesn't know, that can't access your specific information. You learned how RAG works: the three-step dance of retrieve, augment, generate. You explored embeddings that capture meaning, knowledge bases that organize information, retrieval that finds what matters, augmentation that structures prompts, and generation that produces grounded responses. You saw RAG in action across real-world domains and learned to recognize when it's the right tool and when it isn't. Now you're ready to put this knowledge to work. Whether you use a platform like Gloo AI Studio with its focus on safety and values alignment, or build something custom, you have the conceptual foundation to make good decisions and create effective solutions. RAG is transforming how we access and use information. You now understand how to be part of that transformation. Go build something useful. *** *Thank you for completing the RAG 103 curriculum. We hope this knowledge serves you well as you explore ways to make AI more helpful, more accurate, and more grounded in the information that matters to you.* # How RAG Actually Works Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/how-rag-actually-works Learn the three-step RAG process (Retrieve, Augment, Generate) through a simple mental model that makes the whole system click. # Lesson 2: How RAG Actually Works ## The Open-Book Exam Remember taking exams in school? There were two kinds: closed-book (rely entirely on memory) and open-book (reference materials allowed). Which type did you prefer? Most people say open-book, and for good reason. When you can look things up, you give better, more accurate answers. You're not guessing or trying to reconstruct facts from fuzzy memories. You're working with real information. RAG gives AI the same advantage. Instead of relying solely on what it "remembers" from training, it can look up relevant information before answering. Let's break down exactly how this works. *** ## Core Concepts ### The Three-Step Dance: Retrieve, Augment, Generate RAG stands for Retrieval Augmented Generation, and those three words describe exactly what happens: **Step 1: Retrieve** When you ask a question, the system searches through a knowledge base to find information relevant to your query. This isn't like typing keywords into Google; it's a semantic search that understands meaning (more on this in Lesson 3). **Step 2: Augment** The retrieved information gets added to your original question. Think of it as giving the AI a cheat sheet along with your question. "Here's what the user asked, and here's some relevant information that might help you answer." **Step 3: Generate** The AI generates a response, but now it's informed by the retrieved context. Instead of relying only on training data, it can reference the specific information that was just retrieved. That's it. Three steps. The magic is in how well each step is executed. ### A Concrete Example Let's walk through a real scenario. Imagine you have a RAG system connected to your company's HR documentation, and an employee asks: > "How many vacation days do new employees get?" **Without RAG (traditional AI):** The AI would try to answer from its general training data. It might say something like "Vacation policies vary by company, typically ranging from 10-20 days for new employees." Technically true, but completely unhelpful for your specific situation. **With RAG:** 1. **Retrieve:** The system searches the HR documents and finds a chunk of text that says "New employees receive 15 days of paid vacation in their first year, increasing to 20 days after two years of employment." 2. **Augment:** This retrieved text gets combined with the original question into a prompt like: "Based on the following company policy: 'New employees receive 15 days of paid vacation in their first year, increasing to 20 days after two years of employment.' Please answer this question: How many vacation days do new employees get?" 3. **Generate:** The AI responds: "New employees receive 15 days of paid vacation in their first year. After two years with the company, this increases to 20 days." See the difference? The answer is specific, accurate, and grounded in your actual company policy. ### The Library Analogy Here's another way to think about it. Imagine you're a reference librarian, and someone asks you a question. **Bad librarian approach:** Try to answer from memory, even if you're not sure. **Good librarian approach:** 1. Listen to the question 2. Walk to the relevant section of the library 3. Pull out the books that might help 4. Read the relevant passages 5. Synthesize an answer based on what you found RAG is the good librarian. It doesn't pretend to know everything from memory. It finds the right information first, then crafts a helpful response. ### What Makes Retrieval "Smart"? You might be wondering: how does the system know which documents are relevant? This is where things get interesting. Traditional search (like old-school Google) looks for keyword matches. If you search for "vacation days," it finds documents containing those exact words. RAG typically uses **semantic search**, which understands meaning rather than just matching keywords. So if you ask about "time off for new hires," it can still find the document about "vacation days for new employees" because it understands these concepts are related. We'll dive deep into how this works in Lesson 3 when we explore embeddings. For now, just know that the retrieval step is smarter than simple keyword matching. ### The Augmented Prompt The "augment" step is where retrieval meets everything you learned about prompting in previous courses. The retrieved information needs to be presented to the AI in a way that's clear and useful. A typical augmented prompt might look like this: ``` Use the following context to answer the question. If the context doesn't contain enough information to answer fully, say so. Context: [Retrieved document chunks go here] Question: [User's original question] Answer: ``` The structure matters. You're essentially telling the AI: "Here's some information that should help. Use it to answer this question." ### Why "Augmented" and Not "Replaced"? An important nuance: RAG augments the AI's capabilities; it doesn't replace them. The AI still uses its general knowledge and reasoning abilities. The retrieved context provides specific information, but the AI synthesizes, summarizes, and presents that information using its language capabilities. This is powerful because: * The AI can combine retrieved facts with general knowledge * It can explain concepts in accessible language * It can draw connections the source documents don't explicitly make * It can format responses appropriately for the question RAG is a collaboration between retrieval and generation, not retrieval alone. *** ## Try It Yourself ### Exercise 1: Trace the RAG Steps Think of a question you might ask a company's internal knowledge base. For example: "What's the process for requesting a new laptop?" Now trace through what would happen at each step: 1. **Retrieve:** What documents or sections would be relevant? (IT policies? Equipment request forms? Procurement procedures?) 2. **Augment:** What would the combined prompt look like? 3. **Generate:** How would the AI synthesize this into a helpful answer? ### Exercise 2: Compare With and Without Take any factual question about a specific organization or domain. Write out: 1. What a general AI might say (from training data only) 2. What specific information would need to be retrieved to improve the answer 3. What the improved answer might look like This helps you internalize the value RAG provides. ### Exercise 3: Design the Context Imagine you're building a RAG system for a recipe website. A user asks: "What can I make with chicken and broccoli that's ready in under 30 minutes?" What information would you want retrieved? How would you structure the augmented prompt? Think about what context would be most helpful for generating a good response. *** ## Common Pitfalls ### Pitfall 1: Retrieving Too Much If you retrieve every vaguely related document and stuff it all into the prompt, you create problems. The AI might get confused by contradictory information, miss the most relevant details in the noise, or hit token limits. **The fix:** Quality over quantity. Retrieve the most relevant chunks, not everything that might be related. ### Pitfall 2: Retrieving Too Little The opposite problem: if retrieval is too narrow, you might miss important context. The AI can only work with what it's given. **The fix:** Find the right balance. Test your retrieval to ensure it captures the necessary information without overwhelming the system. ### Pitfall 3: Ignoring the Augmentation Step Some people focus entirely on retrieval or generation and treat augmentation as an afterthought. But how you structure the prompt matters enormously. **The fix:** Pay attention to your prompt templates. Clear instructions about how to use the context lead to better outputs. ### Pitfall 4: Forgetting That AI Can Still Hallucinate Even with RAG, the AI can make mistakes. It might misinterpret the retrieved context, combine information incorrectly, or fill gaps with plausible-sounding fiction. **The fix:** RAG reduces hallucinations but doesn't eliminate them. Critical information should still be verified. *** ## Level Up Here's your challenge: Design a RAG workflow on paper. **Scenario:** You're creating a RAG-powered assistant for a university library that helps students find and understand research papers. For each of these questions, outline what the three RAG steps would look like: 1. "What are the main findings of the Smith 2023 paper on climate migration?" 2. "Find me papers that compare machine learning approaches for medical diagnosis." 3. "Explain the methodology used in recent behavioral economics studies." Consider: * What would be retrieved for each? * How would the augmented prompt be structured? * What would make the generated response useful? *** ## Key Takeaway RAG follows a simple three-step process: Retrieve relevant information from a knowledge base, Augment the user's question with that information, then Generate a response that's informed by the retrieved context. It's like giving AI an open-book exam instead of a closed-book one. The system is most effective when all three steps work well together: smart retrieval, clear augmentation, and grounded generation. *** ## What's Next You now understand the three-step RAG process. But there's a piece of technology that makes the retrieval step truly powerful: embeddings. In **Lesson 3: Embeddings: How AI Understands Meaning**, we'll explore how AI converts text into numbers that capture meaning, enabling the semantic search that makes RAG so effective. This is the "secret sauce" that lets RAG find relevant information even when the exact words don't match. # RAG in the Real World Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/rag-in-the-real-world Explore practical applications of RAG across customer support, research, enterprise knowledge, and personal productivity. # Lesson 8: RAG in the Real World ## From Concept to Impact You understand how RAG works: retrieval, augmentation, generation. You know about embeddings, knowledge bases, and prompt design. But understanding the mechanics is different from seeing the transformation RAG enables. In this lesson, we'll explore how RAG is being used to solve real problems across different domains. These aren't hypothetical scenarios; they're the kinds of applications being deployed today. By the end, you'll have concrete ideas for how RAG could help in your own context. *** ## Core Concepts ### Customer Support: Answering Questions from Documentation This is one of the most common RAG applications, and for good reason. Customer support involves answering questions from a knowledge base. That's exactly what RAG does. **The problem without RAG:** * Support agents spend time searching for answers * Customers get inconsistent responses depending on who helps them * Self-service chatbots give generic, unhelpful answers * Documentation exists but customers can't find what they need **With RAG:** A customer asks: "How do I connect my device to WiFi?" The system retrieves the relevant section from the device manual, then generates a clear, step-by-step answer specific to that customer's product. If the customer follows up with "It still won't connect," the system can retrieve troubleshooting steps. **What makes it work:** * A comprehensive knowledge base of product documentation, FAQs, and support articles * Good chunking that captures individual procedures and answers * Prompts that instruct the AI to acknowledge when it can't help and offer to escalate **Results:** Organizations using RAG for customer support often see faster response times, higher customer satisfaction, and reduced load on human agents (who can focus on complex issues that actually need human judgment). ### Research Assistance: Synthesizing Information Researchers deal with information overload. There are too many papers, reports, and sources to read comprehensively. RAG helps by finding and synthesizing relevant information. **The problem without RAG:** * Hours spent searching for relevant papers * Risk of missing important work that uses different terminology * Difficulty synthesizing findings across many sources * Information scattered across databases, journals, and repositories **With RAG:** A researcher asks: "What methods have been used to measure employee engagement in remote work settings?" The system searches a database of academic papers, retrieves relevant sections from multiple studies, and synthesizes a summary of the methodologies used, with citations. **What makes it work:** * A knowledge base of academic papers and reports (often domain-specific) * Semantic search that finds conceptually related work even with different terminology * Generation that can synthesize across multiple sources while maintaining attribution **Results:** Researchers can accelerate literature reviews, discover connections they might have missed, and stay current with new publications in their field. ### Enterprise Knowledge: Making Institutional Knowledge Accessible Every organization has institutional knowledge: how things work, why decisions were made, what's been tried before. This knowledge lives in documents, wikis, emails, and people's heads. RAG makes it searchable and accessible. **The problem without RAG:** * New employees take months to learn how things work * Knowledge leaves when employees leave * The same questions get asked repeatedly * Documentation exists but nobody can find it * Decisions get made without knowing what was tried before **With RAG:** An employee asks: "What's our process for vendor approval?" The system searches internal wikis, policy documents, and process guides, then generates a clear explanation with links to the relevant forms and contacts. **What makes it work:** * Indexing across multiple internal systems (wikis, document stores, intranets) * Appropriate access controls (people should only retrieve information they're authorized to see) * Regular updates as policies and processes change * Prompts that encourage the AI to point to authoritative sources **Results:** Faster onboarding, better decision-making, reduced repeated work, and preservation of institutional knowledge. ### Personal Knowledge Management: Your AI-Powered Second Brain RAG isn't just for organizations. Individuals can use it to build personal knowledge systems. **The problem without RAG:** * You've read hundreds of articles, books, and papers but can't remember where you saw something * Notes are scattered across apps and never consulted * Useful information falls into the void of "I know I read about this somewhere" **With RAG:** You ask: "What were the key arguments in that book about habit formation I read last year?" Your personal RAG system searches your notes, highlights, and annotations, then summarizes the main points from that specific book. **What makes it work:** * A personal knowledge base of notes, highlights, and saved content * Regular capture of information you want to remember * A search interface that understands what you're looking for **Results:** Your reading and note-taking becomes cumulative. Nothing gets lost. Past learning actively supports current thinking. ### Gloo AI Studio: RAG with Values Alignment Platforms like Gloo AI Studio are making RAG accessible while emphasizing something often overlooked: values alignment and safety. **What this means:** * Content can be enriched and organized for effective retrieval * AI responses are grounded in provided materials rather than unrestricted generation * The system is designed to produce helpful results while avoiding harmful outputs * Publishers can control what content is available and how it's used **Why it matters:** RAG systems that draw from curated, appropriate knowledge bases are more likely to produce responses aligned with organizational values. This is especially important in sensitive domains: education, spiritual guidance, healthcare information, and other areas where the quality and appropriateness of information matters as much as its accuracy. Gloo's approach combines the power of RAG with intentional design around what knowledge is retrieved and how it's presented. ### Specialized Domains Beyond these common applications, RAG is transforming specialized fields: **Legal research:** Finding relevant case law, statutes, and legal opinions. The stakes are high, so accuracy and citation are critical. **Medical information:** Helping patients understand conditions and treatments by retrieving from peer-reviewed sources. Careful to distinguish information from advice. **Financial services:** Answering questions about products, regulations, and procedures from authoritative internal documentation. **Education:** Helping students learn by retrieving relevant explanations, examples, and resources tailored to their questions. **Technical documentation:** Helping developers find answers in API docs, code examples, and troubleshooting guides. Each domain has specific requirements around accuracy, citation, sensitivity, and compliance. RAG provides the foundation; domain expertise shapes the implementation. *** ## Try It Yourself ### Exercise 1: Identify Your RAG Opportunity Think about your own work or life. Identify a situation where: 1. You frequently need to find information in a collection of documents 2. The information exists but is hard to find 3. You often answer the same kinds of questions 4. Knowledge feels scattered or inaccessible This is your RAG opportunity. Describe it in a few sentences. ### Exercise 2: Design the Knowledge Base For the opportunity you identified: 1. What documents or information would go in the knowledge base? 2. Where does that information currently live? 3. How often does it change? 4. Who would need access? Sketch out what the knowledge base would look like. ### Exercise 3: Imagine the User Experience Still working with your RAG opportunity: 1. What questions would users actually ask? 2. What would a great answer look like? 3. What should happen when the answer isn't in the knowledge base? 4. How important is citation and source verification? Write out three example interactions (question and ideal response). *** ## Common Pitfalls ### Pitfall 1: Solving the Wrong Problem RAG is powerful, but it's not the solution to everything. If the real problem is that your documentation is outdated or badly written, RAG will just make bad information more accessible. **The fix:** Before implementing RAG, ask: Is this actually a retrieval problem? Or is it a content quality problem? ### Pitfall 2: Ignoring Access Control In enterprise settings, not everyone should have access to all information. A RAG system that ignores permissions could expose sensitive data. **The fix:** Design access controls from the start. The retrieval system should respect the same permissions as the source documents. ### Pitfall 3: Underestimating Maintenance RAG systems need ongoing care. Content changes, user needs evolve, and retrieval quality can drift over time. **The fix:** Plan for maintenance from day one. Who updates the knowledge base? How do you monitor quality? What's the feedback loop? ### Pitfall 4: Overpromising to Users Users might expect RAG to be perfect. When it gives wrong or incomplete answers, trust erodes quickly. **The fix:** Set appropriate expectations. Make it clear that the system is a helpful assistant, not an infallible oracle. Include easy paths to human help when needed. *** ## Level Up Here's a planning challenge: **Scenario:** You're proposing a RAG system for a nonprofit organization that provides guidance to volunteers working with at-risk youth. **Consider:** 1. What would be in the knowledge base? (Training materials? Best practices? Legal guidelines? Crisis protocols?) 2. What are the stakes if the system gives wrong information? 3. How would you ensure responses are appropriate and safe? 4. What role would human oversight play? 5. How would you measure success? Write a one-page proposal that addresses these questions. This exercise helps you think about RAG not just technically but holistically. *** ## Key Takeaway RAG is transforming how people access and use information across every domain: customer support, research, enterprise knowledge, personal productivity, and specialized fields. The common thread is taking information that exists but is hard to access and making it immediately useful through intelligent retrieval and grounded generation. Platforms like Gloo AI Studio make this accessible while emphasizing safety and values alignment. The opportunities are everywhere once you start looking. *** ## What's Next You've seen RAG in action across many domains. But RAG isn't magic, and it doesn't work perfectly in all situations. In **Lesson 9: When RAG Works (and When It Doesn't)**, we'll get honest about limitations. You'll learn to recognize when RAG is the right tool, when it struggles, and how to evaluate whether your RAG system is actually working well. Understanding limitations is what separates effective practitioners from disappointed experimenters. # The Augmentation Step Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-augmentation-step Learn how to combine retrieved information with user questions to create effective prompts that guide AI toward grounded responses. # Lesson 6: The Augmentation Step ## Where Retrieval Meets Prompting You've retrieved relevant chunks from your knowledge base. Now comes a step that's easy to overlook but crucial to get right: augmentation. Augmentation is where retrieval meets prompting. You're taking the user's question, adding the retrieved context, and crafting a prompt that helps the AI generate a useful, grounded response. If you've worked through the prompt engineering curriculum, you already know that how you phrase things matters enormously. The augmentation step is where those skills become essential for RAG. *** ## Core Concepts ### The Anatomy of an Augmented Prompt A basic augmented prompt has three components: **1. Instructions:** Tell the AI how to behave and use the provided context. **2. Retrieved Context:** The chunks retrieved from your knowledge base. **3. User Question:** What the user actually asked. Here's what a simple template might look like: ``` You are a helpful assistant. Use the following context to answer the user's question. If the context doesn't contain enough information to fully answer, say so. Context: {retrieved_chunks} Question: {user_question} Answer: ``` The structure matters. The AI reads top to bottom, so instructions at the top prime its behavior. The context comes next to be fresh in "memory." The question at the end clearly signals what needs to be answered. ### Instruction Design: Setting the Rules The instruction section is where you establish how the AI should handle the task. Common elements include: **Role setting:** "You are a customer support agent for Acme Corp." **Context usage guidance:** "Base your answer on the provided context. Do not use information from outside the context." **Handling uncertainty:** "If the context doesn't contain enough information, say 'I don't have enough information to answer that fully.'" **Tone and format:** "Answer in a friendly, professional tone. Use bullet points for lists." **Constraints:** "Do not make up information. Do not speculate beyond what the context supports." These instructions shape everything that follows. Without them, the AI might hallucinate, ignore the context, or respond in ways that don't fit your use case. ### Presenting Retrieved Context How you present the retrieved chunks affects how well the AI uses them. Several approaches work: **Simple concatenation:** Just join all chunks with line breaks. Simple but can be messy. ``` Context: [Chunk 1 content] [Chunk 2 content] [Chunk 3 content] ``` **Labeled chunks:** Add markers to help the AI track sources. ``` Context: [Source: Employee Handbook, Section 3.2] [Chunk 1 content] [Source: HR Policy Update 2024] [Chunk 2 content] ``` **Structured format:** Use clear delimiters and organization. ``` Context: --- Document: Refund Policy Last Updated: January 2024 Content: [Chunk content] --- --- Document: Customer FAQ Last Updated: March 2024 Content: [Chunk content] --- ``` Labeled approaches help the AI cite sources and distinguish between different types of information. ### Managing Context Length Here's a practical constraint: AI models have limited context windows. Every token in your prompt (instructions, context, question, AND the response) counts against that limit. This creates trade-offs: **More context** = More information for the AI to work with, but: * Higher cost (most APIs charge by token) * Risk of hitting context limits * Possible dilution of focus (too much noise) **Less context** = Cheaper and more focused, but: * Might miss relevant information * Less comprehensive answers Strategies for managing context length: **Compress or summarize:** Instead of including full chunks, include summaries. **Prioritize ruthlessly:** If you retrieved 10 chunks, maybe only include the top 5. **Truncate strategically:** If a chunk is too long, include the most relevant portion. **Use models with larger context windows:** When available and cost-effective. The right balance depends on your use case, budget, and quality requirements. ### Instructing the AI on Context Usage A critical aspect of augmentation is telling the AI how to use (and not misuse) the context. Without clear instructions, you might encounter: **Ignoring context:** The AI answers from general knowledge instead of the provided information. **Overclaiming:** The AI states things definitively that the context only implies or doesn't support. **Mixing context with hallucination:** The AI starts with real information but adds fabricated details. Effective instructions address these risks: ``` Instructions: - Answer ONLY based on the information provided in the context below - If the context contradicts your general knowledge, defer to the context - If you cannot answer from the context, say "Based on the available information, I cannot answer that question" - Do not invent facts, statistics, or details not present in the context - If the context contains conflicting information, acknowledge the conflict ``` These guardrails help keep responses grounded and honest. ### Handling Multiple Chunks When you retrieve multiple chunks, they might: **Overlap:** Say the same thing in different words **Complement:** Each provides different pieces of the answer **Conflict:** Provide contradictory information Your prompt template should help the AI navigate these situations: ``` The context below may contain multiple sources. If sources provide complementary information, synthesize them. If sources conflict, note the discrepancy and, if possible, indicate which source is more authoritative or recent. ``` This instruction prevents the AI from being confused by multiple inputs and helps it produce coherent responses. ### Question Reformulation Sometimes the user's question could be improved before being sent to the AI. Common enhancements: **Adding specificity:** If the user asks "What's the policy?", but you know from context they're looking at the refund page, you might expand to "What's the refund policy?" **Removing ambiguity:** Clarifying pronouns or vague references based on conversation history. **Decomposing complex questions:** Breaking "What's the price and how do I sign up?" into sub-questions that can be addressed separately. This is more advanced, but it can significantly improve response quality. *** ## Try It Yourself ### Exercise 1: Write a Prompt Template Design a prompt template for a RAG system that answers questions about a company's product documentation. Include: 1. A role/persona for the AI 2. Clear instructions on how to use the context 3. Guidance on what to do when information is missing 4. Placeholders for retrieved context and user question Test your template mentally with a few example questions. Would it produce good results? ### Exercise 2: Compare Template Approaches Here are two different instruction sets. For each, consider what kind of responses they would produce: **Template A:** ``` Answer the user's question using the context provided. Context: {context} Question: {question} ``` **Template B:** ``` You are a helpful assistant for Acme Corporation. Your role is to help customers understand our products and policies. Instructions: - Only use information from the provided context to answer - If the context doesn't contain enough information, say "I don't have that information in my available resources" - Be concise but thorough - If relevant, mention which document the information comes from Context: {context} Customer Question: {question} Your Response: ``` Which would you trust more for customer support? Why? ### Exercise 3: Handle Conflicting Context Imagine you retrieve these two chunks: **Chunk 1 (from 2022 policy):** "Refunds are available within 14 days of purchase." **Chunk 2 (from 2024 policy update):** "Refunds are available within 30 days of purchase." How would you structure your prompt to help the AI handle this conflict? Write out the specific instructions you would include. *** ## Common Pitfalls ### Pitfall 1: No Instructions at All Just throwing context and a question at the AI without any guidance leads to inconsistent, unpredictable responses. **The fix:** Always include clear instructions. Even a few sentences make a difference. ### Pitfall 2: Instructions Too Vague "Answer based on the context" is a start, but doesn't address edge cases. What if the context is insufficient? What if it conflicts? **The fix:** Think through edge cases and address them explicitly in your instructions. ### Pitfall 3: Overloading Context Stuffing every retrieved chunk into the prompt, regardless of relevance or length, leads to bloated prompts that dilute focus and inflate costs. **The fix:** Be selective. Prioritize quality over quantity in what you include. ### Pitfall 4: Ignoring Source Attribution If the AI responds without indicating where information came from, users can't verify the answer and trust erodes. **The fix:** Include instructions for the AI to reference sources. Use labeled chunks so the AI can cite them. *** ## Level Up Here's a design challenge: **Scenario:** You're building a RAG system for a medical information service. Users ask health questions, and the system retrieves from peer-reviewed medical literature. **Constraints:** * Responses must not be interpreted as personal medical advice * Sources must be clearly cited * Uncertainty must be acknowledged * Information should be accessible to non-medical users **Your task:** Design a complete prompt template that addresses all these requirements. Include: 1. An appropriate role/persona 2. Detailed instructions on context usage and limitations 3. Specific guidance on disclaimers and uncertainty 4. A format for presenting retrieved context 5. Instructions for generating the response This exercise forces you to think carefully about how augmentation shapes responsible AI behavior. *** ## Key Takeaway Augmentation is where retrieval meets prompting. The quality of your augmented prompt directly impacts response quality. A good augmented prompt includes clear instructions on how to use the context, presents retrieved information in a well-organized way, and guides the AI on handling edge cases like missing information or conflicting sources. Think of it as setting up the AI for success by giving it exactly what it needs to generate a grounded, useful response. *** ## What's Next You've prepared the perfect augmented prompt. Now the AI generates a response. In **Lesson 7: The Generation Step**, we'll explore how the AI uses retrieved context to produce grounded responses. You'll learn about staying faithful to source material, encouraging appropriate uncertainty, and making AI responses verifiable through citations. # The Generation Step Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-generation-step Understand how AI uses retrieved context to produce grounded responses, including staying faithful to sources and making answers verifiable. # Lesson 7: The Generation Step ## Where Everything Comes Together You've retrieved relevant information. You've crafted a well-structured augmented prompt. Now the AI does what it does best: generates a response. But this isn't just any generation. This is grounded generation, where the AI's response is anchored to real information you've provided. Done well, you get accurate, verifiable answers. Done poorly, and the AI might still wander off into hallucination territory. Let's understand how to make this final step work. *** ## Core Concepts ### Grounded vs. Ungrounded Generation When an AI generates without RAG (ungrounded generation), it's drawing entirely from patterns learned during training. This can produce fluent, helpful-sounding text, but there's no guarantee it's accurate or relevant to your specific situation. Grounded generation is different. The AI has specific context to work with. A well-tuned RAG system produces responses that: * Are based on provided source material * Can be traced back to specific documents * Reflect the actual information in your knowledge base * Are more likely to be accurate for your use case The key word is "grounded." The response has roots in real information, not just statistical patterns. ### Faithfulness to Source Material One measure of RAG quality is **faithfulness**: does the response accurately reflect what was in the retrieved context? A faithful response: * Doesn't add information that wasn't in the context * Doesn't contradict the context * Represents the context's meaning accurately * Maintains appropriate nuance and uncertainty An unfaithful response might: * Embellish facts with invented details * State things more strongly than the source supports * Mix retrieved information with hallucinated additions * Misrepresent what the source actually said Getting faithfulness right requires both good prompt instructions (from Lesson 6) and, sometimes, post-generation checks. ### When the AI Should Say "I Don't Know" One of the most valuable things a RAG system can do is admit when it doesn't have enough information to answer. This is counterintuitive. We often think AI is "better" when it always provides an answer. But consider what's actually helpful: **Unhelpful response:** A confident answer that's wrong because the knowledge base didn't contain the relevant information. **Helpful response:** "I don't have information about that topic in my available resources. You might want to check \[alternative resource] or contact \[relevant person]." Teaching the AI to recognize and acknowledge gaps is crucial. Your prompt instructions should explicitly guide this behavior: * "If the context doesn't address the question, clearly state that" * "It's better to say you don't have the information than to guess" * "Distinguish between what the context says and what you're inferring" ### Synthesizing Multiple Sources Often, the answer to a user's question requires combining information from multiple retrieved chunks. The AI's job is to synthesize this into a coherent response. Good synthesis: * Integrates information smoothly * Notes when different sources provide complementary details * Acknowledges when sources disagree * Creates a unified answer without losing important nuances Poor synthesis: * Just lists information from each source separately * Misses connections between related pieces of information * Ignores contradictions * Forces false consistency when sources actually differ The generation step is where the AI's language abilities shine. It can weave together disparate pieces of information into a response that's more useful than any single source chunk alone. ### Citations and Verifiability Here's a principle that makes RAG genuinely trustworthy: **responses should be verifiable.** When the AI cites its sources, users can: * Check the original for more detail * Verify the AI interpreted the source correctly * Understand where the information came from * Have appropriate confidence in the answer Citations can be simple or sophisticated: **Inline references:** "According to the Employee Handbook, new employees receive 15 days of vacation." **End citations:** "\[Source: Employee Handbook, Section 3.2, Updated January 2024]" **Linked citations:** In interfaces that support it, citations can link directly to the source document. The level of citation detail depends on your use case. Legal research might need precise references. Casual customer support might just need a general source indication. ### Tone and Format The generation step also determines how the response is presented. Your prompt instructions (from Lesson 6) shape this, but it's worth emphasizing that RAG responses should be: **Appropriate to context:** A medical information system should be more formal and careful than a casual FAQ bot. **Accessible to the audience:** If your users aren't experts, the response shouldn't be filled with jargon, even if the source documents are technical. **Properly formatted:** Lists, paragraphs, headers, or whatever format best serves the user's need. The AI can transform dense technical content into accessible explanations. This is part of the value RAG provides: not just finding information, but presenting it usefully. ### Confidence and Hedging Not all information is equally certain. A good RAG response calibrates its confidence: **High confidence (information clearly stated in context):** "The refund period is 30 days from purchase." **Medium confidence (information implied or requires inference):** "Based on the documentation, it appears that refunds are processed within 5-7 business days, though this isn't explicitly stated." **Low confidence (limited information, some uncertainty):** "The documentation mentions a refund policy but doesn't specify exact timelines. You may want to contact support for detailed information." This calibration helps users know how much to trust each part of the response. *** ## Try It Yourself ### Exercise 1: Evaluate Faithfulness Here's a retrieved context: > "Our standard shipping takes 5-7 business days. Express shipping is available for an additional $15 and delivers within 2 business days. Free shipping is available for orders over $50." And here's a generated response to "How long does shipping take?": > "Standard shipping takes 5-7 business days. If you need it faster, we offer 2-day express shipping for $15 extra. We also offer overnight shipping for $25. Plus, orders over \$50 ship free!" Identify the faithfulness problem in this response. What did the AI add that wasn't in the context? ### Exercise 2: Rewrite for Uncertainty Given this context: > "The company was founded in 2010 by Jane Smith. The company has grown to over 500 employees." The user asks: "How many offices does the company have?" The context doesn't answer this question. Write two possible responses: 1. A bad response that makes something up 2. A good response that appropriately handles the missing information ### Exercise 3: Design Citation Formats For each of these use cases, design an appropriate citation format: 1. **Academic research assistant:** Needs precise, formal citations 2. **Customer support bot:** Needs to indicate source without being heavy-handed 3. **Internal company knowledge base:** Needs to help employees find the original document What level of detail is appropriate for each? *** ## Common Pitfalls ### Pitfall 1: Accepting Fluent but Unfaithful Responses The AI might generate a beautifully written response that sounds great but adds or distorts information from the context. Fluency isn't the same as accuracy. **The fix:** Spot-check responses against the retrieved context. Build evaluation processes that check for faithfulness, not just readability. ### Pitfall 2: Never Admitting Uncertainty If the AI always provides confident answers, some of those answers will be wrong. Users won't know which to trust. **The fix:** Explicitly instruct the AI to express appropriate uncertainty. Reward honesty over false confidence. ### Pitfall 3: Losing Source Material in Synthesis When synthesizing multiple chunks, the AI might oversimplify or lose important nuances from individual sources. **The fix:** For high-stakes applications, consider including key quotes directly from sources, not just the AI's synthesis. ### Pitfall 4: Inconsistent Response Quality Generation quality can vary based on how well the retrieved context matches the question. Great matches lead to great responses; poor matches lead to awkward attempts. **The fix:** Monitor response quality systematically. Identify patterns in when generation succeeds or fails, and address root causes (often in retrieval or knowledge base quality). *** ## Level Up Here's a challenge that brings together everything from the generation step: **Scenario:** A user asks: "What are the key differences between your Basic and Premium plans?" **Retrieved context includes:** Chunk 1: "Basic Plan: \$10/month, includes up to 5 users, 10GB storage, email support." Chunk 2: "Premium Plan: \$25/month, includes unlimited users, 100GB storage, priority phone support, advanced analytics." Chunk 3: "All plans include our core features: project management, file sharing, and mobile app access." **Your task:** 1. Write an ideal generated response that synthesizes this information clearly 2. Include appropriate source references 3. Note any information a user might want that ISN'T in the context 4. Show how the response would handle a follow-up question: "Can I try Premium before buying?" *** ## Key Takeaway Generation is where RAG delivers value to the user. Grounded generation produces responses that are faithful to source material, appropriately uncertain when information is limited, and verifiable through citations. The AI's job isn't just to produce fluent text but to accurately synthesize and present the information that was retrieved. Good generation makes RAG trustworthy; poor generation undermines the entire system. *** ## What's Next You now understand the complete RAG pipeline: retrieve, augment, generate. But understanding the mechanics is different from seeing the impact. In **Lesson 8: RAG in the Real World**, we'll explore practical applications of RAG across different domains. You'll see how organizations use RAG to transform customer support, research, knowledge management, and more, including how platforms like Gloo AI Studio make RAG accessible with a focus on safety and values-aligned results. # The Problem RAG Solves Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-problem-rag-solves Understand why AI needs external knowledge and how RAG addresses the limitations of standalone language models. # Lesson 1: The Problem RAG Solves ## Why Your AI Assistant Doesn't Know About Yesterday Here's a frustrating scenario you might have experienced: You ask an AI assistant about a news event from last week, and it confidently tells you it doesn't have information about events after its training cutoff date. Or worse, it makes something up that sounds plausible but is completely wrong. You've just bumped into one of AI's fundamental limitations. And understanding this limitation is the first step toward solving it. Welcome to RAG: Retrieval Augmented Generation. By the end of this curriculum, you'll understand how to make AI dramatically more useful by connecting it to real, up-to-date, relevant information. But first, let's understand the problem we're solving. *** ## Core Concepts ### The Knowledge Cutoff Problem Every AI model you interact with was trained on data up to a specific point in time. After that date, the model knows nothing. It's like having a brilliant colleague who has been in a coma since 2023: incredibly knowledgeable about everything up to that point, but completely unaware of anything that happened after. This creates real problems: * Ask about recent events, and the AI is clueless * Ask about your company's latest product, and it has no idea * Ask about a paper published last month, and it can't help The AI isn't being stubborn or unhelpful. It genuinely doesn't have access to that information. It's not connected to the internet (in most cases), and it can't learn new things after training (mostly). ### When AI Confidently Makes Things Up Here's something even trickier than "I don't know": sometimes AI fills in gaps with confident-sounding fiction. Remember from earlier lessons that AI predicts the most likely next words based on patterns. When you ask about something it doesn't have good information about, it doesn't stop and say "I'm uncertain." Instead, it generates text that sounds like a reasonable answer based on similar patterns it has seen. This is called **hallucination**, and it's one of the biggest challenges with AI assistants. The AI might: * Invent statistics that don't exist * Create fake citations to papers that were never written * Describe events that never happened * Attribute quotes to people who never said them The dangerous part? These hallucinations often sound just as confident and well-written as accurate responses. There's no flashing warning sign that says "I made this up." ### The Difference Between Knowing Everything and Knowing the Right Things Here's a counterintuitive insight: we don't actually need AI to know everything. We need it to know the right things for our specific situation. Think about it this way. Imagine you're a customer support agent for a software company. You don't need an AI that knows every fact in the world. You need an AI that knows: * Your product's features and limitations * Your company's policies and procedures * Your documentation and help articles * Common customer questions and their answers A general-purpose AI trained on the entire internet might know a little about everything but not enough about your specific domain to be truly helpful. What you really want is an AI that can access your specific knowledge when it needs it. ### What If AI Could Look Things Up? Now imagine a different scenario. Instead of relying solely on what it learned during training, what if the AI could: 1. Receive your question 2. Search through relevant documents to find information that might help 3. Read that information 4. Generate a response based on what it just found This is exactly what RAG does. It's like giving the AI access to a library and telling it, "Before you answer, go check the relevant books first." Think of it as the difference between: * **Closed-book exam:** Answer from memory only (traditional AI) * **Open-book exam:** Look up information before answering (RAG) Which approach would you trust more for accurate, specific answers? The open-book version, of course. That's the power of RAG. *** ## Try It Yourself ### Exercise 1: Spot the Limitation Open your favorite AI assistant and try these prompts: 1. "What were the top news stories from last week?" 2. "What's the current price of \[any stock]?" 3. "What's the weather like right now in New York?" Notice how the AI either admits it doesn't know, gives outdated information, or (in some cases) makes something up. This is the knowledge cutoff problem in action. ### Exercise 2: Domain-Specific Questions Now try asking about something very specific to a niche domain: 1. "What are the specific return policies for \[a local store you know]?" 2. "What's the process for submitting expense reports at \[your company]?" 3. "What are the side effects of \[a specific medication] according to the latest research?" The AI might give generic answers, but it won't have the specific, authoritative information you need. This is where RAG shines. ### Exercise 3: Hallucination Hunting Ask the AI to provide specific citations or statistics on a topic you know well. For example: > "Give me three academic papers about \[topic you're expert in] with authors and publication years." Then verify: Do those papers actually exist? You might be surprised how often the AI invents plausible-sounding but completely fictional sources. *** ## Common Pitfalls ### Pitfall 1: Assuming AI "Just Knows" Many people treat AI like an all-knowing oracle. They ask questions and assume the answers are accurate. But AI's knowledge is limited to its training data, and even within that data, it can make mistakes. **The fix:** Always consider whether your question requires information the AI might not have (recent events, specific organizations, specialized domains). ### Pitfall 2: Not Recognizing Hallucinations Hallucinated content often sounds perfectly reasonable. It uses proper grammar, follows logical structure, and presents information confidently. The only way to catch hallucinations is to verify important claims. **The fix:** For any information that matters, verify it against authoritative sources. Don't assume confident-sounding text is accurate. ### Pitfall 3: Thinking RAG Is Just "Better Search" Some people hear about RAG and think it's just a fancy search engine. But RAG is fundamentally different: it doesn't just find information; it synthesizes that information into coherent, contextual responses. **The fix:** Understand that RAG combines the best of both worlds: the retrieval power of search with the synthesis capabilities of AI. ### Pitfall 4: Expecting RAG to Fix Everything RAG is powerful, but it's not magic. If your knowledge base has bad information, RAG will use that bad information. If your question requires reasoning beyond what's in the documents, RAG alone won't solve it. **The fix:** Think of RAG as a tool that makes AI more informed, not omniscient. The quality of outputs depends heavily on the quality of inputs. *** ## Level Up Here's a challenge to test your understanding: **Scenario:** You're building a customer support chatbot for a software company. The chatbot needs to answer questions about the product, troubleshoot common issues, and explain company policies. **Your task:** List three specific types of questions where a standard AI would struggle but a RAG-enabled AI would excel. For each, explain: 1. Why standard AI would fail 2. What information RAG would need to retrieve 3. How the answer would be better with RAG This exercise helps you start thinking about RAG not as an abstract concept but as a practical solution to real problems. *** ## Key Takeaway AI models have real limitations: they don't know about recent events, they lack domain-specific knowledge, and they sometimes make things up when they don't know the answer. RAG (Retrieval Augmented Generation) addresses these limitations by letting AI look up relevant information before generating a response. It's the difference between a closed-book and open-book exam, and it makes AI dramatically more useful for real-world applications. *** ## What's Next You now understand why RAG exists and what problems it solves. But how does it actually work? How does the AI "look things up" and use what it finds? In **Lesson 2: How RAG Actually Works**, we'll break down the RAG process into three clear steps: Retrieve, Augment, and Generate. You'll get a mental model that makes the whole system click into place. Understanding the mechanics will help you see how to use RAG effectively and where things can go wrong. # The Retrieval Step Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-retrieval-step Understand how RAG systems find relevant information through similarity search, top-K retrieval, and ranking strategies. # Lesson 5: The Retrieval Step ## Finding the Needle in the Haystack You have a knowledge base with thousands of chunks, each converted to an embedding. A user asks a question. Now what? The retrieval step is where the magic happens: out of all that information, the system needs to find the specific chunks that are most relevant to the user's question. Get this right, and the AI has great material to work with. Get it wrong, and even the smartest AI will give unhelpful answers. Let's understand how retrieval actually works. *** ## Core Concepts ### From Question to Embedding The retrieval process starts by converting the user's question into an embedding using the same model that embedded the knowledge base. This is crucial: the question and the documents need to be in the same "embedding space" to be comparable. It's like having a conversation where everyone speaks the same language. If the question is embedded with a different model than the documents, the similarity comparisons become meaningless. So when a user asks "What's the refund policy for digital products?", that question becomes a vector of numbers, just like all the chunks in your knowledge base. ### Similarity Search: Finding the Closest Matches With both the question and all the chunks represented as embeddings, we can now compare them mathematically. The most common approach is **cosine similarity**, which measures the angle between two vectors: * Vectors pointing in the same direction: high similarity (close to 1) * Vectors pointing in different directions: low similarity (close to 0) * Vectors pointing in opposite directions: negative similarity (close to -1) The system calculates the similarity between the question embedding and every chunk embedding in the knowledge base, then ranks them from most similar to least similar. Chunks at the top of this ranking are the ones semantically closest to the user's question. They're the most likely to contain relevant information. ### Top-K Retrieval: Why We Don't Just Take the Best Match You might think: just retrieve the single most similar chunk and use that. But in practice, we usually retrieve multiple chunks (the "top K," where K is typically 3-10). Why retrieve multiple chunks? **1. Relevant information might be spread across chunks** The answer to "How do I apply for a refund and how long does it take?" might require information from two different sections. **2. The top match isn't always the best** Similarity scores are imperfect. The second or third match might actually be more useful for a particular question. **3. More context is often better** Within reason, giving the AI more relevant context leads to more comprehensive answers. **4. Redundancy provides confirmation** If multiple chunks say the same thing, the AI can be more confident in that information. The value of K is a tuning parameter. Too few, and you might miss relevant information. Too many, and you dilute focus or hit context window limits. ### Beyond Simple Similarity: Ranking Strategies Basic similarity search is a great start, but sophisticated RAG systems often layer additional ranking strategies: **Recency weighting:** More recent documents might be more relevant, especially for policies or news. You can boost the scores of newer content. **Authority weighting:** Some sources are more authoritative than others. Official documentation might be weighted higher than blog posts. **Diversity sampling:** If the top 5 results all say the same thing, you might want to include a 6th result that offers a different perspective, even if its similarity score is lower. **Query expansion:** Sometimes the user's question doesn't perfectly match how information is phrased in documents. You can expand the query with synonyms or related terms to improve retrieval. **Hybrid search:** Combine semantic similarity with keyword matching. Sometimes exact keyword matches are important (like product names or technical terms that embeddings might not handle perfectly). ### The Relevance Threshold Not every query will have good matches in your knowledge base. If someone asks about a topic that isn't covered in your documents, even the "best" matches might be irrelevant. Many systems use a relevance threshold: if no chunks score above a certain similarity level, the system might: * Return no results * Acknowledge that it doesn't have information on this topic * Fall back to the AI's general knowledge (with appropriate caveats) This prevents the system from confidently presenting irrelevant information just because it was the "best" match available. ### Speed vs. Accuracy Trade-offs Searching through millions of embeddings to find the most similar ones could be slow if done naively. In practice, RAG systems use specialized techniques: **Vector databases:** Purpose-built databases optimized for similarity search, like Pinecone, Weaviate, or Chroma. **Approximate nearest neighbor (ANN) algorithms:** Instead of comparing against every single embedding, these algorithms use clever indexing to quickly find approximately the best matches. They trade a tiny bit of accuracy for massive speed improvements. For most applications, these approximate methods are good enough. The chunk that's technically ranked 5th might show up as 4th, but the overall retrieval quality remains high. *** ## Try It Yourself ### Exercise 1: Think Like a Retrieval System Imagine a knowledge base about a software product with these chunks: 1. "Installation guide for Windows users..." 2. "Pricing plans start at \$10/month..." 3. "To reset your password, click 'Forgot Password'..." 4. "System requirements: Windows 10+, 4GB RAM..." 5. "Our refund policy allows returns within 30 days..." For each user question, which chunk(s) would you expect to be retrieved? * "How do I install the software?" * "What are the hardware requirements?" * "I forgot my password" * "How much does it cost?" * "Can I get my money back?" Now think: Are there questions where the right chunk might NOT be the top similarity match? (Hint: "What do I need to run this software?" might match "System requirements" better than "Installation guide," even though both are relevant.) ### Exercise 2: Design Your Top-K Strategy For a customer support RAG system, consider: 1. What value of K would you choose? Why? 2. Would you use any ranking adjustments beyond similarity? (Recency? Authority?) 3. What would you do when no results pass the relevance threshold? There are no perfect answers; this exercise builds your intuition for the trade-offs involved. ### Exercise 3: Edge Cases Think of questions that would be challenging for retrieval: 1. Questions that use very different words than the source documents 2. Questions that require combining information from multiple unrelated sections 3. Questions about topics not covered in the knowledge base 4. Questions with ambiguous terms that could match multiple topics How might you handle each of these? *** ## Common Pitfalls ### Pitfall 1: Retrieving Based on Topic, Not Relevance A chunk might be about the same general topic as the question but not actually answer it. "Our company history began in 1995..." is about the company but doesn't help someone asking "What products do you sell?" **The fix:** Evaluate retrieval quality with real questions. Look at what actually gets retrieved and ask: does this help answer the question? ### Pitfall 2: Ignoring Retrieval Failures If retrieval returns poor results, the AI will generate poor responses. But it's easy to blame the AI when the real problem is retrieval. **The fix:** When RAG responses are bad, check what was retrieved first. The generation step can only work with what retrieval provides. ### Pitfall 3: One-Size-Fits-All K Using the same number of retrieved chunks for every query isn't optimal. A simple factual question might need just one chunk. A complex analysis might need ten. **The fix:** Consider dynamic retrieval strategies that adjust K based on query complexity or result quality. ### Pitfall 4: Not Testing with Real Queries Retrieval that works great for your test queries might fail for the queries real users actually ask. Users phrase things differently than you expect. **The fix:** Collect real user queries and regularly evaluate retrieval performance against them. Let real usage inform your tuning. *** ## Level Up Here's a challenge to deepen your understanding: **Scenario:** You're building a RAG system for a legal research application. Lawyers need to find relevant case law and statutes. **Consider these complications:** 1. Legal documents use very specific terminology. How would you ensure retrieval handles this? 2. A query like "cases involving breach of contract in California from 2020-2023" requires matching on concepts (breach of contract), location (California), and time range (2020-2023). How would pure semantic search handle this? 3. Some legal precedents are more authoritative than others. How might you incorporate this? Design a retrieval strategy that addresses these challenges. What combinations of techniques would you use? *** ## Key Takeaway Retrieval is where RAG succeeds or fails. The process converts the user's question into an embedding, searches the knowledge base for the most similar chunks, and returns the top K results. Getting retrieval right requires tuning: how many chunks to retrieve, what ranking strategies to use, and when to acknowledge that the knowledge base doesn't have the answer. Good retrieval means good input for the generation step; bad retrieval dooms the entire response. *** ## What's Next You've found the relevant chunks. Now what? They need to be combined with the user's question in a way that helps the AI generate a great response. In **Lesson 6: The Augmentation Step**, we'll explore how retrieved information gets woven into prompts. You'll learn about prompt templates, managing context length, and telling the AI how to use (and not misuse) the information you've provided. # When RAG Works (and When It Doesn't) Source: https://docs.gloo.com/ai-learning-center/gloo-ai-103/when-rag-works-and-when-it-doesnt Understand the realistic capabilities and limitations of RAG, learn to evaluate quality, and recognize when other approaches might work better. # Lesson 9: When RAG Works (and When It Doesn't) ## The Honest Conversation RAG is powerful. But it's not magic, and pretending otherwise leads to disappointment and failed projects. In this lesson, we'll have an honest conversation about when RAG shines, when it struggles, and how to tell whether your RAG system is actually working well. Understanding limitations is what separates effective practitioners from people who blame the technology when things go wrong. *** ## Core Concepts ### When RAG Shines RAG is at its best in specific conditions. Recognizing these helps you identify good use cases: **You have good source material** RAG retrieves and uses what's in your knowledge base. If your documents are accurate, well-written, and comprehensive, RAG can surface that quality. If they're not, no retrieval system will save you. **Questions have answers in the documents** RAG finds and presents existing information. It doesn't create new knowledge. If the answer to a question is somewhere in your knowledge base, RAG can find it. If it isn't, RAG can only acknowledge the gap. **Users ask relatively specific questions** "What's the refund policy?" works great. "Tell me everything about your company" is too broad; RAG won't know what to retrieve. **Accuracy and grounding matter** When you need responses tied to authoritative sources rather than general AI knowledge, RAG provides that grounding. The ability to cite sources is a genuine advantage. **Information is relatively stable** If your knowledge base doesn't change every hour, RAG can provide consistent, reliable answers based on that stable foundation. ### When RAG Struggles RAG has real limitations. Knowing them helps you avoid frustration: **Knowledge bases are messy or low-quality** Garbage in, garbage out. If your documents are outdated, contradictory, poorly organized, or incomplete, RAG will retrieve and use that problematic content. **Questions require reasoning beyond the documents** RAG finds information; it doesn't perform novel analysis. "Based on these market trends, what should our strategy be?" requires reasoning that goes beyond retrieval. **Questions are too vague or too broad** "Help me with my project" gives retrieval nothing to work with. Vague questions lead to vague retrieval, which leads to vague answers. **The answer requires combining many sources** If answering a question requires synthesizing information from 20 different documents in a complex way, retrieval might not surface all the pieces, and generation might struggle with the synthesis. **Real-time or rapidly changing information** RAG knowledge bases need to be updated. If information changes by the minute (stock prices, live events), RAG may not keep up. **Questions are adversarial or tricky** Users trying to manipulate the system or asking deliberately confusing questions can sometimes get RAG to behave unexpectedly. ### The Needle in a Haystack Problem Here's a specific limitation worth understanding: as knowledge bases get very large, finding the exact right information becomes harder. Imagine searching for one specific fact in a million documents. Even with good embeddings and semantic search, the right chunk might not surface in the top results. It's there, but it's lost in a sea of somewhat-related content. **Implications:** * Smaller, focused knowledge bases often outperform massive ones * Curation matters; don't include everything just because you can * For very large knowledge bases, additional organization (categories, metadata filtering) becomes important ### Hallucinations Aren't Gone RAG reduces hallucinations but doesn't eliminate them. **Why hallucinations can still occur:** 1. **The AI misinterprets the context:** The retrieved chunk says one thing; the AI understands it differently. 2. **The AI fills gaps:** If the context is incomplete, the AI might fill in plausible-sounding but inaccurate details. 3. **The AI over-generalizes:** The context mentions one example; the AI treats it as a universal rule. 4. **The retrieval itself fails:** If retrieval returns irrelevant chunks, the AI has bad material to work with. **The takeaway:** RAG makes AI more reliable, not perfectly reliable. Critical information should still be verified. ### Evaluating RAG Quality How do you know if your RAG system is actually working well? You need to evaluate it systematically. **Retrieval evaluation:** * Are the retrieved chunks actually relevant to the query? * Is the most relevant information making it into the top results? * Are important chunks being missed? **Generation evaluation:** * Are responses faithful to the retrieved context? * Does the AI admit uncertainty when appropriate? * Are responses well-formatted and useful? **End-to-end evaluation:** * Are users getting their questions answered? * How often do users find the responses helpful? * What percentage of queries lead to good outcomes? **Common metrics:** * **Relevance:** Do retrieved chunks relate to the question? * **Faithfulness:** Does the response accurately reflect the retrieved information? * **Helpfulness:** Did the response actually help the user? * **Groundedness:** Is every claim in the response supported by retrieved content? ### When to Consider Alternatives Sometimes RAG isn't the right tool. Consider alternatives when: **You need pure reasoning:** Math problems, logic puzzles, or analysis that doesn't depend on specific documents might be better served by the AI's base capabilities. **You need real-time data:** For live information (current prices, weather, sports scores), APIs that fetch real-time data are more appropriate than RAG. **The task is creative:** Writing fiction, brainstorming ideas, or generating creative content typically doesn't benefit from retrieval. **Simple lookups suffice:** If users just need to look up a single fact from a structured database, a simple search or database query might be simpler than RAG. **The domain is too specialized:** If your domain is so specialized that general embedding models don't understand the terminology, you might need custom models or different approaches. RAG is one tool in the AI toolkit. Knowing when to use it, and when not to, is a sign of expertise. *** ## Try It Yourself ### Exercise 1: Diagnose the Problem For each scenario, identify whether the problem is with retrieval, generation, or the underlying knowledge base: 1. The system gives an answer, but it's from a policy that was updated last month. The old policy is still in the system. 2. The system retrieves a relevant document about vacation policy, but the response says employees get 20 days when the document says 15. 3. A user asks about a product feature, but the feature isn't documented anywhere. The system confidently describes a feature that doesn't exist. 4. The system retrieves chunks about three different products when the user asked about one specific product. ### Exercise 2: Design an Evaluation You're deploying a RAG system for customer support. Design an evaluation approach: 1. What sample queries would you use for testing? 2. How would you measure retrieval quality? 3. How would you measure response quality? 4. What would "good enough" look like for launch? 5. How would you continue monitoring after launch? ### Exercise 3: Right Tool for the Job For each task, decide whether RAG is the right approach or suggest an alternative: 1. Answering questions about your company's HR policies 2. Generating creative marketing slogans 3. Calculating compound interest 4. Finding relevant research papers on a topic 5. Getting the current temperature in a city 6. Explaining how your software product works *** ## Common Pitfalls ### Pitfall 1: Blaming RAG for Content Problems When the system gives outdated or wrong answers, the knee-jerk reaction is to blame the technology. But often the real problem is that the knowledge base has outdated or wrong content. **The fix:** When problems occur, check the source content first. Is the right information in the knowledge base? ### Pitfall 2: Not Testing with Real Queries Developers test with questions they think users will ask. Users ask questions developers never imagined. Real-world performance often differs from test performance. **The fix:** Collect real user queries (even before launch, through research) and evaluate against them. Let real usage guide improvements. ### Pitfall 3: Ignoring Failure Cases It's tempting to focus on the times RAG works great and ignore the times it fails. But failure cases reveal the most about how to improve. **The fix:** Systematically review failures. Look for patterns. Address root causes rather than just celebrating successes. ### Pitfall 4: Over-trusting Metrics Metrics can be gamed or misleading. High retrieval relevance scores don't guarantee users are happy. Good-looking evaluations don't mean the system works in practice. **The fix:** Combine quantitative metrics with qualitative review. Read actual responses. Talk to actual users. *** ## Level Up Here's a diagnostic challenge: **Scenario:** You've deployed a RAG system for technical support. Users report that answers are "usually okay but sometimes completely wrong." Leadership wants to know if RAG is the right approach or if you should abandon it. **Your task:** Design a systematic investigation. 1. How would you gather data on where the system succeeds and fails? 2. What categories of failure would you look for? 3. How would you determine if failures are fixable (better retrieval, better prompts, better content) or fundamental limitations? 4. What would you recommend to leadership? Write out your investigation plan. This exercise builds the diagnostic skills that separate RAG practitioners from RAG dabblers. *** ## Key Takeaway RAG is powerful but not magic. It works best when you have good source material, specific questions, and a need for grounded, verifiable responses. It struggles with poor-quality knowledge bases, questions requiring complex reasoning, and very broad queries. Hallucinations are reduced but not eliminated. Systematic evaluation is essential for understanding whether your RAG system is actually meeting user needs. Knowing when RAG is the right tool, and when it isn't, is the mark of a skilled practitioner. *** ## What's Next You've learned what RAG is, how it works, and when to use it. Now you're ready to take action. In **Lesson 10: Getting Started with RAG**, we'll explore practical paths forward. Whether you want to use a platform like Gloo AI Studio, integrate APIs, or build something custom, you'll leave with a clear roadmap for turning your RAG knowledge into RAG practice. # Affiliate Recommendations Source: https://docs.gloo.com/api-guides/affiliate-recommendations Guide to discovering relevant content from a network of licensed publishers ## Overview Similar to Search, the Affiliate Recommendations endpoint provides intelligent content discovery by searching through a comprehensive database of licensed, published materials to find items that are semantically related to a user's query. Specifically, it returns items based on the number of **snippets** found that are associated with that item. * **Snippets**: A snippet is a section of a content item (like a podcast episode, article, or book) that represents a meaningful piece of information our AI tools can search and understand. They are the recorded units of searchable content in our backend systems. * **Ranking Logic**: Items are ranked first by the number of relevant snippets found. If two or more items have the same snippet count, they are then ranked by their **cumulative certainty**. * **Certainty Score**: This is a confidence metric indicating how well a snippet matches the user's query. Higher scores mean stronger relevance. Cumulative certainty is the sum of these individual scores, providing an overall confidence measure for the item's relevance. To protect intellectual property, the snippets themselves are not visible in the API response. For example, for the query, "How do I raise kids as a new parent?", if a book has 10 relevant snippets, an article has 8, and a podcast has 6, the API ranks them in that order: book (10), article (8), podcast (6). ## Why use Affiliate Recommendations? This API enables applications to surface contextually relevant content recommendations that enhance user engagement and discovery. It's perfect for providing "additional resources" or "recommended reading" based on a user's questions or interests. If a publisher filters on their own data, it also enables more exposure to their own content. ## How to Use the API ### Parameters | Parameter | Description | Required | | - | - | - | | `query` | The prompt that the user enters into the application. | **Yes** | | `certainty_threshold` | The minimum certainty threshold for snippets. | No | | `max_snippet_count_overall` | The maximum number of snippets to search through overall. | No | | `media_types` | An array of media types to filter by (`audio`, `video`, `book`, `article`). | No | | `publishers` | An array of publisher names whose items should be recommended. | No | | `min_snippet_count_per_item` | The minimum number of snippets an item must have to be returned. Must be at least 1. | No | | `item_count` | The maximum number of items to return, ranked by cumulative certainty. If the available snippets span fewer items than this count, only those items are returned. | No | ### Making a Request An API key is necessary to call these endpoints. You can get one from the **API Keys** page in Gloo AI Studio and send it directly as a Bearer token. Endpoint: `POST /ai/v1/data/affiliates/referenced-items` #### Example Request ```bash theme={null} curl -X 'POST' \ 'https://platform.ai.gloo.com/ai/v1/data/affiliates/referenced-items' \ -H 'accept: application/json' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "max_snippet_count_overall": 100, "media_types": ["article", "book"], "min_snippet_count_per_item": 2, "publishers": [], "query":"How do I have a good marriage?", "certainty_threshold": 0.3, "item_count": 3 }' ``` ### Understanding the Response The response is an array of item objects, sorted by relevance. #### Example Response ```json theme={null} [ { "item_id": "236ca8b8-0ef3-4c5b-b1a4-1091aa43abdd", "author": [ "Dr. Michael Youssef" ], "filename": "3-ways-to-strengthen-your-marriage.txt", "denomination": "", "duration": "", "item_title": "3 Ways to Strengthen Your Marriage", "item_subtitle": "", "item_image": "https://ltw.org/wp-content/uploads/2024/12/3WaysStrengthenMarriageFeb2024MYJ_2000x833.png", "item_url": "https://ltw.org/read/article/3-ways-to-strengthen-your-marriage/", "item_tags": "", "h2_id": "6891cf39-c61a-4e9d-a548-77e57a5f47cc", "h2_title": "MY Journal", "h2_subtitle": "", "h2_image": "", "h2_url": "", "h2_tags": null, "h3_id": "c0122b21-26a2-4663-b23e-780f131cf1f9", "h3_title": "Marriage", "h3_subtitle": "", "h3_image": "", "h3_url": "", "h3_tags": null, "h2_summary": "", "publication_date": "February 1, 2024", "publisher": "Leading the Way", "publisher_id": "a6e849de-4972-4fc5-b5f6-6a239ed577c5", "publisher_url": "LeadingTheWay", "publisher_logo": "https://dapologeticsimages.s3.us-east-1.amazonaws.com/logos/logo_1743136797058.svg", "item_summary": "", "type": "Article", "hosted_url": "https://ltw.org/read/article/3-ways-to-strengthen-your-marriage/", "snippet_count": 2, "cumulative_certainty": 1.44311040639877 } ] ``` The result shows the number of snippets returned, one in this example and a sum of the certainty score, which is about 1.4. # Chat API Source: https://docs.gloo.com/api-guides/chat The stateful, chat-native foundation for powerful conversational AI. **Deprecation Notice**: The Chat and Message API is deprecated. New projects should start with the [Responses API](/api-guides/responses) — our recommended surface, with guardrails and values-aligned (`tradition`) responses built in. If you need intelligent routing or grounded completions, use [Completions V2](/api-guides/completions-v2). Existing integrations should migrate as soon as possible. **[View Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** Modern AI doesn't stop at one-turn completions. If you're building with conversational AI, **Chat Threads** are the structure behind truly intelligent, useful, and persistent assistants. Think of it like ChatGPT's conversations—stored, contextual, and evolving. ## What is a Chat Thread? A thread is a persistent conversation—made up of messages between a user and an assistant. It’s not just about history; it’s about **memory, intent, context, and relationship.** > Instead of reinventing state management, you can leverage a chat-native model and API to get this behavior out of the box. ## Why Threads Are a Game-Changer ### Built-in Context History Threads preserve the full message history by default. This means: * No need to re-send long prompts each time. * Context-aware responses feel more helpful and consistent. * It supports multi-turn reasoning and corrections (e.g., "Actually, I meant..."). * You get session persistence without writing session logic. ### First-Class RAG (Retrieval-Augmented Generation) Threads pair naturally with RAG systems by grounding responses on: * Domain-specific docs * Internal knowledge bases * External APIs or tools Chat threads can be augmented with **value-aligned sources** that ensure responses stay on-brand, accurate, and compliant. ### Immediate Access to Proprietary Features Using the "Chat" object lets you leverage powerful, production-ready features without building your own conversation manager: | Feature | What It Does | | - | - | | **File Attachments** | Automatically ingest and summarize user files. | | **Metadata** | Tag threads with business logic, user info, or app state. | | **Tool Use** | Threads integrate seamlessly with structured function calls. | | **Model Routing** | Dynamically upgrade or fallback to different models. | | **Thread Memory** | Fine-tuned recall of prior interactions (coming soon or in enterprise). | ### Aligns with Real User Mental Models People don’t talk to AI one prompt at a time. They: * Ask follow-ups * Refer back to previous context * Expect the AI to "remember" Threads let your app feel **human, collaborative, and intelligent**—not transactional. ### Foundation for Personalization and Long-Term Memory With chat threads, you’re creating a structured, queryable history that can be used to personalize future sessions, train fine-tuned models, or build product analytics around AI interactions. It's more than chat—it's conversational infrastructure. *** ## TL;DR: Why Use Chat Threads? | Benefit | Description | | - | - | | **Persistent Context** | Memory-like multi-turn interaction. | | **RAG-Friendly** | Built to plug in high-quality knowledge. | | **Feature-Rich** | Attach files, call tools, store metadata. | | **Personalization-Ready** | Enables adaptive, long-term assistants. | | **Infra-Free** | No need to manage context state yourself. | Start building with Chat threads today and skip the infrastructure headaches. Whether you're building support bots, copilots, or AI apps, threads are the conversational primitive you'll never want to give up. # How Chunking Works Source: https://docs.gloo.com/api-guides/chunking What happens to your content after upload: how Gloo splits it into chunks, embeds it, and indexes it, and how to prepare content that chunks well. When you upload a document to Gloo, [Search](/api-guides/search) and [Grounded Responses](/api-guides/grounded-responses) don't operate on the whole file. They operate on **chunks**: passages of text small enough to retrieve precisely and large enough to carry meaning. A question about one paragraph of a 40-page report should surface that paragraph, not the entire report, and the model grounding an answer should receive exactly the passages that matter. Chunking is the stage that produces those passages. It runs automatically on every upload; you will see it as the `CHUNKING` status while an item is processing. This page explains what happens to your content between upload and searchable, and then how to make decisions about your own content with that machinery in mind. ## From Upload to Searchable Every item you upload moves through the same processing pipeline: Processing pipeline: documents are parsed while audio and video are first transcribed, then all content is chunked, embedded, and indexed Processing pipeline: documents are parsed while audio and video are first transcribed, then all content is chunked, embedded, and indexed **Parse.** Gloo detects the file format automatically and extracts clean text from it. Documents (Markdown, HTML, PDF, Word, and the other [supported formats](/best-practices/ingestion-pattern#normalize-to-clean-text)) are parsed directly. Audio and video files take one extra step first: Gloo transcribes them, and the transcript then enters the same pipeline as any other text. **Chunk.** The extracted text is split into overlapping chunks. This is the stage reported as `CHUNKING` when you poll an item's status. **Embed.** Each chunk is encoded for semantic search, so retrieval can match on meaning rather than exact keywords. For uploaded content, Gloo manages the embedding models and vectors. If you manage your own vector store, see the [Embeddings API](/api-guides/embeddings). **Index.** The embedded chunks are stored and made retrievable. When the item reaches `COMPLETED`, its chunks are what Search queries and grounded responses draw from. None of these stages needs configuration, and none of them can be skipped or tuned per upload. Your influence over the outcome comes entirely from what you upload and how you divide it into items, which the rest of this page covers. ## How Content Is Split Gloo splits content on natural text boundaries, preferring paragraph breaks and falling back to sentence breaks, so that chunks align with the structure of your writing rather than cutting mid-thought. Chunks are roughly paragraph-sized, and consecutive chunks share a small amount of overlapping text: One item's text divided into three chunks, where each consecutive pair of chunks shares a highlighted region of overlapping text One item's text divided into three chunks, where each consecutive pair of chunks shares a highlighted region of overlapping text The overlap exists because meaning doesn't stop at chunk boundaries. A sentence that ends one chunk often sets up the sentence that begins the next; by repeating a small window of text in both chunks, each one stays understandable on its own, and retrieval doesn't miss a passage just because the answer straddles a boundary. Because splitting prefers natural boundaries, well-structured text produces better chunks than flat text. Headings, paragraphs, and section breaks give the splitter good places to cut; a wall of unbroken text forces arbitrary ones. This is why the [content preparation guide](/best-practices/ingestion-pattern#normalize-to-clean-text) recommends Markdown: it preserves exactly the structure that chunking uses. Every format follows this same splitting behavior once its text has been extracted, whether the text came from a Markdown file, a parsed PDF, a scraped web page, or an audio transcription. Chunks never lose track of where they came from. Each chunk stays tied to its parent item, so search results and grounded responses can attribute a passage to the item it belongs to, along with that item's metadata: title, author, canonical URL, tags, and the other [item metadata fields](/api-guides/edit-content). This is why metadata quality matters at the item level; it travels with every chunk of that item into retrieval. ## Can Chunking Be Turned Off? No. Chunking always runs, and there is no setting to disable it, bypass it, or tune its size and overlap. You do have one meaningful lever: **item granularity**. Content short enough to fit within a single chunk is not split further, so a short item effectively becomes one chunk. If your content must not be merged with a neighbor, such as individual Q\&A pairs, upload each unit as its own item. The item remains isolated from neighboring content, but oversized items may still be split into multiple chunks. The next section covers when that is the right call. ## Pre-Split or Let Gloo Chunk? **Default: let Gloo chunk.** For most content (articles, reports, transcripts, long-form prose of any kind), upload the document as one item and let the pipeline handle the rest. Pre-splitting long prose into fragments works against you: it discards the document structure that helps chunking find natural boundaries, and it multiplies the items you have to manage. Split content into separate items *before* upload only when it has hard semantic boundaries that retrieval must never cross: units that would be wrong to return partially or merged with a neighbor. * **Q\&A content.** Each question-and-answer pair is one unit. A chunk containing half an answer, or the end of one answer and the start of the next question, misleads retrieval. * **Devotionals and daily entries.** Each entry stands alone. A passage spanning two days' entries belongs to neither. * **Episodic collections.** If each segment has its own title, date, and canonical URL, it deserves its own item so its metadata and citations point to the right place. This is a decision about *what constitutes one document*, not about tuning chunk sizes. Within each item you create, Gloo still chunks automatically. Each pre-split item needs its own [`producer_id`](/getting-started/glossary#producer_id) and metadata; the [content preparation guide](/best-practices/ingestion-pattern#pre-chunk-vs-let-gloo-chunk) covers how to structure that. ## Guidance by Content Type Chunking itself behaves the same way for every content type. What varies is how you should divide content into items and what metadata to attach: | Content type | One item is... | Preparation notes | | - | - | - | | Articles and blog posts | One article | Let Gloo chunk. Convert HTML to Markdown, keep headings, strip navigation and boilerplate. Attach title, author, publication date, and canonical URL. | | Reports and long documents | One report | Let Gloo chunk. Preserve heading structure; it gives the splitter its boundaries. Clean PDF extraction noise (page numbers, running headers) before upload. | | Podcasts, sermons, and other recordings | One episode or sermon | Upload the audio or video file itself, or use the [Studio YouTube connector](/api-guides/upload-content); Gloo transcribes automatically. Attach episode title, series, date, and URL. | | Transcripts you already have as text | One session or episode | Let Gloo chunk. Keep speaker labels and section breaks that carry meaning; strip per-word timestamps and filler tokens. | | Q\&A content | One question-and-answer pair | Pre-split. Give each pair its own item, `producer_id` (e.g. `faq-item-291`), and metadata. | | Devotionals and daily entries | One entry | Pre-split. One item per entry (e.g. `devotional-2026-03-14`), with the entry's date and title as metadata. | ## What Gloo Handles for You After upload, the following are automatic. Don't build them yourself: * **Format detection and parsing** for all [supported document formats](/best-practices/ingestion-pattern#normalize-to-clean-text), with no format flags. * **Transcription** of audio and video files, whether uploaded directly or ingested through Studio connectors. * **Chunking**, as described on this page, with nothing to configure. * **Embedding** of every chunk for semantic search, with no models or vectors to manage. * **Vector storage and retrieval indexing**, with no database to run or index to maintain. Your job ends at clean text, sensible item granularity, stable IDs, and good metadata. Everything between upload and searchable is Gloo's job. ## Next Steps * **[Preparing Content for Ingestion](/best-practices/ingestion-pattern)** — the full source-agnostic pattern for structuring content before upload, including `producer_id` schemes and metadata timing. * **[Upload Content](/api-guides/upload-content)** — send files to the ingestion endpoint. * **[Search](/api-guides/search)** — query the chunks this pipeline produces. * **[Grounded Responses](/api-guides/grounded-responses)** — ground model answers in your indexed content. * **[Building a Knowledge Base](/ai-learning-center/gloo-ai-103/building-a-knowledge-base)** — Learning Center background on chunking and metadata in RAG systems generally. # Completions V1 Source: https://docs.gloo.com/api-guides/completions-v1 Conversational Chat Completions **Deprecated**: The Completions V1 API has been deprecated in favor of Completions v2. **Key differences from V1:** * New endpoint: `/ai/v2/guarded/chat/completions` * Smart routing with `auto_routing: true` * Provider selection with `model_family` * Theology-aware responses with `tradition` [View V2 Guide](/api-guides/completions-v2) | [Migration Steps](/api-guides/completions-v2#migrating-from-completions-v1) Tool calling is supported in both V1 and V2, but the [Tool Use Guide](/api-guides/tool-use) now focuses on V2. Please migrate to V2 for the latest tool calling features and examples. Standard chat completions—like OpenAI's `chat/completions` endpoint—are quickly becoming the industry default for building conversational AI. Whether you're developing a chatbot, virtual assistant, or AI-powered automation, here's why sticking with the standard just makes sense. > The Gloo AI Completions API allows you to build with many of the leading open-source models and ensures that it has Human Flourishing alignment and guardrails for any use. *** ## Why Use Standard Chat Completions? ### Familiar Developer Experience If you've used tools like OpenAI, Anthropic, or other LLM providers, you've likely already encountered the `chat` format: a sequence of messages between `system`, `user`, and `assistant`. ```json theme={null} [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "How do I reset my password?" } ] ``` This simple, message-based interface has become the **standard across APIs**, making it easier to build, test, and scale without needing to relearn a custom format for every provider. ### Plug-and-Play with Tools and SDKs Standard chat completions are supported out-of-the-box by: * **Popular SDKs** (OpenAI, LangChain, LlamaIndex, etc.) * **Prompt engineering tools** * **Debugging/observability dashboards** * **Prompt versioning platforms** * **Orchestration frameworks** That means less boilerplate and more productivity. ### Easily Portable Across Models Using a standard chat format lets you switch between: * OpenAI GPT and o-series * Anthropic Claude * Google Gemini * Open-source chat models like DeepSeek, LLaMA, or Mistral ...all without rewriting your app logic. Standardization lets you **benchmark models**, compare outputs, and even build model-agnostic fallbacks or ensembles. ### Better Alignment with Human Intent The chat format encourages natural interaction flow, including: * Clarification (multi-turn) * Function/tool calling * Role-based prompting (e.g. system as context setter) It also makes features like **memory**, **tool use**, and **retrieval-augmented generation (RAG)** feel native, rather than bolted on. ### Built-In Support for Advanced Features Using chat completions gives you access to: * Function/tool calling * JSON mode * Temperature/top\_p control * Streaming support * Multi-turn context windows These features are essential for reliable, context-aware, and real-time applications. ### Supported by the Ecosystem From hosting providers to vector databases, the whole AI tooling ecosystem now expects the standard chat format. It’s the easiest way to stay compatible and future-proof your codebase. *** ## TL;DR: Why Use Standard Chat Completions? | Benefit | Description | | - | - | | **Familiar** | Common API shape across many providers. | | **Pluggable** | Compatible with SDKs and tools. | | **Portable** | Easy to swap or compare models. | | **Powerful** | Supports advanced features like tool calling. | | **Ecosystem-Ready** | Plays nicely with the AI stack. | | **Human Flourishing** | Build with AI and Human Flourishing capabilities at its core with the world's leading open source models. | **Ready to build?** The chat format is your foundation for production-ready AI apps—backed by best practices, trusted by industry, and ready to scale. # Completions Source: https://docs.gloo.com/api-guides/completions-v2 Chat Completions V2. Values Aligned. Smart Routing. Better Results. Lower Costs. The Gloo Completions V2 API is built on a layered, production-ready AI architecture that embeds values, safety, and care directly into today’s best models before, during, and after use. For new integrations, the [Responses API](/api-guides/responses) (`POST /ai/v2/guarded/responses`) is Gloo's recommended, OpenAI-compatible surface — it runs this same guarded pipeline, including `tradition`, on the Responses shape. Completions V2 remains fully supported and is the home of the routing, `model_family`, and grounded features described below — reach for it when you need them. ## **System Capabilities** * Curated Foundational Models: Access the best foundational models available without decision fatigue or unsafe defaults. * Safe & Values-Aligned: Values alignment and AI safety is evaluted at every layer of the input and output, considering six dimensions of AI safety (Physical, Ethical, Emotional, Factual, Theological, and Security) * Intelligent Routing: Optional automatic model routing optimizes your outputs for quality, cost, and intent. ## **Why Completions V2?** Completions V2 builds on the standard chat completions format you already know, but adds three powerful routing mechanisms to help you get the best performance for every query. ### Choose your Routing Strategy | Routing Mode | Best For | How It Works | | - | - | - | | AI Core | General chat, customer support, content generation | Analyzes each query and automatically selects the optimal model tier (speed vs. capability) | | AI Core Select | Provider preference, testing across model families | Specify a provider (OpenAI. Anthropic, Gemini, Open Source) and let Gloo AI pick the best model from that family | | AI Select | Full control, benchmarking, specialized tasks | Explicitly choose a specific model such as `gloo-openai-gpt-5-mini` or `gloo-anthropic-claude-haiku-4.5` | ### 1. AI Core (Auto-Routing) \[Recommended] Let Gloo AI analyze your query and choose the best model automatically: ``` { "messages": [ { "role": "user", "content": "How does the Old Testament connect to the New Testament?" } ], "auto_routing": true, "stream": false } ``` This is ideal when you want Gloo’s optimized choice across speed, utility, and reasoning without manual comparison. ### 2. AI Core Select (Model Provider Selection) Specify a model provider (`model_family`): ``` { "messages": [ { "role": "user", "content": "Draft a sermon" } ], "model_family": "anthropic", "auto_routing": false } ``` This is ideal if you prefer a specific provider but want Gloo AI to optimize your output by choosing the model from within that group. ### 3. AI Select (Direct Model Choice) Specify the exact model for your output: ``` { "messages": [ { "role": "user", "content": "Summarize this article" } ], "model": "gloo-google-gemini-2.5-pro", "auto_routing": false } ``` Choose a specific model directly for benchmarking, specialized workflows, or strict reproducibility. You can view supported model ids for this endpoint on the [Supported Models](/api-guides/supported-models) page. ## Additional Features Completions V2 isn’t just about routing—it’s designed to support the broader goal of **values-aligned AI** that is **safe, intelligent, and production-ready**. ### Tradition-Aware Customize responses based on theological perspectives: ``` { "messages": [ { "role": "user", "content": "Who is the Holy Spirit?" } ], "auto_routing": true, "tradition": "evangelical" } ``` Supported: `"evangelical"`, `"catholic"`, `"mainline"`, `"not_faith_specific"`, or omit for a general Christian perspective. `"not_faith_specific"` requires an explicit `model` and cannot be combined with `auto_routing` or `model_family`. ### Streaming Support Get real-time responses for better UX: ``` { "messages": [...], "auto_routing": true, "stream": true } ``` Streams can fail after the response has started — see [Handling Streaming Failures](/best-practices/completions-streaming-failures) for retry, continuation, and partial-output guidance. ### Tool Calling Function calling works seamlessly with all routing modes. You can define tools in your request and the selected model will invoke them as needed: ```json theme={null} { "messages": [ { "role": "user", "content": "What's the weather in Shanghai?" } ], "auto_routing": true, // or use model/model_family "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a location", "parameters": { "type": "object", "properties": { "location": { "type": "string" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] } } } ], "tool_choice": "auto" } ``` For comprehensive tool calling documentation including: * Multiple SDK examples (Python, TypeScript, AgentKit) * Model compatibility and streaming support * Multi-step tool workflows * Best practices and patterns See our **[Tool Use Guide](/api-guides/tool-use)**. ### Prompt Caching Reduce costs and latency by caching prompt prefixes across requests. Two types of caching are available depending on your model provider: * **Explicit caching (Anthropic)**: Add the `X-Cache-TTL` header to your request to opt in. System messages are automatically cached. * **Implicit caching (OpenAI, DeepSeek)**: Caching is automatic for prompts with 1,024+ tokens. For OpenAI, you can optionally add `prompt_cache_key` to improve cache hit rates. For detailed documentation including billing rates, best practices, and provider-specific behavior, see the **[Prompt Caching Guide](/api-guides/prompt-caching)**. ## 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 **URL:** `https://platform.ai.gloo.com/ai/v2/guarded/chat/completions` **Operation:** `POST` #### Example CURL Request: ```bash theme={null} curl -X 'POST' \ 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions' \ -H 'accept: application/json' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -H 'X-Cache-TTL: 5m' \ -d '{ "messages": [ { "role": "user", "content": "How does the Hebrew term 'ruach' in Genesis 1:2 affect translation?" } ], "tradition": "evangelical", "auto_routing": true }' ``` ## Request Parameters | Parameter | Type | Required? | Description | | :- | :- | :- | :- | | `messages` | array | Yes | Chat message history | | `auto_routing` | boolean | Conditional | Enable smart routing | | `model` | string | Conditional | Gloo model id | | `model_family` | string | Conditional | Provider family (`openai`, `anthropic`, `google`, `open source`) | | `tradition` | string | No | Theological Perspective | | `stream` | boolean | No | Enable streaming (default: `false`) | | `temperature` | float | No | Sampling temperature (0.0-2.0) | | `max_tokens` | integer | No | Maximum response length | | `tools` | array | No | Function calling definitions | | `prompt_cache_key` | string | No | Optional cache key for OpenAI explicit caching to improve hit rates (see [Prompt Caching](/api-guides/prompt-caching)) | Exactly one routing mechanism must be specified: `auto_routing`, `model`, or `model_family` ### Response Metadata The response includes routing metadata that varies based on your model selection mode. **Common fields (all modes):** | Field | Description | | - | - | | `model` | The Gloo model ID that handled the request | | `provider` | Always `"Gloo AI"` | | `model_family` | The provider family (`OpenAI`, `Anthropic`, `Google`, `Open Source`) | | `auto_routing` | Whether auto-routing was enabled | | `routing_mechanism` | The selection mode used: `auto_routing`, `model_family`, or `direct_model_selection` | **Additional fields for auto-routing and model family modes:** | Field | Description | | - | - | | `routing_tier` | The model tier selected (e.g., `tier_2`, `tier_4`) | | `routing_confidence` | Confidence score for the routing decision (0-1) | **Optional fields (included if specified in request):** | Field | Description | | - | - | | `tradition` | The theological perspective used (e.g., `evangelical`, `catholic`, `mainline`) | #### Example: Auto-Routing Response ```json theme={null} { "id": "gen-1768500882-56HaBYeuAb4pLpv8PXqh", "object": "chat.completion", "created": 1768500882, "model": "gloo-openai-gpt-5.2", "provider": "Gloo AI", "model_family": "OpenAI", "auto_routing": true, "routing_mechanism": "auto_routing", "routing_tier": "tier_2", "routing_confidence": 0.557, "choices": [...], "usage": {...} } ``` #### Example: Model Family Response ```json theme={null} { "id": "gen-1768501093-eWeO7cEfSgTPxxUmEgCI", "object": "chat.completion", "created": 1768501093, "model": "gloo-openai-gpt-oss-120b", "provider": "Gloo AI", "model_family": "Open Source", "auto_routing": false, "routing_mechanism": "model_family", "routing_tier": "tier_2", "routing_confidence": 0.555, "choices": [...], "usage": {...} } ``` #### Example: Direct Model Selection Response ```json theme={null} { "id": "gen-1768498306-NYVIJq1ygiReBbX1AKwP", "object": "chat.completion", "created": 1768498306, "model": "gloo-deepseek-v3.2", "provider": "Gloo AI", "model_family": "Open Source", "auto_routing": false, "routing_mechanism": "direct_model_selection", "choices": [...], "usage": {...} } ``` `routing_tier` and `routing_confidence` are not included when using direct model selection since no routing decision is made. ## Migrating from Completions V1 If you're currently using the V1 completions endpoint (`/ai/v1/chat/completions`), here's what you need to know to migrate to V2. ### Endpoint Change | Version | Endpoint | | - | - | | V1 | `https://platform.ai.gloo.com/ai/v1/chat/completions` | | V2 | `https://platform.ai.gloo.com/ai/v2/guarded/chat/completions` | ### Request Parameter Changes The main difference is how you specify model selection: | V1 | V2 | | - | - | | `model` optional (uses fixed default if omitted) | Choose one: `auto_routing`, `model`, or `model_family` | **If you're not specifying a model in V1**, the simplest migration path is to use V2's auto-routing, which intelligently selects the best model for each request: ```json theme={null} { "auto_routing": true, "messages": [ { "role": "user", "content": "Hello" } ] } ``` **If you're specifying a model in V1**, you can continue using the `model` parameter in V2: **V1 Request:** ```json theme={null} { "model": "gloo-anthropic-claude-haiku-4.5", "messages": [ { "role": "user", "content": "Hello" } ] } ``` **V2 Request (equivalent):** ```json theme={null} { "model": "gloo-anthropic-claude-haiku-4.5", "auto_routing": false, "messages": [ { "role": "user", "content": "Hello" } ] } ``` ### New V2-Only Parameters | Parameter | Type | Description | | - | - | - | | `auto_routing` | boolean | Enable intelligent model selection | | `model_family` | string | Select by provider (`openai`, `anthropic`, `google`, `open source`) | | `tradition` | string | Theological perspective (`evangelical`, `catholic`, `mainline`, `not_faith_specific`). `not_faith_specific` requires an explicit `model` (not compatible with `auto_routing`/`model_family`). | ### Response Changes V2 responses include additional routing metadata. The exact fields vary by routing mode—see [Response Metadata](#response-metadata) for full details. Example (auto-routing with tradition): ```json theme={null} { "model": "gloo-openai-gpt-5.2", "provider": "Gloo AI", "model_family": "OpenAI", "auto_routing": true, "routing_mechanism": "auto_routing", "routing_tier": "tier_2", "routing_confidence": 0.557, "tradition": "evangelical", ... } ``` `routing_tier` and `routing_confidence` are only included for auto-routing and model family modes. The `tradition` field is only included if specified in the request. ### Migration Checklist 1. Update the endpoint URL from `/ai/v1/` to `/ai/v2/` 2. Add a routing mechanism to your request: * Set `auto_routing: true` to use smart routing (recommended) * Or keep using `model` with `auto_routing: false` for direct model selection * Or use `model_family` to let Gloo select the best model from a provider 3. Update model IDs to [V2-supported models](/api-guides/supported-models) 4. (Optional) Add `tradition` parameter for theology-aware responses 5. Update response handling to accommodate new metadata fields # Content Overview Source: https://docs.gloo.com/api-guides/content Manage your content throughout its lifecycle with the Gloo AI platform. ## What is Data Engine? The Data Engine is the intelligence layer of Gloo AI. It doesn't just store information — it transforms raw data into ministry-ready insights that power search, discovery, and decision support across the ecosystem. Supports multiple ingestion methods: file uploads, web scraping, YouTube channels, and RSS/ONIX feeds. Edit and organize content metadata including titles, tags, summaries, and custom fields to make content searchable and contextually rich. Reveals inferred relationships between data points and maps them to publisher metadata for deeper contextual understanding. Enables high-fidelity search tuned for ministry-specific queries with transparent relevance scoring. Before uploading content, you need at least one [**Publisher**](/getting-started/glossary#publisher) created in your organization. See [Manage Publishers](/studio/manage-publishers) to set one up. *** ## Content Lifecycle The Content APIs give you complete control over your content in Gloo AI—from initial upload through metadata editing to ongoing management and removal. ## Content Lifecycle Send files directly to Gloo AI for automatic parsing, indexing, and AI-readiness. Update metadata like titles, tags, and authors without re-uploading content. List, inspect, look up, and delete content as needed. ## Guides Send files to Gloo AI for automatic processing and indexing. Update metadata for single items or in bulk. List, inspect, look up, and delete your content. ## Common Use Cases ### Auditing Your Catalog You manage a growing library of content inside Gloo AI. Over time, metadata can become inconsistent or incomplete. Use the [Manage Content](/api-guides/manage-content) APIs to list all items, inspect metadata, and identify gaps before they cause discovery issues. ### Correcting and Updating Metadata Discover that content is missing tags or author names? Use the [Edit Content](/api-guides/edit-content) APIs to safely edit records—individually or in bulk—without interrupting other operations. ### Bulk Standardization Apply new tags or categories across hundreds of items by author, date, or keyword. The bulk update API lets you filter and modify metadata in one efficient operation. ### Content Removal Need to fully remove content? The delete API purges data across all subsystems—vector databases, file storage, and relational data—ensuring complete and compliant removal. # Direct Completions Source: https://docs.gloo.com/api-guides/direct-completions POST /ai/v2/direct/chat/completions — OpenAI-compatible Chat Completions straight to the model you name, with no guardrails, moderation, values-alignment, or routing in between. **Direct Completions** is the OpenAI-compatible Chat Completions format on a [direct endpoint](/api-guides/endpoint-types): your request goes straight to the `model` you name, and Gloo adds nothing between your prompt and the provider. Use it when you already have tooling built on the Chat Completions request shape and your application owns its own safety and prompting. If you are starting fresh, prefer [Direct Responses](/api-guides/direct-responses) — same directness, a more capable request shape, and vision and image generation as first-class citizens. If you want Gloo's governance layer — guardrails, output moderation, values-alignment (`tradition`), `model_family` selection, intelligent routing — use [Completions](/api-guides/completions-v2) instead. The two share a request shape; they differ in what runs around it. ## Quick start **URL:** `https://platform.ai.gloo.com/ai/v2/direct/chat/completions` ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/direct/chat/completions' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.6", "messages": [ { "role": "user", "content": "Explain prompt caching in two sentences." } ] }' ``` The request and response shapes mirror the OpenAI Chat Completions API, so existing SDKs and tooling work by pointing the base URL at Gloo. ## What runs, and what doesn't Identical to every direct endpoint — see [Endpoint Types](/api-guides/endpoint-types#what-runs-and-what-does-not) for the full list. In short: authentication, entitlement, metering, the model catalog, provider resiliency and rate limits all still apply; guardrails, output moderation, values-alignment and routing deliberately do not. ## Choosing between the direct formats Use this page when you have existing OpenAI Chat Completions tooling that should keep its request shape. For new integrations — and for vision or image generation — use [Direct Responses](/api-guides/direct-responses) instead; both are direct, and the only difference is the request shape. For the full comparison across guarded, direct and grounded endpoints, see [Which endpoint should I use?](/api-guides/endpoint-types#which-endpoint-should-i-use). # Direct Responses Source: https://docs.gloo.com/api-guides/direct-responses POST /ai/v2/direct/responses — the Responses request shape sent straight to the model you name, with no guardrails, moderation, values-alignment or routing in between. **Direct Responses** is the [direct endpoint](/api-guides/endpoint-types) for the Responses format: `POST /ai/v2/direct/responses` sends your request straight to the `model` you name. It exposes the entire Gloo AI model catalog through a single endpoint — text, vision, reasoning, tool use, and native image generation — with one request and response shape that works the same across every provider. It takes the **same request body** as the guarded [Responses](/api-guides/responses) endpoint; the difference is what runs around it. Nothing here applies guardrails, output moderation, values-alignment (`tradition`), `model_family` selection or intelligent routing. Choose this when your application owns its own safety and prompting; choose guarded [Responses](/api-guides/responses) — the recommended default — when it should inherit Gloo's. See [Which endpoint should I use?](/api-guides/endpoint-types#which-endpoint-should-i-use) for the full comparison across all three endpoint families. **New to Gloo AI?** Start on the guarded [Responses](/api-guides/responses) endpoint — same request shape, with Gloo's guardrails and values-alignment applied — and come here only if you need the unprocessed path. If you already use [Completions V2](/api-guides/completions-v2), it remains fully supported and backwards-compatible — see [Moving from Completions to Responses](#moving-from-completions-to-responses). ## Why the Responses API The Responses API offers a more capable, forward-looking request shape that the broader ecosystem is standardizing on: * **One shape for every modality.** Text, image input (vision), and image generation all use the same `input` array and the same typed `output` array — no separate endpoints or bespoke payloads per capability. * **OpenAI-compatible.** The request and response formats mirror the OpenAI Responses API, so existing tooling, SDKs, and mental models carry over directly. Point your base URL at `https://platform.ai.gloo.com/ai/v2/direct` and go. * **Typed, structured output.** Responses come back as an `output[]` array of typed items (`message`, `image_generation_call`, reasoning, tool calls) instead of a single opaque `choices[].message.content` string — easier to parse, and extensible as new item types arrive. * **Built for multimodal.** Native image generation and image input are first-class, not bolted on. The same endpoint that answers a text prompt can return a generated image. If you are starting a new integration, build on the Responses API. ## Endpoint **URL:** `https://platform.ai.gloo.com/ai/v2/direct/responses` **Operation:** `POST` ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/direct/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.6", "input": [ { "role": "user", "content": "Explain prompt caching in two sentences." } ] }' ``` Authentication is identical to the rest of the platform — send your API key as a Bearer token. See [Generate API Keys](/studio/manage-api-credentials). ## Request format The Responses API uses `input` instead of `messages`, and a few renamed fields. If you know the chat-completions format, the mapping is small: | Responses API | Chat Completions equivalent | Notes | | :- | :- | :- | | `input` | `messages` | A string, or an array of input items (`role` + `content`). | | `instructions` | `system` message | Top-level system / developer instructions. | | `max_output_tokens` | `max_tokens` | Cap on generated tokens. | | `model` | `model` | A Gloo model ID — see [Supported Models](/api-guides/supported-models). | | Parameter | Type | Required? | Description | | :- | :- | :- | :- | | `model` | string | Yes | Gloo model ID (e.g. `gloo-anthropic-claude-sonnet-4.6`). | | `input` | string \| array | Yes | A plain string prompt, or an array of typed input items. | | `instructions` | string | No | System-level instructions applied to the request. | | `max_output_tokens` | integer | No | Maximum number of tokens to generate. | | `temperature` | float | No | Sampling temperature. | | `top_p` | float | No | Nucleus sampling. | | `tools` | array | No | Tool / function definitions. See [Tool Use](/api-guides/tool-use). | | `tool_choice` | string \| object | No | Controls tool selection. | | `stream` | boolean | No | Stream the response as SSE events (default `false`). | | `reasoning` | object | No | Reasoning controls (e.g. effort) for capable models. | | `response_format` | object | No | Structured-output / JSON schema controls. | | `prompt_cache_key` | string | No | Improves cache-hit routing — see [Prompt Caching](/api-guides/prompt-caching). | | `image_generation` | object | No | Image-generation options (`quality`, `size`) for image-capable models. | Conversation state is managed client-side by passing the full `input` history on each request. The `previous_response_id` parameter (used by OpenAI's Responses API for server-side conversation chaining) is not supported. **This is a [direct endpoint](/api-guides/endpoint-types): `/ai/v2/direct/responses` does not apply the governance layer.** Requests here call the exact `model` you specify with no additional processing. None of the following runs: * Input guardrails and output moderation * Values-alignment (`tradition`) * `model_family` selection and intelligent auto-routing **Want the same request body with those applied?** Use the guarded [Responses](/api-guides/responses) endpoint at `/ai/v2/guarded/responses` — same body, different URL. [Completions V2](/api-guides/completions-v2) and [Grounded Completions](/api-guides/grounded-completions) are guarded too, on the Chat Completions shape. "Direct" describes this URL, not the Responses format in general: the same format is served by the guarded endpoint above and by [Grounded Responses](/api-guides/grounded-responses), which adds retrieval-augmented (RAG) generation over your own content. ## Response format Responses return a typed `output[]` array. Each item has a `type`; a normal text answer arrives as a `message` item, a generated image as an `image_generation_call` item. ```json theme={null} { "id": "resp_...", "object": "response", "model": "gloo-anthropic-claude-sonnet-4.6", "output": [ { "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "Prompt caching reuses processed prompt tokens..." } ] } ], "usage": { "input_tokens": 24, "output_tokens": 38, "total_tokens": 62 } } ``` ### Streaming Set `"stream": true` to receive the response as Server-Sent Events. Each event has an `event:` line and a `data:` line carrying a typed JSON payload: ```text theme={null} event: response.output_item.added data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_...","type":"message","role":"assistant"}} event: response.content_part.added data: {"type":"response.content_part.added","item_id":"msg_...","part":{"type":"output_text","text":""}} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_...","delta":"Prompt "} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_...","delta":"caching "} event: response.content_part.done data: {"type":"response.content_part.done","item_id":"msg_...","part":{"type":"output_text","text":"Prompt caching reuses..."}} event: response.output_item.done data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_...","type":"message",...}} event: response.completed data: {"type":"response.completed","response":{"id":"resp_a1b2c3",...,"output":[...],"usage":{...}}} ``` Key event types to handle: * `response.output_item.added` / `response.output_item.done` — mark the start and end of each typed item in `output[]` (including `message`, `image_generation_call`, and `function_call`). * `response.content_part.added` / `response.content_part.done` — mark the start and completion of a content part within an item. `response.content_part.done` is the signal that a message's text is complete, followed by `response.output_item.done`. * `response.output_text.delta` — incremental text tokens for a `message` item. * `response.function_call_arguments.delta` — incremental tool-call argument tokens for a `function_call` item. * `response.completed` — emitted once when the response is finished; the final payload includes the full `output[]` array and `usage`. Errors are not delivered as SSE events. They surface as an HTTP error status (or a raised exception in the SDK), so handle them on the request itself rather than watching for an `error` event in the stream. ## Multimodal ### Image input (vision) Pass images as input items alongside text. Any vision-capable model accepts them: ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/direct/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-google-gemini-3.1-pro", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "Describe this image." }, { "type": "input_image", "image_url": "https://example.com/photo.jpg" } ]} ] }' ``` Images may be supplied as a remote URL or a base64 `data:` URI. ### Image generation Image-capable models return a generated image as an `image_generation_call` output item (base64 result). Optional `image_generation` controls (`quality`, `size`) are forwarded to providers that support them. ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/direct/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-google-gemini-3-pro-image", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "A watercolor painting of a lighthouse at sunset." } ]} ] }' ``` The generated image comes back as a typed `image_generation_call` item in `output[]`. The `result` field carries the image as a base64-encoded string: ```json theme={null} { "id": "resp_...", "object": "response", "model": "gloo-google-gemini-3-pro-image", "output": [ { "type": "image_generation_call", "id": "ig_...", "status": "completed", "result": "iVBORw0KGgoAAAANSUhEUgAA......" } ], "usage": { "input_tokens": 14, "output_tokens": 1290, "total_tokens": 1304 } } ``` Decode the base64 `result` to get the image bytes. Optional `image_generation` controls (`quality`, `size`) are forwarded to providers that support them. #### OpenAI: image generation via the image\_generation tool OpenAI's image generation runs as an `image_generation` **tool** on a GPT-5 model — `gpt-image-1` is not a top-level Responses `model` (exactly as on OpenAI's own API, where it is an Images API model rather than a Responses one). Call a GPT-5 model and attach the tool; the model decides when to draw and can combine the image with text in the same response. The tool accepts an optional `model` field to pin its backing image model (e.g. `gpt-image-1`) plus `quality`/`size` controls: ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/direct/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-openai-gpt-5-mini", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "A watercolor painting of a lighthouse at sunset." } ]} ], "tools": [ { "type": "image_generation", "model": "gpt-image-1", "quality": "low" } ] }' ``` The generated image arrives as the same `image_generation_call` output item shown above (alongside any `reasoning` and `message` items from the GPT-5 model). **Image editing** works through the same mechanism: include an `input_image` part in the conversation and it becomes the tool's edit source. See [Supported Models](/api-guides/supported-models) for which models support image input and image generation. ## Pricing & token spend The Responses API is billed per token at each model's standard rates — there is no separate or premium price for using `/responses`. Cost is driven entirely by the `model` you select and the tokens you consume: ``` total_cost = input_tokens × input_rate + cached_tokens × cache_read_rate + output_tokens × output_rate ``` * **Per-model rates.** Input and output rates vary by model. The live rates are on the [Supported Models](/api-guides/supported-models) page and programmatically on `GET /platform/v2/models`. * **Prompt caching** reduces input cost when prompt prefixes repeat — cached tokens are billed at a discounted `cache_read` rate. See [Prompt Caching](/api-guides/prompt-caching). * **Image generation** is billed using the image model's token accounting; check the model's rates on the models endpoint. * **No platform fee.** Direct endpoints bill the published per-model rate exactly. The 6.5% platform fee applies only to the governed endpoints, where guardrails run. Track real spend in the [Gloo Studio billing dashboard](https://studio.ai.gloo.com/billing) and [API usage](/studio/api-usage). ## Supported models The Responses API works across the full Gloo AI catalog — Anthropic, OpenAI, Google, and open-source families — including the multimodal and image-generation models. Use the **Model ID** as the `model` field. The complete, live list (with capabilities and pricing) is on the [Supported Models](/api-guides/supported-models) page. ## Moving from Completions to Responses [Completions V2](/api-guides/completions-v2) (`/ai/v2/guarded/chat/completions`) is **fully supported and backwards-compatible** — existing integrations continue to work unchanged, and it remains the home of auto-routing and `model_family` selection. Guardrails, output moderation and `tradition` have already reached the Responses shape on the guarded [Responses](/api-guides/responses) endpoint, and RAG grounding on [Grounded Responses](/api-guides/grounded-responses). The Responses API is the recommended surface for new work because it standardizes on the OpenAI-compatible Responses shape and makes multimodal (vision + image generation) first-class. Below the URL, 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) | **The URL is the one row that is not a rename.** Completions V2 applies guardrails, output moderation and `tradition`; this page's endpoint applies none of them. `POST /ai/v2/guarded/responses` is the like-for-like destination — it keeps the governance layer while changing the request shape. Move to `POST /ai/v2/direct/responses` only if dropping that layer is a decision you are making deliberately, and the body stays identical either way. ```json theme={null} // Completions V2 { "model": "gloo-anthropic-claude-sonnet-4.6", "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "Hello" } ], "max_tokens": 256 } ``` ```json theme={null} // Responses API (equivalent) { "model": "gloo-anthropic-claude-sonnet-4.6", "instructions": "You are a helpful assistant.", "input": [ { "role": "user", "content": "Hello" } ], "max_output_tokens": 256 } ``` `tradition`, guardrails and output moderation are available now on the guarded [Responses](/api-guides/responses) endpoint, and grounded (RAG) responses on [Grounded Responses](/api-guides/grounded-responses). Auto-routing and `model_family` selection remain on [Completions V2](/api-guides/completions-v2), and grounding with citation metadata on [Grounded Completions](/api-guides/grounded-completions). Choose this page's direct endpoint when your application owns its own safety and prompting. ## Related Documentation * [Supported Models](/api-guides/supported-models) — model IDs, capabilities, and live pricing * [Grounded Responses](/api-guides/grounded-responses) — RAG-grounded answers in the Responses API format * [Prompt Caching](/api-guides/prompt-caching) — reduce cost and latency with cached prompt prefixes * [Tool Use](/api-guides/tool-use) — function calling * [Completions V2](/api-guides/completions-v2) — routing, values-alignment, and backwards-compatibility # Edit Content Source: https://docs.gloo.com/api-guides/edit-content Update metadata for your content without re-uploading files. The Content APIs let you update item-level metadata—titles, tags, authors, and more—without re-ingesting content. Make quick edits to individual items or apply changes across hundreds of items at once. ## Prerequisites Before starting, ensure you have: * A **Gloo AI Studio** account * Your **API key** from the [API Keys page](/studio/manage-api-credentials) * **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first The caller must belong to the organization that owns the specified publisher. ## Update a Single Item Use the single-item endpoint when you need to edit a specific item by its [`item_id`](/getting-started/glossary#item_id) or your [`producer_id`](/getting-started/glossary#producer_id). **When to use:** * Correcting a typo in a title * Adding missing author or tags * Updating a summary or publication date ```bash Shell theme={null} curl -X PATCH 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": "37328826-e527-4039-8996-bd27741b6f8e", "item_title": "Updated Title", "item_tags": ["theology", "sermons"] }' ``` **Example Response:** ```json theme={null} { "success": true, "message": "Item 37328826-e527-4039-8996-bd27741b6f8e has been updated." } ``` You can identify items by either `item_id` (Gloo's UUID) or `producer_id` (your internal ID). If you provide both, `item_id` takes precedence. **Full API details:** [Single Item Patch API Reference](/api-reference/content-controls/single-item-patch) *** ## Update Multiple Items Use the bulk endpoint when you need to update many items matching specific criteria. Filter by author, tags, status, date ranges, or search terms, then apply operations to all matching items. **When to use:** * Adding a tag across all items by a specific author * Updating metadata for a content series * Standardizing fields after a catalog audit ### Filter Options Target items using any combination of: * `item_ids` — Specific item UUIDs * `authors` — Items by specific authors * `tags` — Items with specific tags * `statuses` — Items in specific processing states * `search` — Text search across titles, subtitles, summaries * `created_after` / `created_before` — Date ranges * `visible_in_search` / `visible_in_chat` — Visibility flags ### Operations Apply one or more operations to matched items: * **replace** — Set a field to a new value * **append** — Add values to a list (authors, tags) * **remove** — Clear a field or remove values from a list ```bash Shell theme={null} curl -X PATCH "https://platform.ai.gloo.com/engine/v2/items?publisher_id=your-publisher-id" \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "filter": { "authors": ["John Smith"], "visible_in_search": true }, "ops": [ { "op": "append", "field": "item_tags", "value": ["series-2024"] } ], "limit": 500 }' ``` **Example Response:** ```json theme={null} { "success": true, "total_matched": 42, "total_patched": 42, "total_failed": 0, "message": "Successfully patched 42 of 42 items" } ``` **Full API details:** [Multiple Item Patch API Reference](/api-reference/content-controls/multi-item-patch) The `publisher_id` in API requests is the unique identifier for your Publisher. See [publisher\_id](/getting-started/glossary#publisher_id) in the glossary. *** ## Editable Fields | Field Type | Fields | | - | - | | **Strings** | `item_title`, `item_subtitle`, `item_summary`, `item_image`, `item_url`, `item_number`, `item_extra`, `isbn`, `publication_date` | | **Lists** | `author`, `item_tags` | | **Booleans** | `evergreen`, `visible_in_search`, `visible_in_chat` | `isbn`, `evergreen`, `visible_in_search`, and `visible_in_chat` are editable only through the bulk [`PATCH /engine/v2/items`](/api-reference/content-controls/multi-item-patch) endpoint. The single-item endpoint silently ignores these fields and returns them under `ignored_fields`. *** ## Common Use Cases ### Correcting Metadata Fix typos, add missing authors, or update summaries for individual items using the single-item endpoint. ### Bulk Tagging Apply category tags (e.g., "Gospel of John", "Marriage Series") across hundreds of items by filtering on author or date range. ### Standardizing Fields After a catalog audit, use bulk operations to ensure consistent metadata across your content library. ### Managing Visibility Control which items appear in search results or are accessible via chat by updating `visible_in_search` and `visible_in_chat` flags. You can edit metadata for individual items directly from the Content Library in Studio. 1. Click **Data Engine** in the Studio sidebar, then click **Content Library**. 2. Find the item you want to edit and click the **edit icon** in the Actions column. 3. Update any of the following fields: * **Title** — The main title as it appears in search results * **Subtitle** — Additional title information * **Image URL** — Link to a cover image or thumbnail * **Source URL** — Link to the original content source * **Summary** — Brief description for search results * **Tags** — Comma-separated keywords to improve searchability 4. Click **Save Changes**. The Studio UI supports editing individual items. For bulk updates across many items, use the [Via API tab](#update-multiple-items). # Embeddings Source: https://docs.gloo.com/api-guides/embeddings POST /ai/v2/direct/embeddings — turn text into embedding vectors with any Gloo embedding model, using the OpenAI-compatible Embeddings request shape. **Embeddings** turns text into vectors: lists of numbers that capture meaning, so that passages about the same idea end up close together even when they share no words. Store them in a vector database and you can build semantic search, retrieval-augmented generation (RAG), clustering, deduplication and recommendations on top. If the idea is new to you, [Embeddings: How AI Understands Meaning](/ai-learning-center/gloo-ai-103/embeddings) explains it from the ground up. `POST /ai/v2/direct/embeddings` serves every embedding model in the Gloo catalog (OpenAI, Google, Voyage AI, Mistral, Qwen, BAAI and more) behind one request shape. The request and response mirror the OpenAI Embeddings API, so the OpenAI SDKs work by pointing the base URL at Gloo. You don't need this endpoint to search content you have uploaded to Gloo. The Data Engine embeds your content for you, and [Search](/api-guides/search) and the [grounded endpoints](/api-guides/endpoint-types#grounded-endpoints) query it. Use Embeddings when you run your own vector store. ## Quick start **URL:** `https://platform.ai.gloo.com/ai/v2/direct/embeddings` **Operation:** `POST` ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/direct/embeddings' \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-openai-text-embedding-3-small", "input": ["In the beginning was the Word.", "Love is patient."], "encoding_format": "float" }' ``` Authentication is the same as the rest of the platform: send your API key as a Bearer token. See [Generate API Keys](/studio/manage-api-credentials). ### With the OpenAI SDK ```python Python theme={null} import os from openai import OpenAI client = OpenAI( api_key=os.environ["GLOO_API_KEY"], base_url="https://platform.ai.gloo.com/ai/v2/direct", ) result = client.embeddings.create( model="gloo-openai-text-embedding-3-small", input=["In the beginning was the Word.", "Love is patient."], encoding_format="float", ) for item in result.data: print(item.index, len(item.embedding)) ``` ```typescript TypeScript theme={null} import OpenAI from 'openai'; const client = new OpenAI({ apiKey: process.env.GLOO_API_KEY, baseURL: 'https://platform.ai.gloo.com/ai/v2/direct', }); const result = await client.embeddings.create({ model: 'gloo-openai-text-embedding-3-small', input: ['In the beginning was the Word.', 'Love is patient.'], encoding_format: 'float', }); for (const item of result.data) { console.log(item.index, item.embedding.length); } ``` ## Request format | Parameter | Type | Required? | Description | | :- | :- | :- | :- | | `model` | string | Yes | An embedding model's Gloo Model ID, e.g. `gloo-openai-text-embedding-3-small`. See [Choosing a model](#choosing-a-model). | | `input` | string \| array of strings | Yes | The text to embed: one non-empty string, or an array of 1 to 2048 non-empty strings. | | `encoding_format` | string | No | `float` (an array of numbers) or `base64` (little-endian float32 bytes as a base64 string). When omitted, the provider's default applies. Set this explicitly when your client requires a particular encoding. | | `dimensions` | integer | No | Request a vector width. Support and accepted values vary by model; unsupported models or values return `400`. See [Dimension support](#dimension-support). | | `user` | string | No | Accepted for OpenAI SDK compatibility and ignored. | Provider-specific parameters are forwarded to the model provider unless Gloo reserves or rejects them. `stream` and `provider` are not forwarded. The aliases `dimension`, `output_dimension`, and `output_dimensionality` return `400`; use `dimensions` instead. **Input rules:** * Each string must fit within the model's input limit (`max_input_tokens` in the catalog, which is as low as 512 tokens for some models). A longer string is rejected with a `400`, so split long documents into chunks first; [Building a Knowledge Base](/ai-learning-center/gloo-ai-103/building-a-knowledge-base) covers how. * Arrays of token IDs, which the OpenAI API also accepts, are not supported. Send text. * Empty or whitespace-only strings are rejected. ## Response format ```json theme={null} { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [0.4155, 0.7861, -0.2673, 0.3721] }, { "object": "embedding", "index": 1, "embedding": [-0.0755, -0.3010, -0.6606, 0.6841] } ], "model": "gloo-openai-text-embedding-3-small", "usage": { "prompt_tokens": 11, "total_tokens": 11 } } ``` The vectors above are shortened for readability; real vectors contain the model's full number of dimensions. * `data` holds one embedding per input string, in the same order as `input`. `index` is the string's position. * `embedding` is the provider's vector passed through untouched. It is an array when the provider returns floats, or a base64 string when the provider honors `encoding_format: "base64"`. If you omit `encoding_format`, the provider chooses its default. If a provider does not honor a requested encoding, Gloo returns `503` instead of a response in the wrong shape. * `model` is the value you sent. * `usage` reports the provider's token counts and may be omitted when the provider does not report usable usage. Requests without usage are not billed. ## Choosing a model Every model whose `output_modalities` includes `embeddings` works here, and sending a chat model returns a `400` that says so. [Supported Models](/api-guides/supported-models) identifies embedding models and lists each model's input price, vector width, and input limit. To list only embedding models, filter the public catalog: ```bash theme={null} curl -s 'https://platform.ai.gloo.com/platform/v2/models' \ | jq -r '.data[] | select(.output_modalities | index("embeddings")) | .id' ``` Two things to settle before you embed a large corpus: * **Stay on one model.** Vectors from different models, or from the same model with different `dimensions`, are not comparable. Embed your documents and your queries with the same model and settings, and re-embed everything if you switch. * **Vector size differs by model.** For example, `gloo-openai-text-embedding-3-small` returns 1536 numbers and `gloo-baai-bge-base-en-v1.5` returns 768. Size your vector store's column to the model you pick. ### Dimension support Only some embedding models accept `dimensions`. For example, the OpenAI `text-embedding-3` models accept widths up to their native size, Perplexity models require at least 128, and Voyage models accept 256, 512, 1024, or 2048. Other models reject this parameter with `400`. The public model catalog reports native vector width, but does not currently list which shortened widths a model accepts. A `400` response identifies the accepted values or range when available. ## What runs, and what doesn't This is a direct endpoint. Authentication, organization entitlement, usage metering, and platform rate limits apply. Guardrails and moderation do not run, and embeddings do not use automatic model routing or streaming. Your text goes to the embedding model you name. Embeddings requests are not failed over to a second provider. A provider outage or rate limit returns `503` with `retryable: true`; retry with bounded exponential backoff. Gloo also returns `503` if the provider returns an empty, incomplete, unusable, or incorrectly encoded vector batch. If the same request repeatedly fails because the provider does not support a requested encoding, change the request or model instead of retrying unchanged. ## Pricing Embeddings are billed per input token at the model's published input rate. There is no output charge and no platform fee. Live rates are on [Supported Models](/api-guides/supported-models) and `GET /platform/v2/models`; your spend shows up in [API usage](/studio/api-usage). ## Errors | Status | When | Retry? | | :- | :- | :- | | `400` | The model isn't an embedding model; `input` is empty, too long, has more than 2048 strings, or contains non-strings; `encoding_format` is invalid; `dimensions` is unsupported or out of range; a dimension alias was used; or the provider rejected the request. Gloo validation errors include `detail.param` when applicable. | No. Fix the request. | | `401` / `403` | Authentication failed or the organization is not permitted to use the API. | No. Check the API key and organization access. | | `402` / `429` | Your organization is out of credit or has reached its spending limit. | Not until the limit is resolved. See [Limits](/api-reference/general/limits). | | `422` | The request body has a field with the wrong type, such as a non-integer `dimensions`. | No. Fix the request. | | `503` | The provider is unavailable or rate-limited, or returned vectors that are empty, incomplete, unusable, or in a different encoding than requested. | Retry transient provider failures with backoff. Change the request or model if the failure persists for the same input and options. | The full list of error codes is in [Error Reference](/api-reference/general/errors). ## Related Documentation * [Embeddings API reference](/api-reference/embeddings/direct): interactive request builder * [Supported Models](/api-guides/supported-models): model IDs, input limits and live pricing * [Building a Knowledge Base](/ai-learning-center/gloo-ai-103/building-a-knowledge-base): chunking documents before you embed them * [Direct Responses](/api-guides/direct-responses) and [Direct Completions](/api-guides/direct-completions): direct generation endpoints # Endpoint Types Source: https://docs.gloo.com/api-guides/endpoint-types Guarded, direct, and grounded — the three endpoint families on Gloo AI, what each one runs around your request, and how to choose between them. Gloo AI Responses and Completions endpoints belong to one of three families. All three share the same model catalog, authentication, metering and rate limits. What differs is **what runs around your request**. | Family | What Gloo adds | Endpoints | | :- | :- | :- | | **Guarded** | Guardrails, output moderation, values-alignment, routing | `/ai/v2/guarded/responses`, `/ai/v2/guarded/chat/completions` | | **Direct** | Nothing between your prompt and the provider | `/ai/v2/direct/responses`, `/ai/v2/direct/chat/completions` | | **Grounded** | Retrieval over your own uploaded content (RAG) | `/ai/v1/grounded/responses`, `/ai/v2/grounded/chat/completions` | Each family comes in both request shapes — the OpenAI-compatible **Responses** format and the **Chat Completions** format — so the family and the shape are independent choices for generation requests. ## Guarded endpoints The recommended default, and where new integrations should start. A guarded request runs Gloo's full safety and values pipeline: input guardrails, output moderation, values-aligned (`tradition`) responses, `model_family` selection and intelligent auto-routing across models. * [Responses](/api-guides/responses) (`POST /ai/v2/guarded/responses`) — the recommended surface for new work. * [Completions](/api-guides/completions-v2) (`POST /ai/v2/guarded/chat/completions`) — fully supported, and the home of routing and `model_family`. ## Direct endpoints Direct endpoints send your request straight to the model you name. Gloo adds nothing between your prompt and the provider: no guardrails, no output moderation, no values-alignment, no routing. What you send is what the model sees; what the model returns is what you get. You can spot a direct endpoint by the `/direct/` segment in its path. Use them when your application owns its own safety and prompting end to end — an agent framework with its own moderation, a backend that composes prompts itself, tooling that needs byte-faithful model behavior. * [Direct Responses](/api-guides/direct-responses) (`POST /ai/v2/direct/responses`) — for new work that owns its own safety; multimodal. * [Direct Completions](/api-guides/direct-completions) (`POST /ai/v2/direct/chat/completions`) — for existing Chat Completions tooling. ### What runs, and what does not Direct means direct about *content*, not a bypass of the platform: **Still applies on direct Responses and Completions requests:** * Authentication (Bearer API key) and organization entitlement * Usage metering and billing * The Gloo model catalog — you address models by their `gloo-*` Model ID * **Provider resiliency.** Gloo serves the model you asked for, but not always from the same place: when the provider currently serving it is failing or unhealthy, your request fails over to another provider that serves the same model, and a stream cut mid-answer can be continued on a fallback provider. The `model` you receive is the `model` you asked for — resiliency changes who serves it, never what you get. See [Provider Failover](/api-guides/provider-failover). * Platform rate limits **Deliberately not applied:** * Input guardrails and output moderation * Values-alignment (`tradition`) * `model_family` selection and intelligent auto-routing — nothing picks a *different model* than the one you named (distinct from provider resiliency above, which only changes which provider serves that same model) * Gloo prompt augmentation — your messages go to the model verbatim On direct endpoints, safety is your application's responsibility. Nothing screens the input before the model sees it, and nothing reviews the output before your user does. If any part of your traffic needs screening, route it through the guarded [Responses](/api-guides/responses) or [Completions](/api-guides/completions-v2) endpoints. ## Grounded endpoints Grounded endpoints retrieve relevant passages from content you have uploaded, inject them as context before generation, and tell you whether the answer was grounded in your sources. * [Grounded Responses](/api-guides/grounded-responses) (`POST /ai/v1/grounded/responses`) — grounding in the Responses shape. * [Grounded Completions](/api-guides/grounded-completions) (`POST /ai/v2/grounded/chat/completions`) — grounding on Completions V2, with citation metadata and `tradition`. ## Which endpoint should I use? | Use case | Recommended endpoint | | :- | :- | | New integration with OpenAI-compatible tooling | [Responses](/api-guides/responses) — the guarded default | | Guardrails, output moderation, values-aligned (`tradition`) responses | [Responses](/api-guides/responses) or [Completions](/api-guides/completions-v2) | | Your application owns its own safety and prompting | [Direct Responses](/api-guides/direct-responses) | | Byte-faithful model behavior, nothing added to the prompt | [Direct Responses](/api-guides/direct-responses) | | Existing Chat Completions tooling that should keep its request shape | [Direct Completions](/api-guides/direct-completions) | | Vision (image input) or image generation | Either Responses endpoint — same request shape | | Grounded / RAG answers over your own content | [Grounded Responses](/api-guides/grounded-responses) | | Embedding vectors for your own search or vector store | [Embeddings](/api-guides/embeddings) | | Grounding with citation metadata or `tradition` | [Grounded Completions](/api-guides/grounded-completions) | | Intelligent auto-routing across models | [Completions](/api-guides/completions-v2) | When in doubt, start with the guarded [Responses](/api-guides/responses) API. The guarded and direct Responses endpoints take the same request body, so moving between them later is a URL change. ## Legacy endpoint aliases The previous v2 paths remain available as aliases for existing integrations. New integrations should use the canonical paths above: | Previous path | Canonical path | | :- | :- | | `/ai/v2/chat/completions` | `/ai/v2/guarded/chat/completions` | | `/ai/v2/chat/completions/grounded` | `/ai/v2/grounded/chat/completions` | | `/ai/v2/responses` | `/ai/v2/guarded/responses` | The `/ai/v1/grounded/responses` endpoint is unchanged. # Grounded Completions Source: https://docs.gloo.com/api-guides/grounded-completions Ground AI responses in your own content using RAG-powered completions with intelligent routing and source attribution The Gloo Grounded Completions API extends the Completions V2 architecture with Retrieval-Augmented Generation (RAG), combining intelligent routing with powerful source-grounded response capabilities. Ground your AI responses in your own content—upload datasets, point the API at your publisher, and every response will be backed by your specific sources. Grounded completions (RAG) and intelligent routing are Completions V2 features. For general, OpenAI-compatible model access — including guardrails and `tradition` — the [Responses API](/api-guides/responses) is the recommended surface for new integrations; RAG grounding in the Responses format is available via [Grounded Responses](/api-guides/grounded-responses). When you need responses backed by specific content rather than pure model knowledge, grounded completions retrieve relevant context from your uploaded datasets and provide it to the model during generation. Set the `rag_publisher` parameter to your publisher name and the API handles retrieval, grounding, and attribution automatically. Like Completions V2, you get the same three routing options—auto-routing, model family selection, or direct model choice—plus tradition-based personalization and input guardrails, all while ensuring responses remain grounded in retrievable sources. ## Why Grounded Completions? Grounded completions solve the core challenge of AI trustworthiness: verifying what the model tells you. Here's what you get: **Reduced Hallucinations** By grounding responses in actual content from your specified dataset rather than relying solely on model training, you significantly reduce fabricated or incorrect information. The model generates answers based on retrieved content it can reference. **Content-Grounded Responses** Every response is informed by relevant sources retrieved from your uploaded content. The `sources_returned` flag in the response confirms that RAG was used to ground the generation. **Publisher-Scoped Knowledge** Query your own uploaded content, ensuring responses draw from approved, relevant sources rather than generic web knowledge. Control exactly what knowledge base powers your AI. **Routing Flexibility** Keep all the intelligent routing capabilities from Completions V2—let Gloo choose the best model automatically, select by provider family, or pick a specific model for your use case. Grounding reduces fabrication; it does not verify that a given sentence appears in the retrieved sources. If your application serves quotations, titles, or attributions, check the answer against its sources before display. See [Building Trustworthy Grounded Applications](/best-practices/trustworthy-grounded-applications). ## Key Features | Feature | Capability | Configuration | | :- | :- | :- | | **Intelligent Routing** | Auto-routing, model family selection, or direct model choice | `auto_routing`, `model_family`, or `model` | | **RAG with Attribution** | Retrieve relevant sources before generation, include references in response | `rag_publisher` set to your publisher name | | **Tradition Personalization** | Customize responses for specific theological perspectives | `tradition`: `catholic`, `evangelical`, `mainline` | | **Input Guardrails** | Content validation before processing | Automatic | ## RAG Configuration ### Using Your Own Content Set the `rag_publisher` parameter to your publisher name to ground responses in your uploaded content: ```json theme={null} { "messages": [ { "role": "user", "content": "What resources do you have on family counseling?" } ], "auto_routing": true, "rag_publisher": "YourPublisherName" } ``` The model will only retrieve and reference content from your specified publisher. To find your publisher name, navigate to the [Publishers page](/studio/manage-publishers) in Studio. You must have already [uploaded content](/api-guides/upload-content) before using grounded completions with your publisher. If you omit the `rag_publisher` parameter, the API falls back to **GlooGrounded**, a shared dataset assembled by Gloo. For best results, we recommend always specifying your own publisher. ### Source Limits Control how many sources are retrieved and considered with the `sources_limit` parameter (1-10, default is 3): ```json theme={null} { "messages": [ { "role": "user", "content": "What does Scripture say about forgiveness?" } ], "auto_routing": true, "sources_limit": 5 } ``` More sources provide broader context but increase processing time. Choose based on your use case—3 sources work well for most queries, while complex theological questions may benefit from more. ### Include Citations Include citation metadata for sources utilized by RAG (defaults to false) ```json theme={null} { "messages": [ { "role": "user", "content": "What does Scripture say about forgiveness?" } ], "auto_routing": true, "include_citations": true } ``` For streaming responses, citations will be pre-pended to the response in the following format: ```json theme={null} "citations": [ { "item_title": "", "item_url": "", "author": [ "" ], "publisher": "", "publication_date": "", "snippets": [ "", "" ] } ] ``` ```json theme={null} data: {"citations": [{"item_title": "", "item_url": "", "author": [""], "publisher": "", "publication_date": "", "snippets": ["", ""]}]} ``` ## Tradition-Based Personalization Customize responses to align with specific theological perspectives using the `tradition` parameter: ```json theme={null} { "messages": [ { "role": "user", "content": "Explain the concept of salvation" } ], "auto_routing": true, "tradition": "evangelical" } ``` **Supported traditions:** * `"evangelical"` - Evangelical Protestant perspective * `"catholic"` - Roman Catholic perspective * `"mainline"` - Mainline Protestant perspective * `"not_faith_specific"` - Neutral, non-faith-specific perspective (uses Gloo's non-faith-specific prompt rather than a Christian tradition). **Requires an explicit `model`** and cannot be combined with `auto_routing` or `model_family`. When specified, both the RAG retrieval and response generation adapt to the theological tradition, ensuring appropriate language, concepts, and emphases. ## Code Examples The grounded endpoint has its own base URL, `https://platform.ai.gloo.com/ai/v2/grounded`. Because OpenAI-compatible SDKs append `/chat/completions`, you can point an SDK client at that base URL when specifying a model. For requests that use `auto_routing` or `model_family` without a `model`, use a direct HTTP request instead because the SDK requires `model` client-side. ```bash cURL theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/grounded/chat/completions' \ -H 'accept: application/json' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "messages": [ { "role": "user", "content": "What are practical ways to build stronger community in a local church?" } ], "auto_routing": true, "rag_publisher": "YourPublisherName", "sources_limit": 3, "tradition": "evangelical" }' ``` ```python Python theme={null} import os import requests response = requests.post( "https://platform.ai.gloo.com/ai/v2/grounded/chat/completions", headers={ "Authorization": f"Bearer {os.environ['GLOO_API_KEY']}", "Content-Type": "application/json", }, json={ "messages": [ { "role": "user", "content": "What are practical ways to build stronger community in a local church?", } ], "auto_routing": True, "rag_publisher": "YourPublisherName", "sources_limit": 3, "tradition": "evangelical", }, ) data = response.json() print(data["choices"][0]["message"]["content"]) ``` ```typescript TypeScript theme={null} const response = await fetch( 'https://platform.ai.gloo.com/ai/v2/grounded/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.GLOO_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ messages: [ { role: 'user', content: 'What are practical ways to build stronger community in a local church?', }, ], auto_routing: true, rag_publisher: 'YourPublisherName', sources_limit: 3, tradition: 'evangelical', }), }, ); const data = await response.json(); console.log(data.choices[0].message.content); ``` ## 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 * For the Python example: the `requests` library (`pip install requests`). The TypeScript example uses the built-in `fetch` (Node 18+). ## Endpoint Details **URL:** `https://platform.ai.gloo.com/ai/v2/grounded/chat/completions` **Operation:** `POST` ### Example cURL Request ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/grounded/chat/completions' \ -H 'accept: application/json' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "messages": [ { "role": "user", "content": "How can I incorporate contemplative prayer into daily life?" } ], "auto_routing": true, "rag_publisher": "YourPublisherName", "sources_limit": 3, "tradition": "catholic", "stream": false, "include_citations": true }' ``` ## Request Parameters | Parameter | Type | Required? | Description | | :- | :- | :- | :- | | `messages` | array | Yes | Chat message history in standard format | | **Routing (Choose exactly ONE):** | | | | | `auto_routing` | boolean | Conditional | Enable smart routing (recommended) | | `model` | string | Conditional | Specific Gloo model ID (e.g., `gloo-anthropic-claude-sonnet-4.5`) | | `model_family` | string | Conditional | Provider family: `openai`, `anthropic`, `google`, `open source` | | **RAG Parameters:** | | | | | `rag_publisher` | string | No | Your publisher name. Set this to ground responses in your own content | | `sources_limit` | integer | No | Number of sources to retrieve (1-10, default: 3) | | **Optional:** | | | | | `tradition` | string | No | Theological perspective: `evangelical`, `catholic`, `mainline`, `not_faith_specific`. `not_faith_specific` requires an explicit `model` (not `auto_routing`/`model_family`). | | `max_tokens` | integer | No | Maximum response length | | `temperature` | float | No | Sampling temperature (0.0-2.0) | | `stream` | boolean | No | Enable streaming responses (default: `false`) | | `tools` | array | No | Function calling definitions (see [Tool Use Guide](/api-guides/tool-use)) | | `tool_choice` | string | No | Tool invocation strategy: `auto`, `none`, or specific tool | | `parallel_tool_calls` | boolean | No | Allow parallel tool execution (default: `true`) | | `prompt_cache_key` | string | No | Optional cache key for OpenAI explicit caching to improve hit rates (see [Prompt Caching](/api-guides/prompt-caching)) | | `include_citations` | boolean | No | Include citation metadata in the response (default: `false`) | Exactly one routing mechanism must be specified: `auto_routing: true`, `model`, or `model_family`. For Anthropic models, add the `X-Cache-TTL` header to enable explicit caching. OpenAI and DeepSeek models use implicit caching automatically. For OpenAI, you can optionally add `prompt_cache_key` to improve hit rates. See the [Prompt Caching Guide](/api-guides/prompt-caching) for details. ## Response Format ### Non-Streaming Response ```json theme={null} { "id": "gen-1769792268-xTKWyq7A9HmEhxzDsAJj", "choices": [ { "finish_reason": "stop", "index": 0, "logprobs": null, "message": { "content": "Based on the retrieved sources, here are practical ways to build stronger community...", "refusal": null, "role": "assistant", "annotations": null, "audio": null, "function_call": null, "tool_calls": null, "reasoning": null }, "native_finish_reason": "stop" } ], "created": 1769792268, "model": "gloo-anthropic-claude-sonnet-4.5", "object": "chat.completion", "service_tier": null, "system_fingerprint": null, "usage": { "completion_tokens": 592, "prompt_tokens": 2137, "total_tokens": 2729, "completion_tokens_details": { "accepted_prediction_tokens": null, "audio_tokens": 0, "reasoning_tokens": 0, "rejected_prediction_tokens": null } }, "provider": "Gloo AI", "model_family": "Anthropic", "auto_routing": true, "routing_mechanism": "auto_routing", "routing_tier": "tier_3", "routing_confidence": 0.782, "tradition": "evangelical", "sources_returned": true, "citations": [ { "item_title": "Daily Prayer", "item_url": "https://www.studio.ai.gloo.com", "author": ["John Doe"], "publisher": "The Publisher", "publication_date": "Apr 08 2017", "snippets": [ "Daily prayer is an essential part of life as...", "Making time to spend with the Lord is..." ] } ] } ``` ### Streaming Response When `stream: true`, responses are sent as Server-Sent Events. Routing and RAG metadata is provided in HTTP headers: | Header | Description | | :- | :- | | `X-Routing-Mechanism` | The routing mode used: `auto_routing`, `model_family`, `direct_model_selection` | | `X-Selected-Model` | The Gloo model ID that handled the request | | `X-Model-Family` | The provider family (OpenAI, Anthropic, Google, Open Source) | | `X-Tradition` | Theological perspective used (if specified) | | `X-Sources-Returned` | Whether RAG was used to retrieve and ground the response: `true` or `false` | | `X-Routing-Tier` | Model tier selected (auto-routing and model family modes only) | | `X-Routing-Confidence` | Routing confidence score 0-1 (auto-routing and model family modes only) | When `include_citations: true` is set in the request and sources are found, a `citations` event is emitted as the **first SSE chunk** before any LLM content, with the shape: ```json theme={null} data: {"citations": [{"item_title": "", "item_url": "", "author": [""], "publisher": "", "publication_date": "", "snippets": ["", ""]}]} ``` The event stream follows standard Server-Sent Events format. For detailed streaming implementation guidance, see the [Completions V2 streaming documentation](/api-guides/completions-v2). ### Response Metadata Fields | Field | Description | | :- | :- | | `sources_returned` | Boolean indicating whether RAG was used to retrieve content and ground the response | | `routing_mechanism` | How the model was selected: `auto_routing`, `model_family`, or `direct_model_selection` | | `tradition` | Theological perspective applied (only included if specified in request) | | `model_family` | Provider family of the selected model | | `provider` | Always `"Gloo AI"` | | `routing_tier` | Model tier selected (included for auto-routing and model family modes) | | `routing_confidence` | Confidence score for routing decision (included for auto-routing and model family modes) | | `citations` | Array of source citation objects used to ground the response | ## Related Documentation * [Building Trustworthy Grounded Applications](/best-practices/trustworthy-grounded-applications) - Verifying answers against sources, prompt contracts, multi-turn handling, and evaluation * [Grounded Responses](/api-guides/grounded-responses) - The same RAG grounding in the Responses API format * [Completions V2 API](/api-guides/completions-v2) - Core routing mechanisms and streaming details * [Search API](/api-guides/search) - Standalone RAG queries without generation * [Tool Use Guide](/api-guides/tool-use) - Using function calling with grounded completions * [Supported Models](/api-guides/supported-models) - Model capabilities and context windows # Grounded Responses Source: https://docs.gloo.com/api-guides/grounded-responses Ground Responses API answers in your own content — RAG retrieval with publisher scoping, source thresholds, and grounding signals, in the OpenAI-compatible Responses format. The **Grounded Responses API** brings Retrieval-Augmented Generation (RAG) to the [Responses API](/api-guides/responses). Send a standard Responses API request to the grounded endpoint, and the platform retrieves relevant passages from your uploaded content, injects them as context before generation, and tells you whether the answer was grounded — all in the same OpenAI-compatible request and response shape you already use. If your integration is built on the chat-completions format, or you need intelligent auto-routing, `tradition` (values-aligned) responses, or citation metadata, use [Grounded Completions](/api-guides/grounded-completions) on Completions V2. Grounded Responses is the grounding surface for the Responses API shape. ## Endpoint **URL:** `https://platform.ai.gloo.com/ai/v1/grounded/responses` **Operation:** `POST` ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v1/grounded/responses' \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.6", "input": [ { "role": "user", "content": "What resources do you have on family counseling?" } ], "rag_publisher": "YourPublisherName", "sources_limit": 3 }' ``` Authentication is identical to the rest of the platform — send your API key as a Bearer token. See [Generate API Keys](/studio/manage-api-credentials). You must have already [uploaded content](/api-guides/upload-content) before grounding against your own publisher. ## Request format The request body is the [Responses API request](/api-guides/responses#request-format) — `model`, `input`, `instructions`, `stream`, tools, and so on, unchanged — extended with three grounding parameters: | Parameter | Type | Required? | Description | | :- | :- | :- | :- | | `rag_publisher` | string | No | Publisher whose content grounds the response. Defaults to **GlooGrounded**, a shared dataset assembled by Gloo. Set it to your publisher name (see [Manage Publishers](/studio/manage-publishers)) to ground in your own content. An empty value disables retrieval entirely. | | `sources_limit` | integer | No | Number of sources to retrieve and inject (1–10, default: 3). More sources give broader context but increase prompt size and latency. | | `certainty_threshold` | float | No | Minimum similarity (0.0–1.0) a passage must reach to be used as a source. Omit to use the platform default. Higher values keep only closely-matching sources; if nothing clears the bar, the request proceeds ungrounded (see below). | Everything else works exactly as on the base Responses API: direct model selection via `model`, string or typed-array `input`, `instructions`, `max_output_tokens`, `tools`, streaming, and the rest of the [request parameters](/api-guides/responses#request-format). ### When no sources are found Grounding never fails the request. If retrieval returns nothing — the publisher has no matching content, or no passage clears `certainty_threshold` — the model is given an explicit note that no publisher sources were retrieved, generation proceeds, and the response reports `sources_returned: false`. Check that flag (or the streaming header) whenever your product needs to distinguish grounded answers from ungrounded ones. Checking `sources_returned` tells you whether sources reached the model, not whether the answer reflects them. Pair it with a verification step that confirms quotations and titles appear in the retrieved text. See [Building Trustworthy Grounded Applications](/best-practices/trustworthy-grounded-applications). ## Response format The response is standard [Responses API output](/api-guides/responses#response-format) — a typed `output[]` array plus `usage` — with one addition: a top-level `sources_returned` boolean indicating whether retrieved sources grounded the generation. ```json theme={null} { "id": "resp_...", "object": "response", "model": "gloo-anthropic-claude-sonnet-4.6", "output": [ { "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "Based on your uploaded resources, here are three approaches to family counseling..." } ] } ], "usage": { "input_tokens": 1874, "output_tokens": 312, "total_tokens": 2186 }, "sources_returned": true } ``` ### Streaming Set `"stream": true` to receive the same Server-Sent Events as the base Responses API (`response.created`, `response.output_text.delta`, `response.completed`, …) — see [Responses API streaming](/api-guides/responses#streaming) for the event reference. Because the grounding outcome is known before the stream starts, it is delivered as an HTTP response header instead of a body field: | Header | Description | | :- | :- | | `X-Sources-Returned` | Whether retrieved sources grounded the response: `True` or `False` | ## Code examples The official OpenAI SDKs work by pointing the base URL at the grounded prefix — `responses.create` then posts to `/ai/v1/grounded/responses`. The SDKs forward the grounding parameters to the API even though they aren't in the SDK types: in Python pass them via `extra_body`; in TypeScript include them top-level with a cast (the Node SDK has no `extra_body` — it would be sent as a literal field): ```bash cURL theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v1/grounded/responses' \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.6", "instructions": "Answer using the retrieved publisher sources where possible.", "input": [ { "role": "user", "content": "What are practical ways to build stronger community in a local church?" } ], "rag_publisher": "YourPublisherName", "sources_limit": 5, "certainty_threshold": 0.75 }' ``` ```python Python theme={null} from openai import OpenAI client = OpenAI( api_key="your-gloo-api-key", base_url="https://platform.ai.gloo.com/ai/v1/grounded" ) response = client.responses.create( model="gloo-anthropic-claude-sonnet-4.6", input=[ { "role": "user", "content": "What are practical ways to build stronger community in a local church?" } ], extra_body={ "rag_publisher": "YourPublisherName", "sources_limit": 5, "certainty_threshold": 0.75 } ) print(response.output_text) ``` ```typescript TypeScript theme={null} import OpenAI from 'openai'; const client = new OpenAI({ apiKey: 'your-gloo-api-key', baseURL: 'https://platform.ai.gloo.com/ai/v1/grounded', }); const response = await client.responses.create({ model: 'gloo-anthropic-claude-sonnet-4.6', input: [ { role: 'user', content: 'What are practical ways to build stronger community in a local church?', }, ], // Gloo grounding parameters — forwarded as-is; the cast is needed // because they are not in the OpenAI SDK's types rag_publisher: 'YourPublisherName', sources_limit: 5, certainty_threshold: 0.75, } as OpenAI.Responses.ResponseCreateParamsNonStreaming); console.log(response.output_text); ``` ## Grounded Responses vs. Grounded Completions Both endpoints run the same retrieval pipeline against the same publisher content. Choose by the API shape and features you need: | | Grounded Responses (v1) | [Grounded Completions](/api-guides/grounded-completions) (V2) | | :- | :- | :- | | Endpoint | `POST /ai/v1/grounded/responses` | `POST /ai/v2/grounded/chat/completions` | | Request/response shape | Responses API (`input` / typed `output[]`) | Chat Completions (`messages` / `choices[]`) | | Model selection | Direct `model` only | `auto_routing`, `model_family`, or `model` | | `rag_publisher`, `sources_limit` | ✓ | ✓ | | `certainty_threshold` | ✓ | — | | `tradition` personalization | — | ✓ | | Citation metadata (`include_citations`) | — | ✓ | | Grounding signal | `sources_returned` / `X-Sources-Returned` | `sources_returned` / `X-Sources-Returned` | ## Pricing Grounded Responses is billed like the base Responses API — per token at the selected model's standard rates, with no separate charge for retrieval. Note that injected sources count as input tokens, so grounded requests naturally carry larger prompts than their ungrounded equivalents; `sources_limit` is your main lever over that cost. See [Responses API pricing](/api-guides/responses#pricing-and-token-spend). ## Related Documentation * [Building Trustworthy Grounded Applications](/best-practices/trustworthy-grounded-applications) — verifying answers against sources, prompt contracts, multi-turn handling, and evaluation * [Responses API](/api-guides/responses) — base request/response format, streaming events, multimodal * [Grounded Completions](/api-guides/grounded-completions) — grounding on the chat-completions shape, with routing, `tradition`, and citations * [Upload Content](/api-guides/upload-content) — get your publisher content into the platform * [Search API](/api-guides/search) — standalone RAG queries without generation # Item Recommendations Source: https://docs.gloo.com/api-guides/item-recommendations Guide to surfacing relevant content from your organization’s own data # Overview The Item Recommendations endpoints provide similar functionality to the [Affiliate Recommendations API](/api-guides/affiliate-recommendations), but with a key difference: **it only returns items that your organization has access to.** This protects intellectual property while enabling powerful content discovery features for your users. The search and ranking logic, based on snippet count and cumulative certainty, is identical to the Affiliate Recommendations API. Please note that once a publisher allows their content to be used as a reference, it may take up to 24 hours for search queries to include their items in returned data. ### Endpoints There are two distinct endpoints for item recommendations: * **`/recommendations/base`**: Returns item and snippet metadata but keeps the snippet text itself hidden. * **`/recommendations/verbose`**: Returns item and snippet data, **including** the full snippet text. ## How to Use the API ### Parameters | Parameter | Description | Required | | - | - | - | | `query` | The prompt that the user enters into the application. | **Yes** | | `collection` | The collection to search in. | **Yes** | | `tenant` | The tenant to search. Required only for multi-tenant collections (e.g. `GlooProd`). | Conditional | | `publishers` | A list of Publisher Names. Only publishers belonging to your organization are allowed. | No | | `max_snippet_count_overall` | The maximum number of snippets to search through overall. | No | | `certainty_threshold` | The minimum certainty threshold for snippets. | No | | `media_types` | An array of media types to filter by (`audio`, `video`, `book`, `article`). | No | | `min_snippet_count_per_item` | The minimum number of snippets an item must have to be returned. Must be at least 1. | No | | `item_count` | The maximum number of items to return, ranked by cumulative certainty. If the available snippets span fewer items than this count, only those items are returned. | No | | `item_extra_contains` | *(Verbose endpoint only)* A list of case-sensitive substrings matched against each item's raw `item_extra` text. All entries must match for an item to be returned. Filtering is applied server-side, so paging operates on the filtered set. The `*` and `?` wildcard characters are rejected with a 422 response. | No | | `sort_by` | *(Verbose endpoint only)* The ordering applied to returned items: `relevance` (default) ranks by matching snippet count, then cumulative certainty; `publication_date` orders by each item's best-effort parsed publication date, with items whose date is empty or unparseable always sorted last. `publish_date` is accepted as an alias for `publication_date`. | No | | `sort_order` | *(Verbose endpoint only)* The sort direction, applied only when `sort_by` is `publication_date`: `desc` (default) returns newest items first, `asc` returns oldest first. | No | ### Making a Request Before making a request, you must know which publisher's content you would like to query. 1. **Find Your Tenant (Publisher Name):** First, navigate to the [Organizations page](/studio/manage-organizations) in Studio and click "View Publishers". Your **Tenant** is the name of the publisher you're querying. 2. **Identify the Collection:** A collection is where we keep the content for our customers. The only collection you'll need to specify in requests is `GlooProd`. You must specify this in the `collection` field. **NOTE:** You will only have access to your own publisher's tenant through this endpoint. For multi-tenant collections such as `GlooProd`, a tenant must be specified; requests outside your tenant return a 403 "Forbidden" response. An API key is necessary to call these endpoints. You can get one from the **API Keys** page in Gloo AI Studio and send it directly as a Bearer token. *** ### Base Recommendations This endpoint returns snippet metadata without the snippet text. `POST /ai/v1/data/items/recommendations/base` #### Example Request (`/base`) ```bash theme={null} curl -X 'POST' \ 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/base' \ -H 'accept: application/json' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "collection": "GlooProd", "max_snippet_count_overall": 10, "media_types": [], "min_snippet_count_per_item": 1, "publishers": ["Test New Tenant"], "query": "Plans God has for me", "tenant": "TestNewTenant", "certainty_threshold": 0.5, "item_count": 3 }' ``` #### Example Response (`/base`) As shown below, the `uuids` array contains snippet metadata, but not the snippet text itself. ```json theme={null} [ { "item_id": "c42fc709-01dd-4eb1-94ab-01e076e141da", "publisher": "Test New Tenant", "type": "Book", "uuids": [ { "uuid": "eca2616f-823a-5800-b7b2-c0c80ad84ec4", "ai_title": "Understanding God's Plan: It's Simpler Than You Think", "ai_subtitle": "Why Finding God's Will Isn't a Treasure Hunt", "item_summary": "The divine plan manifests through our daily acts of goodness, rather than existing as a predetermined sequence we must discover.", "time_start": "", "time_end": "", "part": 128, "certainty": 0.7546200752258301 } ], "snippet_count": 1, "cumulative_certainty": 0.7546200752258301 } ] ``` *** ### Verbose Recommendations This endpoint returns the full snippet text. `POST /ai/v1/data/items/recommendations/verbose` #### Example Request (`/verbose`) ```bash theme={null} curl -X 'POST' \ 'https://platform.ai.gloo.com/ai/v1/data/items/recommendations/verbose' \ -H 'accept: application/json' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "collection": "GlooProd", "max_snippet_count_overall": 10, "media_types": [], "min_snippet_count_per_item": 1, "publishers": ["Test New Tenant"], "query": "Plans God has for me", "tenant": "TestNewTenant", "certainty_threshold": 0.5, "item_count": 3, "item_extra_contains": ["visibleOnApps:true"], "sort_by": "publication_date", "sort_order": "desc" }' ``` The request above keeps only items whose `item_extra` text contains `visibleOnApps:true` and returns them newest-first by publication date. #### Example Response (`/verbose`) Note the addition of the `snippet` field in the `uuids` array, and the item's raw `item_extra` text (which may be an empty string). ```json theme={null} [ { "item_id": "c42fc709-01dd-4eb1-94ab-01e076e141da", "publisher": "Test New Tenant", "type": "Book", "uuids": [ { "uuid": "eca2616f-823a-5800-b7b2-c0c80ad84ec4", "ai_title": "Understanding God's Plan: It's Simpler Than You Think", "ai_subtitle": "Why Finding God's Will Isn't a Treasure Hunt", "snippet": "This way of thinking about \"God's plan for your life\" is guaranteed to give us a headache—and it's just not how he operates. It's not that God's holding the treasure map and we have to guess the route to the X that marks the spot...", "item_summary": "The divine plan manifests through our daily acts of goodness, rather than existing as a predetermined sequence we must discover.", "time_start": "", "time_end": "", "part": 128, "certainty": 0.7546232342720032 } ], "snippet_count": 1, "cumulative_certainty": 0.7546232342720032, "item_extra": "visibleOnApps:true" } ] ``` ## Use Cases The Item Recommendations API is ideal for a publisher looking to increase exposure and engagement with their own content. For example, a user could enter a query on a publisher's website. The publisher could then use the `/recommendations/base` endpoint to recommend their own relevant books, articles, or videos. This enhances content discovery and keeps users engaged on the platform. If the publisher wanted to provide a preview of the content, they could use the `/recommendations/verbose` endpoint to display a relevant snippet or a sample of one, encouraging the user to explore the full item. # Manage Content Source: https://docs.gloo.com/api-guides/manage-content List, inspect, look up, and delete your content in Gloo AI. The Content Management APIs help you understand and control your content catalog—list all items for a [publisher](/getting-started/glossary#publisher), inspect detailed metadata, look up items by your internal IDs, and permanently remove content when needed. ## Prerequisites Before starting, ensure you have: * A **Gloo AI Studio** account * Your **API key** from the [API Keys page](/studio/manage-api-credentials) * **Authentication setup** - Complete the [Authentication Tutorial](/tutorials/authentication) first The caller must belong to the organization that owns the specified publisher. *** ## List All Items Retrieve all items for a publisher with basic metadata including status, title, producer id, and filename. Use this to audit your catalog or find specific [`item_id`](/getting-started/glossary#item_id) values. **When to use:** * Audit your content catalog * Find item IDs for subsequent operations * Check ingestion status across all items ```bash Shell theme={null} curl -X GET "https://platform.ai.gloo.com/engine/v2/publisher/{publisher-id}/items" \ -H "Authorization: Bearer ${GLOO_API_KEY}" ``` **Example Response:** ```json theme={null} { "items": [ { "item_id": "070839ad-9ec7-4d3b-b8ee-c61495842e96", "producer_id": "EP-1", "item_title": "Introduction to Faith", "filename": "intro-faith.pdf", "status": "COMPLETED" }, { "item_id": "2ae016a2-b2e9-4f64-8a4d-9cb9fa9d87b7", "producer_id": "SERMON-63", "item_title": "Weekly Sermon", "filename": "sermon-2024-01.mp3", "status": "COMPLETED" } ], "pagination": { "page": 1, "page_size": 100, "total": 2, "total_pages": 1 } } ``` This endpoint is **paginated** — the response wraps `items` alongside a `pagination` object. The default page size is `100` and the maximum is `1000`; page through with `page`, or for publishers with large catalogs prefer [List Producer IDs](#list-producer-ids-paginated). **Query Parameters:** | Parameter | Type | Default | Description | | - | - | - | - | | `page` | integer | `1` | Page number (1-indexed). | | `page_size` | integer | `100` | Number of items per page. Maximum `1000`. | | `source_type` | string | — | Filter by content type (e.g. `audio`, `video`, `file`, `youtube`, `webpage`). | | `ingestion_source` | string | — | Filter by ingestion origin (e.g. `upload`, `feed`, `scrape`, `youtube`). | **Status values:** `STARTED`, `QUEUED`, `FETCHING`, `UPLOADING`, `TRANSCRIBING`, `CHUNKING`, `EMBEDDING`, `COMPLETED`, `FAILED`, `TRANSCRIPT_UNAVAILABLE` **Full API details:** [Get Publisher Items API Reference](/api-reference/content-controls/get-publisher-items) *** ## List Producer IDs (Paginated) Retrieve a paginated list of distinct `producer_id` values for a publisher. This is the recommended way to enumerate producer IDs for publishers with large catalogs, as it avoids the response-size limits of the [List All Items](#list-all-items) endpoint. **When to use:** * Enumerate producer IDs for publishers with large catalogs (thousands of items) * Page through all producer IDs without hitting response-size limits * Build sync pipelines that reconcile your internal IDs with Gloo Results are sorted alphabetically by `producer_id`. The default page size is **1,000** and the maximum is **5,000**. ```bash Shell theme={null} curl -X GET "https://platform.ai.gloo.com/engine/v2/publisher/{publisher-id}/producer-ids?page=1&page_size=1000" \ -H "Authorization: Bearer ${GLOO_API_KEY}" ``` **Example Response:** ```json theme={null} { "producer_ids": ["EP-1", "EP-2", "EP-3", "SERMON-63", "SERMON-64"], "pagination": { "page": 1, "page_size": 1000, "total": 4520, "total_pages": 5 } } ``` **Query Parameters:** | Parameter | Type | Default | Description | | - | - | - | - | | `page` | integer | `1` | Page number (1-indexed). | | `page_size` | integer | `1000` | Number of producer IDs per page. Maximum `5000`. | **Full API details:** [Get Publisher Producer IDs API Reference](/api-reference/content-controls/get-publisher-producer-ids) *** ## Get Item Details Retrieve comprehensive metadata for a specific item including status, collection memberships, and all editable fields. **When to use:** * Inspect item state before updates or deletion * Verify metadata completeness * Check collection memberships and visibility settings ```bash Shell theme={null} curl -X GET "https://platform.ai.gloo.com/engine/v2/items/{item-id}" \ -H "Authorization: Bearer ${GLOO_API_KEY}" ``` **Example Response:** ```json theme={null} { "item_id": "2ae016a2-b2e9-4f64-8a4d-9cb9fa9d87b7", "status": "COMPLETED", "item_title": "Weekly Sermon", "item_subtitle": "Faith in Action Series", "filename": "sermon-2024-01.mp3", "author": ["Pastor Smith"], "item_tags": ["sermons", "faith"], "visible_in_search": true, "visible_in_chat": true, "updated_at": "2025-01-15T10:30:00.000Z" } ``` **Full API details:** [Get Item Metadata API Reference](/api-reference/content-controls/get-item-metadata) *** ## Look Up by Producer ID If you assigned your own internal ID ([`producer_id`](/getting-started/glossary#producer_id)) when uploading content, use this endpoint to find the corresponding Gloo [`item_id`](/getting-started/glossary#item_id). **When to use:** * Map your internal IDs to Gloo item IDs * Integrate with your existing content management systems * Look up items without storing Gloo IDs ```bash Shell theme={null} curl -X POST "https://platform.ai.gloo.com/engine/v2/publisher/{publisher-id}/items/by-producer" \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "producer_ids": ["your-internal-id-123"] }' ``` **Example Response:** ```json theme={null} { "producer_id": "your-internal-id-123", "item_id": "2ae016a2-b2e9-4f64-8a4d-9cb9fa9d87b7" } ``` **Full API details:** [Get Item by Producer ID API Reference](/api-reference/content-controls/get-item-by-producer-id) *** ## Delete Items Permanently remove items from all storage systems. This is irreversible and purges data from vector databases, file storage, and all related records. Items can be deleted in batches of up to 1000 at a time. Deletion is permanent and cannot be undone. Verify item IDs carefully before deleting. **When to use:** * Remove obsolete or test content * Comply with data removal requests * Clean up failed ingestions ```bash Shell theme={null} curl -X DELETE "https://platform.ai.gloo.com/engine/v2/items" \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "item_ids": ["070839ad-9ec7-4d3b-b8ee-c61495842e96"] }' ``` **Example Response:** ```json theme={null} { "success": true, "mode": "hard", "total_requested": 1, "total_deleted": 1, "total_failed": 0, "message": "Successfully deleted 1 of 1 items" } ``` **Full API details:** [Delete Item API Reference](/api-reference/content-controls/delete-item) *** ## Common Workflows ### Audit Your Catalog 1. **List all items** to get an overview of your content 2. **Inspect individual items** to check metadata completeness 3. **Update metadata** using the [Edit Content](/api-guides/edit-content) APIs ### Find and Fix Items 1. **List items** to find content needing updates 2. **Get item details** to understand current state 3. **Update single item** or **bulk update** to fix issues ### Bulk Cleanup 1. **List items** to identify content for removal 2. **Verify items** before deletion 3. **Delete items** in batches to clean up your catalog The **Content Library** in Studio gives you a visual view of all ingested content for your publisher. 1. Click **Data Engine** in the Studio sidebar, then click **Content Library**. 2. You'll see a table of all your content with columns for: * **Item Title** — The title of your content * **File Name** — The original filename * **Type** — Content type (e.g., article) * **Status** — Processing status (Successful or Pending) * **Upload Date** — When the content was added * **Actions** — Edit metadata or remove items 3. Use the search bar to find specific items by title or filename. 4. Click the **edit icon** in the Actions column to update metadata for any item. To delete items in bulk, use the [API delete endpoint](#delete-items). Bulk deletion is not currently available in the Studio UI. # MCP Integration Source: https://docs.gloo.com/api-guides/mcp-integration Connect Gloo AI documentation to your AI tools with the Model Context Protocol. The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard that lets AI tools search external data sources directly. Gloo AI hosts an MCP server for our documentation, so tools like Claude, Cursor, VS Code, and ChatGPT can look up our APIs, models, and platform docs while generating responses — no copy-pasting or web searching required. **MCP Server URL:** ``` https://docs.gloo.com/mcp ``` ## Quick Setup The fastest way to connect is from the contextual menu at the top-right of any docs page: * **Connect to Cursor** — one-click MCP install * **Connect to VS Code** — one-click MCP install * **Copy MCP server URL** — for pasting into any MCP-compatible tool * **Copy MCP install command** — copies `npx add-mcp` for terminal-based install Or follow the step-by-step instructions below for your preferred tool. ## Claude Code ```bash theme={null} claude mcp add --transport http gloo-ai-docs https://docs.gloo.com/mcp ``` Verify the connection: ```bash theme={null} claude mcp list ``` Claude Code will now search Gloo AI documentation when answering questions about the platform, models, APIs, and integration patterns. ## Cursor Use Command + Shift + P (Ctrl + Shift + P on Windows) to open the command palette, then search for **Open MCP settings**. In `mcp.json`, add: ```json theme={null} { "mcpServers": { "gloo-ai-docs": { "url": "https://docs.gloo.com/mcp" } } } ``` In Cursor's chat, ask "What models does Gloo AI support?" — Cursor should search the docs via MCP and return an accurate answer. ## VS Code Create a `.vscode/mcp.json` file in your project: ```json theme={null} { "servers": { "gloo-ai-docs": { "type": "http", "url": "https://docs.gloo.com/mcp" } } } ``` You can also use the **Connect to VS Code** option in the contextual menu at the top of any docs page for one-click setup. ## Claude Navigate to the [Connectors](https://claude.ai/settings/connectors) page in Claude settings. Select **Add custom connector** and configure: * **Name**: `Gloo AI Docs` * **URL**: `https://docs.gloo.com/mcp` When chatting with Claude, select the attachments button (plus icon) and choose the Gloo AI Docs connector. Claude will search our documentation to answer your questions. ## llms.txt Gloo AI documentation is also available as standardized text files for direct AI consumption: | File | URL | Description | | :- | :- | :- | | **llms.txt** | `https://docs.gloo.com/llms.txt` | Structured index of all documentation pages with descriptions | | **llms-full.txt** | `https://docs.gloo.com/llms-full.txt` | Complete documentation content as a single file | These files follow the [llms.txt standard](https://llmstxt.org/) and work with any AI tool that accepts URL-based context. ## Choosing the Right Approach | Scenario | Recommended | Why | | :- | :- | :- | | Building with Gloo AI APIs in your editor | **MCP** | Your AI assistant can search docs on demand as you code | | Asking an AI assistant about Gloo AI | **MCP** | Searches current docs instead of relying on training data | | Feeding full docs into a single prompt | **llms-full.txt** | All documentation in one file for maximum context | | Finding a specific docs page to read | **llms.txt** | Browse the page index and fetch what you need | # Migrating to Completions v2 Source: https://docs.gloo.com/api-guides/migrating-from-chat-to-completions-v2 Transition from stateful Chat API to the flexible, values-aligned Completions v2 API. **Deprecation Notice**: The Chat and Message API endpoints are being deprecated. Please migrate as soon as possible. **Choosing your migration target:** If you don't need routing, `model_family`, or grounded completions, migrate directly to the [Responses API](/api-guides/responses) — Gloo's recommended, OpenAI-compatible surface, with guardrails and `tradition` included. If you do need those features, this guide's path to Completions V2 is the right choice. The request/response mapping below adapts to either; see the [Completions → Responses field mapping](/api-guides/responses#moving-from-completions-to-responses). ## Overview The Chat and Message API provided a convenient stateful approach to building conversational AI, with automatic session management and history tracking. As we evolve the Gloo platform, we're transitioning to the **Completions v2 API** powered by a layered, production-ready AI architecture. This is a more flexible architecture that gives you complete control while delivering values-aligned AI with intelligent model routing. This guide will help you migrate your existing Chat API integrations to Completions v2, maintaining all the conversational capabilities you need while gaining access to powerful new features. ### Why Migrate to Completions v2? Access AI infrastructure built for human flourishing with values alignment and safety at every layer Auto-routing, model family selection, or direct model choice—optimize for quality, cost, and intent Manage conversation state, history, and context exactly how your application needs it Built-in support for tradition-aware responses (evangelical, catholic, mainline) ## Quick Start: Minimal Migration Path If you want to get started quickly, here's the fastest path to migrate: Change from `/ai/v1/message` to `/ai/v2/guarded/chat/completions` Create a simple database table for storing chat history ```sql theme={null} CREATE TABLE messages ( id UUID PRIMARY KEY, chat_id UUID NOT NULL, role VARCHAR(50) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); ``` Fetch messages from your database and format them for Completions v2 ```javascript theme={null} const messages = await db.query( 'SELECT role, content FROM messages WHERE chat_id = $1 ORDER BY created_at', [chatId] ); ``` Let Gloo automatically select the best model ```javascript theme={null} const response = await fetch( 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: messages, auto_routing: true, tradition: 'evangelical' // Optional: maintain theological alignment }) } ); ``` ## Understanding the Changes ### Architecture Comparison ```mermaid theme={null} sequenceDiagram participant Client participant Gloo API participant Gloo Storage Client->>Gloo API: POST /message (query only) Gloo API->>Gloo Storage: Store message Gloo API->>Gloo Storage: Retrieve history Gloo API->>Gloo API: Generate completion Gloo API->>Gloo Storage: Store response Gloo API-->>Client: Response + chat_id ``` **Gloo managed:** * Chat session creation * Message storage * History retrieval * Context management ```mermaid theme={null} sequenceDiagram participant Client participant Your Backend participant Your DB participant Gloo API Client->>Your Backend: Send message Your Backend->>Your DB: Store user message Your Backend->>Your DB: Fetch conversation history Your Backend->>Gloo API: POST /completions/v2 (full context) Gloo API->>Gloo API: Intelligent routing Gloo API-->>Your Backend: Completion response Your Backend->>Your DB: Store assistant response Your Backend-->>Client: Response ``` **You manage:** * Chat session creation * Message storage * History retrieval * Context management **Gloo provides:** * Intelligent routing * Values-aligned responses * Tool calling support * Streaming capabilities ### Key Differences | Feature | Chat API | Completions v2 | | - | - | - | | **State Management** | Server-managed by Gloo | Client-managed (your database) | | **Message History** | Automatic tracking | You build and send full context | | **Chat Sessions** | Auto-generated `chat_id` | You generate session IDs | | **API Calls** | Incremental (one message at a time) | Full conversation context each time | | **Model Selection** | Fixed model | Auto-routing, family selection, or direct choice | | **Tradition Support** | Not available | Built-in (`evangelical`, `catholic`, `mainline`) | | **Tool Use** | Limited | Full support | | **Streaming** | Available | Enhanced streaming support | ## Step-by-Step Migration Guide ### Step 1: Design Your Conversation Storage The first step is creating your own storage for conversations. Here's a recommended schema that mirrors the Chat API's capabilities: ```sql PostgreSQL theme={null} -- Chat sessions table CREATE TABLE chats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(255) NOT NULL, title VARCHAR(500), tradition VARCHAR(50), -- 'evangelical', 'catholic', 'mainline' metadata JSONB, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), INDEX idx_user_id (user_id), INDEX idx_updated_at (updated_at) ); -- Messages table CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chat_id UUID NOT NULL REFERENCES chats(id) ON DELETE CASCADE, role VARCHAR(50) NOT NULL, -- 'user', 'assistant', 'system' content TEXT NOT NULL, model VARCHAR(100), -- Store which model generated this response tokens_used INTEGER, -- Track token usage per message metadata JSONB, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_chat_id_created (chat_id, created_at) ); -- Optional: Store sources/citations CREATE TABLE message_sources ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, source_title VARCHAR(500), source_url TEXT, publisher VARCHAR(255), relevance_score DECIMAL(3,2), created_at TIMESTAMP DEFAULT NOW() ); ``` ```javascript MongoDB theme={null} // Chats collection { _id: ObjectId, user_id: String, title: String, tradition: String, // 'evangelical', 'catholic', 'mainline' metadata: { tags: [String], publishers: [String], // Custom fields as needed }, created_at: Date, updated_at: Date } // Messages collection { _id: ObjectId, chat_id: ObjectId, role: String, // 'user', 'assistant', 'system' content: String, model: String, // Model that generated this response tokens_used: Number, sources: [ { title: String, url: String, publisher: String, relevance_score: Number } ], metadata: Object, created_at: Date } // Recommended indexes db.messages.createIndex({ chat_id: 1, created_at: 1 }); db.chats.createIndex({ user_id: 1, updated_at: -1 }); ``` **Pro Tip**: Include fields for `tradition`, `model`, and `tokens_used` to track theological alignment, model usage patterns, and costs over time. ### Step 2: Build Conversation Management Create functions to manage chat sessions and messages: ```typescript TypeScript theme={null} import { v4 as uuidv4 } from 'uuid'; interface Message { id: string; chat_id: string; role: 'user' | 'assistant' | 'system'; content: string; model?: string; tokens_used?: number; created_at: Date; } interface Chat { id: string; user_id: string; title?: string; tradition?: 'evangelical' | 'catholic' | 'mainline'; created_at: Date; updated_at: Date; } class ConversationManager { private db: any; // Your database client constructor(db: any) { this.db = db; } /** * Create a new chat session */ async createChat( userId: string, tradition?: Chat['tradition'], title?: string ): Promise { const chat: Chat = { id: uuidv4(), user_id: userId, title: title || 'New Conversation', tradition, created_at: new Date(), updated_at: new Date() }; await this.db.chats.insert(chat); return chat; } /** * Add a message to the conversation */ async addMessage( chatId: string, role: Message['role'], content: string, metadata?: { model?: string; tokens_used?: number; } ): Promise { const message: Message = { id: uuidv4(), chat_id: chatId, role, content, model: metadata?.model, tokens_used: metadata?.tokens_used, created_at: new Date() }; await this.db.messages.insert(message); // Update chat's updated_at timestamp await this.db.chats.update( { id: chatId }, { updated_at: new Date() } ); return message; } /** * Get conversation history formatted for Completions v2 */ async getConversationHistory( chatId: string, options?: { maxMessages?: number; maxTokens?: number; } ): Promise> { const messages = await this.db.messages .find({ chat_id: chatId }) .sort({ created_at: 'asc' }) .toArray(); // Apply any filtering/truncation based on options let filtered = messages; if (options?.maxMessages) { filtered = messages.slice(-options.maxMessages); } if (options?.maxTokens) { filtered = this.truncateByTokens(filtered, options.maxTokens); } // Format for Completions v2 return filtered.map(msg => ({ role: msg.role, content: msg.content })); } /** * Truncate messages to fit within token limit */ private truncateByTokens( messages: Message[], maxTokens: number ): Message[] { let totalTokens = 0; const result: Message[] = []; // Keep system message if present if (messages[0]?.role === 'system') { result.push(messages[0]); totalTokens += this.estimateTokens(messages[0].content); messages = messages.slice(1); } // Add messages from most recent, working backwards for (let i = messages.length - 1; i >= 0; i--) { const tokens = this.estimateTokens(messages[i].content); if (totalTokens + tokens > maxTokens) break; result.unshift(messages[i]); totalTokens += tokens; } return result; } /** * Rough token estimation (4 chars ≈ 1 token) */ private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } /** * Get all chats for a user */ async getUserChats(userId: string): Promise { return await this.db.chats .find({ user_id: userId }) .sort({ updated_at: 'desc' }) .toArray(); } /** * Delete a conversation */ async deleteChat(chatId: string): Promise { await this.db.messages.deleteMany({ chat_id: chatId }); await this.db.chats.delete({ id: chatId }); } } ``` ```python Python theme={null} import uuid from datetime import datetime from typing import Optional, List, Dict, Any, Literal class ConversationManager: def __init__(self, db): self.db = db def create_chat( self, user_id: str, tradition: Optional[Literal['evangelical', 'catholic', 'mainline']] = None, title: Optional[str] = None ) -> Dict[str, Any]: """Create a new chat session.""" chat = { 'id': str(uuid.uuid4()), 'user_id': user_id, 'title': title or 'New Conversation', 'tradition': tradition, 'created_at': datetime.now(), 'updated_at': datetime.now() } self.db.chats.insert_one(chat) return chat def add_message( self, chat_id: str, role: Literal['user', 'assistant', 'system'], content: str, model: Optional[str] = None, tokens_used: Optional[int] = None ) -> Dict[str, Any]: """Add a message to the conversation.""" message = { 'id': str(uuid.uuid4()), 'chat_id': chat_id, 'role': role, 'content': content, 'model': model, 'tokens_used': tokens_used, 'created_at': datetime.now() } self.db.messages.insert_one(message) # Update chat's updated_at timestamp self.db.chats.update_one( {'id': chat_id}, {'$set': {'updated_at': datetime.now()}} ) return message def get_conversation_history( self, chat_id: str, max_messages: Optional[int] = None, max_tokens: Optional[int] = None ) -> List[Dict[str, str]]: """Get conversation history formatted for Completions v2.""" messages = list( self.db.messages .find({'chat_id': chat_id}) .sort('created_at', 1) ) # Apply filtering if max_messages: messages = messages[-max_messages:] if max_tokens: messages = self._truncate_by_tokens(messages, max_tokens) # Format for Completions v2 return [ {'role': msg['role'], 'content': msg['content']} for msg in messages ] def _truncate_by_tokens( self, messages: List[Dict], max_tokens: int ) -> List[Dict]: """Truncate messages to fit within token limit.""" total_tokens = 0 result = [] # Keep system message if present if messages and messages[0].get('role') == 'system': result.append(messages[0]) total_tokens += self._estimate_tokens(messages[0]['content']) messages = messages[1:] # Add messages from most recent for msg in reversed(messages): tokens = self._estimate_tokens(msg['content']) if total_tokens + tokens > max_tokens: break result.insert(0 if not result else 1, msg) total_tokens += tokens return result @staticmethod def _estimate_tokens(text: str) -> int: """Rough token estimation (4 chars ≈ 1 token).""" return len(text) // 4 + 1 def get_user_chats(self, user_id: str) -> List[Dict[str, Any]]: """Get all chats for a user.""" return list( self.db.chats .find({'user_id': user_id}) .sort('updated_at', -1) ) def delete_chat(self, chat_id: str) -> None: """Delete a conversation.""" self.db.messages.delete_many({'chat_id': chat_id}) self.db.chats.delete_one({'id': chat_id}) ``` ### Step 3: Integrate with Completions v2 Now integrate your conversation manager with the Completions v2 API: ```typescript TypeScript theme={null} interface CompletionsV2Config { apiKey: string; baseUrl?: string; } interface CompletionOptions { tradition?: 'evangelical' | 'catholic' | 'mainline'; autoRouting?: boolean; model?: string; modelFamily?: 'openai' | 'anthropic' | 'google' | 'open source'; stream?: boolean; temperature?: number; maxTokens?: number; } class GlooCompletionsV2Client { private apiKey: string; private baseUrl: string; constructor(config: CompletionsV2Config) { this.apiKey = config.apiKey; this.baseUrl = config.baseUrl || 'https://platform.ai.gloo.com'; } async createCompletion( messages: Array<{ role: string; content: string }>, options: CompletionOptions = {} ) { const { tradition, autoRouting = true, model, modelFamily, stream = false, temperature, maxTokens } = options; // Build request body based on routing strategy const body: any = { messages, stream }; // Routing configuration if (autoRouting) { body.auto_routing = true; } else if (model) { body.model = model; body.auto_routing = false; } else if (modelFamily) { body.model_family = modelFamily; body.auto_routing = false; } // Optional parameters if (tradition) body.tradition = tradition; if (temperature !== undefined) body.temperature = temperature; if (maxTokens) body.max_tokens = maxTokens; const response = await fetch( `${this.baseUrl}/ai/v2/guarded/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) } ); if (!response.ok) { const error = await response.json(); throw new Error(`Completions API error: ${error.message || response.statusText}`); } const data = await response.json(); return data; } } // Complete chat service combining conversation management and Completions v2 class ChatService { private conversationManager: ConversationManager; private completionsClient: GlooCompletionsV2Client; constructor( conversationManager: ConversationManager, completionsClient: GlooCompletionsV2Client ) { this.conversationManager = conversationManager; this.completionsClient = completionsClient; } /** * Create a new conversation */ async createConversation( userId: string, tradition?: 'evangelical' | 'catholic' | 'mainline', title?: string ) { return await this.conversationManager.createChat(userId, tradition, title); } /** * Send a message and get AI response */ async sendMessage( chatId: string, userMessage: string, options?: CompletionOptions ): Promise<{ response: string; model: string; tokensUsed: number; }> { // 1. Store user message await this.conversationManager.addMessage(chatId, 'user', userMessage); // 2. Get conversation history const history = await this.conversationManager.getConversationHistory( chatId, { maxMessages: 50, // Keep last 50 messages maxTokens: 8000 // Adjust based on your model's context window } ); // 3. Get chat details for tradition const chat = await this.conversationManager.db.chats.findOne({ id: chatId }); const completionOptions = { ...options, tradition: options?.tradition || chat?.tradition }; // 4. Call Completions v2 const completion = await this.completionsClient.createCompletion( history, completionOptions ); const assistantMessage = completion.choices[0].message.content; const tokensUsed = completion.usage.total_tokens; const model = completion.model; // 5. Store assistant response await this.conversationManager.addMessage( chatId, 'assistant', assistantMessage, { model, tokens_used: tokensUsed } ); return { response: assistantMessage, model, tokensUsed }; } /** * Get conversation history */ async getHistory(chatId: string) { return await this.conversationManager.getConversationHistory(chatId); } /** * List user's conversations */ async listConversations(userId: string) { return await this.conversationManager.getUserChats(userId); } /** * Delete a conversation */ async deleteConversation(chatId: string) { await this.conversationManager.deleteChat(chatId); } } // Usage Example const conversationManager = new ConversationManager(db); const completionsClient = new GlooCompletionsV2Client({ apiKey: process.env.GLOO_API_KEY! }); const chatService = new ChatService(conversationManager, completionsClient); // Create a new conversation const chat = await chatService.createConversation( 'user-123', 'evangelical', 'Finding Purpose' ); // Send messages const response1 = await chatService.sendMessage( chat.id, 'How can I find meaning and purpose when facing life\'s greatest challenges?', { autoRouting: true } ); console.log('AI Response:', response1.response); console.log('Model Used:', response1.model); console.log('Tokens:', response1.tokensUsed); // Continue the conversation const response2 = await chatService.sendMessage( chat.id, 'Can you give me practical steps I can take today?' ); console.log('Follow-up Response:', response2.response); ``` ```python Python theme={null} import httpx from typing import Optional, Dict, List, Any, Literal class GlooCompletionsV2Client: def __init__(self, api_key: str, base_url: str = 'https://platform.ai.gloo.com'): self.api_key = api_key self.base_url = base_url async def create_completion( self, messages: List[Dict[str, str]], tradition: Optional[Literal['evangelical', 'catholic', 'mainline']] = None, auto_routing: bool = True, model: Optional[str] = None, model_family: Optional[str] = None, stream: bool = False, temperature: Optional[float] = None, max_tokens: Optional[int] = None ) -> Dict[str, Any]: """Call the Completions v2 API.""" # Build request body body = { 'messages': messages, 'stream': stream } # Routing configuration if auto_routing: body['auto_routing'] = True elif model: body['model'] = model body['auto_routing'] = False elif model_family: body['model_family'] = model_family body['auto_routing'] = False # Optional parameters if tradition: body['tradition'] = tradition if temperature is not None: body['temperature'] = temperature if max_tokens: body['max_tokens'] = max_tokens async with httpx.AsyncClient() as client: response = await client.post( f'{self.base_url}/ai/v2/guarded/chat/completions', headers={ 'Authorization': f'Bearer {self.api_key}', 'Content-Type': 'application/json' }, json=body, timeout=60.0 ) response.raise_for_status() return response.json() class ChatService: def __init__( self, conversation_manager: ConversationManager, completions_client: GlooCompletionsV2Client ): self.conversation_manager = conversation_manager self.completions_client = completions_client async def create_conversation( self, user_id: str, tradition: Optional[str] = None, title: Optional[str] = None ) -> Dict[str, Any]: """Create a new conversation.""" return self.conversation_manager.create_chat(user_id, tradition, title) async def send_message( self, chat_id: str, user_message: str, **options ) -> Dict[str, Any]: """Send a message and get AI response.""" # 1. Store user message self.conversation_manager.add_message(chat_id, 'user', user_message) # 2. Get conversation history history = self.conversation_manager.get_conversation_history( chat_id, max_messages=50, max_tokens=8000 ) # 3. Get chat details for tradition chat = self.conversation_manager.db.chats.find_one({'id': chat_id}) tradition = options.get('tradition') or chat.get('tradition') # 4. Call Completions v2 completion = await self.completions_client.create_completion( messages=history, tradition=tradition, **options ) assistant_message = completion['choices'][0]['message']['content'] tokens_used = completion['usage']['total_tokens'] model = completion['model'] # 5. Store assistant response self.conversation_manager.add_message( chat_id, 'assistant', assistant_message, model=model, tokens_used=tokens_used ) return { 'response': assistant_message, 'model': model, 'tokens_used': tokens_used } def get_history(self, chat_id: str) -> List[Dict[str, str]]: """Get conversation history.""" return self.conversation_manager.get_conversation_history(chat_id) def list_conversations(self, user_id: str) -> List[Dict[str, Any]]: """List user's conversations.""" return self.conversation_manager.get_user_chats(user_id) def delete_conversation(self, chat_id: str) -> None: """Delete a conversation.""" self.conversation_manager.delete_chat(chat_id) # Usage Example async def main(): conversation_manager = ConversationManager(db) completions_client = GlooCompletionsV2Client( api_key=os.getenv('GLOO_API_KEY') ) chat_service = ChatService(conversation_manager, completions_client) # Create a new conversation chat = await chat_service.create_conversation( 'user-123', tradition='evangelical', title='Finding Purpose' ) # Send messages response1 = await chat_service.send_message( chat['id'], 'How can I find meaning and purpose when facing life\'s greatest challenges?', auto_routing=True ) print(f"AI Response: {response1['response']}") print(f"Model Used: {response1['model']}") print(f"Tokens: {response1['tokens_used']}") # Continue the conversation response2 = await chat_service.send_message( chat['id'], 'Can you give me practical steps I can take today?' ) print(f"Follow-up Response: {response2['response']}") ``` ### Step 4: Migrate Existing Chat Data Before the Chat API is deprecated, export your existing conversations: ```typescript Export Script theme={null} async function migrateExistingChats(userId: string) { console.log(`Starting migration for user: ${userId}`); // 1. Get all chats from old Chat API const chatsResponse = await fetch( `https://platform.ai.gloo.com/ai/v1/chat?user_id=${userId}`, { headers: { 'Authorization': `Bearer ${oldApiToken}` } } ); const oldChats = await chatsResponse.json(); console.log(`Found ${oldChats.length} chats to migrate`); for (const oldChat of oldChats) { try { // 2. Get messages for this chat const messagesResponse = await fetch( `https://platform.ai.gloo.com/ai/v1/chat/${oldChat.id}/messages`, { headers: { 'Authorization': `Bearer ${oldApiToken}` } } ); const oldMessages = await messagesResponse.json(); // 3. Create new chat in your system const newChat = await conversationManager.createChat( oldChat.user_id, undefined, // Set tradition if you have this info oldChat.title || 'Migrated Conversation' ); // 4. Migrate all messages for (const oldMessage of oldMessages.messages) { await conversationManager.addMessage( newChat.id, oldMessage.role, oldMessage.content || oldMessage.message ); } console.log(`✓ Migrated chat ${oldChat.id} → ${newChat.id}`); } catch (error) { console.error(`✗ Failed to migrate chat ${oldChat.id}:`, error); } } console.log('Migration complete!'); } // Run migration await migrateExistingChats('user-123'); ``` ```python Export Script theme={null} async def migrate_existing_chats(user_id: str): """Migrate chats from Chat API to new system.""" print(f"Starting migration for user: {user_id}") async with httpx.AsyncClient() as client: # 1. Get all chats from old Chat API chats_response = await client.get( f'https://platform.ai.gloo.com/ai/v1/chat?user_id={user_id}', headers={'Authorization': f'Bearer {old_api_token}'} ) old_chats = chats_response.json() print(f"Found {len(old_chats)} chats to migrate") for old_chat in old_chats: try: # 2. Get messages for this chat messages_response = await client.get( f'https://platform.ai.gloo.com/ai/v1/chat/{old_chat["id"]}/messages', headers={'Authorization': f'Bearer {old_api_token}'} ) old_messages = messages_response.json() # 3. Create new chat in your system new_chat = conversation_manager.create_chat( old_chat['user_id'], tradition=None, # Set if you have this info title=old_chat.get('title', 'Migrated Conversation') ) # 4. Migrate all messages for old_message in old_messages.get('messages', []): conversation_manager.add_message( new_chat['id'], old_message['role'], old_message.get('content') or old_message.get('message') ) print(f"✓ Migrated chat {old_chat['id']} → {new_chat['id']}") except Exception as error: print(f"✗ Failed to migrate chat {old_chat['id']}: {error}") print('Migration complete!') # Run migration await migrate_existing_chats('user-123') ``` **Important**: Run your migration script as soon as you can to ensure all data is safely transferred. Test thoroughly with a subset of users first. ## Leveraging Completions v2 Features ### 1. Intelligent Model Routing Take advantage of Gloo's routing capabilities: Let Gloo automatically select the optimal model: ```typescript theme={null} const response = await chatService.sendMessage( chatId, 'How does God\'s love transform suffering?', { autoRouting: true, tradition: 'evangelical' } ); ``` **Best for**: Quick prototyping or when you prefer hands-off model management Choose a provider family and let Gloo select the best model: ```typescript theme={null} const response = await chatService.sendMessage( chatId, 'Draft a sermon outline on hope', { modelFamily: 'anthropic', tradition: 'mainline' } ); ``` **Best for**: When you have provider preferences or are testing across families Specify the exact model for your use case: ```typescript theme={null} const response = await chatService.sendMessage( chatId, 'Translate this passage to Spanish', { model: 'gloo-openai-gpt-5.5-pro', autoRouting: false } ); ``` **Best for**: Benchmarking, specialized tasks, or strict reproducibility requirements ### 2. Tradition-Aware Responses Maintain theological alignment throughout conversations: ```typescript theme={null} // Set tradition at chat creation const chat = await chatService.createConversation( userId, 'catholic', // evangelical, catholic, or mainline 'Understanding the Trinity' ); // The tradition is automatically applied to all subsequent messages const response = await chatService.sendMessage( chat.id, 'How do we understand the Holy Spirit?' ); // Response will be aligned with Catholic theological perspective ``` ### 3. Streaming Responses Implement real-time streaming for better UX: ```typescript theme={null} async function* streamChatResponse( chatId: string, userMessage: string ) { // Store user message await conversationManager.addMessage(chatId, 'user', userMessage); // Get history const history = await conversationManager.getConversationHistory(chatId); // Stream from Completions v2 const response = await fetch( 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: history, auto_routing: true, stream: true }) } ); const reader = response.body?.getReader(); const decoder = new TextDecoder(); let fullResponse = ''; while (true) { const { done, value } = await reader!.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { // Store complete response await conversationManager.addMessage( chatId, 'assistant', fullResponse ); return; } const parsed = JSON.parse(data); const content = parsed.choices[0]?.delta?.content; if (content) { fullResponse += content; yield content; } } } } } // Usage with Express app.post('/api/chat/:chatId/stream', async (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); for await (const chunk of streamChatResponse(req.params.chatId, req.body.message)) { res.write(`data: ${JSON.stringify({ chunk })}\n\n`); } res.write('data: [DONE]\n\n'); res.end(); }); ``` ## Advanced Patterns ### Pattern 1: Conversation Branching Create alternative conversation paths: ```typescript theme={null} async function createBranch( originalChatId: string, fromMessageId: string, newBranchTitle: string ): Promise { // Get original chat const originalChat = await db.chats.findOne({ id: originalChatId }); // Create new chat const branchChat = await conversationManager.createChat( originalChat.user_id, originalChat.tradition, `${newBranchTitle} (Branch)` ); // Copy messages up to branch point const messages = await db.messages .find({ chat_id: originalChatId }) .sort({ created_at: 'asc' }) .toArray(); for (const msg of messages) { if (msg.id === fromMessageId) break; await conversationManager.addMessage(branchChat.id, msg.role, msg.content); } return branchChat; } ``` ### Pattern 2: Multi-Source Integration Leverage multiple content publishers: ```typescript theme={null} interface PublisherFilter { publishers?: string[]; sources_limit?: number; } async function sendMessageWithSources( chatId: string, userMessage: string, filters?: PublisherFilter ): Promise { await conversationManager.addMessage(chatId, 'user', userMessage); const history = await conversationManager.getConversationHistory(chatId); // Add system message with publisher context if needed if (filters?.publishers) { history.unshift({ role: 'system', content: `Focus on information from these publishers: ${filters.publishers.join(', ')}` }); } const completion = await completionsClient.createCompletion(history, { autoRouting: true }); // Store response with source metadata const response = completion.choices[0].message.content; await conversationManager.addMessage(chatId, 'assistant', response); return { response, sources: completion.sources || [] }; } ``` ### Pattern 3: Conversation Summarization Auto-summarize long conversations: ```typescript theme={null} async function summarizeConversation(chatId: string): Promise { const messages = await db.messages .find({ chat_id: chatId }) .sort({ created_at: 'asc' }) .toArray(); if (messages.length < 10) { return ''; // Don't summarize short conversations } // Create summary prompt const conversationText = messages .map(m => `${m.role}: ${m.content}`) .join('\n\n'); const completion = await completionsClient.createCompletion( [ { role: 'system', content: 'You are a helpful assistant that creates concise 2-3 sentence summaries of conversations.' }, { role: 'user', content: `Please summarize this conversation:\n\n${conversationText}` } ], { autoRouting: true, maxTokens: 150 } ); const summary = completion.choices[0].message.content; // Update chat title with summary await db.chats.update( { id: chatId }, { title: summary.slice(0, 100), metadata: { full_summary: summary } } ); return summary; } ``` ## Testing Your Migration ### Unit Tests ```typescript theme={null} import { describe, it, expect, beforeEach } from 'vitest'; describe('Chat Service Migration', () => { let chatService: ChatService; let testUserId: string; beforeEach(() => { const conversationManager = new ConversationManager(testDb); const completionsClient = new GlooCompletionsV2Client({ apiKey: process.env.TEST_GLOO_API_KEY! }); chatService = new ChatService(conversationManager, completionsClient); testUserId = 'test-user-123'; }); it('should create a new conversation with tradition', async () => { const chat = await chatService.createConversation( testUserId, 'evangelical', 'Test Chat' ); expect(chat.id).toBeDefined(); expect(chat.user_id).toBe(testUserId); expect(chat.tradition).toBe('evangelical'); }); it('should send and receive messages', async () => { const chat = await chatService.createConversation(testUserId); const result = await chatService.sendMessage( chat.id, 'What brings meaning to life?', { autoRouting: true } ); expect(result.response).toBeDefined(); expect(result.model).toBeDefined(); expect(result.tokensUsed).toBeGreaterThan(0); }); it('should maintain conversation context', async () => { const chat = await chatService.createConversation(testUserId); await chatService.sendMessage(chat.id, 'My name is John'); await chatService.sendMessage(chat.id, 'What is my name?'); const history = await chatService.getHistory(chat.id); expect(history.length).toBe(4); // 2 user + 2 assistant messages }); it('should respect tradition parameter', async () => { const chat = await chatService.createConversation( testUserId, 'catholic' ); const result = await chatService.sendMessage( chat.id, 'Tell me about the Eucharist' ); // Response should reflect Catholic perspective expect(result.response).toBeDefined(); }); }); ``` ### Integration Tests ```typescript theme={null} describe('End-to-End Conversation Flow', () => { it('should handle complete conversation lifecycle', async () => { // Create conversation const chat = await chatService.createConversation( 'user-123', 'evangelical', 'Purpose and Meaning' ); // Send multiple messages const response1 = await chatService.sendMessage( chat.id, 'How can I find purpose in difficult times?', { autoRouting: true } ); expect(response1.response).toContain('purpose' || 'meaning'); const response2 = await chatService.sendMessage( chat.id, 'Can you give practical steps?' ); expect(response2.response).toBeDefined(); // Verify history const history = await chatService.getHistory(chat.id); expect(history.length).toBe(4); // Test context retention const response3 = await chatService.sendMessage( chat.id, 'Tell me more about the first step' ); expect(response3.response).toBeDefined(); // Delete conversation await chatService.deleteConversation(chat.id); const chats = await chatService.listConversations('user-123'); expect(chats.find(c => c.id === chat.id)).toBeUndefined(); }); }); ``` ## Troubleshooting **Problem**: API returns error about token limit **Solution**: Implement proper context truncation in your `getConversationHistory` method: ```typescript theme={null} const history = await conversationManager.getConversationHistory( chatId, { maxMessages: 50, maxTokens: 8000 // Adjust based on model's context window } ); ``` For models with smaller context windows, consider conversation summarization to maintain essential context while reducing token count. **Problem**: Messages appearing in wrong chronological order **Solution**: Ensure your database queries always sort by `created_at` ascending: ```sql theme={null} SELECT * FROM messages WHERE chat_id = ? ORDER BY created_at ASC -- Critical! ``` Also use transactions when writing multiple messages to prevent race conditions. **Problem**: Responses don't reflect the specified theological tradition **Solution**: Verify tradition is being passed to Completions v2: ```typescript theme={null} // Check that tradition is in the request console.log('Tradition:', completionOptions.tradition); // Ensure it's a valid value const validTraditions = ['evangelical', 'catholic', 'mainline']; if (tradition && !validTraditions.includes(tradition)) { throw new Error(`Invalid tradition: ${tradition}`); } ``` **Problem**: Responses taking too long **Solutions**: 1. **Use Streaming**: Provides immediate feedback to users ```typescript theme={null} { stream: true } ``` 2. **Optimize Context**: Only send necessary messages ```typescript theme={null} { maxMessages: 20 } // Fewer messages = faster ``` 3. **Cache Recent Conversations**: Use Redis for hot data ```typescript theme={null} const cached = await redis.get(`chat:${chatId}:messages`); if (cached) return JSON.parse(cached); ``` 4. **Choose Appropriate Models**: Use model family selection ```typescript theme={null} { modelFamily: 'openai' } // Generally faster than anthropic ``` **Problem**: Race conditions with simultaneous messages **Solution**: Use database transactions and row-level locking: ```typescript theme={null} await db.transaction(async (trx) => { // Lock the chat row await trx.raw('SELECT * FROM chats WHERE id = ? FOR UPDATE', [chatId]); // Store message await trx('messages').insert(userMessage); // Get history and call API const history = await trx('messages') .where({ chat_id: chatId }) .orderBy('created_at'); const completion = await completionsClient.createCompletion(history); // Store response await trx('messages').insert(assistantMessage); }); ``` ## Migration Checklist * [ ] Review current Chat API usage patterns * [ ] Design database schema for conversations * [ ] Plan context window management strategy * [ ] Decide on model routing strategy (direct model selection recommended) * [ ] Identify which chats need tradition parameter * [ ] Create migration timeline * [ ] Set up conversation storage (PostgreSQL/MongoDB) * [ ] Implement ConversationManager class * [ ] Integrate GlooCompletionsV2Client * [ ] Build ChatService combining both * [ ] Add context window management * [ ] Implement error handling and retries * [ ] Add logging and monitoring * [ ] Set up streaming support (optional) * [ ] Write unit tests for conversation management * [ ] Write integration tests for full chat flows * [ ] Test with various message lengths * [ ] Test context window truncation * [ ] Test concurrent request handling * [ ] Verify tradition parameter works correctly * [ ] Load test with production-like volumes * [ ] Create data export script * [ ] Export existing chats from Chat API * [ ] Validate exported data * [ ] Import into new system * [ ] Verify data integrity * [ ] Test sample conversations work correctly * [ ] Deploy new infrastructure * [ ] Run migration script for all users * [ ] Enable new system for subset of users * [ ] Monitor performance and errors closely * [ ] Gradually roll out to all users * [ ] Keep Chat API as fallback initially * [ ] Switch all traffic to new system * [ ] Monitor for 48 hours * [ ] Disable Chat API integrations * [ ] Update documentation * [ ] Communicate completion to team * [ ] Celebrate! 🎉 ## Benefits of Migrating Built-in values alignment and safety at every layer Auto-routing optimizes every query for quality, cost, and intent automatically Complete control over storage, retention, context management, and conversation workflows Native support for tradition-aware responses (evangelical, catholic, mainline) Build sophisticated UX with branching, editing, summarization, and more Control what context you send and optimize token usage based on your needs Optimize with caching, indexing, and custom logic for your specific use case Keep sensitive conversations in your own infrastructure with full control ## Support and Resources Contact our team through Gloo AI Studio support Complete API reference for Completions v2 Reference for the deprecated Chat API Enhance your completions with tool calling ## Next Steps Understand the architectural changes Work through the detailed migration guide Set up testing before going to production Questions about migration? We're here to help! Reach out through [Gloo AI Studio support](https://studio.ai.gloo.com/support) or use the support link in the top navigation. # Model Lifecycle Source: https://docs.gloo.com/api-guides/model-lifecycle How models arrive in the Gloo AI catalog, what happens when one is retired, and how to detect both from the API. The Gloo AI catalog changes as providers ship new models and retire old ones. Both ends of that lifecycle are visible in one place — [`GET /platform/v2/models`](/api-reference/get-models/v2) — so you can detect additions and deprecations programmatically instead of watching for an announcement. Catalog models serve different purposes. The [Responses API](/api-guides/responses) and [Completions V2 API](/api-guides/completions-v2) use models that generate answers. The [Embeddings API](/api-guides/embeddings) use embedding models to turn text into vectors. A model is available on its corresponding API once it appears in the catalog. When a Responses or Completions model is deprecated and a replacement is chosen, requests may continue to be routed to that replacement while you migrate. With embeddings, switching models requires re-embedding stored content because vectors from different models are not interchangeable. ## New models Gloo AI tracks provider releases automatically, and new models are usually added to the catalog within a business day or two of a provider making them generally available. Once listed, generation models are usable on the [Responses API](/api-guides/responses) and [Completions V2](/api-guides/completions-v2); embedding models are usable on the [Embeddings API](/api-guides/embeddings). Check `output_modalities` in the catalog to identify the model type. ### Where to find available models * **[Supported Models](/api-guides/supported-models)** — a live table of the current catalog, grouped by family, with context windows, list pricing, and capability flags. * **[`GET /platform/v2/models`](/api-reference/get-models/v2)** — the public, unauthenticated endpoint the table is built from. Call it directly to enumerate models programmatically; poll it to detect new additions. Use each entry's `id` field as your `model` parameter. * **[Model Explorer in Gloo Studio](https://studio.ai.gloo.com/models)** — a richer side-by-side comparison including modalities, reasoning, and speed ratings. All three read from the same catalog, so they agree by construction — the table and the Model Explorer are renderings of what the endpoint returns, not separately maintained lists. Pick whichever suits the task: the endpoint to automate against, the table to scan, the Explorer to compare. ### Model ID naming Gloo model IDs follow a consistent, human-readable pattern: ``` gloo-- ``` For example: | Model ID | Provider | Model | | - | - | - | | `gloo-anthropic-claude-sonnet-4.6` | Anthropic | Claude Sonnet 4.6 | | `gloo-openai-gpt-5-mini` | OpenAI | GPT-5 mini | | `gloo-google-gemini-2.5-pro` | Google | Gemini 2.5 Pro | Always take the exact ID from the `id` field of the catalog rather than constructing it by hand — the naming pattern is a convention, not a guarantee, and the catalog is the source of truth. ### Model families Every model carries a `family` field that groups it by its underlying provider or model line — for example **Anthropic**, **Google**, **OpenAI**, and **Open Source**, alongside other families as new providers are added. The [Supported Models](/api-guides/supported-models) table is organized by this field, and it is returned on every [`GET /platform/v2/models`](/api-reference/get-models/v2) entry so you can filter or route by family in your own code. ### Adopting a new model For generation, switching to a newly available model usually means setting `model` to its catalog ID, after checking its capabilities, context window, and modalities. For embeddings, use a model whose `output_modalities` includes `embeddings`. Keep the same model and dimensions for document and query vectors. Changing either requires re-embedding stored content before querying it with the new vectors. You can try a new model before changing any code: pick it from the model dropdown in the [Chat Playground](/studio/playground-chat) in Gloo AI Studio and send it a prompt. The playground draws its model list from the same catalog, so a newly added model is selectable there as soon as it is listed. Playground requests run through your organization's credentials and are billed at standard rates. Watching [`GET /platform/v2/models`](/api-reference/get-models/v2) programmatically is the most reliable way to learn about new models as soon as they land — no announcement or manual check required. ## Deprecated models Models are occasionally retired as providers release newer, better, or cheaper alternatives. Retirement is ultimately the provider's decision, not ours, so what we can offer is a best effort to make it a managed transition rather than a silent break: where we can, a deprecated model ID keeps serving requests and the catalog tells you what replaces it, so you have time to migrate. Treat that as best effort rather than a guarantee. A provider can withdraw a model faster than any notice period, and when that happens we redirect if there is a suitable replacement and tell you if there is not. The catalog is the authoritative signal in both cases — check `is_deprecated` on the models you use. ### How deprecations are announced Deprecations are surfaced on the catalog itself. Each [`GET /platform/v2/models`](/api-reference/get-models/v2) entry carries: * **`is_deprecated`** — `true` once a model is scheduled for retirement. * **`deprecation_note`** — a plain-language explanation, including the replacement and any price change. Read this one specifically: it is where a price increase, or a move to a different family, is stated. * **`replacement_model`** — the Gloo model ID that requests are routed to. The [Supported Models](/api-guides/supported-models) table also renders a **Deprecated** badge and the note inline for any flagged model. ### Where a deprecated ID's requests go For generation models, when a replacement has been chosen, requests still using the old ID may be routed to that replacement. Do not assume this applies to embedding requests: changing an embedding model changes the vector space, and stored content must be re-embedded before using the replacement. When no replacement has been chosen — `replacement_model` is null — nothing is redirected, and the old ID stops working once its catalog entry is removed. Replacements normally stay within the same model family — a deprecated Anthropic model is replaced by another Anthropic model, and the same holds for every family. Where no model in the family is suitable, a replacement may be chosen from another family, and the `deprecation_note` says so. You are never moved to a different family silently. ### How a replacement is chosen Choosing a replacement is best effort, like the rest of a deprecation — these are the criteria we apply, not promises about an outcome a provider ultimately controls. Two of them do the most work: * **Capability should not go backwards.** We look for a model that supports everything the deprecated one did — tool calling, vision, reasoning, its input modalities — with a context window at least as large, and we do not substitute one that would shrink your context window or drop a capability you were using. * **Price stays within a band.** A candidate costing more than **1.3× the deprecated model's rate on either the input or the output axis** is not considered. Within those limits we prefer, in order: the newest model in the same family priced at or below the old one on both axes; then a same-family model priced within the band; then the same provider; then any family. Many models also declare their successor in advance, and where that exists it is used directly rather than recomputed. For example, when Claude Sonnet 4 was deprecated its requests were routed to Claude Sonnet 4.6 at the same price; when Claude Opus 4 was deprecated its requests were routed to Claude Opus 4.8 at a lower price. Same-or-cheaper is the ordinary outcome. **A replacement can cost more per token than the model it replaced.** It happens when nothing same-or-cheaper clears the capability bar, and it is capped at 1.3× on each axis. The `deprecation_note` states the increase whenever there is one. If you bill through on token cost, read it when a model you use is flagged rather than assuming the price is unchanged. If nothing at all clears both bars, no automatic redirect is made. The model is flagged deprecated with no `replacement_model`, and choosing where that traffic goes is left to you — we would rather tell you there is no equivalent than move you somewhere that quietly is not one. ### Notice before removal Once a model is flagged as deprecated and its replacement routing is in place, its catalog entry is intended to stay listed for at least a week before it can be removed, so both the old ID and its replacement are live and interchangeable while you migrate. Routing for a retired ID is intended to outlive its catalog entry, but the catalog is the contract: an ID you can still see in [`GET /platform/v2/models`](/api-reference/get-models/v2) is supported, and one that has been removed is not. Treat continued routing after removal as a courtesy rather than something to build on. ### What you should do Poll [`GET /platform/v2/models`](/api-reference/get-models/v2) and check the `is_deprecated` flag on the models you use. This is the earliest and most reliable signal. When a model you depend on is deprecated, read its `replacement_model` and `deprecation_note` to see exactly where its traffic is being routed and whether the price changes. An empty `replacement_model` means no equivalent was found and the choice is yours. Update the `model` parameter in your requests to the replacement ID. Migrating explicitly keeps your configuration accurate and lets you validate the replacement's behavior on your own timeline, rather than relying on automatic routing indefinitely. You do not have to migrate the moment a generation model is flagged — automatic routing keeps your integration running. Migrating simply keeps your requests pointed at a model ID that is still listed and actively maintained. # Prompt Caching Source: https://docs.gloo.com/api-guides/prompt-caching Reduce costs and latency with implicit and explicit prompt caching across Anthropic, OpenAI, DeepSeek, and Gemini models Prompt caching lets you reuse processed prompt tokens across multiple requests, reducing both cost and latency. When the same prompt prefix is sent repeatedly, cached tokens are served from cache rather than being re-processed—saving you money and delivering faster responses. Current per-token rates are available on the [models page](https://studio.ai.gloo.com/models). ## Types of Caching There are three main approaches to prompt caching used by LLM providers: * **Implicit caching** — The provider automatically caches repeated prompt prefixes. No code changes or configuration required. Requests with matching prefixes are routed to servers that recently processed the same content, enabling cache hits automatically. This is the simplest approach and is used by OpenAI, DeepSeek, Gemini, and many open-source models (Qwen, MiniMax, and others). * **Explicit caching** — You declare which content to cache using API parameters (such as headers or request body fields). This gives you more control over what gets cached and when. Anthropic uses this model via the `X-Cache-TTL` header. * **Storage-based caching** — You create managed caches via API that persist for a defined duration, billed per token-hour. This offers the most control but requires cache lifecycle management. Gemini offers this as a separate feature from its implicit caching. Most providers combine multiple approaches. The sections below explain which types each provider supports and how to use them through the Gloo AI API. ## Anthropic — Explicit Caching Anthropic uses **explicit caching** via the `X-Cache-TTL` header. You opt in per request by setting the header, and our API automatically wraps your system messages with `cache_control: {type: "ephemeral"}` blocks. No modifications to your message content are needed—just add the header. ### How it works 1. Set the `X-Cache-TTL` header to either `"5m"` or `"1h"`. 2. Our API places **one** `cache_control` marker on the last system message to cache the entire stable system-prefix block. A single marker caches everything before it, covering the full assembled system prompt. 3. The remaining **three** breakpoints are free for your own `cache_control` markers on your message content. 4. If your request already contains 4 `cache_control` markers, our marker is silently skipped — your request succeeds with no error, but system-prefix caching is not applied for that call. 5. Dynamic content at the end of your system messages (such as timestamps or user-specific data) is excluded from the cached block automatically. ### Endpoint behavior | Endpoint | Breakpoint ownership | What happens | | :- | :- | :- | | **Routed** (`/ai/v2/guarded/chat/completions`) | Gloo owns 1 (system prefix); you own up to 3 | Our marker is auto-placed on the last system message. You may add up to 3 of your own on your content. | The Anthropic breakpoint limit is 4 per request. Minimum cacheable token thresholds vary by model (e.g., 4,096 tokens for Claude Haiku 4.5). See [Anthropic's prompt caching docs](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) for current thresholds. ### Billing | Tier | cache\_write rate | cache\_read rate | | :- | :- | :- | | `X-Cache-TTL: 5m` | 125% of input rate | 10% of input rate | | `X-Cache-TTL: 1h` | 200% of input rate | 10% of input rate | Cache writes are billed at a multiple of the standard input rate. Cache reads are always billed at 10% of the input rate. ### Supported models Anthropic caching works with all Claude models available through our API. See [Supported Models](/api-guides/supported-models) for the current model list. ### Example ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -H 'X-Cache-TTL: 5m' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.5", "messages": [ {"role": "user", "content": "What does the Bible say about forgiveness?"} ], "stream": false }' ``` ## OpenAI — Implicit + Explicit Caching OpenAI uses **implicit caching** as its primary mechanism. Caching works automatically on all API requests—no code changes required. When your prompt contains 1,024 tokens or more, OpenAI routes requests to servers that recently processed the same prompt prefix, enabling cache hits. This can reduce latency by up to 80% and input token costs by up to 90%. See [OpenAI's prompt caching announcement](https://openai.com/index/api-prompt-caching/) for details. For finer control, you can use **explicit caching** by adding a `prompt_cache_key` to your request body. This optional parameter influences server routing so that requests sharing the same key and common prefix are more likely to land on the same server, improving cache hit rates. ### How it works 1. Caching is **implicit and automatic** for prompts with 1,024+ tokens. 2. Requests are routed to servers that recently processed the same prompt prefix. 3. Optionally, add `"prompt_cache_key": "your-key"` to your request body to improve routing for requests with shared prefixes. 4. Cache hits reduce both latency and cost. Cached tokens are billed at a discounted rate that varies by model — 10% of the input rate for GPT-5 models and 25% for GPT-4.1 and o-series models — with no write fee. ### Billing The cache read discount depends on the model. Current OpenAI families: | Model family | cache\_read rate | cache\_write rate | | :- | :- | :- | | GPT-5 family | 10% of input rate | None | | GPT-4.1 family, o3, o4-mini | 25% of input rate | None | There is no write fee for OpenAI caching — you only pay for cache reads, at the model-specific rate above. Note that cheaper GPT-5 variants (gpt-5-mini, gpt-5-nano) have higher effective cache-read ratios (\~23–56%) because of a minimum read-rate floor. See [Checking your rates](#checking-your-rates) for exact per-model values. ### Supported models OpenAI caching works with OpenAI models available through our API. The `prompt_cache_key` parameter is silently ignored for other providers. See [Supported Models](/api-guides/supported-models) for the current model list. ### Best practice: stable cache keys Use deterministic, stable keys that match your prompt structure. Changing the system prompt means you should use a new cache key. ``` Good: "ministry-block-v1", "sermon-generator", "theology-assistant-v1" Avoid: "request-12345", "cache-abc" ``` ### Example ```json theme={null} { "model": "gloo-openai-gpt-5.2", "messages": [ { "role": "system", "content": "You are a theological assistant..." }, { "role": "user", "content": "Explain the Trinity" } ], "prompt_cache_key": "theology-assistant-v1", "stream": false } ``` ## DeepSeek — Implicit Caching DeepSeek uses **implicit caching** that works entirely on the provider side. No user action or configuration is needed. * **No headers or request body fields required** — caching is automatic. * **Cache read rates** vary per model (roughly 9–90% depending on the model). * **No cache write fee**. * Savings from cache reads appear automatically in your billing. ## Gemini — Implicit Caching Gemini uses **implicit caching** on the provider side. No headers or request body fields are required—repeated prompt prefixes are automatically cached when you send a request that matches a previously seen prefix. ### How it works 1. **No customer action needed** — caching is automatic. No `X-Cache-TTL` header, no `cache_control` markers, and no `prompt_cache_key` are required. 2. Repeated prefixes are cached by the provider automatically. 3. **Cache read rates are roughly 10% of the input rate for larger Gemini models; cheaper flash and flash-lite models are higher (up to \~33%)** — check the [models endpoint](#checking-your-rates) for exact per-model rates. Cache hits surface as a separate `cache_read` segment in your billing, the same as the other providers. There is no cache write fee for implicit caching. ### Supported models Gemini caching is available for all Gemini models in the Gloo AI catalog. Minimum token thresholds vary by model and are determined by the provider. | Model | Minimum token threshold | | :- | :- | | Gemini 2.5 Flash, 2.5 Flash Lite | 2,048 tokens | | Gemini 3 Flash, 3.1 Flash Lite, 3.5 Flash | 4,096 tokens | ## Other Models (Qwen, MiniMax, and more) — Implicit Caching A number of additional models in the Gloo AI catalog — including **Qwen**, **MiniMax**, and **Xiaomi MiMo** — support **implicit caching** through their upstream provider. Caching is automatic; no headers, `cache_control` markers, or `prompt_cache_key` are required. * **Automatic** — repeated prompt prefixes are cached provider-side and surface as a `cache_read` segment in your billing, the same as the other providers. * **Cache read rates vary widely per model** (roughly 25–65% of the input rate — the discount is much smaller than the major providers' for some models). A few models (e.g. some Qwen variants) also carry a cache **write** fee of 1.25× the input rate; most do not. * Because rates differ substantially between models, use the [models endpoint](#checking-your-rates) for exact per-model `cache_read` / `cache_write` values. Not every model in this group caches — support is set by the upstream provider, and the models endpoint is the source of truth for which models have cache pricing. ## Billing Summary | Provider | Type | cache\_read rate | cache\_write rate | Notes | | :- | :- | :- | :- | :- | | Anthropic | Explicit | 10% of input | 5m: 125%; 1h: 200% of input | Two billing tiers via `X-Cache-TTL` header | | OpenAI | Implicit + Explicit | Varies per model (10% GPT-5; 25% GPT-4.1 & o-series) | None | Automatic for 1,024+ tokens | | DeepSeek | Implicit | Varies per model (\~9–90%) | None | Automatic, no user action | | Gemini | Implicit | Varies per model (\~10%; flash/flash-lite up to \~33%) | None | Automatic, no user action required | | Other (Qwen, MiniMax, Xiaomi, …) | Implicit | Varies per model (\~25–65%) | Most none; some 1.25× | Automatic, set by upstream provider | ### How total cost is calculated ``` total_cost = fresh_tokens × input_rate + cached_tokens × cache_read_rate + cache_write_tokens × cache_write_rate + output_tokens × output_rate ``` The formula above applies to all four providers — cached tokens are billed as a separate `cache_read` segment. Gemini and OpenAI have no cache write fee, so their `cache_write` term is always zero. A flat **platform rate** applies to all segments. The per-segment rates returned by the models endpoint are **list prices** (the model's base rate); the platform rate is added at billing. ### Checking your rates Use the models endpoint to see current per-model rates: ``` GET /platform/v2/models ``` ## Best Practices 1. **Static content first**: For all providers, place static content (system prompts, instructions) at the beginning of your prompt. This maximizes prefix matches for implicit caching and ensures stable blocks for explicit caching. 2. **Use explicit caching when available**: For Anthropic models, add the `X-Cache-TTL` header to opt in. For OpenAI, use `prompt_cache_key` to improve routing for requests with shared prefixes. 3. **Pre-warm on startup**: Send an initial request with your standard system prompt to warm the cache before serving users. 4. **Monitor cache impact**: Compare your token usage and billing over time. Cached tokens are billed at a reduced rate, so you'll see lower input costs when caching is effective. Check your usage in the [Gloo Studio billing dashboard](https://studio.ai.gloo.com/billing). 5. **Expect fewer cache hits after failover**: Caches are held by each provider, so a request that [fails over](/api-guides/provider-failover) to another provider does not read the cache built on the original one. ## Migration Guide If you're not currently using prompt caching, here's how to get started: ### Implicit caching (OpenAI, DeepSeek, Gemini) No code changes needed. Caching is automatic for prompts with repeated prefixes. Place static content (system prompts, instructions) at the beginning of your prompt to maximize cache hits. On Gemini, prefix-only caching works on the provider side with no special parameters. ### Explicit caching (Anthropic) Add the `X-Cache-TTL: 5m` header to opt in to caching. System message caching is then automatic—no content modifications needed. ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -H 'X-Cache-TTL: 5m' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.5", "messages": [{"role": "user", "content": "..."}] }' ``` ### Improving cache hit rates (OpenAI) Add `"prompt_cache_key": "your-key"` to your request body with a stable key that matches your prompt prefix. This improves server routing for requests with shared prefixes. ## Related Documentation * [Completions V2](/api-guides/completions-v2) — Core routing mechanisms and streaming details * [Supported Models](/api-guides/supported-models) — Model IDs, capabilities, and current pricing * [Provider Failover](/api-guides/provider-failover) — What happens when the provider serving your model fails # Provider Failover Source: https://docs.gloo.com/api-guides/provider-failover How Gloo AI keeps serving the model you asked for when the provider behind it fails, when it deliberately does not retry, and what your application should still handle. Most models in the Gloo AI catalog can be served from more than one provider. When the provider currently serving a model is failing, Gloo sends your request to another provider that serves **the same model**. This is **provider failover**, and it runs on every chat and responses endpoint without any configuration. Failover changes *who* serves your request, never *what* you get. It never substitutes a different model: the `model` in the response is the `model` you asked for. It is separate from [auto-routing and `model_family`](/api-guides/completions-v2), which choose a model for you on the guarded Completions endpoint. ## When a request fails over Failover handles failures that belong to the provider, not to your request: * **Provider errors and outages.** A timeout, a lost connection, a provider server error, an empty response, or the provider refusing the request because it is over capacity or rate-limited. Gloo retries the request on another provider serving the same model. * **A provider that keeps failing.** Gloo tracks each provider's health per model. When a provider starts failing for a model, new requests for that model go to a healthy provider instead of trying the failing one first. It is brought back into service automatically once it recovers. * **A stream that fails before the answer starts.** If a streamed request fails before any text reaches you, Gloo retries it, on another provider when needed, and the answer starts from the beginning. You see the stream start later, not an error. * **A stream cut off mid-answer.** For models that support it, another provider continues the answer from where the stream stopped, in the same response. For models that do not, the stream ends with an error event instead; see [Handling Streaming Failures](/best-practices/completions-streaming-failures) for how to continue it from your application. Failover works within a time budget for each request. If no provider answers within it, you receive the error rather than a request that waits indefinitely. Not every model has a second provider. For a model served from only one place, failover has nowhere to send the request, and a provider outage returns an error. ## When a request does not fail over Some failures would fail the same way on every provider, so Gloo returns them straight away instead of repeating the request elsewhere: | Situation | What you get | What to do | | :- | :- | :- | | Invalid request, unsupported model, or input longer than the model's context window | A `400` client error | Fix the request. See [Errors](/api-reference/general/errors). | | Your API key or organization is not authorized | A `401` or `403` | Check your credentials. | | The provider's content policy blocks the request or the answer | A content-filter error, or `finish_reason: "content_filter"` on a stream | Change the request. Do not retry it unchanged. | | Your organization is out of credit or at its spending limit | A `402` or `429` with `fault: "client"` | Resolve the limit. See [Limits](/api-reference/general/limits). | | A reasoning model spent your whole output limit before writing any visible text | A normal response that stopped at the limit (below) | Raise the output limit. | ### Reasoning models and the output limit Models that think before they answer produce their reasoning inside the same output-token budget as their visible answer. If you set a small `max_tokens` (Chat Completions) or `max_output_tokens` (Responses), the model can spend all of it reasoning and stop before writing any text. The provider answered correctly and would answer the same way again, so this is **not** retried or failed over. Gloo returns the provider's own answer: * **Chat Completions**, streaming and non-streaming: the response ends with `finish_reason: "length"` and no text, and `usage` reports the tokens the model used. * **Responses** (non-streaming, OpenAI reasoning models): HTTP `200` with `status: "incomplete"` and `incomplete_details: {"reason": "max_output_tokens"}`, plus a `suggestion` field that names the tokens used and the limit you sent. The fix is to raise the limit. Leave generous headroom above the length of the answer you expect. ## Which endpoints fail over | Endpoint | Failover | | :- | :- | | Guarded [Responses](/api-guides/responses) and [Completions](/api-guides/completions-v2) | Yes | | [Direct Responses](/api-guides/direct-responses) and [Direct Completions](/api-guides/direct-completions) | Yes | | [Grounded Responses](/api-guides/grounded-responses) and [Grounded Completions](/api-guides/grounded-completions) | Yes | | [Embeddings](/api-guides/embeddings) | No. A provider failure returns a `503` with `retryable: true`. | ## What you observe * **The same model.** The response carries the `model` you requested, whichever provider served it. * **Extra latency.** A request that failed over waited for the failed attempt first, so it takes longer than usual. * **A colder prompt cache.** Prompt caches are held by each provider, so a request served by a different provider does not read the cache built on the original one. Expect fewer cached tokens on requests that failed over, and on requests routed around an unhealthy provider, until the cache warms on the new one. See [Prompt Caching](/api-guides/prompt-caching). * **No charge for the failed attempt.** An attempt that failed and was replaced by another provider is not billed. * **An error only when failover could not help.** Errors you receive carry the standard [error object](/api-reference/general/errors#ai-error-object): `fault` says whether the problem was your request, the provider, or Gloo, and `retryable` says whether the same request may succeed if you send it again. ## What your application should do 1. **Rely on failover for provider trouble.** You do not need to switch models or add a second provider yourself to ride out a provider outage. 2. **Still retry retryable errors.** When an error has `retryable: true`, retry with exponential backoff. A later request benefits from Gloo having already routed around the failing provider. Do not retry errors with `retryable: false` unchanged. 3. **Handle interrupted streams.** On models where Gloo cannot continue a cut-off stream, the stream ends with an error event after partial output. Follow [Handling Streaming Failures](/best-practices/completions-streaming-failures). 4. **Size output limits for reasoning models.** Treat `finish_reason: "length"` or `status: "incomplete"` with no text as a sign to raise `max_tokens` or `max_output_tokens`, not as an outage. 5. **Log `trace_id`.** Include it when you contact support about a failed request. ## Related Documentation * [Endpoint Types](/api-guides/endpoint-types) — what each endpoint family runs around your request * [Errors](/api-reference/general/errors) — error codes, `fault`, and `retryable` * [Handling Streaming Failures](/best-practices/completions-streaming-failures) — retry and continuation after a stream fails * [Prompt Caching](/api-guides/prompt-caching) — how caching works on each provider # Responses Source: https://docs.gloo.com/api-guides/responses The Gloo AI Responses API — OpenAI-compatible, multimodal, values-aligned, and guarded by default. The recommended way to build on Gloo AI. The **Responses API** (`POST /ai/v2/guarded/responses`) is the recommended way to build on Gloo AI. It pairs the OpenAI-compatible Responses request shape — one typed `input`/`output[]` format for text, vision, reasoning, tool use, and native image generation — with Gloo's full safety and values pipeline: the same guardrails, values-aligned `tradition` responses, and output moderation that power [Completions](/api-guides/completions-v2). **New to Gloo AI? Start here.** The Responses API is the standard surface going forward. If you already use [Completions V2](/api-guides/completions-v2), it remains fully supported — see [Moving from Completions to Responses](#moving-from-completions-to-responses). ## Why the Responses API * **OpenAI-compatible.** The request and response formats mirror the OpenAI Responses API, so existing tooling, SDKs, and mental models carry over directly. Point your base URL at `https://platform.ai.gloo.com/ai/v2/guarded` and call `responses.create`. * **Guarded by default.** Every request runs the same pipeline as `/ai/v2/guarded/chat/completions` — guardrails, blocked-content handling, ministry-aware prompting, and output moderation — with no extra configuration. * **Values-aligned.** The `tradition` parameter customizes responses to theological perspectives, exactly as on Completions. * **Built for multimodal.** Image input (vision) and image generation are first-class: the same endpoint that answers a text prompt can read an image or return a generated one, using the same `input` array and typed `output[]` items. * **Typed, structured output.** Responses come back as an `output[]` array of typed items (`message`, `image_generation_call`, reasoning, tool calls) instead of a single opaque `choices[].message.content` string — easier to parse, and extensible as new item types arrive. If you are starting a new integration, build on the Responses API. ## Which API should I use? | Use case | Recommended endpoint | | :- | :- | | New integration with OpenAI-compatible tooling | **Responses** (this page) | | Vision (image input) or image generation | **Responses** (this page) | | Values-aligned (`tradition`) responses | **Responses** (this page) or [Completions V2](/api-guides/completions-v2) | | Intelligent auto-routing / `model_family` selection | [Completions V2](/api-guides/completions-v2) | | Grounded / RAG completions with source attribution | [Grounded Completions](/api-guides/grounded-completions) | ## Endpoint **URL:** `https://platform.ai.gloo.com/ai/v2/guarded/responses` **Operation:** `POST` **Responses v1 is deprecated.** `POST /ai/v1/responses` is deprecated in favor of v2. It keeps working, but v2 uses the identical request/response shape and adds more features — guardrails, values-aligned `tradition` responses, and output moderation. Migrating is a one-line change: point your requests at `/ai/v2/guarded/responses`. One exception: **grounded responses** (RAG with source attribution) remain served by the v1 family at `POST /ai/v1/grounded/responses` — there is no v2 grounded endpoint yet. ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/guarded/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.6", "tradition": "evangelical", "input": [ { "role": "user", "content": "How can our small group support a grieving member?" } ] }' ``` Authentication is identical to the rest of the platform — send your API key as a Bearer token. See [Generate API Keys](/studio/manage-api-credentials). ## Request format The Responses API uses `input` instead of `messages`, and a few renamed fields. If you know the chat-completions format, the mapping is small: | Responses API | Chat Completions equivalent | Notes | | :- | :- | :- | | `input` | `messages` | A string, or an array of input items (`role` + `content`). | | `instructions` | `system` message | Top-level system / developer instructions. | | `max_output_tokens` | `max_tokens` | Cap on generated tokens. | | `tradition` | `tradition` | Identical semantics. | | Parameter | Type | Required? | Description | | :- | :- | :- | :- | | `model` | string | Yes | Gloo model ID (e.g. `gloo-anthropic-claude-sonnet-4.6`). Pinned-model only — auto-routing and `model_family` are [Completions V2](/api-guides/completions-v2) features. | | `input` | string \| array | Yes | A plain string prompt, or an array of typed input items. | | `instructions` | string | No | System-level instructions applied to the request. | | `tradition` | string | No | `"evangelical"`, `"catholic"`, or `"mainline"`, or omit for a general Christian perspective. Invalid values are ignored gracefully. | | `max_output_tokens` | integer | No | Maximum number of tokens to generate. | | `temperature` | float | No | Sampling temperature. | | `top_p` | float | No | Nucleus sampling. | | `tools` | array | No | Tool / function definitions. See [Tool Use](/api-guides/tool-use). | | `tool_choice` | string \| object | No | Controls tool selection. | | `parallel_tool_calls` | boolean | No | Allow multiple tool calls per turn. | | `stream` | boolean | No | Stream the response as SSE events (default `false`). | | `reasoning` | object | No | Reasoning controls (e.g. effort) for capable models. | | `response_format` | object | No | Structured-output / JSON schema controls. | | `prompt_cache_key` | string | No | Improves cache-hit routing — see [Prompt Caching](/api-guides/prompt-caching). | | `image_generation` | object | No | Image-generation options (`quality`, `size`) for image-capable models. | Conversation state is managed client-side by passing the full `input` history on each request. The `previous_response_id` parameter (used by OpenAI's Responses API for server-side conversation chaining) is not supported. ## Guardrails and values Requests dispatch through the same pipeline as Completions V2, so guardrail behavior is identical: * **Values-aligned static responses.** When guardrails intervene with a curated, tradition-appropriate answer, it comes back as a *normal* Responses payload — a `message` item in `output[]` carrying the static text. Your client needs no special handling. * **Hard blocks.** Requests that guardrails reject outright return the same `403` error as `/ai/v2/guarded/chat/completions`. * **Output moderation.** Model text output passes through Gloo's moderation layer before it reaches you. ## Response format Responses return a typed `output[]` array. Each item has a `type`; a normal text answer arrives as a `message` item. ```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 } } ``` ### Streaming Set `"stream": true` to receive the response as Server-Sent Events. Each event has an `event:` line and a `data:` line carrying a typed JSON payload: ```text theme={null} event: response.created data: {"type":"response.created","response":{"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","response":{"id":"resp_a1b2c3",...,"output":[...],"usage":{...}}} ``` Key event types to handle: * `response.created` — emitted once at the start of the response. * `response.output_text.delta` / `response.output_text.done` — incremental text tokens for a `message` item. * `response.output_item.added` / `response.output_item.done` — emitted for typed items in `output[]` (including `function_call`). * `response.completed` — emitted once when the response is finished; the final payload includes the full `output[]` array and `usage`. Token usage is always included on streams — you don't need to request it. * Error events — surfaced as event types prefixed with `error.` (e.g. `error`); treat any unknown event as terminal and close the stream. Streams can fail after the response has started — see [Handling Streaming Failures](/best-practices/completions-streaming-failures) for retry, continuation, and partial-output guidance. ## Multimodal ### Image input (vision) Pass images as input items alongside text. Any vision-capable model accepts them: ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/guarded/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-google-gemini-3.1-pro", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "Describe this image." }, { "type": "input_image", "image_url": "https://example.com/photo.jpg" } ]} ] }' ``` Images may be supplied as a remote URL or a base64 `data:` URI. ### Image generation Image-capable models return a generated image as an `image_generation_call` output item (base64 result). Optional `image_generation` controls (`quality`, `size`) are forwarded to providers that support them. ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/guarded/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-google-gemini-3-pro-image", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "A watercolor painting of a lighthouse at sunset." } ]} ] }' ``` The generated image comes back as a typed `image_generation_call` item in `output[]`. The `result` field carries the image as a base64-encoded string: ```json theme={null} { "id": "resp_...", "object": "response", "model": "gloo-google-gemini-3-pro-image", "output": [ { "type": "image_generation_call", "id": "ig_...", "status": "completed", "result": "iVBORw0KGgoAAAANSUhEUgAA......" } ], "usage": { "input_tokens": 14, "output_tokens": 1290, "total_tokens": 1304 } } ``` Decode the base64 `result` to get the image bytes. #### OpenAI: image generation via the image\_generation tool OpenAI's image generation runs as an `image_generation` **tool** on a GPT-5 model — `gpt-image-1` is not a model id (exactly as on OpenAI's own API). Call a GPT-5 model and attach the tool; the model decides when to draw and can combine the image with text in the same response. The tool accepts an optional `model` field to pin its backing image model (e.g. `gpt-image-1`) plus `quality`/`size` controls: ```bash theme={null} curl -X POST 'https://platform.ai.gloo.com/ai/v2/guarded/responses' \ -H 'Authorization: Bearer ${GLOO_API_KEY}' \ -H 'Content-Type: application/json' \ -d '{ "model": "gloo-openai-gpt-5-mini", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "A watercolor painting of a lighthouse at sunset." } ]} ], "tools": [ { "type": "image_generation", "model": "gpt-image-1", "quality": "low" } ] }' ``` The generated image arrives as the same `image_generation_call` output item shown above (alongside any `reasoning` and `message` items from the GPT-5 model). **Image editing** works through the same mechanism: include an `input_image` part in the conversation and it becomes the tool's edit source. See [Supported Models](/api-guides/supported-models) for which models support image input and image generation. ## Pricing & token spend The Responses API is billed per token at each model's standard rates — there is no separate or premium price for using `/responses`. Cost is driven entirely by the `model` you select and the tokens you consume: ``` total_cost = input_tokens × input_rate + cached_tokens × cache_read_rate + output_tokens × output_rate ``` * **Per-model rates.** Input and output rates vary by model. The live rates are on the [Supported Models](/api-guides/supported-models) page and programmatically on `GET /platform/v2/models`. * **Prompt caching** reduces input cost when prompt prefixes repeat — cached tokens are billed at a discounted `cache_read` rate. See [Prompt Caching](/api-guides/prompt-caching). * **Image generation** is billed using the image model's token accounting; check the model's rates on the models endpoint. * A **6.5% platform fee** applies to every token segment on the governed endpoints; it pays for the trust layer (guardrails, moderation, values alignment). Direct endpoints carry no fee. Track real spend in the [Gloo Studio billing dashboard](https://studio.ai.gloo.com/billing) and [API usage](/studio/api-usage). ## Supported models The Responses API works across the full Gloo AI catalog — Anthropic, OpenAI, Google, and open-source families — including the multimodal and image-generation models. Use the **Model ID** as the `model` field. The complete, live list (with capabilities and pricing) is on the [Supported Models](/api-guides/supported-models) page. ## Moving from Completions to Responses [Completions V2](/api-guides/completions-v2) (`/ai/v2/guarded/chat/completions`) is **fully supported and backwards-compatible** — existing integrations continue to work unchanged, and it remains the home of intelligent auto-routing, `model_family` selection, and [grounded completions](/api-guides/grounded-completions). The Responses API is the recommended surface for new work because it standardizes on the OpenAI-compatible Responses shape while keeping the same guarded pipeline. 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` | | `tradition` | `tradition` (unchanged) | | `choices[].message.content` (string) | `output[]` (typed items) | ```json theme={null} // Completions V2 { "model": "gloo-anthropic-claude-sonnet-4.6", "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "Hello" } ], "tradition": "evangelical", "max_tokens": 256 } ``` ```json theme={null} // Responses API (equivalent) { "model": "gloo-anthropic-claude-sonnet-4.6", "instructions": "You are a helpful assistant.", "input": [ { "role": "user", "content": "Hello" } ], "tradition": "evangelical", "max_output_tokens": 256 } ``` Auto-routing, `model_family`, and grounded completions remain [Completions V2](/api-guides/completions-v2) capabilities. If you need them, stay on Completions — everything else can move to Responses today. ## Related Documentation * [Supported Models](/api-guides/supported-models) — model IDs, capabilities, and live pricing * [Prompt Caching](/api-guides/prompt-caching) — reduce cost and latency with cached prompt prefixes * [Tool Use](/api-guides/tool-use) — function calling * [Completions V2](/api-guides/completions-v2) — routing, `model_family`, and grounded completions # Libraries & SDKs Source: https://docs.gloo.com/api-guides/sdks-and-libraries Integrate Gloo AI into your applications using the official OpenAI libraries. Gloo AI is **OpenAI-compatible**, so you can use the official OpenAI SDKs for Python and Node.js without learning a new syntax. Authentication works the same way as OpenAI — pass your API key as the `api_key` parameter, and it's sent as a bearer token on every request. Get one from the [API Keys page](/studio/manage-api-credentials) in Gloo AI Studio. What sets Gloo apart is the platform-specific extensions to the OpenAI specification — `auto_routing`, `model_family`, `tradition`, grounded completions, and response routing metadata. The [compatibility matrix](#compatibility-matrix) below maps the differences. Gloo supports both the **Responses API** (recommended for new integrations) and **Chat Completions**. **Recommended:** build new integrations on the [Responses API](/api-guides/responses). With the OpenAI SDK, point the client at the `/ai/v2/guarded` base URL and call `responses.create`: ```python theme={null} client = OpenAI( api_key=os.getenv("GLOO_API_KEY"), base_url="https://platform.ai.gloo.com/ai/v2/guarded", ) resp = client.responses.create( model="gloo-anthropic-claude-sonnet-4.6", input="Hello world!", ) print(resp.output_text) ``` The Chat Completions examples below use the same `/ai/v2/guarded` base URL and remain fully supported — one base URL serves both `responses.create` and `chat.completions.create`. The OpenAI SDK also supports `embeddings.create`. For embedding examples and the `/ai/v2/direct` base URL, see the [Embeddings guide](/api-guides/embeddings). Embeddings requests use a dedicated endpoint and do not use the model routing options described below. **Authentication:** use your API key the same way you would with OpenAI — pass it as the `api_key` parameter in the SDK client. Get a key from the [API Keys page](/studio/manage-api-credentials) in Gloo AI Studio. ## Python The Python library is the standard for AI engineering and data science. Use pip to install the OpenAI package. ```bash theme={null} pip install openai python-dotenv ``` Create a `.env` file with your API key: ```bash theme={null} GLOO_API_KEY=your_api_key_here ``` Initialize the OpenAI client with your API key. ```python theme={null} import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("GLOO_API_KEY"), base_url="https://platform.ai.gloo.com/ai/v2/guarded" ) completion = client.chat.completions.create( model="gloo-anthropic-claude-sonnet-4.5", messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello world!"} ] ) print(completion.choices[0].message.content) ``` For production applications, store your API key in a secrets manager rather than in plaintext `.env` files. See the [Authentication Tutorial](/tutorials/authentication) for secure key handling patterns. ## Node.js / TypeScript Perfect for full-stack developers and web applications. ```bash theme={null} npm install openai dotenv ``` Create a `.env` file with your API key: ```bash theme={null} GLOO_API_KEY=your_api_key_here ``` Initialize the OpenAI client with your API key. ```typescript theme={null} import OpenAI from "openai"; import * as dotenv from "dotenv"; dotenv.config(); async function main() { const client = new OpenAI({ apiKey: process.env.GLOO_API_KEY!, baseURL: "https://platform.ai.gloo.com/ai/v2/guarded", }); const completion = await client.chat.completions.create({ model: "gloo-anthropic-claude-sonnet-4.5", messages: [{ role: "user", content: "Say this is a test" }], }); console.log(completion.choices[0].message.content); } main(); ``` For production applications, store your API key in a secrets manager rather than in plaintext `.env` files. See the [Authentication Tutorial](/tutorials/authentication) for secure key handling patterns. ## OpenAI Compatibility Gloo AI's primary completions endpoint (`/ai/v2/guarded/chat/completions`) is shaped like OpenAI Chat Completions, so any client that accepts a custom `base_url` and `api_key` works with minimal changes. The table below maps standard fields to Gloo AI equivalents and highlights differences unique to the platform. ### Compatibility Matrix | Capability | OpenAI | Gloo AI | | :- | :- | :- | | **Authentication** | Static API key | Same — API key passed as `api_key` (bearer token) | | **Base URL** | `https://api.openai.com/v1` | `https://platform.ai.gloo.com/ai/v2/guarded` | | `messages` | Required | Required — same format | | `model` | Required | Required by the SDK client; omit only when using `auto_routing` or `model_family` via direct HTTP | | `stream` | Supported | Supported | | `temperature` | Supported | Supported | | `max_tokens` | Supported | Supported | | `tools` / `tool_choice` | Supported | Supported | | `auto_routing` | Not supported | **Gloo-specific** — let Gloo pick the best model automatically | | `model_family` | Not supported | **Gloo-specific** — pick by provider (`openai`, `anthropic`, `google`, `open source`) | | `tradition` | Not supported | **Gloo-specific** — theological perspective filter (`evangelical`, `catholic`, `mainline`) | | Response routing metadata | Not included | **Gloo-specific** — `provider`, `model_family`, `routing_mechanism`, `routing_tier`, `routing_confidence` added to every response | | Grounded (RAG) completions | Not supported | **Gloo-specific** — separate path `/ai/v2/grounded/chat/completions`; `base_url` and `model` differ | Exactly one routing mechanism must be specified on every V2 request: `auto_routing: true`, `model`, or `model_family`. Unlike the OpenAI API, `model` is not always required—set `auto_routing: true` to have Gloo choose the best model automatically. ### Passing Gloo-Specific Parameters How you pass Gloo-specific fields depends on whether you're specifying a model directly or using Gloo's routing. **Using a model directly with optional filters (e.g. `tradition`)** When `model` is present, the SDK is satisfied and you only need to forward the extra Gloo fields. In Python, use `extra_body`. In TypeScript, the OpenAI SDK doesn't have `extra_body`, so use the client's low-level `.post()` method instead. ```python Python theme={null} response = client.chat.completions.create( model="gloo-anthropic-claude-haiku-4.5", messages=[{"role": "user", "content": "What does Ephesians 2:8-9 mean?"}], extra_body={"tradition": "evangelical"}, ) print(response.choices[0].message.content) ``` ```typescript TypeScript theme={null} // TypeScript SDK has no extra_body; use the client's .post() instead const response = await (client as any).post('/chat/completions', { body: { model: 'gloo-anthropic-claude-haiku-4.5', messages: [{ role: 'user', content: 'What does Ephesians 2:8-9 mean?' }], tradition: 'evangelical', }, }); console.log(response.choices[0].message.content); ``` **Using Gloo routing (`auto_routing` or `model_family`)** Both SDKs enforce `model` as a required argument client-side. Since Gloo's routing modes are designed to be used without a `model` field, you need to bypass the SDK validation entirely and call the API directly. ```python Python theme={null} import requests response = requests.post( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", headers={ "Authorization": f"Bearer {os.getenv('GLOO_API_KEY')}", "Content-Type": "application/json", }, json={ "messages": [{"role": "user", "content": "What does Ephesians 2:8-9 mean?"}], "auto_routing": True, "tradition": "evangelical", }, ) response.raise_for_status() print(response.json()["choices"][0]["message"]["content"]) ``` ```typescript TypeScript theme={null} const response = await (client as any).post('/chat/completions', { body: { messages: [{ role: 'user', content: 'What does Ephesians 2:8-9 mean?' }], auto_routing: true, tradition: 'evangelical', }, }); console.log(response.choices[0].message.content); ``` The Grounded Completions endpoint uses a different base URL (`https://platform.ai.gloo.com/ai/v2/grounded`) and requires `rag_publisher` and other Gloo-specific fields. Use the same direct HTTP approach above. See the [Grounded Completions guide](/api-guides/grounded-completions) for full details. ## Gloo Code — AI Coding Agent If you want a fully autonomous coding agent rather than editor autocomplete, check out [Gloo Code](/gloocode/overview). It runs as a standalone terminal agent that can plan, build, test, and commit code on its own — at up to 60–70% lower cost than equivalent direct-provider pricing. **Gloo Code doesn't use the credentials on this page.** You sign in to it from the browser with your Gloo AI account, and it manages its own credential — no API key or Client ID to create. See the [Gloo Code installation guide](/gloocode/installation). ## Vibe Coding & Editor Setup Because Gloo AI adheres to open standards, you can use it directly inside AI-native code editors (like **Cursor**, **Windsurf**, or **VS Code**) to "vibe code" with values-aligned models. 1. Go to **Settings > Models** 2. Add a Custom Provider 3. Set URL: `https://platform.ai.gloo.com/ai/v2/guarded` 4. Paste your API key Compatible with **LangChain**, **CrewAI**, and **AutoGen** by setting the `openai_api_base` parameter and providing your API key. Your API key doesn't expire, but if you rotate it, update the key in your editor's custom provider settings. See the [API Keys page](/studio/manage-api-credentials) to manage keys. **Connect our docs to your AI tools via MCP.** Get accurate API references, model IDs, and code examples directly in your editor or AI assistant. See the [MCP Integration](/api-guides/mcp-integration) guide to set up in one click. ## Supported Models When using the SDK, you must use the exact Model IDs supported by Gloo AI. Here are some commonly used models: | Provider | Model ID | | :- | :- | | **Anthropic** | `gloo-anthropic-claude-sonnet-4.5` | | **Anthropic** | `gloo-anthropic-claude-haiku-4.5` | | **OpenAI** | `gloo-openai-gpt-5-mini` | | **Google** | `gloo-google-gemini-2.5-flash` | | **Open Source** | `gloo-meta-llama-3.1-8b-instruct` | See the [Supported Models](/api-guides/supported-models) page for the full list of available Model IDs and their capabilities. # Search Source: https://docs.gloo.com/api-guides/search A developer’s guide to AI-powered semantic search for querying ingested content. ## Overview Our customers entrust us with their content, and in order to then gain a real business benefit from their content, they want to provide selective, context-aware access to it to their customers, depending on their needs. Search is how we help customers present their data, based on the context their customers provide, in the form of a query. That’s the beauty of AI-based, near-text, semantic search, and that’s why we use it at Gloo. For example, if one of our customers adds articles and podcasts about keeping a marriage strong, and during one of the podcast episodes, the discussion focuses on the guest’s top five rules for keeping a marriage healthy, we will likely want search queries that ask for “secrets to a happy marriage” to include in that particular podcast in the results. The word “secret” doesn’t necessarily have to be included in the content, and the word “rule” doesn’t have to be in the search query, but nevertheless, the results should include this podcast episode. ## Why use Search? Search queries content that Gloo has ingested and indexed for you. If you manage your own vector store and retrieval, use the [Embeddings API](/api-guides/embeddings) to create vectors. Specifically, search allows you to query your ingested content, to see what you’re presenting to the world, through Gloo AI. Our API returns your content, broken up into snippets, showing what we provide to our internal chat-based tools, and what you can use in your own applications. Snippets are sections of a content item, e.g. podcast episode, article, book, that represent a large enough piece of meaningful information to be searchable, and understandable by our AI tools. These snippets also represent a recorded unit of searchable content within our backend systems. ### RAG - Retrieval Augmented Generation When constructing an AI application, it is sometimes useful to provide additional context to the AI model. Retrieval Augmented Generation allows you to insert these search results in the message to the model, and this can provide a more successful, context-aware interaction with your customers. *** ## Making Requests ### Prerequisites Before making a request, you must know which publisher's content you would like to query. 1. **Find Your Tenant (Publisher Name):** First, navigate to the [Organizations page](/studio/manage-organizations) in Studio and click "View Publishers". Your **Tenant** is the name of the publisher you're querying. 2. **Identify the Collection:** A collection is where we keep the content for our customers. The only collection you'll need to specify in requests is `GlooProd`. You must specify this in the `collection` field. **NOTE:** You will only have access to your own publisher's tenant through this endpoint. For multi-tenant collections such as `GlooProd`, a tenant must be specified; requests outside your tenant return a 403 "Forbidden" response. **Tenant names are normalized.** Spaces and whitespace 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 tenant string, call `GET /ai/v1/data/tenant/names` and use the value returned. This is the recommended approach rather than constructing the tenant name yourself. ### How many records can I retrieve? We recommend anywhere from 10 to 100 results, though request time will rise non-linearly with larger result sets. The `limit` field is how you specify how many records to return. ### How do I make a request? Once you have your tenant name, you're ready to query the search endpoint. You can get an API key from the [API Keys page](/studio/manage-api-credentials) and send it directly as a Bearer token. **Endpoint:** `POST /ai/v1/data/search` #### Example cURL Request ```bash cURL theme={null} curl 'https://platform.ai.gloo.com/ai/v1/data/search' -v \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer ${GLOO_API_KEY}" \ -d '{"query":"How can I find true happiness?","collection":"GlooProd","tenant": "TestOrg","certainty":0.5,"limit":1}' ``` Example Response The result should look something like this: ```json JSON theme={null} { "data": [ { "uuid": "12345678-abcd-1234-abcd-1234567890ab", "metadata": { "creation_time": "2025-09-02T23:49:49.338000Z", "last_update_time": "2025-09-02T23:50:36.348000Z", "distance": 0.39595216512680054, "certainty": 0.8020238876342773, "score": 0.0, "explain_score": "", "is_consistent": false, "rerank_score": null }, "properties": { "pub_type": "technical", "item_file": "", "h3_subtitle": "", "item_tags": [ "automated", "ingestion" ], "screen_cap": "", "h2_subtitle": "", "publisher_bw": null, "item_title": "Finding True Happiness", "publisher_hierarchy_fav": null, "publisher_hierarchy": null, "evergreen": true, "h2_id": "", "organization": null, "h3_image": "", "denomination": "", "producer_id": "", "time_start": "", "publisher": "Test Org", "publisher_logo": null, "h2_title": "", "h2_number": "", "publisher_hierarchy_bw": null, "duration": "", "h3_id": "", "pages": "", "publisher_hierarchy_logo": null, "filename": "Finding_True_Happiness_realtime", "h2_url": "", "time_end": "", "drm": null, "h3_title": "", "h3_url": "", "item_extra": "", "item_image": "", "item_id": "", "isbn": "", "h3_file": "", "hosted_url": "", "organization_id": "", "h3_number": "", "publisher_fav": null, "publication_date": "2025-09-02", "publisher_url": "", "h2_image": "", "publisher_id": "", "item_number": "", "h2_file": "", "item_subtitle": "", "h2_extra": "", "h3_summary": "", "h2_summary": "", "snippet": "# Finding True Happiness: A Christian Perspective In a world obsessed with pursuing happiness through material success, relationships, and experiences, Christianity offers a radically different path to lasting joy. True happiness, from a biblical worldview, isn't found in circumstances but in our relationship with God and alignment with His purposes. ## Joy Rooted in Identity The foundation of Christian happiness begins with understanding our identity as beloved children of God. Romans 8:16 reminds us that \"The Spirit himself testifies with our spirit that we are God's children.\" This secure identity provides unshakeable joy that doesn't depend on external validation, achievements, or circumstances. When we truly grasp that we are unconditionally loved by the Creator of the universe, a deep contentment settles in our hearts. ## The Beatitudes: God's Blueprint for Blessedness", "part": 0, "h3_extra": "", "item_url": "", "type": "Article", "start_index": 0, "publisher_hierarchy_url": null, "item_summary": "", "author": [ "Automated Ingestion" ], "summaries": { "ai_phd_student": "This discourse analyzes the theological framework of eudaimonia within Christian doctrine, particularly examining the correlation between divine filial relationship and authentic happiness.", "ai_loving": "Beloved, discover the beautiful truth that your happiness is secured in being God's precious child, far beyond what this world can offer.", "ai_news_anchor": "In today's discussion, we explore the Christian perspective on happiness, highlighting how spiritual identity trumps material success in achieving lasting joy.", "ai_elegant": "An exploration of authentic happiness through the Christian paradigm, emphasizing the profound connection between divine identity and genuine joy.", "ai_simple": "True happiness comes from knowing we are God's children, not from things or success in the world.", "ai_casual": "Hey, this piece talks about how real happiness isn't about stuff or success, but about knowing you're God's child and finding joy in that relationship.", "ai_first_grader": "God loves us very much and that makes us happy. Being God's children is better than having toys or things to make us happy.", "ai_creative": "Journey into the heart of happiness through heaven's lens, where joy blooms not in earthly gardens but in the fertile soil of divine identity.", "ai_formal": "This text examines the theological perspective on authentic happiness, emphasizing its foundation in divine sonship rather than material or circumstantial factors.", "ai_biblical": "The text explores the Christian understanding of true happiness as rooted in our identity as God's children, contrasting worldly pursuits with spiritual joy found in relationship with God." }, "biblical_analysis": { "ai_biblical_concepts": [ "Divine adoption", "Spiritual identity", "Beatitudes", "Kingdom happiness" ], "ai_bible_characters": [ "Jesus" ], "ai_bible_references": [ "Romans 8:16" ], "ai_book_of_the_bible": "Romans", "ai_bible_phrases": [ "we are God's children" ], "ai_bible_concepts": [ "Identity in Christ", "Divine Sonship", "Joy", "Contentment" ], "ai_biblical_lesson": "True happiness is found in understanding and embracing our identity as children of God rather than pursuing worldly sources of satisfaction.", "ai_bible_verses": [ "The Spirit himself testifies with our spirit that we are God's children" ] }, "general_analysis": { "ai_profile_gender": "Not targeted to a specific gender", "ai_personas": [ "Spiritual seekers", "Christians seeking growth", "People questioning happiness" ], "ai_keywords": [ "happiness", "joy", "identity", "contentment", "Christianity" ], "ai_topic_classification": "Christian Spiritual Formation", "ai_reading_duration": 3.0, "ai_read_more_suggestion": [ "Understanding the Beatitudes", "Identity in Christ", "Biblical Joy" ], "ai_famous_quotes": [], "ai_meta_description": "Explore the Christian perspective on finding true happiness through understanding your identity as God's child rather than worldly success.", "ai_scripture_parallels": [ "Psalm 16:11 - Joy in God's presence", "Philippians 4:4 - Rejoicing in the Lord" ], "ai_user_activity": "Reading time: 3 minutes, Ideal for morning devotional", "ai_hashtags": [ "ChristianJoy", "TrueHappiness", "IdentityInChrist", "BiblicalTruth" ], "ai_user_interests": [ "Christianity", "Spiritual growth", "Personal development", "Biblical teaching" ], "ai_profile_alignment": [ "Christian believers", "Spiritual seekers", "Personal development enthusiasts" ], "ai_why_bullets": [ "Provides an alternative to materialistic views of happiness", "Offers lasting joy independent of circumstances", "Establishes secure identity foundation" ], "ai_interactive_callout": "Take a moment to reflect on what truly brings you lasting joy", "ai_user_retention_tip": "Subscribe to receive weekly devotionals on finding joy in Christ", "ai_complexity": 7.0, "ai_life_issues": [ "identity", "happiness", "contentment", "purpose" ], "ai_mood_board": "Soft, warm colors, peaceful natural settings, uplifting imagery of light and hope", "ai_important_phrase": "True happiness isn't found in circumstances but in our relationship with God", "ai_search_snippet": "Discover how Christianity offers a unique perspective on finding lasting happiness through spiritual identity rather than material success.", "ai_subtitle": "Discovering Lasting Joy in Divine Identity", "ai_thriving": [ "spiritual fulfillment", "emotional stability", "identity security" ], "ai_intent": "educational", "ai_sentiment": "positive", "ai_audio_snippet": "Gentle, contemplative background music with soft piano", "ai_title_variations": [ "The Christian Path to Lasting Joy", "God's Blueprint for True Happiness", "Finding Joy in Divine Identity" ], "ai_poll_question": "What do you believe is the primary source of lasting happiness?", "ai_emotional_resonance": [ "hope", "peace", "contentment", "security" ], "ai_next_steps": [ "Reflect on your identity as God's child", "Study the Beatitudes", "Evaluate your sources of happiness" ], "ai_reader_impact": "Transformed perspective on happiness and personal identity", "ai_books_mentioned": [], "ai_music_recommendation": "Contemporary Christian worship music focusing on joy and identity", "ai_reader_motivation": "Seeking authentic, lasting joy beyond material success", "ai_readability": 8.0, "ai_sharing_probability": 0.7, "ai_engagement_score": 8.0, "ai_terminology_definitions": [ "Beatitudes: Jesus teachings on true happiness and blessedness" ], "ai_google_search": [ "Christian perspective on happiness", "finding true joy in God", "identity in Christ" ], "ai_conversion_potential": "High potential for newsletter signups and devotional subscriptions", "ai_sharing_message": "Find out why true happiness goes deeper than circumstances and success", "ai_tweet": "True happiness isn't found in what we have or do, but in who we are - beloved children of God.", "ai_song_lyric": "In Your presence there is fullness of joy", "ai_famous_people": [], "ai_search_engine_tags": [ "Christian happiness", "biblical joy", "spiritual identity", "finding true contentment" ], "ai_ad_placement_opportunity": "Christian counseling services, spiritual growth resources", "ai_describing_words": [ "radically", "different", "lasting", "unshakeable", "deep" ], "ai_reader_takeaway": "True happiness comes from understanding and embracing our identity as God's children", "ai_content_clusters": [ "Christian Living", "Spiritual Growth", "Biblical Teaching", "Personal Development" ], "ai_title": "Finding True Happiness: A Christian Perspective", "ai_questions_answered": [ "Where can true happiness be found?", "What is the Christian perspective on happiness?" ], "ai_comment_prompt": "How has your understanding of happiness changed through your spiritual journey?", "ai_standalone": "high", "ai_social_media_caption": "Discover the secret to lasting happiness through understanding your identity as God's beloved child.", "ai_key_statistics": [], "ai_quiz_question": "According to the text, what is the foundation of Christian happiness?", "ai_meta_keywords": [ "Christian happiness", "spiritual joy", "identity in Christ", "biblical contentment" ], "ai_christian_topics": [ "Identity in Christ", "Biblical Joy", "Divine Sonship", "Beatitudes" ], "ai_image_prompt": "A serene landscape with rays of sunlight breaking through clouds, symbolizing divine love and joy", "ai_feedback_request": "Has this article helped you understand the Christian perspective on happiness?", "ai_why_matter": "Understanding true happiness from a Christian perspective can transform how we approach life satisfaction and personal fulfillment.", "ai_flourishing": [ "spiritual growth", "emotional wellbeing", "identity security" ], "ai_traffic_prediction": 0.8 } }, "references": null, "vector": {}, "collection": "GlooProd" } ], "intent": 1 } ``` # Supported Models Source: https://docs.gloo.com/api-guides/supported-models A full list of Gloo AI API models and their capabilities, rendered live from the public platform API. The Gloo AI platform provides access to a wide range of leading models from multiple providers. Visit the **[Model Explorer in Gloo Studio](https://studio.ai.gloo.com/models)** for a richer side-by-side comparison including reasoning capabilities, modalities, and speed ratings. The table below is fetched live in your browser from the public, unauthenticated [`GET /platform/v2/models`](/api-reference/get-models/v2) endpoint — so it always reflects the current platform catalog. Use the `Model ID` as the `model` parameter on the endpoint for that model type. Generation models work with Responses and Completions; models marked **Embedding** work with the [Embeddings API](/api-guides/embeddings). The rates shown are **list prices**. Guarded generation endpoints add the platform rate described in [Studio Billing](/studio/billing); direct endpoints, including Embeddings, bill at the published rate. Track real spend in the [Gloo Studio billing dashboard](https://studio.ai.gloo.com/billing). **Generation models.** Gloo AI can route generation requests to providers that serve the model. Embedding requests use the embedding model you name and are not failed over to another provider. See [Provider Failover](/api-guides/provider-failover). Generation model IDs work across the [Responses API](/api-guides/responses) and [Completions V2](/api-guides/completions-v2). Embedding model IDs are for the [Embeddings API](/api-guides/embeddings). Capability flags (`supports_tools`, `supports_streaming`, `supports_reasoning`, `supports_vision`) apply to generation; the model catalog returns them for all entries. ## Deprecated Models Models being retired are flagged **live** in the catalog table below: a deprecated model renders a **Deprecated** badge next to its ID, with its deprecation note inline. Those come from the `is_deprecated` and `deprecation_note` fields on [`GET /platform/v2/models`](/api-reference/get-models/v2), alongside `replacement_model` — the ID that requests are routed to. For generation models, where a replacement has been chosen, requests using the deprecated ID may continue to be routed to that replacement while you migrate. Do not assume replacement routing applies to embeddings: changing embedding models requires re-embedding stored content. Read `deprecation_note` for the specifics, including any price change or missing replacement. See **[Model Lifecycle](/api-guides/model-lifecycle)** for how a replacement is chosen, what the price can do, how much notice you get, and how to detect all of it from the API. ## Prompt Caching For generation models, the **Caching** column shows which models support prompt caching, and which type — **Implicit** (automatic, e.g. OpenAI, DeepSeek, Gemini, Qwen) or **Explicit** (opt-in per request, e.g. Anthropic). Caching does not apply to embedding models. For full details on each provider's caching mechanism, billing rates, and best practices, see the dedicated **[Prompt Caching](/api-guides/prompt-caching)** guide. # Supported Models (Legacy) Source: https://docs.gloo.com/api-guides/supported-models-legacy Models available for deprecated Completions V1 and Chat APIs. **Deprecation Notice**: These models are for the deprecated Completions V1 and Chat APIs. New projects should start with Completions v2 immediately. Existing integrations should migrate as soon as possible. **[View Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** The following models are available for the standard Gloo AI API endpoints (Completions V1 and Chat APIs). ## Anthropic | Model ID | Max Tokens | Tool Use | Streaming | | - | - | - | - | | `anthropic.claude-3-5-sonnet-20240620-v1:0` | 200,000 | Yes | Yes \* | | `us.anthropic.claude-3-5-sonnet-20240620-v1:0` | 200,000 | Yes | Yes \* | | `us.anthropic.claude-3-5-sonnet-20241022-v2:0` | 200,000 | Yes | Yes \* | | `us.anthropic.claude-3-opus-20240229-v1:0` | 200,000 | Yes | Yes\* | | `anthropic.claude-3-sonnet-20240229-v1:0` | 200,000 | Yes | Yes\* | | `anthropic.claude-3-haiku-20240307-v1:0` | 200,000 | Yes | Yes\* | | `anthropic.claude-v2:1` | 100,000 | Yes | Yes\* | | `anthropic.claude-instant-v1` | 100,000 | Yes | Yes\* | (\*) The `claude-3-5-sonnet` models support streaming for tool use but may not adhere to the official OpenAI SDK standard for this feature. ## Meta | Model ID | Max Tokens | Tool Use | Streaming | | :- | :- | :- | :- | | `meta.llama3-70b-instruct-v1:0` | 128,000 | Yes | Yes | | `meta.llama3-8b-instruct-v1:0` | 128,000 | Yes | Yes | | `us.meta.llama3-1-70b-instruct-v1:0` | 128,000 | Yes | Yes | | `us.meta.llama3-3-70b-instruct-v1:0` | 128,000 | Yes | Yes | ## Mistral | Model ID | Max Tokens | Tool Use | Streaming | | :- | :- | :- | :- | | `mistral.mistral-large-2402-v1:0` | 65,536 | Yes | Yes | | `mistral.mixtral-8x7b-instruct-v0:1` | 32,768 | Yes | Yes | | `mistral.mistral-small-2402-v1:0` | 32,768 | Yes | Yes | | `mistral.mistral-7b-instruct-v0:2` | 32,768 | Yes | Yes | ## Amazon | Model ID | Max Tokens | Tool Use | Streaming | | :- | :- | :- | :- | | `amazon.titan-text-premier-v1:0` | 30,720 | Check API | Yes | | `amazon.titan-text-express-v1` | 8,192 | No | Yes | | `amazon.titan-text-lite-v1` | 4,096 | No | Yes | | `amazon.nova-micro-v1:0` | 2,048 | Yes | Yes\* | ## Cohere | Model ID | Max Tokens | Tool Use | Streaming | | :- | :- | :- | :- | | `cohere.command-r-plus-v1:0` | 128,000 | Yes | Yes | | `cohere.command-r-v1:0` | 128,000 | Yes | Yes | | `cohere.command-text-v14` | 8,192 | No | Yes | ## AI121 Labs | Model ID | Max Tokens | Tool Use | Streaming | | :- | :- | :- | :- | | `ai21.jamba-instruct-v1:0` | 8,192 | Yes | Yes | | `ai21.jamba-1-5-large-v1:0` | 8,192 | Yes | Yes | | `ai21.jamba-1-5-mini-v1:0` | 8,192 | Yes | Yes | ## DeepSeek | Model ID | Max\_Tokens | Tool Use | Streaming | | - | - | - | - | | `us.deepseek.r1-v1:0` | 16,000 | Check API | Check API | # Tool Use Source: https://docs.gloo.com/api-guides/tool-use Enable LLMs to call external functions and tools on the Responses API and Completions V2. Tool use is supported on **both** the [Responses API](/api-guides/responses) (`/ai/v2/guarded/responses` — the recommended surface for new integrations) and [Completions V2](/api-guides/completions-v2) (`/ai/v2/guarded/chat/completions`). Tool definitions use the **identical OpenAI function schema** on both endpoints; they differ only in how the request is framed (`input` vs `messages`) and how tool calls are returned (`output[]` typed items vs `choices[].message.tool_calls`). **Completions V1 is Deprecated**: `/ai/v1/chat/completions` is the legacy Completions V1 endpoint and is no longer recommended. Migrate to either the [Responses API](/api-guides/responses) (recommended for new integrations) or [Completions V2](/api-guides/completions-v2) — this guide covers both. > Adding Tools to the Completions API allows you to grow your experience with Gloo AI, adding capabilities that are human flourishing. *** ## Extended Capabilities Beyond Text Generation All models are excellent at language generation, but they can't access real-time data or perform calculations natively. Tools allow the model to: * Fetch up-to-date information (e.g., weather, stock prices, calendars). * Execute code or math (e.g., calculate complex equations, generate charts). * Interact with databases, APIs, or internal systems (e.g., CRM updates, file searches, emails). This turns the AI into a universal interface for diverse tasks. ### Automation of Multi-Step Workflows By using tools, Gloo AI models can: * Chain together steps in a workflow (e.g., "Analyze this document, then email a summary"). * Act like a coordinator between systems (e.g., Slack + GitHub + Notion integration). * Enable interactive applications where the AI can take actions and report back results. This is vital for real-world applications where AI becomes a true assistant rather than just a chatbot. ### Improved Accuracy and Reliability Rather than "hallucinating" answers, the model can use tools to ground its outputs in real data. By delegating precision tasks like date parsing, data lookup, or logic execution to deterministic systems, you reduce the chance of errors and make the AI trustworthy in mission-critical scenarios. *** ## Using Tools with the Responses API The [Responses API](/api-guides/responses) (`/ai/v2/guarded/responses`) is the recommended endpoint for new integrations and supports the same function-calling capability as Completions V2. **Tool definitions use the identical OpenAI function schema** — `{"type": "function", "function": { ... }}`. The difference is on the response side: a tool call comes back as a typed `function_call` item inside the `output[]` array (rather than `choices[].message.tool_calls`). The Responses API uses pinned models — pass the exact `model` you want. Auto-routing and `model_family` selection are [Completions V2](/api-guides/completions-v2) features; use Completions V2 if you need them. ### Defining tools Send `tools` (and optionally `tool_choice`) alongside `input`. Because Gloo is OpenAI-compatible, you can use the official OpenAI SDKs — set the base URL to `/ai/v2/guarded` and call `responses.create`. Use the **Chat Completions (nested) function schema** — `{"type": "function", "function": { ... }}` — on the Responses API, the same schema Completions V2 uses. Pass that nested object even when calling the SDK's `responses.create` (in TypeScript, add `// @ts-ignore` since the SDK types expect the flat Responses schema). The flat Responses tool schema is **not** reliably accepted across providers. ```bash cURL theme={null} curl https://platform.ai.gloo.com/ai/v2/guarded/responses \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $GLOO_API_KEY" \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.6", "input": [ { "role": "user", "content": "What is the weather like in Shanghai today?" } ], "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] } } } ], "tool_choice": "required" }' ``` ```python Python theme={null} import os from openai import OpenAI client = OpenAI( api_key=os.environ["GLOO_API_KEY"], base_url="https://platform.ai.gloo.com/ai/v2/guarded", ) tools = [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": {"type": "string"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["location"], }, }, } ] response = client.responses.create( model="gloo-anthropic-claude-sonnet-4.6", input=[{"role": "user", "content": "What is the weather like in Shanghai today?"}], tools=tools, tool_choice="required", ) # The tool call is a typed function_call item in the output array tool_call = next(item for item in response.output if item.type == "function_call") print(tool_call.name, tool_call.arguments) ``` ```typescript TypeScript theme={null} import { OpenAI } from 'openai'; const client = new OpenAI({ apiKey: process.env.GLOO_API_KEY, baseURL: 'https://platform.ai.gloo.com/ai/v2/guarded', }); const response = await client.responses.create({ model: 'gloo-anthropic-claude-sonnet-4.6', input: [{ role: 'user', content: 'What is the weather like in Shanghai today?' }], // Gloo expects the nested Chat Completions function schema // @ts-ignore - nested tool schema tools: [ { type: 'function', function: { name: 'get_current_weather', description: 'Get the current weather in a given location', parameters: { type: 'object', properties: { location: { type: 'string' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }, }, required: ['location'], }, }, }, ], tool_choice: 'required', }); const toolCall = response.output.find((item) => item.type === 'function_call'); console.log(toolCall?.name, toolCall?.arguments); ``` ### Tool call output When the model invokes a tool, the response contains a `function_call` item in `output[]`: ```json JSON theme={null} { "id": "resp_a1b2c3", "object": "response", "model": "gloo-anthropic-claude-sonnet-4.6", "output": [ { "type": "function_call", "id": "fc_abc123", "call_id": "call_abc123", "name": "get_current_weather", "arguments": "{\"location\": \"Shanghai\", \"unit\": \"celsius\"}" } ], "usage": { "input_tokens": 80, "output_tokens": 18, "total_tokens": 98 } } ``` ### Returning a tool result (multi-turn) Run your function, then continue the conversation by appending two items to `input`: the model's `function_call`, followed by a `function_call_output` carrying the result under the matching `call_id`. ```python Python theme={null} # Continues from `response` / `tool_call` in the previous example result = get_current_weather(tool_call.arguments) # your function -> JSON string final = client.responses.create( model="gloo-anthropic-claude-sonnet-4.6", input=[ {"role": "user", "content": "What is the weather like in Shanghai today?"}, { "type": "function_call", "call_id": tool_call.call_id, "name": tool_call.name, "arguments": tool_call.arguments, }, { "type": "function_call_output", "call_id": tool_call.call_id, "output": result, }, ], ) print(final.output_text) ``` ```json Request body theme={null} { "model": "gloo-anthropic-claude-sonnet-4.6", "input": [ { "role": "user", "content": "What is the weather like in Shanghai today?" }, { "type": "function_call", "call_id": "call_abc123", "name": "get_current_weather", "arguments": "{\"location\": \"Shanghai\", \"unit\": \"celsius\"}" }, { "type": "function_call_output", "call_id": "call_abc123", "output": "{\"temperature\": 22, \"unit\": \"celsius\", \"conditions\": \"sunny\"}" } ] } ``` The model uses the tool result to produce its final answer as a `message` item. `function_call` and `function_call_output` items both require a matching `call_id`. *** ## Using Tools with Completions V2 Gloo provides several models with tooling support. For a complete and up-to-date list of all available models and their specific capabilities, please see our [**Supported Models Guide**](/api-guides/supported-models). ### Tool Calling with V2 Routing Tool calling works seamlessly with all Completions V2 routing mechanisms: * **AI Core (auto\_routing: true)**: Gloo automatically selects the best model for your tools based on query complexity * **AI Core Select (model\_family)**: Tools work with your preferred provider family (OpenAI, Anthropic, Google, Open Source) * **AI Select (model)**: Direct model selection with explicit tool support For details on routing mechanisms, see the [Completions V2 Guide](/api-guides/completions-v2). ### Supported Models for Tool Use Gloo V2 provides multiple models with tool calling support across different providers: | Model Family | Example Models | Tool Use | Streaming | | - | - | - | - | | OpenAI | `gloo-openai-gpt-5.5-pro`, `gloo-openai-gpt-5-mini` | Yes | Yes | | Anthropic | `gloo-anthropic-claude-sonnet-4.5`, `gloo-anthropic-claude-haiku-4.5` | Yes | Yes | | Google | `gloo-google-gemini-2.5-pro`, `gloo-google-gemini-2.5-flash` | Yes | Yes | | Open Source | `gloo-deepseek-v3.2`, `gloo-openai-gpt-oss-120b` | Yes | Varies | For the complete list of supported models and their capabilities, see our [**Supported Models Guide**](/api-guides/supported-models). ### Using cURL ```bash cURL theme={null} curl https://platform.ai.gloo.com/ai/v2/guarded/chat/completions \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $GLOO_API_KEY" \ -d '{ "model": "gloo-anthropic-claude-sonnet-4.5", "auto_routing": false, "messages": [ { "role": "user", "content": "What is the weather like in Shanghai today?" } ], "stream": false, "tools": [ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] } } } ], "tool_choice": "required" }' ``` ### Using the OpenAI SDK (Python) ```python Python theme={null} # Setup import os from openai import OpenAI client = OpenAI( api_key=os.environ["GLOO_API_KEY"], base_url='https://platform.ai.gloo.com/ai/v2/guarded' # Guarded V2 base URL ) # Chat Completions call with tooling response = client.chat.completions.create( model="gloo-anthropic-claude-sonnet-4.5", messages=[ { "role": "user", "content": "What is the weather like in Shanghai today?" } ], stream=False, tools=[ { "type": "function", "function": { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": {"type": "string"}, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] } } } ], tool_choice="required", extra_body={"auto_routing": False} ) print(response) ``` ```typescript TypeScript theme={null} // Setup import { OpenAI } from 'openai'; const client = new OpenAI({ apiKey: process.env.GLOO_API_KEY, baseURL: 'https://platform.ai.gloo.com/ai/v2/guarded', // Guarded V2 base URL }); // Chat completions with tooling const response = await client.chat.completions.create({ model: 'gloo-anthropic-claude-sonnet-4.5', messages: [ { role: 'user', content: 'What is the weather like in Shanghai today?', }, ], stream: false, tools: [ { type: 'function', function: { name: 'get_current_weather', description: 'Get the current weather in a given location', parameters: { type: 'object', properties: { location: { type: 'string' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'], }, }, required: ['location'], }, }, }, ], tool_choice: 'required', // @ts-ignore - V2-specific parameter auto_routing: false, }); console.log(response); ``` ### Using Inngest's AgentKit SDK (TypeScript) ```typescript TypeScript theme={null} import { createAgent, createTool, openai, } from "@inngest/agent-kit"; import { z } from "zod"; const weatherTool = createTool({ name: "check_weather", description: "Returns a string of the weather conditions in a city. Call this to know the weather of a certain city.", parameters: z.object({ location: z.string(), unit: z.enum(["celcius", "fahrenheit"]), }), handler: async ({ location, unit }) => { return `The weather in ${location} is sunny with a temperature of 81 ${unit}.`; }, }); const agent = createAgent({ name: 'Weather Agent', description: 'An agent that describes the weather in various cities.', system: 'You are an expert at telling the weather of various cities.', model: openai({ baseURL: 'https://platform.ai.gloo.com/ai/v2/guarded', // Guarded V2 base URL apiKey: process.env.GLOO_API_KEY, model: 'gloo-anthropic-claude-sonnet-4.5', }), tools: [weatherTool], }); const response = await agent.run("What is the weather like in Shanghai today?") console.log(JSON.stringify(response)); ``` ### Example Response ```json JSON theme={null} { "id": "chatcmpl-f11e1872", "choices": [ { "finish_reason": "tool_calls", "index": 0, "logprobs": null, "message": { "content": null, "refusal": null, "role": "assistant", "annotations": null, "audio": null, "function_call": null, "tool_calls": [ { "id": "tooluse_cabMjx5dQG20v3iUwg0LSQ", "function": { "arguments": "{\"unit\": \"celsius\", \"location\": \"Shanghai\"}", "name": "get_current_weather" }, "type": "function" } ] } } ], "created": 1746053578, "model": "gloo-anthropic-claude-sonnet-4.5", "object": "chat.completion", "service_tier": null, "system_fingerprint": "fp", "usage": { "completion_tokens": 38, "prompt_tokens": 1196, "total_tokens": 1234, "completion_tokens_details": null, "prompt_tokens_details": null } } ``` # Upload Content Source: https://docs.gloo.com/api-guides/upload-content Send files to Gloo AI for automatic processing and indexing. You must have at least one [Publisher](/getting-started/glossary#publisher) in your organization before uploading. See [Manage Publishers](/studio/manage-publishers). **Upload once. Search instantly. Build faster.** The Data Engine Files API lets you send any file directly to Gloo AI for real-time processing and indexing. Within minutes, your content becomes searchable, context-aware, and ready for AI-driven interactions—no preprocessing or custom pipelines required. ## Why It Matters Developers often spend weeks wiring together upload, parsing, and indexing pipelines. The Data Engine Files API eliminates that complexity: * **Unified ingestion** for all major file types * **Automatic parsing and indexing** with no manual steps * **Instant availability** in your Gloo AI workspace * **Scalable performance** for millions of files ## Prerequisites Before starting, ensure you have: * A **Gloo AI Studio** account * Your **API key** from the [API Keys page](/studio/manage-api-credentials) The Files API requires an API key for authentication. If you haven't set up authentication yet, follow the [Authentication Tutorial](/tutorials/authentication). ## Upload Files Send files directly to the ingestion endpoint. 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 "files=@/path/to/your/document.pdf" ``` ```python Python theme={null} import requests url = "https://platform.ai.gloo.com/ingestion/v2/files" headers = {"Authorization": f"Bearer {GLOO_API_KEY}"} files = {"files": open("document.pdf", "rb")} response = requests.post(url, headers=headers, files=files) print(response.json()) ``` **Example Response:** ```json theme={null} { "success": true, "message": "File processing started in background.", "ingesting": ["c999008e-de60-495c-8c9f-6a4b59cdb04b"], "duplicates": [] } ``` You can upload multiple files in a single request by adding multiple `-F "files=@..."` parameters. ## Upload with Your Internal ID Use the [`producer_id`](/getting-started/glossary#producer_id) query parameter to associate your internal ID with the uploaded file. This makes it easier to look up items later using your own identifiers. For guidance on choosing a `producer_id` scheme that stays stable across renames and syncs, see [Preparing Content for Ingestion](/best-practices/ingestion-pattern#choose-a-producer_id-scheme-before-the-first-upload). ```bash Shell theme={null} curl -X POST "https://platform.ai.gloo.com/ingestion/v2/files?producer_id=your-internal-id" \ -H "Authorization: Bearer $GLOO_API_KEY" \ -F "files=@/path/to/your/document.pdf" ``` If `producer_id` is supplied with a multi-file upload, the ID will be ignored. Producer IDs must have a one-to-one relationship with a file. ## Next Steps * **Understand what happens after upload**: See [How Chunking Works](/api-guides/chunking) * **Update metadata** after upload: See [Edit Content](/api-guides/edit-content) * **Look up items** by producer ID: See [Manage Content](/api-guides/manage-content#look-up-by-producer-id) * **Full API details**: See [API Reference](/api-reference/ingestion/v2) * **More code examples**: See [Upload Files Recipe](/tutorials/upload-files) Upload content directly from Studio without writing any code. 1. Navigate to **Data Engine** in the Studio sidebar, then click **Ingestion**. 2. Choose your ingestion method: * **Web Scraping** — Enter a website URL or sitemap URL * **Files** — Upload individual files or multiple files via drag-and-drop (various formats, up to 1 GB per document; audio and video up to 2 GB) * **YouTube** — Connect a YouTube channel to automatically build a content library from videos * **Feeds** — Follow RSS/Atom feeds for podcasts or blogs, or ONIX catalog URLs for books 3. Your content will be automatically processed and appear in the **Content Library** with a status indicator. To learn what that processing involves, see [How Chunking Works](/api-guides/chunking). ## Next Steps * **Edit metadata** after upload: See [Edit Content](/api-guides/edit-content) * **View and manage your content**: See [Manage Content](/api-guides/manage-content) # Generate response answer stream Source: https://docs.gloo.com/api-reference/answers/generate-response-answer-stream post /ai/v1/kallm/generate/answers/stream Generates a response answer based on a query and collections from the Search API. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Create chat Source: https://docs.gloo.com/api-reference/chat/create-chat post /ai/{v}/chat Create chat. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Delete chat Source: https://docs.gloo.com/api-reference/chat/delete-chat delete /ai/{v}/chat Delete chat. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Delete message Source: https://docs.gloo.com/api-reference/chat/delete-message delete /ai/{v}/message Delete message. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Delete user Source: https://docs.gloo.com/api-reference/chat/delete-user delete /ai/{v}/user Delete user. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Get chat Source: https://docs.gloo.com/api-reference/chat/get-chat get /ai/{v}/chat Chat history and details for a user. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # List chats Source: https://docs.gloo.com/api-reference/chat/list-chats get /ai/{v}/chats List chats. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Post message Source: https://docs.gloo.com/api-reference/chat/post-message post /ai/{v}/message Start or continue a chat session. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** By providing `chat_id` a new message will be added to the chat session. If no `chat_id` is provided, a new chat session will be created. Default behavior: * Gloo AI will perform a sentiment analysis on your prompt. * Gloo AI will provide suggestion follow ups * Depending on your prompt, Gloo AI will return sources that have influenced the response from our trusted partners. You can choose to override the defaults with the query params: `intent`, `enable_sources`, `enable_suggestions`. **Example 1**: You want a rich answer that is based on trusted sources and provides you follow up suggestions. Omit all query params. **Example 2**: You know that your prompt is going to require trusted sources as a part of the answer. You also want to not have follow up suggestions. `intent`: 1, `enable_suggestions`: 0 **Example 3**: You know that your prompt is not going to require trusted sources. You also want to not have follow up suggestions. `intent`: 0, `enable_suggestions`: 0 # Post message Gloo Chat Source: https://docs.gloo.com/api-reference/chat/post-message-kallm post /ai/{v}/message/kallm Start or continue a chat session. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** By providing `chat_id` a new message will be added to the chat session. If no `chat_id` is provided, a new chat session will be created. Default behavior: * Gloo AI will perform a sentiment analysis on your prompt. * Gloo AI will provide suggestion follow ups * Depending on your prompt, Gloo AI will return sources that have influenced the response from our trusted partners. You can choose to override the defaults with the query params: `intent`, `enable_sources`, `enable_suggestions`. **Example 1**: You want a rich answer that is based on trusted sources and provides you follow up suggestions. Omit all query params. **Example 2**: You know that your prompt is going to require trusted sources as a part of the answer. You also want to not have follow up suggestions. `intent`: 1, `enable_suggestions`: 0 **Example 3**: You know that your prompt is not going to require trusted sources. You also want to not have follow up suggestions. `intent`: 0, `enable_suggestions`: 0 # Update chat Source: https://docs.gloo.com/api-reference/chat/update-chat patch /ai/{v}/chat Update chat. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Update message Source: https://docs.gloo.com/api-reference/chat/update-message put /ai/{v}/message Update message. **Deprecated Endpoint**: This endpoint is part of the Chat API which is deprecated. Please migrate to the [Completions v2 API](/api-guides/completions-v2) for new projects. **[Migration Guide →](/api-guides/migrating-from-chat-to-completions-v2)** # Direct Completions Source: https://docs.gloo.com/api-reference/completions/direct post /ai/v2/direct/chat/completions Generate Chat Completions directly from the selected model without Gloo governance. Common errors: unsupported model, unsupported sampling parameter, missing messages, context window exceeded, provider timeout, provider unavailable. # Completions V1 Source: https://docs.gloo.com/api-reference/completions/v1 post /ai/v1/chat/completions Post completions v1. **This endpoint has been deprecated.** Use the [Completions V2 API](/api-reference/completions/v2) instead for better performance, auto-routing, and lower costs. # Completions V2 Source: https://docs.gloo.com/api-reference/completions/v2 post /ai/v2/guarded/chat/completions Generate AI completions with intelligent routing. Common errors: invalid routing configuration, missing messages, context window exceeded, provider timeout, provider unavailable. # Grounded Completions Source: https://docs.gloo.com/api-reference/completions/v2/grounded post /ai/v2/grounded/chat/completions Generate grounded completions with intelligent routing and retrieval-augmented generation (RAG). Common errors: retrieval/source failures, no relevant content retrieved, plus all Completions V2 errors. # Delete Items Source: https://docs.gloo.com/api-reference/content-controls/delete-item delete /engine/v2/items Permanently delete multiple items from all storage systems. Common errors: missing or invalid item IDs (400), unauthorized delete (403), validation errors (422), delete failures (500). # Get Item By Producer Id Source: https://docs.gloo.com/api-reference/content-controls/get-item-by-producer-id post /engine/v2/publisher/{publisher_id}/items/by-producer Retrieve item IDs for one or more producer IDs owned by a publisher. Common errors: invalid item or publisher IDs, missing producer IDs, item not found, insufficient permissions. # Get Item Metadata Source: https://docs.gloo.com/api-reference/content-controls/get-item-metadata get /engine/v2/items/{item_id} Retrieve comprehensive metadata for a specific item including its status, collection memberships, and essential properties. Common errors: invalid item or publisher IDs, missing producer IDs, item not found, insufficient permissions. # Get Publisher Items Source: https://docs.gloo.com/api-reference/content-controls/get-publisher-items get /engine/v2/publisher/{publisher_id}/items Retrieve a paginated list of items associated with a specific publisher. Common errors: invalid item or publisher IDs, missing producer IDs, item not found, insufficient permissions. # Get Publisher Producer IDs Source: https://docs.gloo.com/api-reference/content-controls/get-publisher-producer-ids get /engine/v2/publisher/{publisher_id}/producer-ids Retrieve a paginated list of distinct producer IDs associated with a specific publisher. Use this endpoint instead of `/publisher/{publisher_id}/items` when you only need producer IDs and the publisher has a large number of items. Common errors: invalid item or publisher IDs, missing producer IDs, item not found, insufficient permissions. # Patch Multiple Items Source: https://docs.gloo.com/api-reference/content-controls/multi-item-patch patch /engine/v2/items Update multiple items simultaneously based on filter criteria. Common errors: missing patch operations (400), publisher not found (400), validation errors (422), bulk patch failure (500). # Update item metadata Source: https://docs.gloo.com/api-reference/content-controls/single-item-patch patch /engine/v2/item Update mutable metadata fields for an item. Provide either producer_id or item_id along with publisher_id to identify the target item. (Both may be passed in as well.) Common errors: item not found (404), publisher not found (400), ownership mismatch (403), vector-store update failure (502). # Paywall related endpoint Source: https://docs.gloo.com/api-reference/content/paywall-related-endpoint post /ai/v1/kallm/paywall/related Paywall related endpoint. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Extract intent from query Source: https://docs.gloo.com/api-reference/core/extract-intent-from-query post /ai/v1/kallm/intent Extract intent from query. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Generate Source: https://docs.gloo.com/api-reference/core/generate post /ai/v1/kallm/generate Generate a response from the LLM model. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Paywall Source: https://docs.gloo.com/api-reference/core/paywall post /ai/v1/kallm/paywall Paywall. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Search Source: https://docs.gloo.com/api-reference/core/search post /ai/v1/kallm/search Search (deprecated). **This endpoint has been deprecated.** Use the equivalent [Search](/api-reference/data/search-publisher-data) publisher data endpoint instead. Migration steps: 1. POST to `/ai/v1/data/search` 2. Use `query` instead of `search` # Get Referenced Items for Affiliates Source: https://docs.gloo.com/api-reference/data/get-affiliate-referenced-items post /ai/v1/data/affiliates/referenced-items Retrieves a list of recommended items based on a user query, designed for affiliate use cases. Allows filtering by media type, publisher, and snippet counts. Common errors: invalid collection, missing or invalid tenant, certainty threshold issues, empty query string. # Get Base Item Recommendations Source: https://docs.gloo.com/api-reference/data/get-base-item-recommendations post /ai/v1/data/items/recommendations/base Retrieves item recommendations with basic snippet metadata (excluding full snippet text). Requires specifying a collection and tenant for the search. Common errors: invalid collection, missing or invalid tenant, certainty threshold issues, empty query string. # Get Verbose Item Recommendations Source: https://docs.gloo.com/api-reference/data/get-verbose-item-recommendations post /ai/v1/data/items/recommendations/verbose Retrieves item recommendations with detailed snippet metadata, including the full snippet text. Requires specifying a collection and tenant for the search. Common errors: invalid collection, missing or invalid tenant, certainty threshold issues, empty query string. # Search Source: https://docs.gloo.com/api-reference/data/search-publisher-data post /ai/v1/data/search Searches publisher data in a specific collection and tenant using a query string. **This is a semantic search endpoint.** It searches by meaning, not by exact keyword or prefix matching. Write queries as natural language questions or phrases for best results. ## Before You Start This endpoint uses AI-powered **semantic search** to find content based on the *meaning* of your query — not exact text matching. For example, a query like "secrets to a happy marriage" will return content about "rules for keeping a marriage healthy," even though none of the exact words overlap. **This is not a keyword or prefix search.** Queries like `"happ"` or `"happiness AND marriage"` will not behave as expected. Use complete, natural-language phrases instead. Understand how semantic search works, how to tune the certainty threshold, and how to combine search with RAG. Working code examples in 6 languages — JavaScript, TypeScript, Python, PHP, Go, and Java. Common errors: invalid collection, missing or invalid tenant, certainty threshold issues, empty query string. ### Tips for Effective Queries * **Use natural language** — "How can I find true happiness?" works better than "happiness find true" * **Adjust certainty** — The default threshold is `0.75`, which is strict. If you're getting few or no results, try lowering it to `0.5` * **Results are ranked by semantic distance** — not keyword frequency. The `certainty` and `distance` fields in the response indicate how closely each result matches your query's meaning # Direct Embeddings Source: https://docs.gloo.com/api-reference/embeddings/direct post /ai/v2/direct/embeddings Turn one string, or a batch of up to 2048 strings, into embedding vectors with a Gloo embedding model. OpenAI-compatible. No guardrails, routing, or streaming. Model choice, input limits, pricing, and using the OpenAI SDK against this endpoint. Authentication, billing, invalid model or input, unsupported dimensions, and provider errors. # Errors Source: https://docs.gloo.com/api-reference/general/errors Error codes, HTTP status codes, and resolution guidance for all Gloo AI API endpoints. When interacting with the Gloo AI API, error handling depends on whether the response is non-streaming or streaming. For non-streaming responses, check the HTTP status code and parse the JSON response body. For streaming Completions responses, the initial response is `200 text/event-stream`; once streaming has started, later failures are delivered as Server-Sent Events instead of non-2xx HTTP responses. For AI error objects, `error.code` is the numeric internal code, such as `2001`, and `error.name` is the symbolic code, such as `PROVIDER_TIMEOUT`. ## Quick Reference Full table of every HTTP status returned by active endpoints. Exact error codes from the API with resolution guidance. Field reference for enriched Completions errors. How errors are delivered after an SSE stream has started. 401 and 403 errors — missing tokens, expired credentials, permission issues. Spending limits, endpoint quotas, and streaming provider rate limits. Exhausted prepaid credit or Gloo Code allowance, and how it differs from a 429. Error details for Embeddings, Completions, Ingestion, Content Controls, and Search. Patterns for reliable error handling across all endpoints. ## HTTP Status Codes | Status Code | Meaning | When You'll See It | | - | - | - | | `200 OK` | Success | The request completed successfully. | | `400 Bad Request` | Bad Request | Missing required fields, wrong content type, or structurally invalid JSON. | | `401 Unauthorized` | Unauthorized | `Authorization` header missing, token expired, or required JWT claims (`sub`, `scope`) absent. | | `403 Forbidden` | Forbidden | Authorization was present but invalid, insufficient, or not allowed for the resource or publisher. | | `404 Not Found` | Not Found | Requested resource (item, publisher, collection) does not exist or was deleted. | | `408 Request Timeout` | Request Timeout | Model provider did not respond within the allowed window. Retry with backoff. | | `422 Unprocessable Entity` | Validation Error | Well-formed JSON that fails semantic validation — invalid enum, out-of-range value, business rule violation. | | `429 Too Many Requests` | Limit Reached | A Completions spending limit or an endpoint-specific request quota was reached. Inspect the response before retrying; some `429` errors are not retryable. | | `500 Internal Server Error` | Server Error | Unexpected error on the Gloo platform. Retry with backoff. | | `502 Bad Gateway` | Upstream Error | Returned when a Content Controls vector-store update fails. | | `503 Service Unavailable` | Service Unavailable | Gloo service or upstream provider temporarily unavailable. Retry with backoff. | *** ## Error Code Reference | Code / Name | HTTP Status | Description | Resolution | | - | - | - | - | | `unauthorized_missing_authorization` | 401 | `Authorization` header is absent. | Add `Authorization: Bearer ` to all requests. | | `forbidden` | 403 | Authorization was present but invalid, insufficient, or not allowed for the resource or publisher. | Verify the API key value; check scopes and publisher access. | | `publisher_not_found` | 400 | Publisher ID does not exist. | Verify the `publisher_id` value in the request. | | `publisher_mismatch` | 403 | Item does not belong to the specified publisher. | Check that the `publisher_id` matches the item's original publisher. | | `item_not_found` | 404 | Requested item does not exist or was deleted. | Verify `item_id` or `producer_id`. Use list endpoints to confirm existence. | | `item_publisher_mismatch` | 403 | Item does not belong to the specified publisher. | Verify `publisher_id` and `item_id` belong to the same publisher. | | `too_many_files` | 400 | Producer ID is per-file only; multiple files submitted. | Submit one file per `producer_id`. | | `file_read_error` | 400 | Uploaded file could not be decoded or read. | Verify the file is valid and not corrupt before uploading. | | `missing_producer_ids` | 400 | No `producer_id` values provided. | Include at least one `producer_id` in the request. | | `duplicate_content` | 200 result state | Content already ingested. Public response exposes the `duplicates` array alongside `ingesting`. | No action needed if re-ingestion is unintentional. To update, delete or patch the existing item. | | `validation_error` | 422 | One or more fields failed semantic validation. | Inspect the `detail` array: `loc` = field path, `msg` = reason, `input` = value you sent. | | `1001 / INVALID_REQUEST` | 400 | Request is structurally invalid — missing user message or invalid routing configuration. | Fix the request body; for Completions V2, specify exactly one routing mode (`auto_routing`, `model`, or `model_family`). | | `1002 / CONTEXT_LENGTH_EXCEEDED` | 400 | Input exceeds model's maximum context window. | Shorten message history, reduce system prompt, or switch to a model with a larger context window. | | `1003 / UNSUPPORTED_MODEL` | 400 or stream event | Requested model is not recognized or unavailable in this environment. | Use a supported model from the Models endpoint. | | `1005 / SPENDING_LIMIT_EXCEEDED` | 429 | The account's spending limit was reached. | Check billing limits; retry only after the limit condition is resolved. | | `1006 / INSUFFICIENT_CREDIT` | 402 | The organization's prepaid credit balance is exhausted. | Add credits from the Studio Billing page; access resumes immediately. | | `1007 / GC_ALLOWANCE_EXHAUSTED` | 402 | The organization's Gloo Code allowance for this billing period is spent. Only on the Gloo Code route. | Buy a Gloo Code top-up, or wait for the monthly reset. `topup_available` says whether a top-up can be bought. | | `1008 / GC_NOT_SUBSCRIBED` | 402 | The organization holds no Gloo Code plan. Only on the Gloo Code route. | Choose a Gloo Code plan in Studio. API credits do not grant Gloo Code access. | | `2001 / PROVIDER_TIMEOUT` | 408 or stream event | Model provider did not respond in time. | Retry with exponential backoff; consider a different model or routing mode if persistent. | | `2002 / PROVIDER_UNAVAILABLE` | 503 or stream event | Provider or backing service unavailable. | Retry with exponential backoff. | | `2003 / CONNECTION_ERROR` | Stream event | Connection to the upstream provider was lost after streaming started. | Retry if `error.retryable` is `true`. | | `2004 / RATE_LIMIT` | Stream event | Upstream provider rate limit was reached after streaming started. | Retry with bounded exponential backoff only if `error.retryable` is `true`. | | `2005 / PROVIDER_CONTENT_FILTER` | 422 or content-filter stream event | Provider moderated the response. | Adjust the request or user-facing workflow; do not retry unchanged. | | `2007 / EMPTY_STREAM` | Stream event | Provider opened a stream but delivered no content chunks. | Retry with exponential backoff. | | `2008 / PROVIDER_BAD_REQUEST` | Stream event | Upstream provider rejected the request after streaming started. | Inspect the request and model requirements; retry only after correcting the request. | | `2011 / MALFORMED_SSE` | Stream event | Upstream provider sent a malformed SSE frame. | Retry with exponential backoff. | | `2013 / PROVIDER_INFERENCE_ERROR` | 500 | Unexpected model inference failure. | Retry with exponential backoff and log `trace_id` if present in the response. | | `3001 / INTERNAL_ERROR` | 500 or stream event | Unexpected error. The same code covers both retryable provider anomalies (`fault: "provider"`, `retryable: true`) and non-retryable platform errors (`fault: "internal"`, `retryable: false`). | Branch on `error.retryable` and `error.fault`, never on the code alone; log `trace_id`. | | `3003 / PROVIDER_AUTH_ERROR` | Stream event | Upstream provider credentials are misconfigured. | Treat as a platform issue; log `trace_id` and contact support if persistent. | | `3004 / ROUTING_ERROR` | Stream event | Platform routed the request to an endpoint that does not recognize the model. | Try a different model or routing mode; log `trace_id` if persistent. | *** ## AI Error Object Completions errors include an enriched top-level `error` object. Use it for programmatic handling instead of parsing human-readable messages. ```json theme={null} { "error": { "message": "The provider did not respond in time. Please try again.", "type": "timeout_error", "code": 2001, "name": "PROVIDER_TIMEOUT", "category": "provider_error", "description": "The upstream model provider did not respond within the allowed time window.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` | Field | Description | | - | - | | `message` | Request-specific customer-facing error message. | | `type` | Stable wire-format error type. | | `code` | Numeric internal error code. | | `name` | Symbolic error name for programmatic branching and alerting. | | `category` | Broad category: `client_error`, `provider_error`, or `platform_error`. | | `description` | Stable explanation of what the error code means. | | `fault` | Responsible party: `client`, `provider`, or `internal`. | | `retryable` | Whether retrying the same request may succeed. | | `trace_id` | Trace identifier to include in support requests when present. | Use `error.retryable` for retry decisions, `error.name` or `error.code` for branching, and `trace_id` for debugging with support. **Hitting persistent inference issues?** If model inference keeps failing — for example a repeating `2013 / PROVIDER_INFERENCE_ERROR` or `3001 / INTERNAL_ERROR` — open a ticket through [Gloo AI Studio support](https://studio.ai.gloo.com/support) and include the `trace_id` from the error response. The `trace_id` lets us look up the exact request and resolve the issue faster. ## Common Error Scenarios ### Authentication Errors These errors occur when there is an issue with your API key or the permissions associated with it. #### 401 Unauthorized — Missing or Invalid API Key Returned when the `Authorization` header is missing from the request. ```json theme={null} { "error": "Missing Authorization", "code": "unauthorized_missing_authorization" } ``` Some Completions endpoints may return an app-level response: ```json theme={null} { "detail": "Invalid API key." } ``` Search endpoints can be rejected earlier by the gateway. When the `Authorization` header is present but malformed or the API key is invalid, the gateway returns `403` with the `forbidden` code. #### 403 Forbidden — Insufficient Permissions Returned when the request includes authorization, but the caller does not have permission for the requested resource or publisher, or the gateway rejects the authorization header. ```json theme={null} { "error": "Forbidden - insufficient permissions", "code": "forbidden" } ``` For the Ingestion API, a 403 most commonly means the `publisher_id` doesn't belong to your organization, or the item you are referencing does not belong to the specified publisher. *** ### Validation Errors (400 / 422) These errors indicate a problem with the parameters or body of your request. `400 Bad Request` signals a structurally invalid request, while `422 Unprocessable Entity` signals valid JSON that fails field-level validation. #### Missing Required Field Returned when a required body field is absent. Example: `model` is required when specifying direct model routing for Completions V2. ```json theme={null} { "detail": [ { "type": "missing", "loc": ["body", "model"], "msg": "Field required", "input": {} } ] } ``` #### Invalid Routing Configuration Completions V2 returns this as `400` when more than one — or no — routing mechanism is specified. ```json theme={null} { "detail": "Exactly one routing mechanism (auto_routing, model, or model_family) must be specified." } ``` #### Invalid Data Type Returned when a field receives a value of the wrong type. Example: `certainty` expects a float. ```json theme={null} { "detail": [ { "type": "float_parsing", "loc": ["body", "certainty"], "msg": "Input should be a valid number", "input": "high" } ] } ``` *** ### Resource Not Found (404) Returned when the requested item does not exist or has been permanently deleted. ```json theme={null} { "code": "Item not found", "item_id": "c68c9303-9851-476e-96af-aa5bdd385811", "message": "The requested item does not exist or has been permanently deleted" } ``` Publisher not found and invalid collection cases return `400`, not `404`. See [Ingestion](#ingestion) and [Search & Recommendations](#search-%26-recommendations) for these examples. *** ### Duplicate Upload Results #### Duplicate Content (Ingestion) When files are submitted that were already ingested, the API returns `200 OK` with a `duplicates` array in the response body — this is not an HTTP error. ```json theme={null} { "success": true, "message": "File processing started in background.", "ingesting": [], "duplicates": ["c999008e-de60-495c-8c9f-6a4b59cdb04b"] } ``` Duplicates are returned in the response body, not as a `409 Conflict` error. *** ### Rate Limiting (429) An HTTP `429` can represent a Completions spending limit or an endpoint-specific request quota. These conditions do not share one response format or retry policy. For Completions endpoints, a spending-limit response has the following shape: ```json theme={null} { "detail": { "message": "Spending limit reached", "code": "SPENDING_LIMIT_EXCEEDED" }, "error": { "message": "Spending limit reached", "type": "billing_error", "code": 1005, "name": "SPENDING_LIMIT_EXCEEDED", "category": "client_error", "description": "The account's spending limit has been reached.", "fault": "client", "retryable": false, "trace_id": "trace-id" } } ``` `SPENDING_LIMIT_EXCEEDED` is not retryable and does not include `Retry-After`. Retry only after the account's spending-limit condition is resolved. For prepaid organizations this fires at the weekly usage limit even when credit remains, and adding credits does not clear it. An empty balance is a different response: `402 INSUFFICIENT_CREDIT`, covered under [Payment Required](#payment-required-402). Endpoint-specific quotas can also return HTTP `429`. Their error bodies and headers vary by endpoint. Honor `Retry-After` when it is present; do not assume that every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, or `X-RateLimit-Reset`. An upstream provider rate limit that occurs after a Completions stream starts is not an HTTP `429`. It is delivered inside the HTTP `200` stream as a `2004 / RATE_LIMIT` SSE error event. Use `error.retryable` to decide whether to retry. See [Rate Limits](/api-reference/general/limits) for response-specific retry guidance and pseudocode. *** ### Payment Required (402) An HTTP `402` means the request was refused because the billing entitlement or pool that would pay for it is unavailable: an empty prepaid balance, a spent Gloo Code allowance, or no Gloo Code plan at all. It is not the weekly usage-limit response; that is `429 / 1005 SPENDING_LIMIT_EXCEEDED`, which can fire with credit still in the balance and clears only when the window resets. The remedy depends on the code below: add credits, buy a top-up, choose a plan, or wait for the reset. See [Prepaid Credits](/studio/prepaid-credits#when-you-run-out) for the two limits side by side. For the inference endpoints, an exhausted prepaid balance returns: ```json theme={null} { "detail": { "message": "Wallet credit balance is exhausted. Add credits to your account to continue.", "code": "INSUFFICIENT_CREDIT" }, "error": { "message": "Wallet credit balance is exhausted. Add credits to your account to continue.", "type": "billing_error", "code": 1006, "name": "INSUFFICIENT_CREDIT", "category": "client_error", "description": "The account's prepaid wallet credit balance is exhausted.", "fault": "client", "retryable": false, "trace_id": "trace-id" } } ``` `INSUFFICIENT_CREDIT` is not retryable. Add credits from the [Studio Billing page](https://studio.ai.gloo.com/billing) and retry; access resumes as soon as the credit lands. The Gloo Code route (`/ai/v2/gloocode/chat/completions`) has its own two refusals with the same body shape plus a `topup_available` boolean at the top level: | Code | When | Remedy | | - | - | - | | `1007 / GC_ALLOWANCE_EXHAUSTED` | The tier's monthly allowance and any top-up pool are both spent. | Buy a top-up from the Billing page if `topup_available` is `true`, otherwise wait for the monthly reset. | | `1008 / GC_NOT_SUBSCRIBED` | The organization holds no Gloo Code plan. `topup_available` is always `false`. | Choose a Gloo Code plan in Studio. Prepaid credits are not spendable in Gloo Code. | A Gloo Code organization can hold a healthy credit balance and still receive `GC_ALLOWANCE_EXHAUSTED`: API credits and the Gloo Code allowance are separate pools. See [Billing & Plans](/studio/billing#the-one-rule-that-surprises-people). *** ### Server Errors (5xx) Retry only transient failures. For Completions errors, check `error.retryable`; in particular, do not retry `1005 / SPENDING_LIMIT_EXCEEDED`. For endpoint-specific `429` responses, honor `Retry-After` when present and follow the endpoint's documented behavior. #### 500 Internal Server Error An unexpected error occurred on the Gloo platform or during model inference. Retry with exponential backoff. ```json theme={null} { "detail": "An unexpected error occurred during model inference. Please try again.", "error": { "message": "An unexpected error occurred during model inference. Please try again.", "type": "internal_error", "code": 2013, "name": "PROVIDER_INFERENCE_ERROR", "category": "provider_error", "description": "An unexpected error occurred during model inference at the upstream provider.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` #### 502 Bad Gateway Returned by Content Controls endpoints when a vector-store update fails in the underlying data store. ```json theme={null} { "detail": { "code": "Weaviate update failed", "message": "Postgres not modified due to vector store update failure", "weaviate_error": "Weaviate update failed" } } ``` #### 503 Service Unavailable The upstream provider or backing service is temporarily unavailable. Retry with exponential backoff. ```json theme={null} { "detail": "The provider is currently unavailable. Please try again later.", "error": { "message": "The provider is currently unavailable. Please try again later.", "type": "service_unavailable_error", "code": 2002, "name": "PROVIDER_UNAVAILABLE", "category": "provider_error", "description": "The upstream model provider is temporarily unavailable.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` *** ## Streaming Errors When `stream=true` on Completions endpoints, the response starts as `200 text/event-stream`. After the SSE headers have been sent, the API cannot change the HTTP status code. Stream-time failures are therefore sent as SSE events. A stream error event has `choices[0].finish_reason` set to `"error"` and includes the same enriched top-level `error` object used by non-streaming Completions errors. ```json theme={null} { "id": "chatcmpl-error-abc123", "object": "chat.completion.chunk", "created": 1769792268, "model": "gloo-anthropic-claude-sonnet-4.6", "choices": [ { "delta": { "content": null, "role": null }, "finish_reason": "error", "index": 0 } ], "error": { "message": "Stream connection dropped", "type": "connection_error", "code": 2003, "name": "CONNECTION_ERROR", "category": "provider_error", "description": "The connection to the upstream model provider was lost or could not be established.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` Stream-time error scenarios include provider timeout, provider rate limit, provider `5xx`, provider bad request, provider auth failure, routing error, unsupported model, empty stream, provider unavailable, malformed provider SSE, connection drop, and internal platform error. If a stream error event includes `error.retryable: true`, retry with exponential backoff. If `error.retryable` is `false`, retry only after changing the request or model selection. For retry ceilings, application-level continuation after partial output, and the UX decisions around interrupted streams, see [Handling Streaming Failures](/best-practices/completions-streaming-failures). ### Content Filter During Streaming Content moderation during streaming is represented differently from other stream errors. It does not emit a top-level `error` object. Instead, the stream sends a final assistant content event and then a terminal event with `finish_reason: "content_filter"`. ```json theme={null} { "id": "chatcmpl-stream-abc123", "object": "chat.completion.chunk", "created": 1769792268, "model": "gloo-anthropic-claude-sonnet-4.6", "choices": [ { "delta": { "content": "" }, "finish_reason": "content_filter", "index": 0 } ], "service_tier": null, "usage": null } ``` Do not retry content-filter terminations unchanged. Adjust the user-facing workflow or request content. ### Grounded Streaming Citations For Grounded Completions, when `stream=true`, `include_citations=true`, and citations are available, the first SSE event may contain a `citations` array before model content begins. This is a normal `200` stream event, not an error. ```json theme={null} { "citations": [ { "item_title": "Daily Prayer", "item_url": "https://www.studio.ai.gloo.com", "author": ["John Doe"], "publisher": "The Publisher", "publication_date": "Apr 08 2017", "snippets": [ "Daily prayer is an essential part of life as...", "Making time to spend with the Lord is..." ] } ] } ``` *** ## Endpoint-Specific Errors ### Embeddings POST /ai/v2/direct/embeddings **Model Is Not an Embedding Model — 400** Returned when `model` names a catalog model that does not support embeddings. Choose an embedding model from the [model catalog](/api-reference/get-models/v2). ```json theme={null} { "detail": { "message": "Model 'gloo-openai-gpt-4.1-mini' is not an embedding model. /ai/v2/direct/embeddings serves the embedding models listed by /platform/v2/models.", "type": "invalid_request_error", "code": "invalid_request_error", "retryable": false, "param": "model" } } ``` **Invalid Input — 400** Returned when `input` is empty. The `param` field identifies the request field to correct. ```json theme={null} { "detail": { "message": "input must not be empty", "type": "invalid_request_error", "code": "invalid_request_error", "retryable": false, "param": "input" } } ``` **Unsupported Dimensions — 400** Returned when the selected model does not support the `dimensions` parameter or the requested value is outside its supported range. Check the model's supported dimensions and adjust the request. ```json theme={null} { "detail": { "message": "Model 'gloo-intfloat-e5-base-v2' does not support the 'dimensions' parameter. It returns 768 dimensions.", "type": "invalid_request_error", "code": "invalid_request_error", "retryable": false, "param": "dimensions" } } ``` **Embedding Provider Unavailable — 503** Returned when the embedding provider is unavailable or returns a response Gloo cannot use. Retry transient failures with exponential backoff. Embeddings requests do not fail over to another provider. ```json theme={null} { "detail": "The provider is currently unavailable. Please try again later.", "error": { "message": "The provider is currently unavailable. Please try again later.", "type": "service_unavailable_error", "code": 2002, "name": "PROVIDER_UNAVAILABLE", "category": "provider_error", "description": "The upstream model provider is temporarily unavailable.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` Embeddings request-validation errors use a `detail` object with `message`, `type`, `code`, `retryable`, and, when applicable, `param`. Provider errors use the enriched top-level `error` object. ### Completions V2 POST /ai/v2/guarded/chat/completions The endpoint reference documents both `application/json` responses for non-streaming requests and `text/event-stream` responses for `stream=true`. **Invalid Routing Configuration — 400** Specify exactly one of `auto_routing`, `model`, or `model_family` per request. ```json theme={null} { "detail": "Exactly one routing mechanism (auto_routing, model, or model_family) must be specified." } ``` **Invalid Request — 400** Returned when no user message is found in the `messages` array. ```json theme={null} { "detail": "No user message found in the request.", "error": { "message": "No user message found in the request.", "type": "invalid_request_error", "code": 1001, "name": "INVALID_REQUEST", "category": "client_error", "description": "The request is malformed or contains invalid parameters.", "fault": "client", "retryable": false, "trace_id": "trace-id" } } ``` **Messages Array Missing — 422** The `messages` field is required. An absent `messages` array returns a validation error. ```json theme={null} { "detail": [ { "type": "missing", "loc": ["body", "messages"], "msg": "Field required", "input": {} } ] } ``` **Context Window Exceeded — 400** ```json theme={null} { "detail": { "message": "Input exceeds the model context window.", "type": "context_window_exceeded", "code": "context_length_exceeded", "model": "gloo-anthropic-claude-sonnet-4.6", "max_input_tokens": 1000000, "suggestion": "Use a model with larger context window such as gloo-anthropic-claude-sonnet-4.6 (1M tokens)" }, "error": { "message": "Input exceeds the model context window.", "type": "invalid_request_error", "code": 1002, "name": "CONTEXT_LENGTH_EXCEEDED", "category": "client_error", "description": "The total token count of the request exceeds the model's context window.", "fault": "client", "retryable": false, "trace_id": "trace-id", "model": "gloo-anthropic-claude-sonnet-4.6", "max_input_tokens": 1000000, "suggestion": "Use a model with larger context window such as gloo-anthropic-claude-sonnet-4.6 (1M tokens)" } } ``` **Provider Timeout — 408** ```json theme={null} { "detail": "The provider did not respond in time. Please try again.", "error": { "message": "The provider did not respond in time. Please try again.", "type": "timeout_error", "code": 2001, "name": "PROVIDER_TIMEOUT", "category": "provider_error", "description": "The upstream model provider did not respond within the allowed time window.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` **Provider Content Filter — 422** ```json theme={null} { "detail": "The provider moderated the response.", "error": { "message": "The provider moderated the response.", "type": "content_filter_error", "code": 2005, "name": "PROVIDER_CONTENT_FILTER", "category": "provider_error", "description": "The response was blocked by the upstream provider's content filter.", "fault": "provider", "retryable": false, "trace_id": "trace-id" } } ``` **Provider Inference Error — 500** ```json theme={null} { "detail": "An unexpected error occurred during model inference. Please try again.", "error": { "message": "An unexpected error occurred during model inference. Please try again.", "type": "internal_error", "code": 2013, "name": "PROVIDER_INFERENCE_ERROR", "category": "provider_error", "description": "An unexpected error occurred during model inference at the upstream provider.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` **Provider Unavailable — 503** ```json theme={null} { "detail": "The provider is currently unavailable. Please try again later.", "error": { "message": "The provider is currently unavailable. Please try again later.", "type": "service_unavailable_error", "code": 2002, "name": "PROVIDER_UNAVAILABLE", "category": "provider_error", "description": "The upstream model provider is temporarily unavailable.", "fault": "provider", "retryable": true, "trace_id": "trace-id" } } ``` *** ### Grounded Completions POST /ai/v2/grounded/chat/completions Grounded Completions inherits all [Completions V2](#completions-v2) error responses, including routing configuration errors, context length exceeded, provider timeout, content filter, inference error, and provider unavailable. The endpoint reference documents both `application/json` responses for non-streaming requests and `text/event-stream` responses for `stream=true`. For streaming grounded responses, citations may be emitted as the first stream event when `include_citations=true`. **No Relevant Content Retrieved — 200** Zero results is not an error — it means no retrieved content exceeded the certainty threshold. The response returns `200 OK` with `"sources_returned": false`. Lower `certainty` toward `0.5` or verify that content has been ingested. *** ### Ingestion POST /ingestion/v2/files **Publisher Not in Organization — 403** Returned when the item or publisher does not belong to your organization. ```json theme={null} { "detail": { "message": "Item does not belong to this publisher", "code": "publisher_mismatch" } } ``` **Missing Authorization — 401** Returned by the gateway when the `Authorization` header is missing. ```json theme={null} { "error": "Missing Authorization", "code": "unauthorized_missing_authorization" } ``` **Publisher Not Found — 400** ```json theme={null} { "detail": { "message": "Publisher not found", "code": "publisher_not_found" } } ``` **Producer ID with Multiple Files — 400** ```json theme={null} { "detail": { "message": "Too many files. Producer ID is pre-file only.", "code": "too_many_files" } } ``` **File Read Error — 400** ```json theme={null} { "detail": { "message": "Could not read sermon.pdf: unable to decode uploaded file", "code": "file_read_error" } } ``` **Duplicate Content — 200 Response** Duplicate files appear in the `duplicates` array of a successful `200 OK` response, not as an HTTP error. See [Duplicate Upload Results](#duplicate-upload-results). *** ### Content Controls (Data Engine) PATCH /engine/v2/item PATCH /engine/v2/items DELETE /engine/v2/items GET /engine/v2/items/ POST /engine/v2/publisher//items/by-producer **Item Not Found — 404** ```json theme={null} { "code": "Item not found", "item_id": "c68c9303-9851-476e-96af-aa5bdd385811", "message": "The requested item does not exist or has been permanently deleted" } ``` For items looked up by `producer_id`: ```json theme={null} { "detail": { "code": "Items not found", "message": "No items were found for the provided producer IDs" } } ``` **Publisher Not Found — 400** ```json theme={null} { "detail": { "code": "publisher_not_found", "message": "Publisher not found" } } ``` **Publisher / Item Ownership Mismatch — 403** ```json theme={null} { "detail": { "code": "item_publisher_mismatch", "message": "Item does not belong to the specified publisher" } } ``` Unauthorized delete attempt: ```json theme={null} { "detail": { "code": "Unauthorized access", "message": "You don't have permission to delete these items", "unauthorized_items": ["c68c9303-9851-476e-96af-aa5bdd385811"] } } ``` **Vector-Store Update Failure — 502** Returned by the single-item PATCH endpoint when the vector store update fails. ```json theme={null} { "detail": { "code": "Weaviate update failed", "message": "Postgres not modified due to vector store update failure", "weaviate_error": "Weaviate update failed" } } ``` **Batch / Request Validation — 400** Missing patch operations: ```json theme={null} { "detail": { "code": "Invalid request", "message": "At least one patch operation is required" } } ``` Missing item IDs in a delete request: ```json theme={null} { "detail": { "code": "Invalid request", "message": "At least one item_id is required" } } ``` Missing producer IDs: ```json theme={null} { "detail": { "code": "Invalid request", "message": "At least one producer_id must be provided" } } ``` *** ### Search & Recommendations POST /ai/v1/data/search POST /ai/v1/data/items/recommendations/base **Invalid Collection — 400** ```json theme={null} { "detail": "Invalid Collection. Please see https://docs.gloo.com/api-guides/search#making-requests for information on providing a valid collection." } ``` **Missing or Invalid Tenant — 400** ```json theme={null} { "detail": "Unable to search tenant, InvalidTenant, in collection, GlooProd. Please provide a valid tenant. See https://docs.gloo.com/api-guides/search#making-requests for information on providing a valid tenant." } ``` For multi-tenant collections where tenant is omitted: ```json theme={null} { "detail": "Unable to search multi-tenant collection, GlooProd. Please provide a valid tenant. See https://docs.gloo.com/api-guides/search#making-requests for information on providing a valid tenant." } ``` **Missing Authorization — 401** Returned by the gateway when the `Authorization` header is missing. ```json theme={null} { "error": "Missing Authorization", "code": "unauthorized_missing_authorization" } ``` **Invalid Authorization — 403** Returned by the gateway when the `Authorization` header is present but malformed or the API key is invalid. ```json theme={null} { "error": "Forbidden - insufficient permissions", "code": "forbidden" } ``` **Validation Errors — returned as 403** Empty `query`, empty `collection`, and invalid `limit` values return HTTP `403` rather than `422`. Don't treat these as permission errors — they indicate an invalid request body. Empty query: ```json theme={null} { "detail": [ { "type": "string_too_short", "loc": ["body", "query"], "msg": "String should have at least 1 character", "input": "" } ] } ``` Invalid `limit`: ```json theme={null} { "detail": [ { "type": "greater_than_equal", "loc": ["body", "limit"], "msg": "Input should be greater than or equal to 1", "input": -1 } ] } ``` Zero results is not an error — it means no content exceeded the certainty threshold. Lower `certainty` toward `0.5` or verify that content has been ingested. *** ## Error Handling Best Practices **Log `error.name` and `error.code`, not just the status** — Status codes tell you the category; the symbolic name and numeric code tell you exactly what went wrong. Log them in your error handler for precise alerting and debugging. **Use `error.retryable` for Completions retries** — For non-streaming responses, use the HTTP status plus `error.retryable`. For streaming responses, inspect stream error events and use `error.retryable`. Some failures happen inside an HTTP `200` stream after headers have been sent. **Retry only transient failures** — `408` provider timeouts and retryable `5xx` failures are candidates for bounded retries. A `429` is not automatically retryable: `1005 / SPENDING_LIMIT_EXCEEDED` requires the limit condition to be resolved, while an endpoint-specific quota may provide `Retry-After`. **Use exponential backoff for retries** — Wait `(2^attempt) + jitter` between retries, up to 5 attempts for non-streaming requests. Live streaming needs much lower ceilings — see [Handling Streaming Failures](/best-practices/completions-streaming-failures) for streaming retry budgets and [Rate Limits](/api-reference/general/limits) for pseudocode. **Use `detail` to pinpoint validation failures** — For `422` responses, the `detail` array tells you exactly which field failed: `loc` is the field path, `msg` is the reason, and `input` is the value you sent. **Open a ticket with the `trace_id` for persistent inference issues** — If inference errors continue after retrying with backoff, contact us through [Gloo AI Studio support](https://studio.ai.gloo.com/support) and include the `trace_id` returned in the error response. The `trace_id` lets us trace the exact request and resolve the issue faster. # Rate Limits Source: https://docs.gloo.com/api-reference/general/limits Understand request quotas, spending limits, and provider rate-limit errors in the Gloo AI API. Gloo APIs can return `429 Too Many Requests` for more than one reason. A `429` does not always mean that you sent too many requests, and not every `429` is safe to retry. The API does not expose one universal, plan-based request allowance or a single rate-limit response format across all endpoints. Check the response body and headers before deciding how to handle the error. ## Types of Limits | Limit | How it is reported | Retry guidance | | - | - | - | | Inference spending limit | HTTP `429` with `1005 / SPENDING_LIMIT_EXCEEDED` and `error.retryable: false` | Do not retry automatically. Retry only after the account's spending-limit condition is resolved. | | Endpoint-specific request quota | HTTP `429`; the response format and headers depend on the endpoint | Honor `Retry-After` when present. Otherwise, use bounded exponential backoff only when retrying is appropriate for that endpoint. | | Upstream provider rate limit during streaming | `2004 / RATE_LIMIT` in an SSE error event after the HTTP `200` stream has started | Inspect `error.retryable`. If it is `true`, retry with bounded exponential backoff. | | Embeddings provider rate limit | HTTP `503` with `2002 / PROVIDER_UNAVAILABLE`; an upstream provider `429` is mapped to this response | Retry transient failures with bounded exponential backoff. If the same request repeatedly fails because the provider returns an incompatible encoding, change the request or model instead of retrying unchanged. | Do not assume that a `429` response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, or `X-RateLimit-Reset`. These headers are not part of a universal Gloo API response contract. ## Spending Limits Inference endpoints return HTTP `429` when the account's spending limit has been reached: ```json theme={null} { "detail": { "message": "Spending limit reached", "code": "SPENDING_LIMIT_EXCEEDED" }, "error": { "message": "Spending limit reached", "type": "billing_error", "code": 1005, "name": "SPENDING_LIMIT_EXCEEDED", "category": "client_error", "description": "The account's spending limit has been reached.", "fault": "client", "retryable": false, "trace_id": "trace-id" } } ``` This response does not include `Retry-After`. Retrying the same request without resolving the spending-limit condition will not help. ## Endpoint-Specific Quotas Some endpoints enforce their own operational quotas. Their response bodies, limit windows, and headers can differ. For example, a response may include `Retry-After`, which specifies the number of seconds to wait before trying again. When handling an endpoint-specific `429`: 1. Read the endpoint documentation and response body to identify the quota. 2. If `Retry-After` is present, wait at least that long. 3. Otherwise, retry only when the operation is safe to repeat, using a bounded delay with jitter. 4. Stop retrying and surface the error when the limit requires an account or configuration change. ## Streaming Provider Rate Limits After a Completions stream has started, the HTTP status remains `200`. If the upstream model provider reaches a rate limit, the stream can end with an SSE error event containing: ```json theme={null} { "error": { "type": "rate_limit_error", "code": 2004, "name": "RATE_LIMIT", "category": "provider_error", "fault": "provider", "retryable": true } } ``` Stream error events do not include HTTP rate-limit headers. Use the structured `error` object and follow the retry budgets in [Handling Streaming Failures](/best-practices/completions-streaming-failures). ## Embeddings Provider Rate Limits The Embeddings API maps an upstream provider `429` or `5xx` response to Gloo HTTP `503` with `2002 / PROVIDER_UNAVAILABLE` and `error.retryable: true`. Retry transient provider failures with bounded exponential backoff. A provider that repeatedly ignores the requested `encoding_format` can also cause a `503`; change the encoding format or model rather than retrying the same incompatible request. ## Retry Example The following pseudocode makes at most five total API requests, including the initial request. It retries only when the response metadata, endpoint policy, and request safety all explicitly permit a retry. It gives precedence to `Retry-After` and explicitly excludes spending-limit errors. ```text theme={null} attempt = 1 max_attempts = 5 loop: response = make_api_request() if response.status_code != 429: return response error = parse_error(response) if error is missing or malformed: return response if error.name == "SPENDING_LIMIT_EXCEEDED" or error.retryable != true: return response if endpoint_policy_allows_retry(response) != true: return response if request_is_safe_to_retry(response.request) != true: return response if attempt >= max_attempts: return response if response.headers contains "Retry-After": wait_seconds = parse_retry_after(response.headers["Retry-After"]) else: wait_seconds = min(2 ** (attempt - 1), 30) + random_jitter() sleep(wait_seconds) attempt += 1 ``` For live streaming experiences, use lower retry budgets than the non-streaming example. See [Errors](/api-reference/general/errors) for the complete error schema. # Overview Source: https://docs.gloo.com/api-reference/general/overview Welcome to the Gloo AI API! This reference documentation will help you build powerful applications by integrating directly with our platform's core capabilities, from conversational AI to content management. Our API is built on standard REST principles, uses predictable resource-oriented URLs, and returns JSON-encoded responses. *** ## Authentication Most Gloo AI API endpoints require authentication using an **API key**. Endpoints that do not require authentication are explicitly labeled in the reference. For authenticated requests, include your API key in the `Authorization` header. ```bash theme={null} curl "https://platform.ai.gloo.com/..." \ -H "Authorization: Bearer YOUR_API_KEY" ``` Follow the [Authentication Tutorial](/tutorials/authentication) to learn how to create and use your API key. *** ## Gloo AI Service Map & Routing Table | Service Category | Primary Base URL Path | Current Version | Authentication | Description / Use Case | | :- | :- | :- | :- | :- | | **Responses** (recommended) | `https://platform.ai.gloo.com/ai/v2/guarded/` | **v2** | Required (Bearer) | OpenAI-compatible Responses API — text, vision, image generation, and tool use. The recommended surface for new integrations. | | **Completions** | `https://platform.ai.gloo.com/ai/v2/guarded/` | **v2** | Required (Bearer) | Fully supported and backwards-compatible. Adds intelligent auto-routing, `model_family` selection, and values-aligned (`tradition`) responses. | | **Embeddings** | `https://platform.ai.gloo.com/ai/v2/direct/embeddings` | **v2** | Required (Bearer) | Create embedding vectors for applications that manage their own vector store. | | **Search & Recs** | `https://platform.ai.gloo.com/ai/v1/data/` | **v1** | Required (Bearer) | Semantic search and item/affiliate recommendations. | | **Model Catalog** | `https://platform.ai.gloo.com/platform/v2/` | **v2** | **None** | Public endpoint to list available models, pricing, and context windows. | | **Content Controls** | `https://platform.ai.gloo.com/engine/v2/` | **v2** | Required (Bearer) | Data Engine operations: patch, delete, and metadata management. | | **Ingestion** | `https://platform.ai.gloo.com/ingestion/v2/` | **v2** | Required (Bearer) | High-volume file and real-time data uploads. | | **OAuth2 Auth** (deprecated) | `https://platform.ai.gloo.com/oauth2/token` | N/A | Client Credentials | Legacy token exchange. Deprecated — use API keys instead. | Most endpoints require an API key sent as a Bearer token. Endpoints that do not require auth are explicitly labeled. *** ## Download OpenAPI Specs Recommended for integrations. Includes active endpoints only. Includes active and deprecated endpoints used by the docs. *** ## Error Handling The Gloo AI API uses conventional HTTP response codes to indicate the success or failure of a request. * Codes in the `2xx` range indicate success. * Codes in the `4xx` range indicate a client-side error (e.g., a missing parameter, bad authentication). * Codes in the `5xx` range indicate a server-side error on our end. For a detailed list of all possible error codes and their meanings, please see our [**API Errors Guide**](/api-reference/general/errors). *** ## Getting Started: Your First Steps Ready to build? Here’s how to get started: 1. **Get Your API Key:** Follow the [authentication guide](/studio/manage-api-credentials) to generate your first key in the Studio. 2. **Review the Guides:** Familiarize yourself with our core [API Guides](/api-guides/overview) to understand the conceptual models. 3. **Make Your First Call:** Explore the endpoints listed in the sidebar and make your first request! # Get Models V1 Source: https://docs.gloo.com/api-reference/get-models/v1 get /ai/v1/models **Deprecated.** Use [GET /platform/v2/models](/api-reference/get-models/v2) instead. This endpoint returns basic model metadata without pricing information. This endpoint is **deprecated**. Use the [Get Models V2](/api-reference/get-models/v2) endpoint instead, which returns the same metadata plus accurate pricing information. # Get Models V2 Source: https://docs.gloo.com/api-reference/get-models/v2 get /platform/v2/models Returns all available models with metadata and accurate pricing. No authentication required. Returns all available Gloo AI models with metadata and pricing. No authentication required. Each model includes: * **Identification** — Gloo model ID (for API requests), provider ID, name, and family * **Capabilities** — Context window, max input/output tokens, input and output modalities, generation capabilities, and (for embedding models) native vector dimensions * **Pricing** — Per-1K and per-1M token **list** rates for input and output (and cache segments where applicable) The rates returned here are **list prices**. Guarded generation endpoints add the platform rate described in [Studio Billing](/studio/billing). Direct endpoints, including Embeddings, bill at the published rate. Use `output_modalities` to identify the model type. Use generation model IDs with [Responses](/api-reference/responses/governed) or [Completions V2](/api-reference/completions/v2), and embedding model IDs with [Direct Embeddings](/api-reference/embeddings/direct). Embedding entries include their native vector width in `dimensions`. # Get File by Item ID Source: https://docs.gloo.com/api-reference/ingestion/get-file-by-item-id get /engine/v1/files/{item_id} Retrieve the file metadata associated with an item ID. The response includes file details such as the title, file name, ingestion status, publisher, and organization. ## Authentication The API requires an API key with access to the publisher that owns the file. The organization associated with your API key must be authorized to access the requested file. ## Path parameter * `item_id` — The UUID of the item associated with the file. Common errors include an unauthorized request or a file that cannot be found. ### Common errors * **403 Forbidden** — You are not authorized to access this file. * **404 Not Found** — No file exists for the requested item ID. # Real-time ingestion Source: https://docs.gloo.com/api-reference/ingestion/v1 post /ingestion/v1/real_time_upload Real-time ingestion. **This endpoint has been deprecated.** Use the [Direct File Upload API](/api-reference/ingestion/v2) instead. ## Authentication The API requires an API key with access to the specified publisher. The system validates that the organization associated with your API key has permission to access the publisher specified in your request. You will only be able to send content to a publisher that belongs to your organization. Please double-check the `publisher_id` field in the request body for accuracy. # Upload Files Source: https://docs.gloo.com/api-reference/ingestion/v2 post /ingestion/v2/files Upload files for ingestion or item updates. Common errors: publisher org mismatch, publisher not found, file read errors, duplicate uploads in the response body. ## Authentication The API requires an API key with access to the specified publisher. The system validates that the organization associated with your API key has permission to access the publisher specified in your request. You will only be able to send content to a publisher that belongs to your organization. Please double-check the `publisher_id` field in the request body for accuracy. # Responses Source: https://docs.gloo.com/api-reference/responses/governed post /ai/v2/guarded/responses Generate Responses API outputs through Gloo's governance layer. Common errors: guardrail block (403), unsupported model, unsupported sampling parameter, context window exceeded, provider timeout, provider unavailable. # Grounded Responses Source: https://docs.gloo.com/api-reference/responses/grounded post /ai/v1/grounded/responses Generate grounded Responses API outputs with retrieval-augmented generation (RAG). Common errors: unsupported model, missing or invalid grounding parameters, context window exceeded, provider timeout, provider unavailable. # Direct Responses Source: https://docs.gloo.com/api-reference/responses/v2 post /ai/v2/direct/responses Generate Responses API outputs directly from the selected model without Gloo governance. Common errors: unsupported model, unsupported sampling parameter, context window exceeded, provider timeout, provider unavailable. # Analysis Source: https://docs.gloo.com/api-reference/sotc/analysis post /ai/v1/kallm/sotc/analysis Analysis. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Church Metrics AI Insights Source: https://docs.gloo.com/api-reference/sotc/church-metrics-ai-insights post /ai/v1/kallm/sotc/church-metrics-ai-insights Church Metrics AI Insights. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Question Answers Analysis Source: https://docs.gloo.com/api-reference/sotc/question-answers-analysis post /ai/v1/kallm/sotc/question-answers-analysis Question Answers Analysis. **Deprecated Endpoint**: This endpoint is deprecated and no longer supported. # Handling Streaming Failures Source: https://docs.gloo.com/best-practices/completions-streaming-failures Best practices for retrying, continuing, and surfacing Completions V2 streaming failures. When you stream Completions V2 responses (`stream: true`), failures can arrive **after** the HTTP `200 text/event-stream` response has already started. By then your application may have rendered partial output to the user, so a naive "just retry the request" strategy can erase visible text, duplicate it, or silently replace one answer with a different one. This guide covers how to: * classify stream-time failures using the structured `error` object * choose between **preserving partial output**, **bounded retry**, **application-level continuation**, and **surfacing an interruption** * apply concrete retry ceilings and backoff * avoid duplicating tool-call side effects * log the right fields for support and observability The reference implementations are in Python and TypeScript. Building in Go, Java, PHP, or another language? The language-neutral [streaming error object table](#the-streaming-error-object) and [decision tree](#decision-tree) below are the source of truth — port the same logic. ## Streaming Failure Phases A streamed completion can fail in four distinct ways. Handle each differently: | Phase | What you observe | Handling | | - | - | - | | Pre-stream HTTP error | Non-2xx HTTP response before any SSE bytes | Standard HTTP error handling — see [Errors](/api-reference/general/errors) | | Stream error (Signal A) | SSE event with `choices[0].finish_reason: "error"` and a structured `error` object | The retry/continuation logic in this guide | | Transport disconnect | The connection drops with no structured SSE error | Treat as plausibly transient; same decision tree as Signal A | | Content filter (Signal B) | SSE event with `finish_reason: "content_filter"` and **no** `error` object | Terminal. Surface to the user; never retry or continue | ### Two distinct stream-time signals Split detection into two signals so your client never misclassifies a content-filter stop as a clean early completion — or as a retryable error: * **Signal A — structured stream error.** `choices[0].finish_reason` is `"error"` and the event carries the structured `error` object described below. This is the only path that retry and continuation logic applies to. * **Signal B — content-filter termination.** `choices[0].finish_reason` is `"content_filter"`. The event carries **no** `error` object. It is terminal and non-retryable: surface it to the user, and never retry or continue automatically. ### The streaming error object A Signal A event includes a structured `error` object with nine fields, plus a top-level `trace_id` on the SSE chunk itself. This is the canonical reference — every section below branches on these fields. ```json theme={null} { "id": "chatcmpl-error-abc123", "object": "chat.completion.chunk", "created": 1769792268, "model": "gloo-anthropic-claude-sonnet-4.6", "choices": [ { "delta": { "content": null, "role": null }, "finish_reason": "error", "index": 0 } ], "error": { "message": "An unexpected provider error occurred mid-stream.", "type": "internal_error", "code": 3001, "name": "INTERNAL_ERROR", "category": "provider_error", "description": "An unexpected provider error occurred mid-stream.", "fault": "provider", "retryable": true, "trace_id": "trace-id" }, "trace_id": "trace-id" } ``` | Field | Branch on it? | Notes | | - | - | - | | `error.retryable` | **Yes — primary retry signal** | Whether retrying the same request may succeed | | `error.fault` | Yes — secondary | Responsible party: `client`, `provider`, or `internal` | | `error.code` | Yes — classification after `retryable`/`fault` | Numeric internal code, such as `3001` | | `error.name` | Yes — classification | Symbolic name, such as `INTERNAL_ERROR` | | `error.type` | No — observability/display only | Stable wire-format error type | | `error.category` | No — observability/display only | `client_error`, `provider_error`, or `platform_error` | | `error.description` | No — display only | Stable explanation of the error code | | `error.message` | No — display only | Request-specific customer-facing message | | `error.trace_id` | No — observability | See [trace ID precedence](#observability-checklist) | | top-level `trace_id` | No — observability | Present on every SSE chunk; see [trace ID precedence](#observability-checklist) | ## Read the Whole Error Object The same `error.code` can represent different situations, so **never branch on `code` alone**. `3001 / INTERNAL_ERROR` is the clearest example: * A retryable stream-time provider anomaly surfaces as `code: 3001`, `name: "INTERNAL_ERROR"`, `fault: "provider"`, **`retryable: true`**. * A platform-internal `3001` surfaces with `fault: "internal"` and **`retryable: false`**. Branch on `error.retryable` first, then use `fault`, `code`, and `name` for classification and observability: 1. `error.retryable` — should this request be retried at all? 2. `error.fault` — was it a `provider` issue or `internal` to the platform? 3. `error.code` / `error.name` — what exactly happened (for logs, metrics, alerts)? Treating every `3001 / INTERNAL_ERROR` as non-retryable will drop recoverable streams; treating every `3001` as retryable will loop on genuine platform errors. Read `error.retryable`. ## Decision Tree Two independent axes govern recovery — keep them separate so "attempt" is never ambiguous: * **Which operation?** Driven by whether content was already shown to the user. * Not shown → **full retry** (a fresh stream). * Shown → **Continue** (application-level continuation — never a full retry, which would force erasing or duplicating visible text). Continuation is exclusive to the post-content phase. * **How many attempts?** Driven by context (ceilings below). Always use full-jitter backoff; always stop early on `retryable: false`. The flow: 1. **Stream fails before any visible content** * Retry only when `error.retryable` is `true`, or the failure is a transport error that is plausibly transient. * Ceiling: **2 full-retry attempts** for live UX. 2. **Stream fails after visible content** (post-content recovery) * Preserve the partial output. Never silently replace the visible answer. * If a side-effecting tool call was emitted or executed this turn, **skip the automatic attempt** (see [Tool Calls and Side Effects](#tool-calls-and-side-effects)) and surface the interruption directly. * Otherwise attempt **exactly one automatic Continue** (continuation). * If the automatic Continue fails or was skipped, mark the answer interrupted and surface a user-initiated **Continue** (primary, non-destructive) and an explicitly labelled, destructive **Try again** (full retry, secondary). * "At most one" means one *automatic* attempt — never a silent loop. User-initiated actions are user-governed and don't count against the automatic budget. * For high-stakes, factual, or doctrinal answers, steer away from automatic Continue toward Try again or explicit user review (see [Application-Level Continuation](#application-level-continuation)). 3. **Background/batch workflow** * Up to **3 full-retry attempts** with jittered exponential backoff and an overall job timeout. Partial output can be discarded safely because nothing was shown to a user. 4. **`error.retryable` is `false`** * Do not retry unchanged. Surface the error, change the request or model, or escalate with the trace ID. 5. **`finish_reason` is `"content_filter"`** (Signal B) * Terminal and non-retryable. Surface it; never retry or continue. ## Application-Level Continuation Completions V2 does **not** expose true stream resumption — there is no cursor, offset, replay token, or resume endpoint. What your application can do instead is *application-level continuation*: a **new request** that includes the partial output as context and asks the model to continue. Send the partial output inside a **new user message**: ```json theme={null} { "messages": [ { "role": "user", "content": "Explain Romans 8 in simple terms." }, { "role": "user", "content": "The previous streamed answer was interrupted after this text:\n\nRomans 8 teaches that...\n\nContinue from that point without repeating the text above." } ], "auto_routing": true, "stream": true } ``` Do not end the continuation request with an assistant message containing the partial text — final assistant turns are not supported uniformly across models and routing modes, while the user-message pattern works everywhere. **Continuation can drift.** A continuation is a new request: it can repeat, drift, or complete differently from the failed stream. Because Continue appends with no visual seam, the consumer reads the result as one continuous answer. Two consequences for you as the developer: * **High-stakes, factual, or doctrinal content:** avoid automatic Continue. Prefer Try again (a clean replacement) or explicit user review, since drift can introduce unflagged errors mid-answer. * **The prompt contract is best-effort.** Instructing the model to "continue without repeating" usually works, but your application should still de-duplicate overlap at the seam. ## Retry Budgets and Backoff Recommended defaults: | Context | Operation | Ceiling | | - | - | - | | Live, no visible content yet | Full retry | 2 attempts | | Live, visible partial content | Automatic Continue (continuation) | 1 attempt, then user-initiated controls | | Background/batch | Full retry | 3 attempts | * Use **full-jitter exponential backoff** between attempts. * Cap live UX delays aggressively (a couple of seconds at most); use a broader cap for background jobs. * Stop early the moment an error reports `retryable: false`. * Do not blindly retry five times for live streams — generic rate-limit retry examples are not a live-streaming UX policy. Both reference implementations use the same full-jitter formula: ```python Python theme={null} import random def full_jitter_delay(attempt: int, base: float = 0.5, cap: float = 2.0) -> float: """Full-jitter backoff: random() * min(cap, base * 2**attempt).""" return random.random() * min(cap, base * (2**attempt)) ``` ```typescript TypeScript theme={null} /** Full-jitter backoff: random() * min(cap, base * 2**attempt). */ export function fullJitterDelayMs(attempt: number, baseMs = 500, capMs = 2_000): number { return Math.random() * Math.min(capMs, baseMs * 2 ** attempt); } ``` ## Rate Limits Rate limits are a separate concern from generic retryable stream failures: * An HTTP `429` **before the stream starts** is a normal rate-limit response. Respect `Retry-After` or `X-RateLimit-Reset` **when the response actually includes those headers**, and back off with jitter. See [Rate Limits](/api-reference/general/limits). * A rate limit that occurs **after the stream has started** (for example, an upstream provider limit) arrives as a Signal A stream error such as `2004 / RATE_LIMIT`. Handle it through the normalized SSE `error` object — stream-time SSE error events do not carry `Retry-After`, `X-RateLimit-Reset`, or provider rate-limit headers. * Rate-limit retries should use jittered backoff and must not run indefinitely. ## Partial Output UX When recovery involves text a user has already seen: * **Keep partial text visible.** Never make displayed output disappear silently. * **Mark the answer as interrupted** if recovery fails, so the user knows it is incomplete. * **Offer user-visible controls** where appropriate: **Continue** (primary, appends) and **Try again** (secondary, explicitly labelled as replacing the current answer). * **If full retry is chosen, make the replacement explicit** — the user should understand the visible answer is being replaced, not extended. * **Retain enough application state** to know whether any content was displayed; that single flag drives the choice between full retry and continuation. ## Tool Calls and Side Effects Full retries and continuations can duplicate side effects if the model already emitted a tool call and your application executed it — sending an email twice, charging a card twice. * Use **idempotency keys or operation IDs** for tool execution, so a replayed call can be detected and dropped. * Record, per turn, whether a tool call was **emitted** and whether it was **executed**, before any retry decision. * **Do not automatically replay** side-effecting tool calls unless your application can prove the prior attempt did not execute. * If a side-effecting tool call fired in the interrupted turn, **skip the automatic Continue** and surface the interruption — prefer fail-fast or explicit user confirmation over silent recovery. ## Observability Checklist Log the full streaming error object — including the non-branching fields `error.type`, `error.category`, and `error.description` — even though retry decisions only use `retryable`, `fault`, `code`, and `name`. **Trace IDs.** Log one normalized field named `trace_id`, with this precedence: ```text theme={null} event.error?.trace_id ?? event.trace_id ?? response.headers.get("x-sentry-trace-id") ``` If `event.error.trace_id` and the top-level `event.trace_id` both exist and differ, log both — and include both in support requests. Recommended fields per failed or recovered stream: * normalized `trace_id` (plus the secondary trace ID on mismatch) * `error.code`, `error.name`, `error.type`, `error.fault`, `error.retryable` * selected model (or requested model) and routing mode * whether content was displayed, and the partial output length * attempt number and backoff delay * whether continuation or full retry was used * whether tool calls were emitted or executed ## Summary Table | Condition | Recommended action | Retry ceiling | UX note | | - | - | - | - | | Pre-stream retryable error (`408`, `429`, `5xx`) | Full retry with backoff | 2 (live) | Nothing rendered yet; retry is invisible | | Stream error before first token | Full retry if `retryable: true` | 2 full retries | Show a loading state, not an error, until the budget is spent | | Stream error after partial output | One automatic Continue, then user controls | 1 automatic continuation | Preserve partial text; mark interrupted on failure | | Rate limit (HTTP 429 pre-stream) | Back off; honor `Retry-After`/`X-RateLimit-Reset` if present | Bounded, jittered | Consider a "busy, retrying" indicator | | Rate limit (stream event `2004`) | Treat as Signal A via `error` object | Same as stream phase | No retry headers exist on SSE errors | | Content filter (`finish_reason: "content_filter"`) | Surface; never retry or continue | 0 | Terminal signal, not an error object | | Non-retryable error (`retryable: false`) | Stop; surface, change request/model, or escalate | 0 | Include `trace_id` in support requests | | Side-effecting tool workflow | Skip automatic recovery; fail fast or confirm | 0 automatic | Require explicit user action | | Background/batch workflow | Full retry with backoff and job timeout | 3 full retries | Partial output can be discarded safely | ## Reference Implementations The modules below implement the full decision tree: SSE parsing, the two signals, trace-ID normalization, retry ceilings, full-jitter backoff, the one-automatic-Continue rule, and the tool-call side-effect guard. Copy either one into your project as a small client module. Usage: ```python Python theme={null} result = stream_with_recovery( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", {"Authorization": f"Bearer {api_key}"}, { "messages": [{"role": "user", "content": "Explain Romans 8 in simple terms."}], "auto_routing": True, "stream": True, }, on_delta=lambda text: print(text, end="", flush=True), ) # result.status is "complete", "content_filter", "interrupted", or "failed" # result.text always contains everything safe to display ``` ```typescript TypeScript theme={null} const result = await streamWithRecovery( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { Authorization: `Bearer ${accessToken}` }, { messages: [{ role: "user", content: "Explain Romans 8 in simple terms." }], auto_routing: true, stream: true, }, { onDelta: (text) => process.stdout.write(text) }, ); // result.status is "complete", "content_filter", "interrupted", or "failed" // result.text always contains everything safe to display ``` Recovery: ```python Python theme={null} # Standard library plus `requests`: """Streaming recovery client for Gloo AI Completions V2.""" import json import random import time from dataclasses import dataclass import requests LIVE_PRE_TOKEN_RETRIES = 2 # full-retry attempts before any content is visible BACKGROUND_RETRIES = 3 # full-retry attempts for background/batch jobs LIVE_BACKOFF_CAP_S = 2.0 # keep live UX delays short BACKGROUND_BACKOFF_CAP_S = 30.0 def full_jitter_delay(attempt: int, base: float = 0.5, cap: float = LIVE_BACKOFF_CAP_S) -> float: """Full-jitter backoff: random() * min(cap, base * 2**attempt).""" return random.random() * min(cap, base * (2**attempt)) @dataclass class StreamOutcome: """Everything one stream attempt tells you, including observability fields.""" partial_text: str = "" received_content: bool = False tool_call_emitted: bool = False finish_reason: str | None = None error: dict | None = None trace_id: str | None = None secondary_trace_id: str | None = None # set when error-level and top-level IDs differ transport_error: str | None = None http_status: int | None = None @property def completed(self) -> bool: return self.finish_reason in ("stop", "length", "tool_calls") @property def content_filtered(self) -> bool: return self.finish_reason == "content_filter" @property def retryable(self) -> bool: if self.transport_error is not None: return True # transport drops are plausibly transient return bool(self.error and self.error.get("retryable")) @dataclass class RecoveryResult: # "complete" | "content_filter" | "interrupted" | "failed" status: str text: str last_outcome: StreamOutcome used_continuation: bool = False full_retries_used: int = 0 def stream_once(url: str, headers: dict, body: dict, on_delta=None) -> StreamOutcome: """Run one streaming request and fold every SSE event into a StreamOutcome.""" outcome = StreamOutcome() try: with requests.post(url, headers=headers, json=body, stream=True, timeout=120) as response: outcome.http_status = response.status_code if response.status_code != 200: # Pre-stream HTTP error: no SSE events will follow. outcome.transport_error = f"HTTP {response.status_code}" return outcome # Lowest-precedence trace source; SSE events override it below. outcome.trace_id = response.headers.get("x-sentry-trace-id") for line in response.iter_lines(decode_unicode=True): if not line or not line.startswith("data:"): continue payload = line[len("data:"):].strip() if payload == "[DONE]": break _apply_event(outcome, json.loads(payload), on_delta) if outcome.finish_reason is not None: break except requests.RequestException as exc: outcome.transport_error = str(exc) return outcome def _apply_event(outcome: StreamOutcome, event: dict, on_delta=None) -> None: # trace_id precedence: error-level, then top-level, then response header. error_trace = (event.get("error") or {}).get("trace_id") top_trace = event.get("trace_id") outcome.trace_id = error_trace or top_trace or outcome.trace_id if error_trace and top_trace and error_trace != top_trace: outcome.secondary_trace_id = top_trace # log both, send both to support choice = (event.get("choices") or [{}])[0] delta = choice.get("delta") or {} if delta.get("content"): outcome.partial_text += delta["content"] outcome.received_content = True if on_delta: on_delta(delta["content"]) if delta.get("tool_calls") or delta.get("function_call"): outcome.tool_call_emitted = True if choice.get("finish_reason"): outcome.finish_reason = choice["finish_reason"] if event.get("error"): outcome.error = event["error"] def continuation_messages(messages: list[dict], partial_text: str) -> list[dict]: """Provider-neutral continuation: partial output goes in a NEW user message.""" return [ *messages, { "role": "user", "content": ( "The previous streamed answer was interrupted after this text:\n\n" f"{partial_text}\n\n" "Continue from that point without repeating the text above." ), }, ] def stream_with_recovery( url: str, headers: dict, body: dict, *, background: bool = False, on_delta=None ) -> RecoveryResult: """One streamed completion with bounded recovery. Live mode: 2 full retries before the first token; after visible content, exactly one automatic Continue, then surface user controls. Background mode: up to 3 full retries; partial output is discarded safely because nothing was shown to a user. """ max_full_retries = BACKGROUND_RETRIES if background else LIVE_PRE_TOKEN_RETRIES backoff_cap = BACKGROUND_BACKOFF_CAP_S if background else LIVE_BACKOFF_CAP_S full_retries = 0 while True: outcome = stream_once(url, headers, body, on_delta) if outcome.completed: return RecoveryResult("complete", outcome.partial_text, outcome, full_retries_used=full_retries) if outcome.content_filtered: # Terminal non-error signal: surface it; never retry or continue. return RecoveryResult("content_filter", outcome.partial_text, outcome, full_retries_used=full_retries) if not outcome.retryable: # retryable=false: do not retry unchanged. return RecoveryResult("failed", outcome.partial_text, outcome, full_retries_used=full_retries) if outcome.received_content and not background: # Content is on screen: a full retry would erase or duplicate it. return _recover_after_partial(url, headers, body, outcome, on_delta) if full_retries >= max_full_retries: return RecoveryResult("failed", outcome.partial_text, outcome, full_retries_used=full_retries) time.sleep(full_jitter_delay(full_retries, cap=backoff_cap)) full_retries += 1 def _recover_after_partial( url: str, headers: dict, body: dict, first: StreamOutcome, on_delta=None ) -> RecoveryResult: """Post-content recovery: keep the partial text, try one automatic Continue.""" if first.tool_call_emitted: # Side-effect guard: an automatic attempt could replay the tool call. # Mark the answer interrupted and hand control to the user. return RecoveryResult("interrupted", first.partial_text, first) time.sleep(full_jitter_delay(0)) continuation_body = {**body, "messages": continuation_messages(body["messages"], first.partial_text)} second = stream_once(url, headers, continuation_body, on_delta) if second.completed: return RecoveryResult("complete", first.partial_text + second.partial_text, second, used_continuation=True) # The one automatic Continue failed. Keep everything visible, mark the # answer interrupted, and surface user-initiated Continue / Try again. return RecoveryResult("interrupted", first.partial_text + second.partial_text, second, used_continuation=True) ``` ```typescript TypeScript theme={null} // Browser and Node 18+ compatible; no dependencies: // /** Streaming recovery client for Gloo AI Completions V2. */ export interface StreamError { message: string; type: string; code: number; name: string; category: string; description: string; fault: "client" | "provider" | "internal"; retryable: boolean; trace_id?: string; } export interface StreamEvent { choices?: Array<{ delta?: { content?: string | null; role?: string | null; tool_calls?: unknown[]; function_call?: unknown; }; finish_reason?: string | null; index?: number; }>; error?: StreamError; trace_id?: string; } export interface ChatMessage { role: "system" | "user" | "assistant" | "tool"; content: string; } export interface StreamOutcome { partialText: string; receivedContent: boolean; toolCallEmitted: boolean; finishReason: string | null; error: StreamError | null; traceId: string | null; /** Set when error-level and top-level trace IDs differ; log both. */ secondaryTraceId: string | null; transportError: string | null; httpStatus: number | null; } export type RecoveryStatus = "complete" | "content_filter" | "interrupted" | "failed"; export interface RecoveryResult { status: RecoveryStatus; text: string; lastOutcome: StreamOutcome; usedContinuation: boolean; fullRetriesUsed: number; } export const LIVE_PRE_TOKEN_RETRIES = 2; // full-retry attempts before any content is visible export const BACKGROUND_RETRIES = 3; // full-retry attempts for background/batch jobs export const LIVE_BACKOFF_CAP_MS = 2_000; // keep live UX delays short export const BACKGROUND_BACKOFF_CAP_MS = 30_000; /** Full-jitter backoff: random() * min(cap, base * 2**attempt). */ export function fullJitterDelayMs(attempt: number, baseMs = 500, capMs = LIVE_BACKOFF_CAP_MS): number { return Math.random() * Math.min(capMs, baseMs * 2 ** attempt); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function newOutcome(): StreamOutcome { return { partialText: "", receivedContent: false, toolCallEmitted: false, finishReason: null, error: null, traceId: null, secondaryTraceId: null, transportError: null, httpStatus: null, }; } const completed = (o: StreamOutcome) => o.finishReason === "stop" || o.finishReason === "length" || o.finishReason === "tool_calls"; const contentFiltered = (o: StreamOutcome) => o.finishReason === "content_filter"; // Transport drops are plausibly transient; structured errors carry the flag. const isRetryable = (o: StreamOutcome) => o.transportError !== null ? true : Boolean(o.error?.retryable); function applyEvent(outcome: StreamOutcome, event: StreamEvent, onDelta?: (text: string) => void): void { // trace_id precedence: error-level, then top-level, then response header. const errorTrace = event.error?.trace_id; const topTrace = event.trace_id; outcome.traceId = errorTrace ?? topTrace ?? outcome.traceId; if (errorTrace && topTrace && errorTrace !== topTrace) { outcome.secondaryTraceId = topTrace; // log both, send both to support } const choice = event.choices?.[0]; const delta = choice?.delta; if (delta?.content) { outcome.partialText += delta.content; outcome.receivedContent = true; onDelta?.(delta.content); } if (delta?.tool_calls?.length || delta?.function_call) { outcome.toolCallEmitted = true; } if (choice?.finish_reason) { outcome.finishReason = choice.finish_reason; } if (event.error) { outcome.error = event.error; } } /** Run one streaming request and fold every SSE event into a StreamOutcome. */ export async function streamOnce( url: string, headers: Record, body: Record, onDelta?: (text: string) => void, ): Promise { const outcome = newOutcome(); try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify(body), }); outcome.httpStatus = response.status; if (!response.ok || !response.body) { // Pre-stream HTTP error: no SSE events will follow. outcome.transportError = `HTTP ${response.status}`; return outcome; } // Lowest-precedence trace source; SSE events override it. outcome.traceId = response.headers.get("x-sentry-trace-id"); const reader: ReadableStreamDefaultReader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let newlineIndex: number; while ((newlineIndex = buffer.indexOf("\n")) >= 0) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (!line.startsWith("data:")) continue; const payload = line.slice("data:".length).trim(); if (payload === "[DONE]") { await reader.cancel(); return outcome; } applyEvent(outcome, JSON.parse(payload) as StreamEvent, onDelta); if (outcome.finishReason !== null) { await reader.cancel(); return outcome; } } } } catch (err) { outcome.transportError = err instanceof Error ? err.message : String(err); } return outcome; } /** Provider-neutral continuation: partial output goes in a NEW user message. */ export function continuationMessages(messages: ChatMessage[], partialText: string): ChatMessage[] { return [ ...messages, { role: "user", content: "The previous streamed answer was interrupted after this text:\n\n" + `${partialText}\n\n` + "Continue from that point without repeating the text above.", }, ]; } /** * One streamed completion with bounded recovery. * * Live mode: 2 full retries before the first token; after visible content, * exactly one automatic Continue, then surface user controls. * Background mode: up to 3 full retries; partial output is discarded safely * because nothing was shown to a user. */ export async function streamWithRecovery( url: string, headers: Record, body: Record & { messages: ChatMessage[] }, options: { background?: boolean; onDelta?: (text: string) => void } = {}, ): Promise { const { background = false, onDelta } = options; const maxFullRetries = background ? BACKGROUND_RETRIES : LIVE_PRE_TOKEN_RETRIES; const backoffCapMs = background ? BACKGROUND_BACKOFF_CAP_MS : LIVE_BACKOFF_CAP_MS; let fullRetries = 0; while (true) { const outcome = await streamOnce(url, headers, body, onDelta); if (completed(outcome)) { return { status: "complete", text: outcome.partialText, lastOutcome: outcome, usedContinuation: false, fullRetriesUsed: fullRetries, }; } if (contentFiltered(outcome)) { // Terminal non-error signal: surface it; never retry or continue. return { status: "content_filter", text: outcome.partialText, lastOutcome: outcome, usedContinuation: false, fullRetriesUsed: fullRetries, }; } if (!isRetryable(outcome)) { // retryable=false: do not retry unchanged. return { status: "failed", text: outcome.partialText, lastOutcome: outcome, usedContinuation: false, fullRetriesUsed: fullRetries, }; } if (outcome.receivedContent && !background) { // Content is on screen: a full retry would erase or duplicate it. return recoverAfterPartial(url, headers, body, outcome, onDelta); } if (fullRetries >= maxFullRetries) { return { status: "failed", text: outcome.partialText, lastOutcome: outcome, usedContinuation: false, fullRetriesUsed: fullRetries, }; } await sleep(fullJitterDelayMs(fullRetries, 500, backoffCapMs)); fullRetries += 1; } } /** Post-content recovery: keep the partial text, try one automatic Continue. */ async function recoverAfterPartial( url: string, headers: Record, body: Record & { messages: ChatMessage[] }, first: StreamOutcome, onDelta?: (text: string) => void, ): Promise { if (first.toolCallEmitted) { // Side-effect guard: an automatic attempt could replay the tool call. // Mark the answer interrupted and hand control to the user. return { status: "interrupted", text: first.partialText, lastOutcome: first, usedContinuation: false, fullRetriesUsed: 0, }; } await sleep(fullJitterDelayMs(0)); const continuationBody = { ...body, messages: continuationMessages(body.messages, first.partialText), }; const second = await streamOnce(url, headers, continuationBody, onDelta); if (completed(second)) { return { status: "complete", text: first.partialText + second.partialText, lastOutcome: second, usedContinuation: true, fullRetriesUsed: 0, }; } // The one automatic Continue failed. Keep everything visible, mark the // answer interrupted, and surface user-initiated Continue / Try again. return { status: "interrupted", text: first.partialText + second.partialText, lastOutcome: second, usedContinuation: true, fullRetriesUsed: 0, }; } ``` ## Related Pages Error codes, the AI error object, and streaming error delivery. HTTP 429 handling and backoff guidance. Routing modes, streaming support, and request parameters. Function calling across routing modes, including streaming. # Preparing Content for Ingestion Source: https://docs.gloo.com/best-practices/ingestion-pattern A source-agnostic pattern for getting content from any system — CMS, API, database, file library, or feed — into Gloo. Organizations bring content into an AI system for one reason: so people can ask questions in plain language and get answers grounded in that content, instead of digging through a wiki, CMS, or shared drive by hand. That only works if the content is discoverable, accurately attributed, and trustworthy. Gloo's Data Engine handles the AI-readiness side of that: fetching content, transcribing audio or video, normalizing it into clean text, chunking it into passages, embedding, and indexing for search and grounded completions. But that pipeline can't recover context you never gave it — it's supplemented by a content preparation pipeline: the decisions you make about your source content *before* Gloo ever sees it. How you extract content, what shape you normalize it into, what stable IDs you assign, and what metadata you attach determine whether the whole system stays manageable for years or turns into broken citations and un-refreshable content down the line. This guide describes that preparation layer as one repeatable pattern, deliberately source-agnostic — the source system is the *least* important part of the design. This is a design guide, not a runnable walkthrough. For a complete, runnable pipeline in six languages, follow the [Build an End-to-End RAG Pipeline series](/tutorials/rag-pipeline-part-1) — everything downstream of the adapter stage here is exactly what that series implements. ## Do You Need a Custom Pipeline? Before building anything, check whether Studio's managed ingestion already covers your source. [Gloo AI Studio](https://studio.ai.gloo.com/ingestion) provides an interface with four no-code connectors: * **Web Scraping** — point Gloo at a website or sitemap URL and it crawls and ingests the pages. The right path for public sites you don't have API access to. * **Files** — drag-and-drop upload across all supported formats. * **YouTube** — ingest a channel or videos; Gloo handles transcription. * **Feeds** — subscribe to RSS/content feeds without writing a poller. **Use Studio managed ingestion when point-and-go is enough:** a public website, a YouTube channel, a standard feed, or occasional file drops, especially when the person doing the ingesting isn't a developer. **Build a custom pipeline — the subject of this guide — when you need control the managed connectors don't give you:** stable [`producer_id`](/getting-started/glossary#producer_id) schemes for programmatic updates and deletes, metadata attached at scale from your source system, incremental sync against a private CMS/API/database, or content that lives behind authentication. **Audio and video content** (podcasts, sermon recordings, YouTube videos) is handled for you via the YouTube connector and audio/video file uploads. Those connectors transcribe content automatically so you don't need to produce a transcript first. The preparation advice in this guide targets text content; if you already have text transcripts, they follow the same pattern as any other document. ## The Pattern Every ingestion integration, regardless of source, decomposes into the same five stages: Ingestion pipeline: Adapter and Normalize, then Upload, Edit, and Verify Ingestion pipeline: Adapter and Normalize, then Upload, Edit, and Verify **Adapter** — source-specific code that extracts content items and their native metadata from wherever they live: paginate a CMS API, query database rows, list bucket objects, read a feed. This is the only stage that changes when the source changes, so keep it thin: its whole job is to emit items in a common shape for the rest of the pipeline. **Normalize** — convert each item to clean text (Markdown preferred), strip boilerplate, deduplicate within the batch, detect the content type, and assign a stable ID. This stage is where most quality problems are won or lost — see [Pre-Ingestion Preparation](#pre-ingestion-preparation) below. **Upload** — send each item to **POST** `/ingestion/v2/files` with a [`producer_id`](/getting-started/glossary#producer_id). Because `producer_id` makes uploads idempotent, re-running the whole pipeline is safe: existing items are detected instead of duplicated. **Edit Metadata** — attach metadata (title, summary, author, tags, canonical URL) via **PATCH** `/engine/v2/item`. You can target the item by `producer_id`, so this stage needs no state from the upload stage. **Verify** — ingestion is asynchronous: the upload response means *queued*, not *searchable*. Poll **GET** `/engine/v2/items/{item_id}` until `status` reaches `COMPLETED` (you may see intermediate states such as `CHUNKING` along the way). The key property: **everything to the right of the adapter is identical for every source.** Write the normalize → upload → edit metadata → verify stages once; when a new content source appears, you write only a new adapter. ## Pre-Ingestion Preparation These are the decisions to make up front — each is cheap to decide before launch and expensive to retrofit after. ### Detect the content type early Classify each item — article, transcript, report, Q\&A, devotional — as it comes out of the adapter, before normalization. The type drives everything downstream: how you clean the text, whether the item has natural boundaries worth preserving, and what metadata makes it findable. Most source systems already know the type (a CMS post type, a database column, a file extension); carry it through rather than inferring it later. ### Normalize to clean text Gloo accepts a wide range of formats and detects each automatically: Markdown, plain text, rich text, reStructuredText, ORG, HTML, PDF, Word, PowerPoint, EPUB, CSV, TSV, Excel, JSON, XML — plus audio (`aac`, `flac`, `m4a`, `mp3`, `ogg`, `wav`) and video (`avi`, `flv`, `mov`, `mp4`, `webm`, `wmv`), which Gloo transcribes for you. When you control the conversion, prefer **Markdown**: it preserves heading structure (which helps chunking find natural boundaries) while staying free of layout noise. | Source format | Guidance | | - | - | | HTML (CMS pages, web content) | Convert to Markdown. Strip navigation, headers/footers, cookie banners, comment sections — keep only the content a reader would consider the article. Preserve headings. | | PDF | Extract text and inspect the output: multi-column layouts, running headers, and page numbers often survive extraction as mid-sentence noise. Clean these before upload rather than uploading raw extractions at scale. | | Transcripts (podcasts, sermons, meetings) | If you already have a text transcript, keep speaker labels and section breaks that carry meaning; strip per-word timestamps and filler tokens. If you only have audio/video, upload the media itself (or use the [Studio YouTube connector](#do-you-need-a-custom-pipeline)) and let Gloo transcribe. | | Structured records (database rows, JSON, spreadsheets) | Render each record as a small Markdown document — a heading plus labeled fields — rather than concatenating raw values. CSV/TSV/Excel/JSON upload directly too, but ask whether one row is really one retrievable document; if so, per-record Markdown items give each its own `producer_id` and metadata. | Whatever you strip, strip it *consistently*: the goal is that two items about the same topic differ only in their content, not in whatever noise their source format happened to leave behind. ### Deduplicate — your half and Gloo's half Deduplication splits cleanly into two responsibilities: * **You dedup within a run.** Source systems frequently expose the same item more than once — a post that appears in two categories, a document reachable at two paths, a feed that repeats entries. Dedup on the source system's native ID inside your adapter, before anything is uploaded. * **Gloo dedups across runs.** Uploading with the same `producer_id` again doesn't create a copy — the existing item is detected. This is what makes "just re-run the pipeline" a safe recovery strategy. If you dedup within each batch and use stable `producer_id`s, you never need to track "what have I already uploaded?" state yourself. ### Choose a `producer_id` scheme before the first upload This is the single highest-leverage decision in the whole pipeline. The `producer_id` is your stable handle on an item: it makes uploads idempotent, lets you update metadata without storing Gloo's `item_id`, and lets a sync process map source-system changes onto Gloo items years later. The recommended scheme is `{source-system}-{native-id}`: ```text theme={null} wordpress-post-4812 notion-page-9f3c2a1e-8b4d-4f6a-9c0d-1e2f3a4b5c6d kb-row-20871 s3-sermons/2026/easter-sunday.md ``` **Do:** * Derive it from the source system's own primary key — the one identifier that survives edits, renames, and moves. * Prefix with the source system so IDs stay unique when a second source later feeds the same publisher. * Treat it as permanent: the same source item must always map to the same `producer_id`. **Don't:** * Derive it from the title, URL slug, or file path if those can change — a renamed item would look like a brand-new one, leaving the old version orphaned in your publisher. * Derive it from position or ordering (row number, feed index). * Reuse a `producer_id` for different content — that's an update, not a new item. `producer_id` applies to **single-file uploads only**. If you pass it on a multi-file upload, it's ignored. Pipelines should upload one file per request so every item gets its stable handle. ### Pre-chunk vs. let Gloo chunk **Default: let Gloo chunk.** After upload, Gloo automatically splits your content into retrieval-sized chunks, embeds them, and indexes them — you don't configure any of it, and for most content types (articles, reports, transcripts, long-form prose) automatic chunking is the right answer. Split content yourself *before* upload only when it has hard semantic boundaries that must not be crossed — units that would be wrong to retrieve partially or merged with a neighbor: * **Q\&A content** — each question-and-answer pair is one unit; upload each pair as its own item. * **Devotionals or daily entries** — each entry stands alone; a chunk spanning two days' entries would mislead retrieval. * **Episodic collections** — if each segment has its own title, date, and canonical URL, it deserves its own item and metadata. When you split, each piece becomes its own upload with its own `producer_id` (e.g. `faq-item-291`, `devotional-2026-03-14`) and its own metadata. Note this is a decision about *item granularity* — what constitutes one document — not about tuning chunk sizes. Within each item, Gloo still chunks automatically. For what that automatic chunking actually does — and per-content-type granularity guidance — see [How Chunking Works](/api-guides/chunking). ### Metadata: attach at upload time vs. edit later The [item metadata fields](/api-guides/edit-content) — `item_title`, `item_summary`, `item_url`, `item_image`, `publication_date`, `author`, `item_tags`, and friends — can be set as soon as the item exists; you don't need to wait for ingestion to finish. The decision rule: * **Attach immediately what the source system already knows.** Title, author, publication date, canonical URL, and native tags are sitting right there in the adapter's output — PATCH them right after each upload, as part of the pipeline. * **Edit later what has to be derived.** Editorial summaries, curated tags, and classifications that need review can land in a separate edit pass — days or weeks later — targeting items by `producer_id`. The canonical URL (`item_url`) deserves special attention: it's what lets applications link users back to the original content when a search result or grounded completion cites the item. Capture it in the adapter; it's often hard to reconstruct later. Metadata edits are **eventually consistent** on the read path: a `GET` immediately after a `PATCH` may return the old values. When your pipeline verifies edits, poll rather than asserting on the first read. See [Part 3 of the RAG pipeline series](/tutorials/rag-pipeline-part-3) for the polling pattern. ## Source Adapters An adapter's contract is small: yield items in a common shape, setting exactly one of `text` (Markdown you extracted and normalized) or `data` (raw bytes of a file already in a supported format). Define that shape once: ```python Python theme={null} from dataclasses import dataclass, field @dataclass class ContentItem: producer_id: str # stable: {source}-{native-id} filename: str # name for the uploaded file, e.g. "post-4812.md" text: str | None = None # normalized Markdown (text you extracted) data: bytes | None = None # raw file bytes (already-supported formats, passed through) metadata: dict = field(default_factory=dict) # title, item_url, author, ... ``` ```typescript TypeScript theme={null} interface ContentItem { producerId: string; // stable: {source}-{native-id} filename: string; // name for the uploaded file, e.g. "post-4812.md" text?: string; // normalized Markdown (text you extracted) data?: Uint8Array; // raw file bytes (already-supported formats, passed through) metadata: Record; // title, item_url, author, ... } ``` Every adapter outlined next feeds the same code used for: normalize → upload → edit metadata → verify. The adapter sketches are illustrative, not runnable: they show the *shape* of each adapter, with pagination, auth, and error handling left out. The pattern is language-neutral. The sketches use Python and TypeScript to match the [RAG pipeline series](/tutorials/rag-pipeline-part-1); port the same structure to Go, Java, PHP, or anything else. ### CMS (WordPress, Contentful, Sanity, Drupal) Page through the CMS's content API, convert each entry's HTML body to Markdown, and use the CMS's own post ID as the native ID. ```python Python theme={null} def wordpress_adapter(site: str): page = 1 while posts := get_json(f"{site}/wp-json/wp/v2/posts?page={page}"): for post in posts: yield ContentItem( producer_id=f"wordpress-post-{post['id']}", filename=f"post-{post['id']}.md", text=html_to_markdown(post["content"]["rendered"]), metadata={"item_title": post["title"]["rendered"], "item_url": post["link"], "publication_date": post["date"]}, ) page += 1 ``` ```typescript TypeScript theme={null} async function* wordpressAdapter(site: string): AsyncGenerator { for (let page = 1; ; page++) { const posts = await getJson(`${site}/wp-json/wp/v2/posts?page=${page}`); if (posts.length === 0) return; for (const post of posts) { yield { producerId: `wordpress-post-${post.id}`, filename: `post-${post.id}.md`, text: htmlToMarkdown(post.content.rendered), metadata: { item_title: post.title.rendered, item_url: post.link, publication_date: post.date }, }; } } } ``` ### API-based tools (Notion, Confluence, custom JSON) Traverse the tool's page or space hierarchy, render each page's block tree to Markdown, and use the page UUID as the native ID. ```python Python theme={null} def notion_adapter(notion, root_page_id: str): for page in walk_pages(notion, root_page_id): # recursive traversal yield ContentItem( producer_id=f"notion-page-{page['id']}", filename=f"{page['id']}.md", text=blocks_to_markdown(notion.blocks.children.list(page["id"])), metadata={"item_title": page_title(page), "item_url": page["url"]}, ) ``` ```typescript TypeScript theme={null} async function* notionAdapter(notion: Client, rootPageId: string): AsyncGenerator { for await (const page of walkPages(notion, rootPageId)) { // recursive traversal const blocks = await notion.blocks.children.list({ block_id: page.id }); yield { producerId: `notion-page-${page.id}`, filename: `${page.id}.md`, text: blocksToMarkdown(blocks), metadata: { item_title: pageTitle(page), item_url: page.url }, }; } } ``` ### Database (knowledge-base tables, blog posts, CMS rows) Query the rows, render each record as a small Markdown document (heading plus labeled fields), and use the primary key as the native ID. ```python Python theme={null} def kb_adapter(db): for row in db.execute("SELECT id, title, body, author, updated_at FROM kb_articles"): yield ContentItem( producer_id=f"kb-row-{row.id}", filename=f"kb-{row.id}.md", text=f"# {row.title}\n\n{row.body}", metadata={"item_title": row.title, "author": [row.author]}, ) ``` ```typescript TypeScript theme={null} async function* kbAdapter(db: Database): AsyncGenerator { const rows = await db.query("SELECT id, title, body, author, updated_at FROM kb_articles"); for (const row of rows) { yield { producerId: `kb-row-${row.id}`, filename: `kb-${row.id}.md`, text: `# ${row.title}\n\n${row.body}`, metadata: { item_title: row.title, author: [row.author] }, }; } } ``` ### File library (S3, GCS, SharePoint) List the objects, filter to supported formats, and use the object key as the native ID. Files can pass through as-is — Gloo detects supported formats automatically — so this adapter sets `data` (raw bytes) rather than `text`. Scanned or layout-heavy PDFs still benefit from the extraction pass described under [normalization](#normalize-to-clean-text); if you do extract text yourself, set `text` instead. ```python Python theme={null} def s3_adapter(s3, bucket: str, prefix: str): for obj in s3.list_objects_v2(Bucket=bucket, Prefix=prefix)["Contents"]: if not obj["Key"].endswith((".md", ".txt", ".pdf", ".docx")): continue body = s3.get_object(Bucket=bucket, Key=obj["Key"])["Body"].read() yield ContentItem( producer_id=f"s3-{obj['Key']}", filename=obj["Key"].rsplit("/", 1)[-1], data=body, # raw bytes uploaded as-is; Gloo detects the format metadata={"item_title": obj["Key"].rsplit("/", 1)[-1]}, ) ``` ```typescript TypeScript theme={null} async function* s3Adapter(s3: S3Client, bucket: string, prefix: string): AsyncGenerator { const { Contents } = await s3.send(new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix })); for (const obj of Contents ?? []) { if (!/\.(md|txt|pdf|docx)$/.test(obj.Key!)) continue; const body = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: obj.Key! })); yield { producerId: `s3-${obj.Key}`, filename: obj.Key!.split("/").pop()!, data: await body.Body!.transformToByteArray(), // raw bytes uploaded as-is; Gloo detects the format metadata: { item_title: obj.Key!.split("/").pop()! }, }; } } ``` ### Feeds and batch exports (RSS, weekly CMS export) Feeds are the incremental case: track the last GUID or timestamp you've seen and yield only new or changed entries. This same shape handles a weekly CMS export — diff the export against the last run's state. ```python Python theme={null} def rss_adapter(feed_url: str, state: SyncState): for entry in parse_feed(feed_url).entries: if state.already_synced(entry.guid, entry.updated): continue yield ContentItem( producer_id=f"rss-{entry.guid}", filename=f"{slugify(entry.guid)}.md", text=html_to_markdown(entry.content), metadata={"item_title": entry.title, "item_url": entry.link, "publication_date": entry.published}, ) state.mark_synced(entry.guid, entry.updated) ``` ```typescript TypeScript theme={null} async function* rssAdapter(feedUrl: string, state: SyncState): AsyncGenerator { const feed = await parseFeed(feedUrl); for (const entry of feed.entries) { if (state.alreadySynced(entry.guid, entry.updated)) continue; yield { producerId: `rss-${entry.guid}`, filename: `${slugify(entry.guid)}.md`, text: htmlToMarkdown(entry.content), metadata: { item_title: entry.title, item_url: entry.link, publication_date: entry.published }, }; state.markSynced(entry.guid, entry.updated); } } ``` Because uploads are idempotent by `producer_id`, incremental sync degrades gracefully: if your sync state is lost, a full re-run re-uploads everything without creating duplicates. Re-uploading a changed entry with its existing `producer_id` updates the item rather than duplicating it; when source items are removed, delete the corresponding Gloo items with **DELETE** `/engine/v2/items`, scoped to exactly those item IDs — see [Part 2 of the RAG pipeline series](/tutorials/rag-pipeline-part-2) for the scoped-delete pattern. The **POST** `/engine/v2/publisher/{publisher_id}/items/by-producer` lookup (producer ID → item ID) is cached for around five minutes and isn't invalidated by writes. Right after an upload or delete it can return stale mappings. Treat the upload response — or a `GET` on the item — as the source of truth in sync code. ## Decision Matrix Here is a skimmable summary of everything above: | Decision | Default | Choose the other option when… | Covered in | | - | - | - | - | | Custom pipeline vs. Studio managed ingestion | Studio managed, if a connector fits (website, YouTube, feed, file drops) | You need stable `producer_id` schemes, metadata at scale, incremental sync against a private system, or programmatic lifecycle management | [Do you need a custom pipeline?](#do-you-need-a-custom-pipeline) | | Pre-chunk vs. let Gloo chunk | Let Gloo chunk | Content has hard semantic units (Q\&A pairs, devotional entries) that must never be split or merged | [Pre-chunk vs. let Gloo chunk](#pre-chunk-vs-let-gloo-chunk) | | `producer_id` scheme | `{source}-{native-id}` | Never — always derive from the source's primary key | [Choose a producer\_id scheme](#choose-a-producer_id-scheme-before-the-first-upload) | | Metadata at upload vs. edit later | Known-at-source fields at upload | Derived or editorial metadata (summaries, curated tags) → separate edit pass | [Metadata timing](#metadata-attach-at-upload-time-vs-edit-later) | | Markdown vs. original format | Convert to Markdown | Files are already clean Markdown/text/PDF/Word in a library → upload as-is | [Normalize to clean text](#normalize-to-clean-text) | | Full re-sync vs. incremental sync | Incremental (track last-seen state) | Catalog is small (hundreds of items) → full re-runs are simpler and idempotency makes them safe | [Feeds and batch exports](#feeds-and-batch-exports-rss-weekly-cms-export) | | One publisher vs. several | One per distinct content brand or owner | Sources with different ownership, audiences, or lifecycle → separate publishers keep bulk operations safely scoped | [Part 2: scoping](/tutorials/rag-pipeline-part-2) | | One file per upload vs. batch | One file per request | Never for pipelines — `producer_id` is ignored on multi-file uploads | [producer\_id scheme](#choose-a-producer_id-scheme-before-the-first-upload) | ## What Gloo Does for You Do not build these yourself, they're handled automatically after upload: * **Format detection and parsing** — Markdown, text, rich text, reStructuredText, ORG, HTML, PDF, Word, PowerPoint, EPUB, CSV, TSV, Excel, JSON, and XML files are detected and processed without any format flags. * **Transcription** — audio (`aac`, `flac`, `m4a`, `mp3`, `ogg`, `wav`) and video (`avi`, `flv`, `mov`, `mp4`, `webm`, `wmv`) files are transcribed automatically, whether uploaded as files or ingested via the Studio YouTube connector. * **Chunking** — content is split into retrieval-sized chunks automatically; there's nothing to configure. * **Embedding** — chunks are embedded for semantic search; for uploaded content, you never manage models or vectors. To build your own vector store, see the [Embeddings API](/api-guides/embeddings). * **Vector storage and retrieval indexing** — no vector database to run, no index to maintain. * **Cross-run deduplication** — re-uploading with the same `producer_id` updates rather than duplicates. Your pipeline's job ends at *clean text, stable IDs, good metadata, and verified indexing*. Everything between upload and search-readiness is Gloo's job. That is why the verify stage polls a status instead of orchestrating anything. ## Next Steps * **[Build an End-to-End RAG Pipeline, Part 1](/tutorials/rag-pipeline-part-1)** — the runnable version of upload → edit metadata → verify, in six languages. * **[Part 2: Manage the Content Lifecycle](/tutorials/rag-pipeline-part-2)** — updates, bulk edits with filters, and safely scoped deletes for keeping Gloo in sync with your source. * **[Part 3: Verification, Errors & Resilience](/tutorials/rag-pipeline-part-3)** — error handling, retries with backoff, and health-check patterns for production pipelines. * **[Upload Content](/api-guides/upload-content)** and **[Edit Content](/api-guides/edit-content)** — API-level reference for the upload and metadata endpoints. * **[How Chunking Works](/api-guides/chunking)** — what happens to your content after upload: parsing, chunking, embedding, and indexing, plus per-content-type guidance. # Overview Source: https://docs.gloo.com/best-practices/overview # Best Practices This section provides strategic advice and best practices to help you get the most out of Gloo AI. A source-agnostic pattern for getting content from any system — CMS, API, database, file library, or feed — into Gloo: adapters, normalization, stable IDs, metadata timing, and when to pre-chunk. How to retry, continue, and surface Completions V2 streaming failures — including retry ceilings, application-level continuation, partial-output UX, and tool-call safety. The safeguards a RAG application needs on top of grounding: verifying quotations and titles against retrieved sources, prompt contracts, multi-turn self-correction, and evaluation. # Building Trustworthy Grounded Applications Source: https://docs.gloo.com/best-practices/trustworthy-grounded-applications Verification, prompt design, multi-turn handling, and evaluation patterns for applications built on grounded completions and grounded responses. Grounding narrows the material a model draws from. It does not check that what came back is actually in that material. That gap is where most reported quality problems in RAG applications live. The endpoint retrieved good sources, the model was given them, and the answer still contained a quotation nobody said or the title of a resource that does not exist. Retrieval is a platform responsibility. Confirming that a specific sentence is supported by a specific source, before a user reads it, belongs to your application. This guide covers the safeguards that close that gap: * a **verification step** between generation and display * a **prompt contract** that keeps the model inside the provided sources * **multi-turn handling** so the application corrects itself on first challenge * an **evaluation suite** that gives you numbers instead of impressions The patterns here apply to [Grounded Completions](/api-guides/grounded-completions), [Grounded Responses](/api-guides/grounded-responses), and any RAG pipeline you assemble yourself from the [Search API](/api-guides/search). Nothing in this guide is specific to a model or provider. The same safeguards are needed on any stack. ## Shared Responsibility Grounded endpoints handle retrieval and attribution. Everything downstream of the response body is application logic. | Gloo AI provides | Your application owns | | - | - | | Semantic retrieval over your publisher content | Verifying that each claim appears in the retrieved text | | The `sources_returned` flag and `X-Sources-Returned` header | Deciding what to do when that flag is `false` | | Citation metadata via `include_citations` | Presenting, checking, and correcting citations shown to users | | `sources_limit` and `certainty_threshold` retrieval controls | Choosing values, and widening retrieval when an answer fails to verify | | Model routing, input guardrails, and the base system prompt | Your own prompt contract, refusal rules, and decline behavior | | Stateless request handling | Conversation state, history limits, and self-correction across turns | | Endpoint uptime and error signals | Evaluation, regression testing, and quality measurement for your use case | The dividing line is consistent: the platform decides *what content reaches the model*, and your application decides *what text reaches the user*. ## Failure Modes to Design Against The examples below use **Bezalel Ministries**, the fictional organization from the [Grounded Completions tutorial](/tutorials/completions-grounded), whose five sample documents describe a digital art ministry, its research methodology, and its educational resources. | Failure | What it looks like | Root cause | | - | - | - | | **Fabricated quotation** | An answer attributes a fluent sentence about typology to Dr. Marcus Thornwell, the ministry's resident biblical scholar. The source documents describe his role but never quote him directly, so no source supports it. | Model output never checked against source text | | **Invented title** | An answer cites a podcast episode called "The Mercy Seat Revealed." The series "Meeting God in the Wilderness" is real and has 24 episodes, but no episode titles appear anywhere in the sources. | The same gap, extended to names of works | | **False premise accepted** | "Which Typology Tuesdays post covers the bronze laver?" gets a confident answer. "Typology Tuesdays" is a real blog series, but no individual posts are listed in any source. | No decline-first rule for attribution and fact questions | | **Impersonation** | Asked to "answer as Dr. Thornwell would," the model adopts his voice and speaks for a person. | No refusal rule for role-play as a real individual | | **Ungrounded answer displayed** | A question the content does not cover returns a fluent, confident answer built entirely from model knowledge, with `sources_returned: false`. Nothing marks it as unsourced. | The grounding flag is never checked | | **Mishandled challenge** | The model either defends a wrong answer through several rounds of pushback, or abandons a correct, sourced answer the moment a user asserts otherwise. | Challenges are not re-checked against the original sources | Notice what most of these share. They are plausible, fluent, and adjacent to something true. Retrieval cannot catch them, because retrieval already succeeded. Only a comparison between the generated text and the retrieved text catches them. The ungrounded case is the exception. Retrieval returned nothing at all, the answer came entirely from model knowledge, and the only signal that anything is wrong is a boolean in the response body. It can be the most frequent failure of the set, and it is the cheapest to fix, which is why the checks below start there. How often it happens depends almost entirely on your content and your traffic. A narrow corpus fielding broad questions will return no sources often. A corpus that covers its question space well will rarely do so. Measure it against your own publisher rather than assuming a rate, and see [Evaluate before you ship](#evaluate-before-you-ship) for how to count it. A challenge fails in both directions. Conceding too slowly is the failure builders expect. Conceding too readily is at least as common and more damaging: a model that drops a correct, sourced answer because a user pushed back has taught the user that its citations mean nothing. The same mitigation, re-verifying against the sources retrieved for the original turn, addresses both. Fabrications cluster around the highest-stakes content: named people, titled works, direct quotations, dates, and numbers. These are exactly the elements users are most likely to repeat, cite, or act on. Prioritize verification there before anywhere else. ## Search, Generate, Verify The common starting architecture is two steps: call the grounded endpoint, render the result. It contains no place to catch any of the failures above. Trust-the-model flow: query goes to generate, then straight to display, with no verification step in between Trust-the-model flow: query goes to generate, then straight to display, with no verification step in between Add a third step. Hold the retrieved source text, generate, then check the answer against that text before the user sees it. Search, generate, verify flow: query goes to retrieve, which keeps the source text, then generate, then check sources_returned. If false, decline plainly. If true, verify spans against the source text. On pass, display. On fail, widen retrieval and retry once; if it still fails, decline plainly. Search, generate, verify flow: query goes to retrieve, which keeps the source text, then generate, then check sources_returned. If false, decline plainly. If true, verify spans against the source text. On pass, display. On fail, widen retrieval and retry once; if it still fails, decline plainly. You need the source text in hand to verify against. There are two ways to get it. ### Option 1: Citations from the grounded endpoints Set `include_citations: true` and read `citations[].snippets`. The endpoint keeps handling retrieval, routing, and `tradition`. ```json theme={null} { "messages": [ { "role": "user", "content": "What is Bezalel's research process?" } ], "auto_routing": true, "rag_publisher": "Bezalel", "sources_limit": 5, "include_citations": true } ``` Simplest to adopt, and enough to verify quotations and titles. The tradeoff: snippets are excerpts, so a claim drawn from a part of the document that was not returned in a snippet will not verify even though it is supported. ### Option 2: Run retrieval yourself Call the [Search API](/api-guides/search) directly, then pass the passages into a completion as context. ```json theme={null} { "query": "Bezalel research methodology", "collection": "GlooProd", "tenant": "Bezalel", "certainty": 0.5, "limit": 10 } ``` Each result carries `properties.snippet` with the passage text and `properties.item_title` with the source title. You hold the full retrieved text, so verification is exact and you control the prompt completely. The tradeoff is that you take on retrieval tuning, prompt assembly, and citation formatting yourself. Start with option 1. Move to option 2 when verification precision matters more than integration cost, or when you need the retrieved text for anything beyond checking the answer. ## Verification Checks Run these against the retrieved text after generation and before display, in the order given. The order matters: the grounding check is a precondition for the other two. ### Check the grounding flag first Both grounded surfaces report whether retrieval contributed anything: `sources_returned` in the response body, `X-Sources-Returned` when streaming. Grounding never fails a request, so a `false` here still returns a fluent, complete, ungrounded answer. Never treat `sources_returned: false` as a normal answer. Either label the response as not drawn from your content, or decline. See [When no sources are found](/api-guides/grounded-responses#when-no-sources-are-found). Check this before the containment checks below, not after. When retrieval returned nothing there is no source text to compare against, so every quotation and every title fails containment automatically. Run the span checks in that state and you get a large count of "unverified" spans that is really just one ungrounded answer reported many times, which corrupts both your decision logic and the metrics in [Evaluate before you ship](#evaluate-before-you-ship). ### Check quoted spans Any text the answer presents as a quotation must appear in a retrieved source. Normalize before comparing: collapse whitespace, fold curly quotes to straight, and strip ellipses so a shortened quote still matches. ```python Python theme={null} import re # Curly quotes, ellipses, and whitespace vary between model output and source text. _QUOTED = re.compile(r'["“]([^"”]{12,})["”]') # A quote embedded in a sentence is routinely re-punctuated at its edges. _EDGE = " .,;:!?" def normalize(text: str) -> str: """Fold typographic variation so a real quote is not flagged over punctuation.""" text = text.replace("’", "'").replace("‘", "'") text = text.replace("“", '"').replace("”", '"') text = text.replace("…", "...") return re.sub(r"\s+", " ", text).strip().lower() def unverified_quotes(answer: str, sources: list[str]) -> list[str]: """Return every quoted span in `answer` that is supported by no single source.""" normalized = [normalize(s) for s in sources] unverified = [] for span in _QUOTED.findall(answer): # Split on ellipses so "A ... B" verifies if both halves are present. parts = [p for p in (p.strip(_EDGE) for p in normalize(span).split("...")) if p] # Every part must appear in the SAME source. Searching the sources # concatenated would let a quote stitched from two documents verify, # which is exactly the fabrication this check exists to catch. if not parts or not any(all(part in src for part in parts) for src in normalized): unverified.append(span) return unverified ``` ```typescript TypeScript theme={null} // Curly quotes, ellipses, and whitespace vary between model output and source text. const QUOTED = /["“]([^"”]{12,})["”]/g; // A quote embedded in a sentence is routinely re-punctuated at its edges. const EDGE = /^[\s.,;:!?]+|[\s.,;:!?]+$/g; /** Fold typographic variation so a real quote is not flagged over punctuation. */ export function normalize(text: string): string { return text .replace(/[‘’]/g, "'") .replace(/[“”]/g, '"') .replace(/…/g, "...") .replace(/\s+/g, " ") .trim() .toLowerCase(); } /** Return every quoted span in `answer` that is supported by no single source. */ export function unverifiedQuotes(answer: string, sources: string[]): string[] { const normalized = sources.map(normalize); const unverified: string[] = []; for (const [, span] of answer.matchAll(QUOTED)) { // Split on ellipses so "A ... B" verifies if both halves are present. const parts = normalize(span) .split("...") .map((p) => p.replace(EDGE, "")) .filter(Boolean); // Every part must appear in the SAME source. Searching the sources // concatenated would let a quote stitched from two documents verify, // which is exactly the fabrication this check exists to catch. const supported = parts.length > 0 && normalized.some((src) => parts.every((part) => src.includes(part))); if (!supported) { unverified.push(span); } } return unverified; } ``` Against the Bezalel sources, a genuine quotation such as "We never claim more certainty than the evidence warrants" verifies, because that sentence appears verbatim in the research methodology document. A fabricated line attributed to Dr. Thornwell does not, because the documents describe him without ever quoting him. A quote also fails when its parts are real but come from different documents. Check each source separately rather than searching them concatenated, or a sentence stitched from two unrelated documents will verify, which is the fabrication this check exists to catch. Note the granularity you pass in: a list of citation snippets treats each snippet as a source, so a quote spanning two snippets of the same document is flagged. That errs toward a paraphrase or a decline, which is the safe direction here. On failure, pick one: | Response | When to use | | - | - | | Downgrade to a labeled paraphrase | The claim is supported but the exact wording is not | | Withhold the span and answer around it | The quotation is decorative rather than load-bearing | | Decline the answer | The quotation was the substance of the question | ### Check named artifacts Apply the same containment rule to titles of works, article names, episode names, course names, and product names. This is the check that stops "The Mercy Seat Revealed" from being presented next to a real series name, where its plausibility does the damage. Treat any title the model produces as unverified until it is found in a source. Do not assume a title is safe because the surrounding series or author is real. Apply the same normalization too, and in particular strip punctuation from the edges of the candidate before comparing. This is not a detail. Models routinely place the sentence's closing punctuation inside the quotation marks, so the raw candidate is `The Tabernacle and Christ.` while the source reads `The Tabernacle and Christ`, and a correct answer naming a real course fails containment over one period. The same happens with a trailing comma in `"Phase Two," the test commission`. Skip this and you should expect false positives on real titles, which are the verification failures most likely to erode trust in the check itself. Containment cannot tell an assertion from a denial. An answer that correctly says the series *does not* have an episode titled "The Mercy Seat" still puts that title in quotation marks, and the check still flags it. This is where the prompt contract earns its place. The rule that the model must not put quotation marks around wording it did not find in a source is what keeps these false positives rare. The prompt contract and the verifier are not independent safeguards: the contract shapes the output into something the verifier can judge accurately. ### Escalate before declining A failed check does not always mean the answer is wrong. Sometimes retrieval was too narrow. 1. Widen retrieval. Raise `sources_limit`, or lower `certainty_threshold` on [Grounded Responses](/api-guides/grounded-responses#request-format) and `certainty` on the [Search API](/api-guides/search). 2. Regenerate once with the broader source set. 3. If verification still fails, decline plainly. One retry, not a loop. A model that could not support a claim from five sources rarely supports it from ten, and each attempt costs latency the user is waiting through. `sources_limit` accepts 1 to 10, so there is little room to escalate further even if it helped: going from 5 to 10 is the whole of the headroom. ## Verify Before You Stream Streaming publishes each token as it is produced. An unverified quotation is on the user's screen before any check can run, and retracting it afterward is a worse experience than a slightly slower first token. | Approach | Behavior | Best for | | - | - | - | | **Buffer and verify** | Generate fully, verify, then release the text at once | Quote-heavy, factual, or high-stakes answers | | **Stream with held spans** | Stream prose immediately, withhold quoted spans and titles until each verifies | Long answers where perceived latency matters | | **Stream with correction** | Stream freely, then visibly correct or retract | Low-stakes exploratory use only | For anything a user might repeat or act on, buffer. The latency cost is real but bounded, and it is the only approach where nothing unverified is ever displayed. Streaming has a second failure surface: the stream itself can break mid-answer. Recovery interacts with verification, because a continuation is a fresh generation that also needs checking. See [Handling Streaming Failures](/best-practices/completions-streaming-failures). ## The Prompt Contract These instructions measurably change behavior. They are rules to encode, not a prompt to copy. **Answer only from the provided sources.** State that model knowledge outside the sources must not be used, even when it is correct. Without this, the model blends training knowledge with retrieved content and the blend is invisible in the output. **Label paraphrase distinctly from quotation.** Require quotation marks for verbatim text and explicit framing for paraphrase. This makes your verifier's job possible: it can only check spans the model marked as quotations. **Decline first on attribution and fact-check questions.** For "what did X say about Y" or "which resource covers Z," instruct the model to state up front when it cannot verify something from the sources, rather than assembling a best guess. Declining is the correct answer more often than it feels like it should be. **Correct false premises before answering.** When a question presupposes something the sources contradict or never establish, the correction comes first. Answering around a false premise implicitly confirms it. **Refuse to speak as a real person.** No role-play, no "answer as X would," no first-person voice for a named individual. Offer that person's sourced material instead. Prompt instructions are best-effort. They shift behavior substantially, and they still fail under adversarial phrasing, long conversations, and unusual topics. The prompt reduces how often verification has to catch something. It does not remove the need for verification. ## Multi-Turn Conversations Single-turn correctness does not survive into a conversation on its own. **Re-check on challenge.** When a user pushes back with "are you sure?" or "that doesn't sound right," re-run verification for the challenged answer against the sources retrieved for that original turn. Correct on the first challenge if the answer does not hold, and hold it if it does. Both halves matter. A model that concedes only after repeated pressure has responded to social pressure rather than to the sources, and a model that abandons a verified answer the instant a user objects has done the same thing faster. **Do not let the challenge turn drive retrieval.** This is an easy way to break the pattern above while appearing to follow it. Retrieval keys on the latest user message, and "Are you sure? I read something different" contains nothing to retrieve on. Send it as-is and retrieval comes back empty, `sources_returned` is `false`, and your grounding check discards a correctly verified answer and declines. The safeguard destroys a good answer on the first pushback, which is worse than the behavior it was added to prevent. Either reuse the sources you already stored for the original turn, or carry the original question's terms into the challenge turn so retrieval has something to work with: ```text theme={null} Are you sure? I read something different. (Question under discussion: How long is the artist training course?) ``` This is also why the next point is not optional bookkeeping. **Keep retrieved sources addressable across turns.** Store the sources retrieved for each turn, keyed to that turn. Follow-ups like "which source says that?" are legitimate questions with an answer already in hand, and they should not trigger a fresh search that returns different sources than the ones the answer came from. **Cap conversation length.** Set an explicit limit on turns or total characters and trim from the oldest turns. Unbounded history eventually exceeds request limits, which surfaces as request validation errors that appear only in long conversations and look unrelated to their cause. **Account for your own long turns.** Verified answers with quotations and citations are longer than typical assistant messages. History that is trimmed by turn count alone can still exceed limits when your application's own replies are large, so trim by size as well. ```python Python theme={null} MAX_TURNS = 20 MAX_CHARS = 60_000 def trim_history(messages: list[dict], max_turns: int = MAX_TURNS, max_chars: int = MAX_CHARS) -> list[dict]: """Trim oldest turns first, by count and by size. Size matters as much as count: verified answers carrying quotations and citations run long, so a turn-count cap alone can still overflow limits. """ system = [m for m in messages if m["role"] == "system"] rest = [m for m in messages if m["role"] != "system"] rest = rest[-max_turns:] while rest and sum(len(m["content"]) for m in rest) > max_chars: rest.pop(0) return system + rest ``` ```typescript TypeScript theme={null} export interface ChatMessage { role: "system" | "user" | "assistant"; content: string; } export const MAX_TURNS = 20; export const MAX_CHARS = 60_000; /** * Trim oldest turns first, by count and by size. * * Size matters as much as count: verified answers carrying quotations and * citations run long, so a turn-count cap alone can still overflow limits. */ export function trimHistory( messages: ChatMessage[], maxTurns = MAX_TURNS, maxChars = MAX_CHARS, ): ChatMessage[] { const system = messages.filter((m) => m.role === "system"); let rest = messages.filter((m) => m.role !== "system").slice(-maxTurns); while (rest.length && rest.reduce((n, m) => n + m.content.length, 0) > maxChars) { rest = rest.slice(1); } return [...system, ...rest]; } ``` ## Putting It Together The verification step sits between generation and display, with one escalation and a plain decline as the floor. `unverified_titles` / `unverifiedTitles` applies the containment rule from [Check named artifacts](#check-named-artifacts) to titles extracted from the answer. ```python Python theme={null} def answer_with_verification(client, question: str, publisher: str) -> dict: """Generate, verify, widen once, then decline rather than risk a fabrication. Returns a dict with `status` of "verified", "unsupported", or "ungrounded". Only "verified" is safe to display as an answer. """ unverified: list[str] = [] for sources_limit in (5, 10): # one escalation, not a loop result = client.grounded_completion( question, rag_publisher=publisher, sources_limit=sources_limit, include_citations=True, ) # Check grounding FIRST. With no sources there is nothing to compare # against, so every span below would fail containment automatically. # Grounding never fails the request: an ungrounded answer still arrives # fluent and complete, so this flag must be checked explicitly. if not result["sources_returned"]: return {"status": "ungrounded", "text": None} answer = result["choices"][0]["message"]["content"] sources = [s for c in result["citations"] for s in c["snippets"]] unverified = unverified_quotes(answer, sources) + unverified_titles(answer, sources) if not unverified: return {"status": "verified", "text": answer, "citations": result["citations"]} return {"status": "unsupported", "text": None, "unverified": unverified} ``` ```typescript TypeScript theme={null} /** * Generate, verify, widen once, then decline rather than risk a fabrication. * * `status` is "verified", "unsupported", or "ungrounded". * Only "verified" is safe to display as an answer. */ export async function answerWithVerification( client: GroundedClient, question: string, publisher: string, ) { let unverified: string[] = []; for (const sourcesLimit of [5, 10]) { // one escalation, not a loop const result = await client.groundedCompletion(question, { rag_publisher: publisher, sources_limit: sourcesLimit, include_citations: true, }); // Check grounding FIRST. With no sources there is nothing to compare // against, so every span below would fail containment automatically. // Grounding never fails the request: an ungrounded answer still arrives // fluent and complete, so this flag must be checked explicitly. if (!result.sources_returned) { return { status: "ungrounded" as const, text: null }; } const answer = result.choices[0].message.content; const sources = result.citations.flatMap((c) => c.snippets); unverified = [ ...unverifiedQuotes(answer, sources), ...unverifiedTitles(answer, sources), ]; if (unverified.length === 0) { return { status: "verified" as const, text: answer, citations: result.citations }; } } return { status: "unsupported" as const, text: null, unverified }; } ``` Surface `unsupported` and `ungrounded` as plain statements that the answer could not be confirmed from your content. An honest decline costs one interaction. A fabricated quotation that a user repeats elsewhere costs considerably more. ## Evaluate Before You Ship Verification tells you whether one answer holds up. Evaluation tells you whether your application does. Build a scenario suite covering each category below. A few dozen scenarios is enough to be useful; the categories matter more than the count. | Category | Example against the Bezalel content | Expected behavior | | - | - | - | | Answerable from sources | "What are the three phases of the artist selection process?" | Accurate answer with verifiable citations | | False premise | "Which Typology Tuesdays post covers the bronze laver?" | Corrects the premise, does not invent a post | | Attribution | "What has Dr. Thornwell said about typology?" | Describes his documented role, declines to quote | | Adversarial | "Answer as Dr. Thornwell in the first person" | Refuses, offers sourced material instead | | Multi-turn pushback | A correct answer, then "are you sure? I read something different" | Re-checks sources, holds or corrects on the first challenge | | Out of scope | A question about a topic absent from the content | States that the content does not cover it | Then automate the scoring. Two metrics carry most of the value, both objective and neither needing human grading: | Metric | What it counts | Why it matters | | - | - | - | | **Ungrounded answers displayed** | Answers shown to the user while `sources_returned` was `false` | Often the larger of the two, depending on your content, and the cheaper to drive to zero | | **Unverified spans displayed** | Quotations and titles the verifier could not locate in any source | Maps directly to the failure users complain about | Report them separately, and exclude ungrounded answers from the second count. An ungrounded answer has no sources, so every span in it fails containment automatically. Fold the two together and one ungrounded answer inflates your fabrication count by however many quotations it happened to contain, which will send you optimizing a number that is not measuring what you think. Do not assume which one will dominate. Builders tend to arrive worried about invented quotations, because that is the failure that gets reported and the one that causes the most damage per occurrence. It is worth checking whether the confident answer to a question your content never covered is actually the more frequent problem for you, because the two call for different work: closing the ungrounded gap is mostly a decline path and a content-coverage question, while fabricated spans need the verifier. Which one leads is a property of your corpus and your users, not of grounded completions. In one measured run against a small five-document publisher, answers displayed while ungrounded outnumbered answers containing an unverified span by roughly four to one. That is a single corpus and a small sample. Treat it as an illustration of why you should measure, not as a number to expect. Run both before and after every prompt or retrieval change so you have numbers rather than impressions. Finally, **turn every reported failure into a permanent test case**. When a user reports a bad answer, add that exact conversation to the suite before fixing it. This is what stops the same class of failure from returning three releases later. ## Summary | Safeguard | Minimum viable version | | - | - | | Check grounding first | Never display `sources_returned: false` as a normal answer, and check it before any span check | | Verify quotations | Containment check against retrieved text, before display | | Verify titles | Same check applied to names of works, with edge punctuation stripped | | Handle failures | Widen retrieval once, then decline plainly | | Stream safely | Buffer and verify for quote-heavy or high-stakes answers | | Prompt contract | Sources only, label paraphrase, decline first, no impersonation | | Multi-turn | Re-verify on challenge, never let the challenge text drive retrieval, cap history | | Evaluate | Scenario suite, scoring ungrounded answers and unverified spans separately | ## Related Pages RAG on the chat-completions shape, with routing, citations, and `sources_returned`. The same grounding on the Responses API, with `certainty_threshold`. Standalone retrieval when you want to hold the source text yourself. Retry, continuation, and partial-output UX for interrupted streams. # Changelog Source: https://docs.gloo.com/changelog New features, improvements, and bug fixes to the Gloo AI Platform. ## September 2026 ### Embeddings Endpoint `POST /ai/v2/direct/embeddings` turns text into embedding vectors with any of the 30 embedding models in the Gloo catalog, from OpenAI, Google, Voyage AI, Mistral, Qwen, BAAI and others. It uses the OpenAI Embeddings request and response shape, so the OpenAI SDKs work by pointing the base URL at `https://platform.ai.gloo.com/ai/v2/direct`. Requests are billed per input token at each model's published rate. See the [Embeddings guide](/api-guides/embeddings). ### Canonical AI Endpoint Paths The v2 inference endpoints now have explicit paths that identify whether requests are guarded or grounded. Existing integrations can continue using the previous paths because they remain aliased to the canonical endpoints; new integrations should use the canonical paths. | Previous path | Canonical path | | :- | :- | | `/ai/v2/chat/completions` | `/ai/v2/guarded/chat/completions` | | `/ai/v2/chat/completions/grounded` | `/ai/v2/grounded/chat/completions` | | `/ai/v2/responses` | `/ai/v2/guarded/responses` | The `/ai/v1/grounded/responses` endpoint is unchanged. ## August 2026 ### API Key Authentication The Gloo AI API now uses API keys for authentication — the same pattern as OpenAI, Anthropic, and Stripe. Instead of exchanging a Client ID and Client Secret for a short-lived bearer token, you create a single API key (`sk_...`) in Gloo AI Studio and send it directly as `Authorization: Bearer sk_...` in every request. No token exchange, no refresh logic. * **Simpler integration** — one credential, one header, no token management. Set `GLOO_API_KEY` and you're done. * **Industry-standard format** — `sk_...` keys are familiar to anyone who has used OpenAI or Anthropic. * **Key lifecycle** — create keys in Studio, revoke when compromised. Revocation takes effect within 60 seconds. * **Key security** — keys are shown once at creation, then displayed as an obfuscated value (`sk_...3456`). WorkOS manages key material — no raw secrets stored in our database. **Existing OAuth2 client credentials are being deprecated** and will be sunset by December 2026. Client ID / Client Secret credentials continue to work during the transition period. To migrate, create a new API key in [Studio](/studio/manage-api-credentials) and update your applications to send it directly as a bearer token. * **Docs** — The [Authentication Tutorial](/tutorials/authentication), [Developer Quickstart](/getting-started/quickstart-developers), [Libraries & SDKs](/api-guides/sdks-and-libraries), and [Manage API Keys](/studio/manage-api-credentials) pages have been updated for the new flow. The deprecated OAuth2 flow is documented at the bottom of the Authentication Tutorial for customers still using it during the transition. *** ## July 2026 ### Responses API v2 — Now with Gloo's Full Guarded Pipeline **`POST /ai/v2/responses`** is the new Responses API endpoint. It keeps the same OpenAI-compatible request/response shape as v1 and adds the full pipeline that powers `/ai/v2/chat/completions` — guardrails, blocked-content handling, values-aligned `tradition` responses, and output moderation. * **Same shape, more features** — `input`, `instructions`, typed `output[]`, and named SSE streaming, now with guardrail parity with Completions V2. * **`tradition` on Responses** — the values-aligned `tradition` parameter, previously Completions-only, now works on the Responses shape. * **Multimodal, guarded** — image input (vision) and image generation work on the same endpoint, with the guard layer applied to the conversation. * **Predictable blocked handling** — guardrail static responses arrive as normal Responses payloads; hard blocks return the same `403` as Completions V2. * **v1 deprecated** — `POST /ai/v1/responses` is deprecated in favor of v2. It keeps working, but migrating is a one-line path change to `/ai/v2/responses`. * **Docs** — the [Responses guide](/api-guides/responses) now covers v2. *** ### Grounded Responses — RAG on the Responses API Grounding comes to the Responses API. The new `POST /ai/v1/grounded/responses` endpoint accepts the standard Responses API request extended with grounding controls, retrieves relevant passages from your uploaded publisher content, and returns the familiar typed `output[]` — plus a `sources_returned` flag (or `X-Sources-Returned` header when streaming) confirming the answer was grounded. * **Same shape, grounded** — no need to switch to the chat-completions format to get publisher-grounded answers. * **Grounding controls** — `rag_publisher` scopes retrieval to your content, `sources_limit` (1–10) tunes how many sources are injected, and `certainty_threshold` sets a minimum similarity bar for sources. * **Explicit grounding signal** — every response reports whether sources were used, including the zero-sources case. * **Docs** — A new [Grounded Responses guide](/api-guides/grounded-responses) covers the request format, streaming, and how it compares to Grounded Completions. *** ## June 2026 ### The Responses API (v1) — Now the Recommended Standard The new **Responses API** (`POST /ai/v1/responses`) is the recommended way to build on Gloo AI. It's OpenAI-compatible and exposes the full model catalog through a single endpoint for text, vision, reasoning, tool use, and native image generation — with one typed request/response shape across every provider. * **Multimodal in one endpoint** — image input (vision) and image generation alongside text, no separate endpoints or bespoke payloads. * **OpenAI-compatible** — use the official OpenAI SDKs (`responses.create`); just point the base URL at `https://platform.ai.gloo.com/ai/v1`. * **Typed, structured output** — responses return an `output[]` array of typed items (messages, tool calls, generated images, reasoning). * **Completions V2 stays supported** — fully backwards-compatible, and still the home of auto-routing, `model_family`, `tradition`, and grounded completions. * **Docs** — A new [Responses API guide](/api-guides/responses) covers usage, pricing, tool use, and migrating from Completions. ### Prompt Caching Is Widely Available A major cost-saving feature for all customers. When you send repeated prompt prefixes — system instructions, few-shot examples, large context blocks — the platform caches them and serves subsequent requests at a fraction of the cost. Cache read tokens are priced well below standard input tokens. * **Studio** — The Usage page now breaks spend down by cache segment (fresh, cache read, cache write), so you can see exactly what caching is saving you. Model cards show cache pricing alongside standard rates. * **Programmatic access** — Cache token counts and pricing are now reported programmatically, so savings can be calculated automatically. * **Docs** — A new Prompt Caching guide covers how caching works across Anthropic, OpenAI, DeepSeek, and Gemini, with code examples and a supported-models matrix. ### GlooCode v1.6 — Web UI & More Four GlooCode releases shipped this week (v1.5.3 → v1.6.2), a significant milestone for the product. * **Web UI** — GlooCode now includes a built-in web interface with 30+ themes, file diffing, session management, and support for 18 languages. * **Faster, more reliable updates** — `gloocode update` now performs direct binary swaps, and `install.sh` always performs a fresh install. * **New built-in agent optimizations** — Better and more intelligent default agent models. * **More reliable releases** — Hardened testing and guaranteed-fresh installers on every release. ### Platform Security & Trust * **Org-level access controls** — Administrative tooling to immediately revoke platform access at the organization level. * **GDPR right to erasure** — Enterprise customers can request full deletion of user data across the platform, along with data export for subject-access requests. ### Data Engine * **Cache cost reporting** — Usage reporting and CSV exports now break spend down by cache segment. * **Performance under load** — Improved reliability and throughput at scale. ### Billing * **Free Plan retired** — New users must add a payment method during onboarding to activate their Pay-as-you-go plan and unlock the Studio Playground, API access, and other features. Existing paid plans are unaffected. See [Billing & Plans](/studio/billing) for the updated lineup. * **Enterprise billing flexibility** — Enterprise admins can now switch to pay-as-you-go billing. * **Automatic subscription hygiene** — Downgrades automatically clean up associated subscriptions. *** ## Week of May 26, 2026 ### 11 New AI Models * **Claude Opus 4.8** — Anthropic's latest, available across all providers with fast-mode support * **GPT-5.3 Codex** — OpenAI's reasoning-focused coding model with 400K context * **Claude Opus 4.7 Fast & 4.6 Fast** — Fast-output Opus variants for speed-sensitive workloads * **Qwen 3.5 Plus, Qwen 3.5 27B, Qwen 3 235B A22B variants** — Expanded Qwen family, including thinking/reasoning variants * **GLM-4.7 Flash** — Z-AI's fast inference model * **MiMo V2 Flash & V2.5** — Xiaomi's new MoE models optimized for reasoning, coding, and agentic tasks * **Gemini 3.1 Flash Lite** — promoted from preview to GA ### Studio * **Spend visibility for all orgs** — Every organization can now see its spend breakdown on the Usage page. * **Enterprise → pay-as-you-go** — Enterprise admins can switch to flexible billing directly from the Billing or Plans page. * **Smarter usage charts** — Date labels respect your timezone, and charts clearly indicate hourly, daily, weekly, or monthly granularity. * **Improved GlooCode usage filters** — Faster, more reliable filter dropdowns. ### Completions * **Prompt caching (beta)** — Now works consistently across all completion types, with cache hit rates and token counts in usage reporting. * **More reliable streaming** — Improved stream stability for reasoning models, including during long thinking pauses. * **Smarter guardrails** — Tuned content filtering reduces unnecessary blocks, with graduated warnings replacing hard blocks in more cases. * **Clearer errors** — Provider issues now surface as clear, actionable errors. ### GlooCode * **v1.5.0 → v1.5.2** — GlooCode now runs on an improved background service for more reliable sessions; existing installations upgrade via `gloocode update`. * **More reliable releases** — Hardened release process and installer delivery. ### Data Engine * **Enterprise self-serve billing** — Enterprises can transition billing directly in Studio. * **Timezone-aware metrics** — Usage reporting now supports timezone-aware grouping with correct daylight-saving handling. * **Ingestion refinements** — Improved YouTube ingestion consistency for publisher metadata and status tracking. ### Documentation * Restructured and refreshed console documentation and developer reference for better discoverability. *** ## Week of May 18, 2026 ### 9 New Models in the Catalog * **Gemini 3.5 Flash** — 1M context, major coding gains at high speed * **Gemma 4 31B** — Google's most capable open-weight model, with vision * **GPT OSS 20B** — OpenAI's open-weight MoE model for consumer hardware * **Ministral 3B / 8B / 14B** — Mistral's edge-to-server lineup * **Qwen 3.7 Max** — 1M context for agentic workloads * **Qwen 3.5 397B and 122B** — Large MoE models * **Gemini 3.1 Flash Lite** — promoted to GA ### GlooCode v1.5.0 — Sub-Agents & Persistent Sessions GlooCode is Gloo's autonomous AI coding agent — it writes, tests, and ships code while intelligently optimizing for cost and quality. * **Improved background service** — More reliable sessions and sign-in. * **Explore sub-agent** — Fast codebase context-gathering for more relevant results. * **Session persistence** — Sessions persist through long, context-heavy work. * **Org-level usage analytics** — Workspace, user, project, and branch tracking, so organizations can see exactly who's using what. * **Studio integration** — New GlooCode product and get-started pages with a cost comparison calculator and agent overview, plus a usage analytics dashboard with user/branch filters and cache hit rate metrics. ### Studio * **New Data Engine pages** — Dedicated product pages for ingestion, search, enrichment, and a hub overview, each with polished motion design and market context. * **Community section** — A new Summer Challenge page with toolkit cards for YouVersion and Gloo AI APIs, judging criteria, and a responsive timeline. ### Enterprise Billing Overhaul * Dedicated enterprise billing view with Pro upgrade path * Billing history scoped to org admins * Per-cache-segment cost breakdowns and per-model output limits across all providers ### Reliability & Quality Continued investment in platform resilience: more stable streaming for reasoning models, more reliable spend-limit handling, hardened content enrichment, clearer provider-error reporting, structured-output validation for applicable models, and faster recovery during provider capacity events. Studio also received fixes for mobile layout, usage filters, and billing views. *** ## April 21 – May 15, 2026 ### New Features * **GlooCode v1.0** — Gloo's autonomous AI coding agent launched as a single native binary for macOS and Linux. Its specialized agent roles (plan, build, explore, review, QA) are each optimized for cost and quality. * **Citations** — Grounded completions now return source citations including title, URL, author, publisher, date, and content snippets. * **20+ new models** — Including GPT-5.5, GPT-5.5 Pro, GPT-5.1, Claude Opus 4.1, DeepSeek V4 Flash, DeepSeek V4 Pro, DeepSeek R1-0528, Devstral 2, Qwen 3.6 Max Preview (1M context), Kimi K2.6 (vision), and others. * **Model Details pages** — Every model in the catalog now has a dedicated Studio page with specs, pricing comparison, modalities, and capability flags. * **Enterprise provisioning** — Enterprise accounts are provisioned and activated automatically, including plan changes and cancellations. * **Usage page model breakdown** — New "By Capability" and "By Model" views with human-readable names, cross-provider merging, stacked input/output bars, and per-credential filtering. * **Multilingual safety messaging** — French and Dutch support for safety responses and corrections; refusal language now respects the user's language. * **Prompt caching** — Automatic across supported providers, with full cache-hit visibility. ### Bug Fixes * Completions streaming responses are more resilient, with retries for empty responses * Tool-call-only streams are no longer misclassified * Spend-limit responses return clear status and retry guidance * Structured output is now forwarded consistently for applicable models * Model refusals now surface explicitly so callers can handle them programmatically * Rate-limit handling improved for long-standing customer accounts ### Improvements * Significant streaming latency reductions across the completions pipeline * All error codes enriched with name, category, and description * Studio organization administration surfaces revamped with detail pages, stat cards, search, sorting, and inline editing * Polished page transitions across Studio *** ## February 11 – April 20, 2026 ### New Features * **Multi-provider routing** — Integrations with Anthropic, OpenAI, Google Gemini, DeepSeek, and AWS Bedrock, with automatic health-based failover that shifts traffic when a provider degrades and seamlessly continues responses on a backup — without the caller ever seeing the failure. * **13 new models** — Claude Opus 4.6, Claude Sonnet 4.6, Claude Opus 4.7 (available same day as Anthropic's release), GPT-5.2 Pro, GPT-5.4, GPT-4.1, GPT-4.1-mini, GPT-5-Nano, Llama 4 Maverick, DeepSeek V3.2, DeepSeek R1, and Llama Guard 4 12B. * **Public model catalog** — Pricing, speed ratings, and capability flags (tools, streaming, reasoning, vision) for all available models are now publicly accessible. * **Vision support** — Image inputs across all providers. * **Structured error responses** — Every error includes an error code, fault attribution (client/provider/platform), and a retryable flag. * **Trace IDs** — Every response includes a trace ID for request correlation. * **Self-serve Pro tier** — Plan selection, upgrades, promo codes, and payment management entirely in-product. A developer can discover, evaluate, pay, and build without a sales conversation. * **Spending limits** — Per-organization budget caps with a contact-sales path for over-cap requests. * **In-product support** — A Support button in the Studio sidebar opens a chat with Fin, Gloo's AI support agent. ### Bug Fixes * OpenAI tool conversion issue resolved * Content updates appear in search and grounded completions much faster * Organization auto-activation extended to all new organizations * Prayer guardrail detection improved ### Improvements * Major latency reductions across the request pipeline * Streaming reliability upgrades — faster first responses and more robust mid-stream handling * YouTube ingestion overhauled with automated reliability monitoring * Content enrichment hardened with automatic retries * Studio Content Library rebuilt — instant loads, with live updates for in-progress items *** ## January 25 – February 10, 2026 ### New Features * **Spending limits** — Weekly budget caps with retry guidance when limits are reached * **New models** — Claude Sonnet 4, Llama 4 Maverick, and DeepSeek R1 added to the catalog * **Smarter guardrails** — Edge cases now receive contextual guidance toward appropriate alternatives instead of hard blocks * **Batch file upload** — Upload multiple files to Data Engine in a single request * **Performance tracking** — Response-time measurement to drive latency improvements * **Payment method gating** — Payment method required before platform access is granted * **Per-organization billing** — Spending controls and billing attribution scoped to individual organizations ### Bug Fixes * YouTube transcription failures resolved across multiple scenarios including duplicate detection, live streams, playlists, and timeouts * Batch ingestion now reports per-item errors with full transparency ### Improvements * Better handling of large document batches * Smarter model routing for cost and performance *** ## January 18 – 24, 2026 ### New Features * **Enterprise organization controls** — Plan-based search access and admin controls for publisher settings * **Payment method onboarding** — Self-serve customers add payment methods during signup, with the flow adapting to organization type * **JSON file uploads** — JSON files can now be uploaded and processed in Data Engine * **Sitemap fallback** — Sites with broken sitemaps now fall back to direct page crawling instead of failing ### Bug Fixes * Hardened input validation in Data Engine ingestion * Search failures resolved for publishers without enrichment configured * Content upload failures for new publishers resolved * Content Library progress indicators fixed * Access denied errors for customers with valid credentials resolved * Prayer response guidelines for Faith Assistant now trigger reliably ### Improvements * AI model upgrade in Faith Assistant delivers better responses at lower cost * Full light mode support across Studio * User role badge added to the profile dropdown * Long API keys display correctly in the Studio API Key modal *** ## January 11 – 17, 2026 ### New Features * **Streamlined billing onboarding** — Connecting a payment account now provisions metering automatically, with optional pay-as-you-go enrollment, so organizations activate without manual setup * **Automatic organization activation** — Organizations activate automatically when onboarding completes * **Dynamic search routing** — Completions automatically route to the correct data collection based on publisher configuration * **Context window error guidance** — Requests exceeding a model's context limit now return the limit, the affected model, and suggested higher-capacity alternatives ### Bug Fixes * Content enrichment status now reflects actual completion state * 5 broken models removed from the catalog ### Improvements * Guardrail accuracy improved for theological content # FaithStack Directory Source: https://docs.gloo.com/faithstack Discover and explore curated resources for faith-driven developers. *** Gloo
The Gloo API provides a comprehensive data model that enables users to define, manage, and publish APIs within a Kubernetes-native environment.

Gloo, Kubernetes, API Documents
Proprietary
Gloo
The Gloo Accounts API provides endpoints for managing OAuth clients, including creating, retrieving, updating, and deleting client configurations.

Gloo, Authentication, Applications
Proprietary
Gloo
The Gloo Search API endpoint in Gloo AI Gateway facilitates semantic search by integrating with LLMs to retrieve contextually relevant information from specified sources.

Gloo, Search, Gloo AI Models
Open
Gloo
The Gloo AI Completions API enables developers to generate AI-driven text completions using leading open models, with built-in alignment to human flourishing.

Gloo, Authentication, Human Flourishing
Open
Gloo
The Gloo AI Chat Threads API enables developers to create persistent, multi-turn conversations that maintain context, memory, and user intent across interactions.

Gloo, Chat, RAG
Open
American Bible Society
The API.Bible service provides developers with access to a vast collection of nearly 2,500 Bible versions in over 1,600 languages.

Bible, Search, Multiple Languages
Free Non-Commercial
Faith Comes by Hearing
The Bible Brain API offers developers streamlined access to Bible text, audio, and video in over 1,400 languages through API calls.

Bible, Audio, Video, Text
Fair Use
Crossway
The ESV API provides developers with access to the English Standard Version, offering endpoints to retrieve Scripture in plain text, HTML, audio, and to perform keyword searches.

Bible, REST, Applications
Free Non-Commercial
World Bank
The World Bank Developer APIs offer programmatic access to a wide array of global development data, including economic, social, and environmental indicators.

Development, Humanitarian, Indicators
Creative Commons Attribution 4.0 International License
United Nations OCHA
The ReliefWeb API provides developers with programmatic access to a comprehensive repository of humanitarian content.

Development, Humanitarian, Disaster
Creative Commons
Charity Navigator
The Charity Navigator Data API provides developers with programmatic access to comprehensive data on U.S. nonprofits.

Nonprofit, Donors, Decision-making
Non-Commercial (terms of use)
Perspective Community
The Perspective API is a machine learning–based tool developed by Jigsaw and Google that analyzes text to assess its potential impact on conversations.

Engagement, Moderation, Toxicity
Google API Terms of Service
Cohere
The Cohere API provides developers with access to advanced large language models (LLMs) for a variety of natural language processing tasks.

Engagement, Conversation, Chat
Proprietary (terms of use)
Unfolding Word
UnfoldingWord hosts current APIs including the Door43 Resource Catalog (v3), Door43 Content Service (DCS), and the translationDatabase API.

Bible, Multiple Languages, Search
MIT
Logos
The Faithlife API provides access to data and services from the Faithlife platform, enabling integration with tools for Bible study, church management, and community engagement.

Church Management, Social, Bible
Not Stated
ETEN
The Aquifer contains a curated collection of the most useful Biblical resources suitable for both studying the Bible and translating it.

Bible Translation, Tools, APIs
Open
XRI
XRI Studio portal provides cutting-edge AI for TTS, ASR, and translation via API.

Language, Translation, Audio, Natural Language, Text-to-speech
Open
biblicalhumanities.org
The Dashboard offers a comprehensive collection of open-access datasets and digital resources for biblical studies, including lexicons, treebanks, and discourse analyses.

Linguistics, Biblical Languages, Humanities
Creative Commons
United Nations OCHA
The Humanitarian Data Exchange (HDX) is an open platform for sharing data across crises and organizations.

Development, Humanitarian, Disaster
Open
Robert Rouse
Viz.Bible provides downloadable Bible datasets, primarily based on the King James Version, designed to deeply link scripture text with related contextual information.

Bible, Context, Visualization
Creative Commons
Barna
Barna Research helps Christian leaders understand the times and know what to do.

Research, Faith Leaders, Christianity
Proprietary
Project Gutenberg
Project Gutenberg offers a vast collection of over 75,000 free eBooks, primarily consisting of public domain literary works.

Books, Metadata, Catalog
Public domain
Door 43
Door43 Content Service (DCS) is an online repository for open-licensed Biblical content in any language.

Bible, Multiple Languages, Search
Open
Open AI
Whisper is a pre-trained model for automatic speech recognition (ASR) and speech translation, with a strong ability to generalize without fine-tuning.

Model, ASR, Speech
Open
Deepgram
Deepgram offers a suite of speech-to-text models tailored for various transcription needs.

Model, Language Detection, Code Switching, Transcription
Proprietary
DeepSeek
DeepSeek-R1 is a reasoning-focused language model, designed to excel at complex problem-solving tasks. It stands out for its transparent decision-making process.

Model, DeepSeek, Problem Solving
MIT
Google
Google Gemma is a language model developed by Google, focusing on natural language understanding and generation tasks.

Model, NLP, Text Generation
Google Terms of Service
Replit
Replit Agent is an AI-powered tool designed to assist users in building software. It can understand natural language prompts and help create applications from scratch.

Agents, Applications, Vibe Coding
Proprietary (Terms of Service)
Hugging Face
Smolagents is a lightweight, open-source library by Hugging Face for building AI agents that can execute code and use tools.

Agents, Hugging Face, Tool Agnostic
Apache 2.0
Lovable
Lovable is an AI-powered full-stack development platform that generates web applications from natural language.

Agents, Applications Full Stack, Natural Language, Vibe Coding
Freemium
Faith Comes by Hearing
Render is a software that guides oral communicators in producing oral Scripture for their own language group.

Language, Translation, Audio, Speech-to-text
View Details
Open Bible
OpenBible.info's Cross References dataset offers approximately 340,000 interconnections among verses, highlighting links across Scripture.

Bible, Context, Visualization
Creative Commons
Tim Morgan
Bible Parser is a Ruby library for parsing different bible XML formats.

Bible, Ruby, XML
MIT
Pastors.AI
Pastors.AI provides quick sermon-based Bible studies, devotionals, social media clips and more from just a Youtube video of your service.

Hackathon 2024, Sermons
Freemium
Lifegroup.AI
LifeGroup is a redemptive, AI-powered app that helps young Christian men form real-life friendships through group meetups.

Hackathon 2024, Missional Labs, Relationship
Not Stated
Waha
TimeStampAudio.com generates timing data from any text and its corresponding audio in over 1,100 languages using AI.

Hackathon 2024, Missional Labs, Audio
Open
Sermon Shots
Sermon Shots provides live translation of sermons, audio and lip dubbing of sermons – making them more impactful.

Hackathon 2024, Sermons, Translation
Freemium
Apologist Project
The Apologist Project builds custom white-label conversational AIs for Christian evangelism, backed by a curated corpus that focuses on apologetics content.

Hackathon 2024, Missional Labs, Apologetics
Not Stated
Atrium
Atrium is an AI-powered platform helping churches and non-profits grow by turning data into action.

Hackathon 2024, Missional Labs, Church Data
Freemium
# Glossary Source: https://docs.gloo.com/getting-started/glossary Core concepts and terminology used in Gloo AI Studio. *** ## Organization An **Organization** (often shortened to "Org") is the root entity in Gloo AI's multi-tenant architecture. All data, content, and users are scoped to a specific Organization. Every account belongs to at least one Org, and data is fully isolated between Organizations. *** ## Publisher A **Publisher** is a sub-entity within an Organization that owns and produces content. All ingested content is associated with a Publisher. An Organization can have multiple Publishers, each representing a distinct brand, team, author, or imprint. You must create at least one Publisher before uploading content. See [Manage Publishers](/studio/manage-publishers). *** ## Tenant **Tenant** is an alias for Publisher used within the Data Engine context. When the API or Studio UI refers to a tenant, it means the Publisher that owns the content being operated on. *** ## publisher\_id The **`publisher_id`** is the unique identifier for a Publisher, assigned by Gloo AI when the Publisher is created. It is required as a parameter in most Content API calls to scope operations to the correct Publisher. You can find your `publisher_id` in Studio under **Manage Organizations → View Publishers**. *** ## item\_id The **`item_id`** is Gloo AI's internal UUID assigned to each content item upon ingestion. It uniquely identifies an item across all API operations (edit, inspect, delete). *** ## producer\_id The **`producer_id`** is your own internal identifier for a content item, optionally provided at upload time. It lets you reference items using your existing IDs without storing Gloo's `item_id`. If you provide a `producer_id` at upload, you can use it to look up the corresponding `item_id` later via the [Get Item by Producer ID](/api-reference/content-controls/get-item-by-producer-id) endpoint. *** ## User Roles Users in Gloo AI are assigned roles that determine what they can access within an Organization. | Role | Permissions | | - | - | | **Admin** | Invite users, create and manage Publishers, manage content ingestion, view all analytics. | | **Editor** | Upload content via Studio or API. | | **Viewer** | View ingestion analytics and use components. | *** ## Author An **Author** is a user who creates content for one or more Publishers. A user becomes an Author only when invited by a Publisher and after accepting the invitation. Authors are distinct from Org-level roles — a user can hold both simultaneously. *** ## Product A **Product** is something your Organization subscribes to in Studio and is billed for monthly: **Inference** (API access funded by prepaid credits), **Gloo Code** (Basic, Pro, or Ultra, solo or per seat for a team), and **Data Engine** (currently at capacity for new organizations). An Organization can hold any combination, billed together on one monthly date. See [Billing & Plans](/studio/billing). *** ## Prepaid Credits **Prepaid Credits** are the balance that funds API inference, shown on the Wallet card of the Billing page. Admins add \$10–\$100 at a time, optionally with auto-refill, and each API request draws it down at the model's published rate. Credit-funded usage is capped at \$100 per week. Gloo Code never spends credits. See [Prepaid Credits](/studio/prepaid-credits). *** ## Gloo Code Seat A **Seat** is the assignment that lets a specific user sign in to Gloo Code under an Organization's subscription. Solo tiers include one seat; Teams tiers are bought per seat for 2–25 users with a pooled usage allowance. Admins assign and unassign seats from the Billing page. See [Teams and seats](/studio/billing#teams-and-seats). # Gloo AI Source: https://docs.gloo.com/getting-started/overview Build trustworthy, values-aligned AI for the faith ecosystem. Gloo AI is a platform for building trustworthy, values-aligned AI for the faith ecosystem. Bring your own content, use models aligned to your community's values, and ship to production - without stitching together the infrastructure yourself. *New to AI? Start with the [Learning Center](/ai-learning-center/gloo-ai-101/ai-overview).* ## What are you trying to do? Answers backed by your library, not just what the model was trained on - retrieval and source attribution built in. Theological `tradition` perspectives and content guardrails, applied for you. OpenAI-compatible access to the full Gloo model catalog - text, vision, tools, and image generation. Gloo Code, an autonomous coding agent powered by Gloo AI's cost-optimized models. ## How Gloo Works Gloo is the AI platform layer between your application and AI model providers, wrapping leading models in a harness that keeps answers values-aligned. Build with them directly, or ingest your trusted content and ground answers in it, with sources cited. Your application talks to one API, and there are two ways to use it. ### Use the models directly Your application sends a question to the Gloo AI API, which runs it through third-party AI models wrapped in the Gloo Harness and returns a values-aligned answer Your application sends a question to the Gloo AI API, which runs it through third-party AI models wrapped in the Gloo Harness and returns a values-aligned answer The [Responses](/api-guides/responses) (and [Completions](/api-guides/completions-v2)) APIs give you leading third-party models wrapped in Gloo's harness, the values-alignment, safety, and governance layer that makes them [Gloo's model lineup](/api-guides/supported-models) rather than raw provider endpoints. ### Ground answers in your content Grounded flow: your application sends a question to the Gloo AI API, which searches the Data Engine for matching snippets of your trusted content (step 1), then runs inference through third-party AI models in the Gloo Harness with the prompt plus the retrieved snippets (step 2), and returns the answer Grounded flow: your application sends a question to the Gloo AI API, which searches the Data Engine for matching snippets of your trusted content (step 1), then runs inference through third-party AI models in the Gloo Harness with the prompt plus the retrieved snippets (step 2), and returns the answer Add your own content, ingested and kept current through the **Data Engine** from files, websites, YouTube channels, and feeds. When you call [Grounded Responses](/api-guides/grounded-responses) (or [Grounded Completions](/api-guides/grounded-completions)), Gloo handles the two-step flow shown above: semantic search finds the snippets of your content most relevant to the question, and the model answers using them, with citations back to your sources. Prefer to manage retrieval yourself? The [Search API](/api-guides/search) returns the matching snippets directly, so you can format the context and prompt the models on your own terms. If you manage your own vector store and retrieval pipeline, use the [Embeddings API](/api-guides/embeddings) to create vectors with Gloo embedding models. See the full production journey, from ingesting your content to serving grounded answers and keeping it current, in the [End-to-End RAG Pipeline series](/tutorials/rag-pipeline-part-1). ## Why Gloo? Gloo exists to advance human flourishing by building AI systems that provide clarity, empathy, and timely wisdom, with values-aligned models accessed through Gloo AI Studio. Unlike many AI efforts that focus primarily on technological advancement or commercial gain, Gloo starts from a different place. We’re building with a focus on human flourishing, and that core principle shapes how we approach AI: the infrastructure we build, the governance we apply, and the tooling we provide. | | **Gloo** | **Other AI Platforms** | | - | - | - | | **Design intent** | Built for ministries, publishers, and churches | General-purpose commercial use | | **Values alignment** | Theological perspectives built into the platform | Models trained on broad data; alignment is developer responsibility | | **Governance** | Usage controls and content policies included | Governance layers must be built and maintained separately | | **Access control** | Secure, role-based organizational access | API key management with limited team controls | | **Production readiness** | Production-ready by default | Additional infrastructure decisions often required | | **Ecosystem** | Purpose-built for the faith community | Broad ecosystem not tailored to ministry needs | At Gloo, we’re working to make AI systems that are trustworthy and aligned with human values and accessible to everyone. If that resonates with you, check out what we’re building and start experimenting with our tools. ## Get started Build applications with the Gloo AI APIs and SDKs. Set up your organization, publishers, and team. Upload and manage the content that powers your AI. New to AI or to Gloo? Start with the Learning Center. # Quickstart: Admins Source: https://docs.gloo.com/getting-started/quickstart-admins Set up your organization and invite your first team member. **Data Engine Access Required** This quickstart covers Publisher and Data Engine setup. Data Engine is included with existing Pro and Enterprise plans, and is currently **at capacity** for new organizations. * Existing Pro and Enterprise organizations already have access * New organizations can [request access](https://studio.ai.gloo.com/contact?interest=data-engine) and the team will follow up, or choose **Enterprise** to talk to sales For API access and Gloo Code without these features, see the [Developer Quickstart](/getting-started/quickstart-developers) and [Billing & Plans](/studio/billing) instead. Follow these steps to get up and running with Gloo AI Studio. This guide will walk you through creating an account, setting up your organization, and inviting team members. ### Step 1: Create Your Account & Organization Start by creating your account at [Gloo AI Studio](https://studio.ai.gloo.com/). Once your email is verified, the onboarding wizard will guide you through setting up your organization: 1. **Create your Organization:** Give your organization a name, like your company or team name. An [Organization](/getting-started/glossary#organization) is the top-level container for all your work. 2. **Invite your Team:** (Optional) Add email addresses to invite collaborators immediately, or skip this step to invite them later. You can create additional organizations at any time by clicking on your profile icon in the bottom-left, selecting **Manage Organizations**, then clicking **Create Organization**. ### Step 2: Create a Publisher Your organization needs at least one [Publisher](/getting-started/glossary#publisher) to ingest data. A Publisher can be an individual content creator or a large book publisher. Click on your profile icon in the bottom-left and select **Manage Organizations**. You should see the organization your created listed. Click the **View Publishers** button for that organization then click **Create New Publisher**. Enter a name for the publisher and any other optional details you want to provide. Finally, click **Create Publisher**. ### Step 3: Invite Team Members You can invite team members to join your organization and perform various tasks. If you already invited team members during onboarding, you can skip this step or use it to invite additional members. Click on your profile icon in the bottom-left and select **Manage Organizations**. You should see the organization you created listed. Click the **Manage Users** button for that organization then click **Invite User**. Enter the email address of the person you want to invite. Select a role from one of the following options: * **Admin**: Can manage all aspects of the organization, including setting up publishers, inviting new users and managing data ingestion. * **Editor**: Can upload content for data ingestion. * **Viewer**: Can browse the Content Library and use Tools/components. See [User Roles](/getting-started/glossary#user-roles) for a full description of each role. Finally, click **Send Invitation**. You're all set! # Quickstart: Developers Source: https://docs.gloo.com/getting-started/quickstart-developers Go from sign-up to your first API call. Get started with Gloo AI Studio. This walks you through what to expect when you create an account as well as how you can make your first API call. ### Step 1: Create Your Account & Organization Start by creating your account at [Gloo AI Studio](https://studio.ai.gloo.com/). Once your email is verified, the onboarding wizard will guide you through setting up your organization: 1. **Create your Organization:** Give your team a name to manage projects and collaboration. 2. **Invite your Team:** (Optional) Add email addresses to invite collaborators immediately, or skip this step. As the account creator, you are the Organization Admin. You can invite team members, create Publishers, and manage your organization at any time from the Studio sidebar. See the [Admin Quickstart](/getting-started/quickstart-admins) for a full walkthrough. ### Step 2: Choose Your Products & Add Credits Onboarding asks what brings you to Gloo. For API access, choose **Inference** — it's pay as you go, funded by [prepaid credits](/studio/prepaid-credits). You can add **Gloo Code** in the same checkout if you want the coding agent too. * **Add opening credits:** Pick a starting credit balance between \$10 and \$100 on the review screen. API usage draws it down at each model's rate, and you can turn on auto-refill later from the Billing page. * **Pay once for everything:** One checkout covers your opening credits and the first month of any Gloo Code tier. Your card is saved for future fills. * **Know the cap:** Credit-funded API usage is capped at \$100 per week regardless of balance. Support can raise it on request. **Want more details?** See our comprehensive guides: - [Billing & Plans](/studio/billing) - Plan comparison, upgrades, and payment management - [API Usage](/studio/api-usage) - Monitor your consumption and costs Once your products are active, continue below to generate API credentials. After your credentials are created, you can either call the API directly or open the **Studio Playground** to chat with models in the browser — both routes use the same credentials and count against your usage cap. ### Step 3: Create Your API Key The Gloo AI API uses API keys for authentication. A single API key is all you need to make API calls. Here's how to get your API key: 1. Navigate to **API Keys** in the Studio Dashboard. 2. Click **Create New Key**. Enter a name for the key and click **Create**. 3. Copy the API key immediately. It is shown only once; after that, Studio displays only the obfuscated value (e.g., `sk_...3456`). **Treat your API key like a password.** Store it in a secure location, such as a secrets manager or environment variable. Do not commit it to source control. ### Step 4: Set Your API Key Set `GLOO_API_KEY` to the key you copied in Step 3. For example, in your shell: ```bash theme={null} export GLOO_API_KEY="sk_your_api_key_here" ``` Or add it to a `.env` file in your project: ```bash theme={null} GLOO_API_KEY="sk_your_api_key_here" ``` ### Step 5: Make An API Call Make a call to the [Responses API](/api-guides/responses), the recommended endpoint for OpenAI-compatible, multimodal integrations. Send your API key directly in the `Authorization` header as a bearer token. ```python Python theme={null} import os import requests response = requests.post( "https://platform.ai.gloo.com/ai/v2/guarded/responses", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {os.getenv('GLOO_API_KEY')}" }, json={ "model": "gloo-openai-gpt-5-mini", "instructions": "You are a human-flourishing assistant.", "input": [ {"role": "user", "content": "How do I discover my purpose?"} ] } ) # Find the assistant message in the typed output array (a reasoning item # may precede it on reasoning-capable models). output = response.json()["output"] message = next(item for item in output if item["type"] == "message") print(message["content"][0]["text"]) ``` ```bash cURL theme={null} curl https://platform.ai.gloo.com/ai/v2/guarded/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $GLOO_API_KEY" \ -d '{ "model": "gloo-openai-gpt-5-mini", "instructions": "You are a human-flourishing assistant.", "input": [ { "role": "user", "content": "How do I discover my purpose?" } ] }' ``` The response should look like this: ```json theme={null} { "id": "resp_1768500882-56HaBYeuAb4pLpv8PXqh", "object": "response", "created_at": 1768500882, "model": "gloo-openai-gpt-5-mini", "output": [ { "type": "message", "role": "assistant", "status": "completed", "content": [ { "type": "output_text", "text": "What a profound and meaningful question! ..." } ] } ], "usage": { "input_tokens": 1115, "output_tokens": 459, "total_tokens": 1574 } } ``` Prefer routing, values-aligned (`tradition`) responses, or grounded/RAG completions? Use [Completions V2](/api-guides/completions-v2) — it's fully supported and backwards-compatible. ## Next Steps Use the OpenAI Python or Node.js SDK with Gloo AI directly with your API key. Explore all available models and their capabilities. An AI coding agent that plans, builds, and tests from your terminal. Signs in with your Gloo AI account — no API key needed. # Quickstart: Invited Users Source: https://docs.gloo.com/getting-started/quickstart-invited-users Accept your invitation and get started with Gloo AI Studio. Some features in this guide — including Data Engine and Publisher access — require a Pro or Enterprise plan. If you can't access a feature mentioned here, contact your organization admin or see [Billing & Plans](/studio/billing#upgrading-to-enterprise). This guide is for users who have received an invitation to join an existing Gloo AI organization. If you're creating a new account and organization from scratch, see the [Admin Quickstart](/getting-started/quickstart-admins) instead. ### Step 1: Create Your Account Sign up for a Gloo AI account at [Gloo AI Studio](https://studio.ai.gloo.com/). If you already have an account, sign in. ### Step 2: Accept Your Invitation After signing in, you'll see a notification for any pending organization invites. Click the button to accept the invitation. If you don't see a notification, contact the organization admin who sent the invite. ### Step 3: What to do next — based on your role Your organization admin assigned you one of three [roles](/getting-started/glossary#user-roles) when they invited you. Here's what each role can do: As an **Editor**, you can upload content for data ingestion. 1. Click **Data Engine** in the Studio sidebar, then click **Ingestion**. 2. Choose an ingestion method: * **Web Scraping** — Ingest content from websites or sitemaps * **Files** — Upload documents directly (various formats, up to 1GB) * **YouTube** — Connect a YouTube channel * **Feeds** — Follow RSS/Atom feeds or ONIX catalogs 3. Choose **Files** and upload a PDF to test the flow. Your content will be processed and appear in the **Content Library**. **Edit content metadata:** 1. Click **Data Engine**, then **Content Library** 2. Find your uploaded file and click the edit icon in the Actions column 3. Update fields like Title, Subtitle, Summary, Tags, and Source URL 4. Click **Save Changes** For more detail, see [Upload Content](/api-guides/upload-content) and [Edit Content](/api-guides/edit-content). As a **Viewer**, you can browse the Content Library and use the Tools your organization has set up, but cannot upload or edit content. Ingestion analytics is admin-only, so it won't be available to you. 1. Click **Data Engine** in the Studio sidebar, then **Content Library**, to browse the content your organization has ingested. 2. Use the **Tools** section to interact with any components your organization has set up. Contact your organization admin if you need Editor access to upload content. If you were invited as an **Admin**, you have full access to the organization. See the [Admin Quickstart](/getting-started/quickstart-admins) for a complete walkthrough of setting up publishers, inviting additional users, and configuring your organization. # Ask Gloo Code Source: https://docs.gloo.com/gloocode/ask-gloocode Gloo Code has built-in self-documentation — ask it how to use any feature instead of reading the docs. Gloo Code has built-in self-documentation. It knows its own agents, commands, configuration, permissions, and workflows — so instead of hunting through these docs for every detail, you can just ask Gloo Code directly. These docs intentionally stay lean: they cover how to **install** and **launch**. Everything else, Gloo Code can explain to you itself. ## How to ask Open a Gloo Code session (CLI or Desktop) and ask the **plan** or **build** agent a question about Gloo Code, in plain language. For example: * *"How do I create a custom agent?"* * *"What permissions can I give an agent?"* * *"How do I migrate my Cursor rules into Gloo Code?"* * *"Which model should I pick for code review?"* * *"Where is my config file and what can I put in it?"* Gloo Code answers from its own built-in knowledge of itself — it's restricted to its own documentation, so the answers are accurate to your installed version. ## What you can ask Think of anything you'd normally look up in a product manual: | Topic | Example question | | - | - | | **Built-in agents** | "What's the difference between the plan and build agents?" | | **Custom agents** | "Walk me through defining a read-only code review agent." | | **Permissions** | "Which permissions does the explore agent have by default?" | | **Configuration** | "Show me the full config file format." | | **Migrating from other tools** | "I'm coming from Windsurf — how do I bring my rules over?" | | **Model selection** | "Which model is cheapest for large refactors?" | ## Why this works Gloo Code ships with knowledge of its own capabilities. Reference content lives inside the product rather than on this site, which means the answers stay accurate as Gloo Code updates — no stale docs to wade through. For **account, billing, or API credential** issues — things that aren't about how Gloo Code works — head to [Gloo AI Studio](https://studio.ai.gloo.com/) or contact Support. Gloo Code's self-documentation covers product behavior, not your account. # Gloo Code Desktop Source: https://docs.gloo.com/gloocode/desktop Install the Gloo Code macOS app, sign in with your Gloo AI account, and run your first session in a native window. Gloo Code Desktop is the macOS app for Gloo Code. It runs the same agents as the CLI and adds a sidebar of projects and sessions, a diff review panel, a built-in terminal, and system notifications. It ships separately from the CLI and does not require the CLI to be installed. Gloo Code Desktop Home screen with the Recent projects list and Open project button ## Requirements * **macOS 13 (Ventura) or later**, Apple Silicon or Intel. * **A Gloo AI account** — [sign up here](https://studio.ai.gloo.com/sign-up) if you don't have one. Nothing else. The app bundles its own copy of the Gloo Code server, so there is no Node, Bun, Homebrew, or command-line step. Gloo Code Desktop is currently macOS only. Windows and Linux builds are not available yet. On those platforms, use the [Gloo Code CLI](/gloocode/installation). ## Install M1 and later Macs from before 2021 Not sure which you have? Click the Apple menu, then **About This Mac**. A chip named "Apple M-something" is Apple Silicon. Both links always point at the current release, and the app updates itself after that. You can also download from [Gloo AI Studio](https://studio.ai.gloo.com/gloocode?tab=desktop) if you're already signed in. Open the DMG and drag **Gloo Code** into your **Applications** folder, then eject the DMG. The Gloo Code DMG window with the app icon next to the Applications folder shortcut Launch it from **Applications**, Launchpad, or Spotlight — not from inside the DMG. The app is signed and notarized, so macOS opens it without a Gatekeeper warning. ## Sign in On first launch the **Login to Gloo** dialog opens on its own. 1. Click **Sign in with browser**. Your default browser opens the Gloo AI Studio sign-in page while the dialog shows "Waiting for sign-in in your browser…". 2. Sign in and approve access. The browser shows a Gloo Code success page you can close. 3. Back in the app, a **Credentials saved** toast appears. Your profile card in the bottom-left of the sidebar now shows your initials, organization, and email. Login to Gloo dialog showing Waiting for sign-in in your browser with Cancel and Copy sign-in link buttons If the browser didn't open, click **Copy sign-in link** and paste it into any browser. A sign-in attempt expires after 10 minutes. Each organization you sign in with becomes its own **profile**. Click your profile card to **Switch profile**, or type `/login` in the prompt to add another account. Credentials stay on your Mac and are used only to authenticate requests to Gloo AI. ## Open a project Press **⌘O**, choose **File ▸ Open Project…**, or click **Open project** on the Home screen. A standard macOS folder picker opens; you can select more than one folder at once. Each folder becomes a project in the left sidebar, and Gloo Code opens a new, empty session in it. Sidebar with a project expanded to show its sessions, and a new empty session showing Build anything Projects you've opened before are listed under **Recent projects** on the Home screen. Right-click a project in the sidebar and choose **Close** to remove it from the sidebar. Nothing on disk is touched. ## Run your first task Type a request in the prompt at the bottom and press **Enter**. ```text theme={null} Fix the failing test in src/auth.test.ts ``` Tool calls show up in the timeline as cards, so you can see every file the agent reads, every command it runs, and every edit it makes. While a run is in flight the **Send** button becomes **Stop**; press **Esc** to interrupt. Messages you send during a run are queued and shown above the prompt with **Send now** and **Edit** buttons. **Attach context** * Type **@** to mention a project file. * Press **⌘U** or drag files onto the window to attach images, PDFs, or text files. You can also paste an image from the clipboard. **Slash commands** — type **/** at the start of the prompt to see them all. The ones you'll use most: | Command | What it does | | - | - | | `/new` | Start a new session | | `/sessions` | Switch to an earlier session | | `/agents` | Switch agent | | `/undo`, `/redo` | Roll back or restore the last message and its file changes | | `/compact` | Summarize the session to free up context | | `/terminal` | Toggle the built-in terminal | | `/help` | Keyboard shortcut cheat sheet | ## Approve what the agent does When the agent wants to do something that needs your say, a **Permission required** dock appears above the prompt. It names the tool, describes it in plain language, and shows the matching path or command pattern. * **Allow once** runs it this time. * **Allow always** stops asking for this pattern in this project. * **Deny** blocks it. Permission required dock above the prompt describing the request, with Deny, Allow always, and Allow once buttons The agent doesn't touch your files without approval. If you trust a session, press **⇧⌘A** to turn on **Auto-accept permissions** for it; press it again to turn it off. The agent may also ask you a question, which arrives in the same spot as a short form. ## Review changes Press **⇧⌘R** to open the review panel on the right. The **Review** tab lists every changed file with **Added**, **Removed**, or **Modified** badges and a unified or split diff. The dropdown at the top chooses what you're comparing: * **Git changes** — everything uncommitted in the working tree. * **Branch changes** — everything on this branch that isn't on the base branch. * **Last turn changes** — only what the agent just did. Review panel showing two added files with expanded diffs, the Git changes scope selector, and Unified and Split toggles Select lines in a diff and click **Comment** to leave a note; comments collect in the prompt so you can send them back to the agent as one message. **⇧⌘L** adds selected lines to the prompt as context. If you don't like what the agent did, `/undo` reverts to before your last message and puts that prompt back in the composer so you can rephrase. If the project folder has no git repository, the Review tab offers **Create Git repository** so changes can be tracked and undone. That's the only git action in the app itself; commits, branches, and pushes happen through the agent or the terminal. ## Built-in terminal Press **⌃\`** to open a terminal at the bottom of the session. It runs in the project folder. **⌃⌥T** opens another tab. The terminal is a plain shell for you; it shares nothing with the agent. ## Agents Model selection is automatic. Gloo picks and tunes the model for each built-in agent and for the sub-agents it delegates to, so there is nothing to configure. You choose the agent: **Build** does the work, **Plan** reads and proposes without editing. * **⌘.** cycles agents. `/agents` opens the picker. ## Work across sessions Every session is saved automatically. In the sidebar, each session row has a status dot: amber means the agent is waiting on you, red means an error, a spinner means it's working, and a highlighted dot means a session finished while you were elsewhere. * **⇧⌘S** starts a new session in the current project. The **+** on a hovered project row does the same. * **⌥↑ / ⌥↓** move between sessions. **⌘⌥↑ / ⌘⌥↓** move between projects. * The **⋯** menu in the session header offers **Rename**, **Delete**, **Export transcript…**, and **Open in New Window**. * **File ▸ New Window** (⇧⌘N) opens a second full window. Both windows share the same server, so sessions stay in sync. When sessions in several projects need attention at once, a **Needs you** list appears above your profile card. The Gloo Code icon in the macOS menu bar shows the same count and lists each waiting session; choosing one brings the app to the front on that session. The Dock badge matches. Needs you list in the sidebar showing waiting sessions across projects, with permission notifications in the corner Gloo Code menu-bar icon with a count badge and its menu listing the session that needs you ## Notifications Gloo Code posts a macOS notification when a session finishes, needs a permission, or hits an error, but only while the app window isn't in front. Click the notification to jump to that session. Individual notifications and sounds are toggled in **Settings ▸ General**. ## Settings **Gloo Code ▸ Settings** (⌘,) has two tabs. The app version is in the bottom-left corner. Settings window on the General tab with the app version in the bottom-left corner **General** | Setting | Effect | | - | - | | Language | Interface language (17 bundled) | | Auto-accept permissions | Approve every permission request in the current project | | Show reasoning summaries | Show the model's reasoning summaries in the timeline | | Expand shell / edit tool parts | Show those tool cards expanded by default | | Show session progress bar | Animated bar at the top of the session while the agent works | | Color scheme, Theme | **System**, **Light**, or **Dark**, and a theme picker with live preview | | UI, Code, and Terminal fonts | Any font family installed on your Mac | | System notifications, Sound effects | Per-event toggles for **Agent**, **Permissions**, and **Errors** | | Check for updates on startup, Release notes | Update checks at launch and a What's New dialog after updates | **Shortcuts** lists every command with its binding. Click a row and press new keys to rebind; **Reset to defaults** restores everything. MCP servers, custom agents, and skills are configured through files in the project or home directory. Ask Gloo Code how, or see [Ask Gloo Code](/gloocode/ask-gloocode). ## Keyboard shortcuts Every shortcut can be changed in **Settings ▸ Shortcuts**. Defaults: | Action | Shortcut | | - | - | | Command palette | ⇧⌘P | | Open file / palette | ⌘K or ⌘P | | Settings | ⌘, | | Open project | ⌘O | | New session | ⇧⌘S | | New window | ⇧⌘N | | Toggle sidebar | ⌘B | | Toggle review panel | ⇧⌘R | | Toggle file tree | ⌘\\ | | Toggle terminal | ⌃\` | | New terminal tab | ⌃⌥T | | Add files | ⌘U | | Add selected lines to context | ⇧⌘L | | Shell mode / back to prompt | ⇧⌘X / ⇧⌘E | | Auto-accept permissions on/off | ⇧⌘A | | Cycle agent / backwards | ⌘. / ⇧⌘. | | Previous / next session | ⌥↑ / ⌥↓ | | Previous / next project | ⌘⌥↑ / ⌘⌥↓ | | Previous / next message | ⌘⌥\[ / ⌘⌥] | | Back / forward | ⌘\[ / ⌘] | | Stop the current run | Esc | Shortcuts without a modifier are ignored while you're typing, and all shortcuts pause while a dialog is open. Type `/help` for an in-app cheat sheet. ## Updates The app checks for updates at launch and prompts you to **Install and restart** when one is available. To check manually, choose **Gloo Code ▸ Check for Updates…**. ## Where things are stored | What | Where | | - | - | | Sessions, messages, and project data | `~/.local/share/gloocode/`, shared with the CLI if it's installed, so sessions started in one show up in the other | | Credentials and profiles | Your macOS login keychain, readable only by your user. Profile names live in `~/.config/gloo/credentials.json` | | App state (window layout, sidebar order, prompt history, settings) | `~/Library/Application Support/com.gloocode.desktop/` | ## Troubleshooting Choose **Gloo Code ▸ Reload Webview**. If that doesn't help, **Gloo Code ▸ Restart**. Quit and reopen the app. The app starts its own local server on launch and restarts it automatically if it crashes; if this message persists, check for another process holding many local ports. Use **Copy sign-in link** in the login dialog and paste it into a browser. Contact your organization's Gloo AI admin. The app can't proceed without an eligible organization. Allow notifications for Gloo Code in **System Settings ▸ Notifications**, and note they're suppressed while the app window is in front. The bundled server restarted. Close the terminal tab and open a new one with **⌃⌥T**. ## Next Steps Not sure how to do something? Gloo Code can answer questions about itself — learn how to ask. # Installation & Setup Source: https://docs.gloo.com/gloocode/installation Install Gloo Code and sign in with your Gloo AI account in under 5 minutes. This guide walks you through installing Gloo Code and signing in with your Gloo AI account. The whole process takes about 5 minutes. ## Prerequisites Before you start, make sure you have: * **macOS or Linux** (ARM64 or x64) — see [platform support](#platform-support) below * **A Gloo AI account** — [sign up here](https://studio.ai.gloo.com/sign-up) if you don't have one * **An organization with a Gloo Code plan** — and a seat assigned to you. Your Gloo AI admin can sort this out; see [Billing & Plans](/studio/billing#choosing-a-gloo-code-tier) for tiers, seats, and how to buy. ## Platform Support Gloo Code ships as a self-contained binary for macOS and Linux: | OS | Architecture | Variant | | - | - | - | | macOS | ARM64 (Apple Silicon) | — | | macOS | x64 (Intel) | — | | Linux | ARM64 | glibc | | Linux | x64 | glibc | | Linux | ARM64 | musl | | Linux | x64 | musl | No runtime dependencies are required. The installer detects your platform and downloads the correct binary. **Windows** is not currently supported. Windows users can run Gloo Code through WSL (Windows Subsystem for Linux) using the Linux binary. ## Step 1: Install the Binary Run the installer script. It detects your OS and architecture, downloads the correct binary, and adds it to your PATH. ```bash theme={null} curl -fsSL https://gloocode.ai.gloo.com/install.sh | sh ``` The installer places the binary in your PATH (typically `/usr/local/bin/gloocode` on macOS) and updates your shell profile if needed. **Open a new terminal window** after installing. Your current session won't have the updated PATH until you do. Verify it worked in a new terminal: ```bash theme={null} gloocode --version ``` If you see a version number, the install succeeded. If you get "command not found", see [Troubleshooting](#command-not-found-gloocode) below. ## Step 2: Sign In Sign-in happens in your browser. Start Gloo Code in any project directory: ```bash theme={null} gloocode ``` On a new install it opens **Sign in to Gloo** straight away. Press **Enter** on **Sign in with browser** and your browser opens Gloo AI Studio. 1. Sign in to Studio and choose the organization you want to use Gloo Code with. 2. Approve access. Studio shows a success page you can close. 3. Back in the terminal, Gloo Code confirms you're signed in and drops you at the prompt. **Already used Gloo Code on this machine?** Then it starts straight into the TUI without asking — including if you set it up with the old Client ID and Secret. Type `/login` to open the same dialog. The browser only opens after you press Enter, so a session started over SSH or in a container never spawns a window unasked. If the browser doesn't open, Gloo Code prints the sign-in URL — open it on any machine and finish there. A sign-in attempt expires after 10 minutes. ### Where your sign-in is stored On macOS, Gloo Code stores your credential in the **login keychain**, readable only by your user. On Linux it lives in `~/.config/gloo/credentials.json` with `0600` permissions. Either way it stays on your machine and is used only to authenticate your requests to Gloo AI. Requests route through Gloo AI, which handles provider selection, safety screening, and failover. You never manage API keys for individual model providers. ### Working across several organizations Each organization you sign in with is saved as its own **profile**. | Command | What it does | | - | - | | `/login` | Sign in to another organization and switch to it | | `/profiles` | Switch between saved profiles, or press **Ctrl+D** to delete the highlighted one | Switching profiles persists, so your next session starts on the same one. Deleting a profile removes it locally and revokes its access on the Gloo AI platform. [Gloo Code Desktop](/gloocode/desktop) signs in exactly the same way and keeps its own profiles. The two share sessions and project data, so a session you start in one shows up in the other. ## Updating Gloo Code Re-run the installer to update to the latest version: ```bash theme={null} curl -fsSL https://gloocode.ai.gloo.com/install.sh | sh ``` The installer checks your current version and only downloads if an update is available. You can also use the built-in update command: ```bash theme={null} gloocode update ``` ## Troubleshooting ### Sign-in fails or won't finish Gloo Code names the reason in the terminal. The ones you're most likely to see: | Message | What to do | | - | - | | Your account has no organizations eligible for Gloo Code | Ask your Gloo AI admin to add a Gloo Code plan, or to assign you a seat — see [Teams and seats](/studio/billing#teams-and-seats) | | Your organization's billing setup does not allow API keys yet | Finish billing setup in [Studio](https://studio.ai.gloo.com/), then sign in again | | The sign-in request expired | Attempts expire after 10 minutes — run `/login` to start a new one | | Sign-in was denied in the browser | You (or your identity provider) declined the request; run `/login` to retry | If the browser never opened, copy the sign-in URL Gloo Code prints and open it yourself. If a run later stops working, your access may have been revoked — for example an admin removing your seat. Run `/login` to sign in again. If your team used Gloo Code with a Client ID and Secret before, those credentials no longer sign you in. Your organization needs a Gloo Code plan and a seat for each user; see [re-enabling a team that used client credentials](/studio/billing#if-your-team-used-gloo-code-before-with-a-client-id-and-secret). ### "Command not found: gloocode" The most common cause is running the command in the same terminal session where you installed. Open a **new** terminal window and try again. If it still doesn't work, check where the binary was installed: ```bash theme={null} which gloocode # should show /usr/local/bin/gloocode or ~/bin/gloocode ls /usr/local/bin/gloocode ~/bin/gloocode 2>/dev/null ``` If the binary exists but isn't on your PATH, add its directory manually: ```bash theme={null} export PATH="/usr/local/bin:$HOME/bin:$PATH" ``` Add this line to your shell profile (`~/.zshrc` or `~/.bashrc`) so it persists across sessions. ### Binary won't run on Linux On older Linux systems, the installer may select the wrong libc variant. If you see errors about missing shared libraries: * Check your libc: `ldd --version` (glibc) or `musl-ldd --version` (musl) * The installer auto-detects this, but you can also download the correct variant manually from the install URL ## Next Steps Start the terminal UI and run your first task. # Launch Gloo Code Source: https://docs.gloo.com/gloocode/launch Start Gloo Code in the terminal and run your first task. The Gloo Code CLI runs as a **terminal UI (TUI)** in any project directory. If you'd rather work in an app, [Gloo Code Desktop](/gloocode/desktop) runs the same agents on macOS. You sign in to it the same way, and it shares your sessions and project data. ## Launch the TUI From any project directory, run: ```bash theme={null} cd ~/my-project gloocode ``` You'll land in the terminal interface. Type a task in natural language and Gloo Code will plan the approach, write the code, run tests, and commit the result. You review and approve every change before it lands. ### Example prompts Not sure what to try first? Here are some good starting points: | Prompt | What Gloo Code will do | | - | - | | `Explain this codebase` | Read your project structure and give you an overview | | `Add input validation to the signup form` | Find the form, add validation logic, write tests | | `Find and fix the failing test in src/auth` | Diagnose the failure, fix the root cause, verify it passes | | `Refactor the payment module to use the new Stripe API` | Plan the migration, update the code, run tests | ## Next Steps Not sure how to do something? Gloo Code can answer questions about itself — learn how to ask. Prefer an app? Install the macOS desktop app and run the same agents in a native window. # Gloo Code Source: https://docs.gloo.com/gloocode/overview An autonomous AI coding agent that runs in your terminal — powered by cost-optimized models through Gloo AI. Gloo Code is an AI coding agent. You describe what you want in plain English, and it handles the rest — planning the approach, writing the code, running tests, and committing the result via git. It ships as a single binary with no runtime dependencies and routes every model request through Gloo AI for cost-optimized routing and values-aligned AI. Gloo Code is available two ways: as a **command-line tool** for macOS and Linux, and as a **desktop app** for macOS with a sidebar of projects and sessions, a diff review panel, and a built-in terminal. Both run the same agent. See [Gloo Code Desktop](/gloocode/desktop) for the app. **It's cheaper.** Gloo Code routes requests through Gloo AI's optimized model infrastructure. Based on internal benchmarks of typical agentic coding sessions, customers can see **up to 60–70% lower cost** compared to equivalent direct-provider pricing. Actual savings vary by workload and model selection. ## Plans and seats Gloo Code is a monthly subscription your organization buys in Gloo AI Studio: **Basic**, **Pro**, or **Ultra**, either for yourself or per seat for a team of 2–25. Each tier includes a monthly usage allowance, and admins can buy a top-up if you run out early. Gloo Code never spends your organization's prepaid API credits, and credits never grant Gloo Code access. An organization admin picks the tier on the Studio products page and assigns seats from the Billing page. See [Billing & Plans](/studio/billing#choosing-a-gloo-code-tier) for pricing, allowances, top-ups, and seats. ## Get started in 3 steps 1. **[Install Gloo Code](/gloocode/installation)** — download the binary and sign in with your Gloo AI account. 2. **[Launch Gloo Code](/gloocode/launch)** — start the terminal UI and run your first task. 3. **[Ask Gloo Code](/gloocode/ask-gloocode)** — Gloo Code has built-in self-documentation. Ask it how to use any feature instead of hunting through docs. ## Next Steps Get up and running in under 5 minutes — install, sign in, and start coding. Start the terminal UI and run your first task. Install the macOS app, sign in with your Gloo AI account, and run your first session in a native window. Gloo Code can answer questions about itself. Learn how to ask. # What is Gloo AI? Source: https://docs.gloo.com/learn-more/about-gloo-ai Trusted and Transparent, Value-Aligned AI. ## Purpose & Vision Gloo AI is our platform to responsibly integrate artificial intelligence into every layer of the Gloo platform—and beyond. We’re not just building AI features; we’re embedding intelligent capabilities into the heart of our ecosystem to empower ministry, strengthen connections, and support resilient organizations. While many AI platforms optimize for engagement or monetization, Gloo AI is built for **trust** at its core. We're creating products, models, and capabilities that are ethically designed, transparently operated, and contextually relevant. *** ## Our Differentiator: Contextual, Human-Centered AI Unlike general-purpose AI platforms, Gloo AI is rooted in **contextual intelligence**. Our models are informed by ministry data, enhanced with domain expertise, and designed for usability and trust. Every interaction is designed to support human discernment—not replace it. We encourage collaboration and allow the AI community to build together by advancing values-based AI through open-source research, models, learnings, data, and standards. We want to protect our users and our publishers with their personal information and content. While other companies may sell your data or use content without permission, we strive to give credit where credit is due and protect that credit. We are dedicated to developing and deploying AI solutions that embody the highest ethical standards. We have developed Flourishing AI Standards making sure our solutions are tools for the common good. We will build trust with our products, ensuring strong performance without sacrificing security. *** ## Our Strategic Foundations ### 1. Transparent Infrastructure We are building a future where every component—prompts, provenance, training data, and behaviors—is explainable, auditable, and, where possible, open. ### 2. Security as a Moral Obligation Our architecture is zero-trust by default. All data access is rights-aware, opt-in, and governed by ethical design—not just compliance. ### 3. Radical Transparency in Interactions AI answers must be explainable. Our Assistants link outputs to source documents, prompts, and logic—empowering users to see how results were generated. *** ## Core Platform Capabilities The control center of the Gloo platform. It serves as the interface where leaders engage with AI tools, personalize interactions, and take action with insight. Enriches and transforms raw data into context-ready insights. It powers search, discovery, and contextual understanding for knowledge bases. Lightweight, powerful RAG chatbots grounded in public or partner-approved datasets. They are role-specific, trustworthy tools designed for use in real-world ministry contexts. Our data trust layer. It facilitates the secure, permissioned exchange of information between creators and consumers. Our consumer-facing application that enables trusted, transparent AI conversations—built for real life, not hype. *** ## Final Word: Intelligence Without Compromise Gloo AI is not here to win benchmark tests. We’re building the most trusted, human-centered AI infrastructure for people who need clarity over complexity, and discernment over disruption. This includes faith leaders, counselors, nonprofits, and anyone disillusioned with generic AI systems. Trust will be the differentiator in the next era of intelligence. That’s what Gloo AI is here to deliver. # Benchmarking Source: https://docs.gloo.com/learn-more/benchmarking The Flourishing AI Initiative (FAI) framework for measuring model alignment. Gloo AI is committed to building transparent, value-aligned evaluation systems. We evaluate Large Language Models (LLMs) using the **Flourishing AI Initiative (FAI)** framework. This is an empirical standard designed to measure how well AI models align with human well-being in general. An application of FAI known as FAI-Christian (FAI-C), evaluates how models align to the Christian worldview. ## The Flourishing AI Initiative (FAI) Framework Unlike standard benchmarks that focus solely on reasoning or code generation, FAI evaluates models against **seven dimensions of human flourishing**. This allows you as the developer to understand how models perform across specific domains of life. ### The 7 Dimensions 1. **Character:** Virtue, moral reasoning, and self-regulation. 2. **Happiness:** Life satisfaction and emotional well-being. 3. **Relationships:** Interpersonal connection and community building. 4. **Meaning:** Purpose, significance, and goal orientation. 5. **Health:** Physical and mental wellness. 6. **Finances:** Economic stability and stewardship. 7. **Faith:** Spiritual formation, theological coherence, and religious engagement. ## Scoring Methodology The benchmark utilizes a **Judge-LLM** approach. A "Judge Persona" (an LLM with defined expert criteria) evaluates responses based on a curated dataset of \~800+ prompts. To prevent overfitting, we calculate a **Geometric Mean** across three distinct metric types for every response: 1. Objective Score: Accuracy on fact-based questions. 2. Subjective Score: Alignment with the specific dimension's flourishing criteria (e.g., *Does this advice promote long-term character growth?*). 3. Tangential Score: Relevance and safety (checking for off-topic drift or hallucinations). ### Dual Standards: FAI-G vs. FAI-C Depending on the target audience that you are building for, you may want to evaluate models against two distinct standards: | Standard | Description | Judge Persona | | :- | :- | :- | | **FAI-G (General)** | Measures broad human well-being and general flourishing. | Secular Domain Experts | | **FAI-C (Christian)** | Measures alignment with biblical grounding and Christian moral clarity. | Christian Domain Experts | ## Key Insights for Developers Our latest [Insights Report](https://gloo.com/resources/blog/flourishing-ai-initiative-insights-report) highlights critical performance deltas relevant to application development: * **Frontier models** (like GPT-4o or Claude 3.5 Sonnet) often perform well on pragmatic dimensions (Health, Finances) but **systematically underperform on values-based dimensions** (Faith, Meaning) due to safety-tuning that forces neutral responses. * There is often a significant score drop when comparing a model's FAI-G score to its FAI-C score, indicating that **generic models struggle with theological distinctiveness**. * **Gloo-hybrid models** (fine-tuned on the Gloo stack) have demonstrated a \~30+ point **increase in the Faith dimension** compared to base frontier models. ## Additional Resources If you want to learn more, check out the full research findings and access the leaderboards below: * **[Flourishing AI Research Hub](https://gloo.com/flourishing-hub/research)**: View the live leaderboard comparing open-source and proprietary models. * **[FAI Insights Report](https://gloo.com/resources/blog/flourishing-ai-initiative-insights-report)**: Read the deep dive on methodology and findings. # Open Source: https://docs.gloo.com/learn-more/open Transparency builds trust. Collaboration fuels innovation. Gloo Open is a commitment to building in the open—with integrity, clarity, and shared purpose. By releasing models, data, and tools under open licenses, we're not just making resources available—we're inviting others into a transparent and collaborative ecosystem where impact and innovation go hand in hand. ## Why Openness Matters In an age of increasing complexity, trust is built through transparency. Gloo Open provides: * **Visibility** into what powers our AI, enabling informed use and critique. * **Confidence** in the integrity of what's being built, ensuring alignment with values of human dignity and flourishing. * **Freedom** to explore, adapt, and build upon shared resources, sparking new possibilities for impact. This approach isn't just idealistic—it's practical. It lays the foundation for long-term trust, collective learning, and meaningful progress. *** ## What's Gloo Open ### Open Data, Code & Models Core building blocks—released under open licenses—are available to explore, customize, and extend: * Language models tuned with ethical alignment. * Structured datasets and ontologies. * Evaluation tools for responsible use. Everything is structured for clarity, auditability, and reuse. ### Ethical Grounding and Human Flourishing Beyond tools, Gloo Open promotes standards that put human values at the core: * Benchmarks focused on human flourishing. * Model evaluators for safety and alignment. * Guidance rooted in ethical responsibility. This is technology designed not just to work, but to serve. ### Built for Shared Progress Open ecosystems thrive when ideas are exchanged freely and progress is made collectively. That's why Gloo Open supports: * **Knowledge sharing** through articles, white papers, and insights. * **Collaboration opportunities** with ministries, institutions, and values-aligned technologists. * **Spaces for co-creation**, from events to research to community initiatives. Together, these tools and forums help shape an ecosystem greater than the sum of its parts. *** ## A Living Ecosystem Gloo Open isn’t static—it’s growing. That growth is continually mapped, measured, and nurtured through: * Community engagement and feedback. * Ecosystem health metrics. * Insight reports that guide future development. Participation fuels the momentum and helps focus investment in what matters most. # Trust Source: https://docs.gloo.com/learn-more/trust How we build trust with contextual intelligence, transparency, and ethical design. # Trust in AI-Driven Ministry Gloo AI is fundamentally different from generic AI platforms—it's built on contextual intelligence and human-centered design. Our approach ensures that every interaction supports discernment and enriches ministry without sacrificing trust. ## How We Build Trust * **Contextual Intelligence:** Our models are informed by real ministry data and domain expertise to deliver insights that are both relevant and reliable. * **Radical Transparency:** Every AI output is tied to its source data, prompt patterns, and decision logic. This allows users to understand exactly how an answer was generated. * **Ethical Design:** We prioritize security, privacy, and ethical data use, embedding these principles into every layer of our infrastructure. * **Collaborative Innovation:** Gloo AI is developed with direct input from faith leaders, nonprofits, and partners to ensure our products address real-world needs. ## Developer Impact Our commitment to trust provides tangible benefits for developers building on our platform. * **Auditability:** Transparent mechanisms ensure that every AI decision can be traced back to its source—essential for both debugging and building user trust. * **Customizability:** The contextual nature of our models means that developers can tailor applications to reflect the unique values and requirements of different ministry contexts. ## Data Processing Agreement For details on how Gloo AI handles personal information on behalf of organizations, see the [Gloo Data Processing Agreement (DPA)](https://gloo.com/legal/data-processing-agreement). It describes the data processing terms for Gloo offerings when incorporated into an organization's agreement, including security, sub-processors, and data subject rights. ## Sub-Processors (As of August 2026) | Sub-processor | Purpose | | - | - | | WorkOS | Authentication | | GitHub | Code Repository, CI/CD Orchestration | | Porter | Infrastructure | | Vercel | Application Infrastructure | | AWS | Infrastructure, Inference | | Anthropic | Inference | | Google | Inference | | OpenAI | Inference | | OpenRouter | Inference | | Weaviate | AI Knowledge Base and Agents | | PostHog | Analytics | | Sentry | Analytics, Security | | Linear | Project and Engineering Management | | Mintlify | Developer Documentation | | BetterStack | Monitoring | | OpenMeter | Metering and Billing | # API Usage Source: https://docs.gloo.com/studio/api-usage Monitor API requests, tokens, and costs. ## Overview The **Usage** dashboard tracks your API consumption, helping you monitor requests, tokens, and costs. ## Accessing Usage 1. Log in to [Gloo AI Studio](https://studio.ai.gloo.com) 2. Select your organization from the organization switcher 3. Click **Usage** in the BUILD section of the main navigation The dashboard displays your API usage metrics, costs, and trends over time. ## Available Metrics The Usage dashboard tracks the following API metrics: **API Requests:** Total number of API calls made to Gloo AI services **Token Usage:** * Prompt tokens (input sent to the API) * Completion tokens (output generated by the API) * Total tokens (combined) **Cost/Spend:** Actual costs incurred based on your API usage **Date Ranges:** View usage over custom time periods (day, week, month, or custom range) Understanding your token usage helps you optimize costs. Shorter prompts and responses consume fewer tokens and cost less. ## Filtering Usage Data The Usage dashboard provides multiple filtering options to help you analyze API usage patterns. **By Organization** If you're an admin with access to multiple organizations, you can filter usage by organization using the organization filter dropdown. Select the organization you want to analyze, and usage metrics will update to show only that organization's data. **When this is useful:** Admins managing multiple organizations can track usage and costs separately for each org. **By API Key** Filter usage by a specific API key to see which applications or integrations are consuming resources. In the **API Credential** dropdown, select an API key by its name to view usage specific to that key. **By Client ID** (legacy) If your organization still uses OAuth2 client credentials, you can also filter by Client ID in the same dropdown. **When this is useful:** Track usage per application, environment (development vs. production), or team to identify optimization opportunities. **By View (Requests or Tokens)** Toggle between different views to focus on specific metrics: * **Requests:** View the number of API calls over time * **Tokens:** View prompt and completion token consumption Use the **View by** selector to switch between these views. **By Date Range** Select custom date ranges to analyze usage patterns using the **Date Range** selector. Choose a preset range or select a custom date range to view usage for that period. ## API Usage by Capability The Usage dashboard also shows your API usage broken down by capability (Models, Chat, etc.), helping you understand which features are driving your consumption. ## Understanding Your API Usage **Request Counts** Each call to the Gloo AI API counts as one request, regardless of the size of the input or output. Requests are tracked by: * Endpoint called (e.g., Chat, Recommendations) * Timestamp * API credential used * Response status (success or error) Use request counts to understand your application's call patterns and identify peak usage times. **Token Metrics** Tokens are the fundamental units of text processing in AI models. Understanding token usage helps you optimize costs: * **Prompt Tokens:** The number of tokens in your input to the API (your request, context, and system messages) * **Completion Tokens:** The number of tokens in the API's response (the generated text) Pricing is typically based on token consumption. More tokens = higher cost. Completion tokens often cost more than prompt tokens. Different models have different token costs. Check our [pricing page](https://studio.ai.gloo.com/pricing) for current rates. **Cost Tracking** The Usage dashboard displays your costs based on actual API usage: * **Daily costs:** Total spend per day * **Weekly totals:** Rolling 7-day spend (relevant for Pay-as-you-go limits) * **Monthly totals:** Cumulative spend for the billing period **For organizations on prepaid credits:** Costs draw down your balance, and are tracked against the fixed \$100 per week usage limit. The Usage page shows a **Weekly usage limit** card with how much of the limit is used and when it resets. Gloo Code usage does not count toward it. See [Prepaid Credits](/studio/prepaid-credits). **For Gloo Code subscriptions:** The Usage page shows how much of the current month's Gloo Code allowance has been used, as a percentage. Gloo Code usage is covered by the tier's allowance and top-ups, not by prepaid credits. **For legacy Pay-as-you-go and Pro accounts:** Costs are tracked against your editable weekly or monthly spend limit. You'll see warnings as you approach your limit. **For Enterprise accounts:** Costs contribute to your monthly invoice total. ## Rate Limits vs. Spend Limits It's important to understand the difference between rate limits and spend limits: * **Rate Limits:** Control how quickly you can make API requests (requests per minute/hour). Rate limits prevent abuse and ensure fair usage. See the [API Reference](/api-reference/general/limits) for current rate limits. * **Spend Limits:** Control how much you can spend in a period. Organizations on prepaid credits have a fixed \$100 per week usage limit; legacy Pay-as-you-go and Pro accounts have an editable weekly or monthly limit. When you hit the limit, API access pauses until it resets. Rate limits control request speed. Spend limits and the weekly usage limit control spending. See [Billing & Plans](/studio/billing#spend-limits) for details. ## Frequently Asked Questions **How often is usage data updated?** API usage data is updated in near real-time, typically within a few minutes of API calls being made. Some aggregated metrics (like daily totals) may take up to an hour to fully update. **Can I export usage data?** Self-serve accounts can view usage in the dashboard. Enterprise accounts have access to export features for detailed reports. Contact your account manager for export options. **How far back can I view historical data?** * **Self-serve accounts:** 90 days of historical API usage data * **Enterprise:** Extended historical data for the duration of your account Set up regular usage reviews to identify optimization opportunities and ensure you're getting the most value from your Gloo AI investment. # Billing & Plans Source: https://docs.gloo.com/studio/billing Choose the products you need, pay for one combined subscription, and manage payment methods and spend. ## Overview Gloo AI Studio is priced per product, and the products stack: one account, one payment method, one billing history. Add the ones you need and Gloo combines the monthly ones into a single subscription that renews on one billing date. Prepaid credits work differently: each credit purchase, whether you buy it yourself or auto-refill does, is charged to your card at the moment it happens, not on that date. Pay-as-you-go access to the Gloo AI API, funded with [prepaid credits](/studio/prepaid-credits). Usage is billed at the published per-model rates. A monthly plan for the [Gloo Code](/gloocode/overview) coding agent in Basic, Pro and Ultra tiers, each with a Teams variant priced per seat. Every tier includes a monthly usage allowance. Ingest, ground and search your own content. Data Engine is currently at capacity; [request access](https://studio.ai.gloo.com/contact?interest=data-engine) and the team will follow up. Custom terms, monthly invoicing, audit trails and dedicated support. [Contact us](https://studio.ai.gloo.com/contact) to talk it through. Current prices are on the [pricing page](https://studio.ai.gloo.com/pricing). Once you are signed in, the [products page](https://studio.ai.gloo.com/products) is where you add, change or remove products, and the [Billing page](https://studio.ai.gloo.com/billing) is where you manage credits, seats, payment methods and invoices. Organizations created before this model, on the older Pro plan or on weekly arrears billing, keep working as they do today. See [Legacy plans](#legacy-plans). Enterprise organizations are invoiced under their agreement and do not buy prepaid credits. ## Prices | Gloo Code tier | Single seat | Teams, per seat | | - | - | - | | **Basic** | \$20 / month | \$18 / seat / month | | **Pro** | \$100 / month | \$90 / seat / month | | **Ultra** | \$200 / month | \$180 / seat / month | Teams plans are bought for 2 to 25 seats; 26 or more seats is an Enterprise conversation. Each tier's allowance is shown in Studio as a percentage used, never as a dollar figure, and higher tiers include a larger allowance and access to more models. Inference has no monthly fee. You buy credits and each request draws them down at the model's published rate: * **[Direct endpoints](/api-guides/endpoint-types#direct-endpoints)**, Direct Responses, Direct Completions, and [Embeddings](/api-guides/embeddings), bill the provider's published per-model rate with nothing added. Embeddings are billed per input token with no output charge. [Grounded Responses](/api-guides/grounded-responses) runs on the same direct path, so retrieval over your own content adds no fee either. * **[Guarded endpoints](/api-guides/endpoint-types#guarded-endpoints)**, Responses and Completions, where guardrails, moderation and values alignment run, add a 6.5% platform fee on top of the provider rate. [Grounded Completions](/api-guides/grounded-completions) runs on the guarded pipeline and carries the same fee. See [Responses](/api-guides/responses) for how the fee is applied. The live rate for every model is on the [Supported Models](/api-guides/supported-models) page and on `GET /platform/v2/models`. ## The one rule that surprises people **A Gloo Code allowance and API credits are two separate pools, and neither can be spent as the other.** * Your Gloo Code allowance pays for work you do **inside Gloo Code**. It cannot be spent on direct API calls. * Your credits pay for **direct API calls**. They are not consumed by Gloo Code. This is deliberate, and it is enforced in both directions: * An organization with only a Gloo Code plan that calls the API directly is refused. The Gloo Code allowance does not grant API access. * An organization with only credits that uses Gloo Code is refused with `402 GC_NOT_SUBSCRIBED`. Credits do not grant Gloo Code access. If you use both Gloo Code and the API directly, add both products. Having one does not grant the other, and running out of one does not fall back to the other. ## Choosing a Gloo Code tier Gloo Code comes in **Basic**, **Pro** and **Ultra**. Each is a single-seat plan, and each has a **Teams** variant for organizations that need several people on Gloo Code under one subscription. **You can only hold one Gloo Code tier at a time.** Adding Ultra when you already have Pro replaces it rather than stacking. You get one allowance, not two. ### Teams and seats A Teams plan is bought in seats, from 2 to 25 per subscription. The fee and the included allowance both scale with the seat count: five seats means five times the per-seat fee and five times the per-seat allowance, pooled across the organization rather than divided per person. Two different things happen with seats on the Billing page: * **How many seats you pay for** is a product change, made from the Gloo Code product's **Manage** dialog. Adding seats takes effect immediately, prorated for the rest of the period. Removing seats takes effect at your next billing date, and until then you keep the seats you paid for. * **Who holds a seat** is set from the **Manage seats** dialog. An organization admin assigns and unassigns seats to members, and only a member holding a seat can sign in to Gloo Code. Unassigning a member frees the seat and signs them out within about a minute. Only organization **Admins** can buy or change products, assign seats, and buy credits. Other members see *Billing is managed by your organization admins* in place of those controls. See [Manage Users](/studio/manage-users) for roles. ## When the allowance runs out Each pool runs out on its own terms: | | Gloo Code allowance | Credits | | - | - | - | | **What it funds** | Work inside Gloo Code | Direct API calls | | **Renews** | Monthly, with your subscription | Never. You top it up | | **When exhausted** | Gloo Code requests are refused with `402 GC_ALLOWANCE_EXHAUSTED` until you buy a top-up or the allowance renews | API requests return `402 INSUFFICIENT_CREDIT`; add credits and access resumes immediately. The separate weekly usage limit is not cleared by buying credits; see [Spend limits](#spend-limits) | | **Top up early?** | Yes, from the Billing page | Yes, any time | The full error bodies are in the [error reference](/api-reference/general/errors#payment-required-402). ### Gloo Code top-ups If your allowance runs out before the month does, buy a **top-up** from the Gloo Code card on the Billing page. A top-up costs the same as a month of your tier and adds the same allowance again. It goes into a separate pool that is drawn only after the monthly allowance is exhausted, and it carries over when the month renews, so unused top-up units are not lost at the boundary. Top-up units expire 12 months after purchase and can only be spent while the organization holds an active Gloo Code tier. On a Teams plan the top-up is priced and sized for your full seat count. The Usage page shows the monthly allowance and any top-up as two separate percentages. ## Tracking your Gloo Code usage The Usage page in Studio has a Gloo Code view. It shows: * Your organization * Your Gloo Code plan tier * How much of this month's allowance you have used, as a percentage * When your allowance resets * How much of any top-up you have used It does not show dollar amounts. Usage here is always a percentage of your allowance, not a cost. Gloo Code sign-ins do not appear on the [API Keys](/studio/manage-api-credentials) page. Organization admins can see each member's Gloo Code activity on the Usage page, and can revoke a member's Gloo Code sign-ins from there. ## Changing your plan Adding, removing or swapping a product takes effect without interrupting service. **When it takes effect depends on which direction the money moves:** * **Anything that increases your fee**, adding a product, moving up a Gloo Code tier or adding seats, applies **immediately**, prorated for the rest of the current period. You get the new access straight away and pay the difference for the remaining days. * **Anything that decreases your fee**, removing a product, moving down a tier or removing seats, applies at the **start of your next billing period**. You keep what you have already paid for until then, and you are not refunded the difference. If a single change does both at once, say adding credits while dropping to a lower Gloo Code tier, the increase applies immediately and the decrease is scheduled for the next period. You are never charged twice, and you never lose access you have paid for. The dialog states the timing and today's charge before you confirm. Your credits are untouched by any of this. They belong to your organization, not to a product, and they survive plan changes. No page reload is needed. The Studio UI refreshes automatically after a plan change completes. ## Adding a payment method Your card is saved the first time you pay: buying credits, or starting a Gloo Code plan, during onboarding or from the Billing page. After that you can add, replace or remove cards from the **Payment Methods** tab. 1. Navigate to the [Studio Billing page](https://studio.ai.gloo.com/billing) 2. Go to the **Payment Methods** tab 3. Click **Add payment method** 4. Enter your card details 5. Save your changes A card on its own does not activate anything. API access starts when you buy your first credits; see [Prepaid Credits](/studio/prepaid-credits). Gloo Code access starts with a Gloo Code plan. Once you have credits, you can start using the API and monitoring your usage in the [API Usage](/studio/api-usage) dashboard. ## Spend limits **Inference (prepaid credits).** API usage paid from credit has a fixed **\$100 weekly usage limit**. It is not a setting you adjust; it exists to bound how fast a card can be charged. It counts only usage drawn from paid credit, so heavy Gloo Code use never eats into it. The Wallet card on the Billing page shows how much of the week you have used and when the window resets. When you reach the weekly usage limit, API access is paused until the window resets. Adding credits does not restore access early. See [Prepaid Credits](/studio/prepaid-credits#when-you-run-out) for the two limits and the two errors they produce. **Gloo Code.** There is no spend limit to set. Your tier's monthly allowance is the ceiling, and a top-up is the only way to add to it. **Enterprise.** Spend limits are managed through your service agreement, not the Studio UI. Contact your account manager to adjust them. Need more than \$100/week of API usage? Contact support. A higher ceiling applied to your organization takes effect straight away and persists across plan changes. Organizations on [legacy plans](#legacy-plans) still set their own limit from the Spend Limit card. ## Upgrading to Enterprise Enterprise plans unlock the full power of Gloo AI, including: * **Data Engine:** Ingest and manage your organization's content * **Search:** Implement intelligent search across your data * **Item Recommendations API:** Build personalized recommendation systems * **Extended Analytics:** Track ingestion metrics * **Private Transcription:** Paid add-on, available on request (see below) * **Monthly Invoicing:** Simplified billing with monthly invoices * **Dedicated Support:** Priority support from our team ### Private Transcription Audio and video you ingest is transcribed automatically so it becomes searchable. By default that transcription is performed by a third-party speech-to-text provider, which means the audio is sent outside Gloo's infrastructure to be processed. **Private Transcription** keeps that processing on Gloo-managed infrastructure instead. It is a **paid add-on** for Enterprise customers, available on request. It is not enabled by default and is not included automatically with an Enterprise agreement. If your organization has data-residency, regulatory, or contractual requirements that prevent audio being processed by a third party, raise it with your account manager or contact sales. It is not something you can enable yourself from the Studio. **How to contact sales:** * Use [Contact us](https://studio.ai.gloo.com/contact) from the [pricing page](https://studio.ai.gloo.com/pricing) * Click the **Contact sales** button on any Enterprise tier in Studio **What to expect:** Our sales team will work with you to: 1. Understand your use case and requirements 2. Configure the right plan and features for your needs 3. Set up monthly invoicing and service agreements 4. Migrate your existing organization to Enterprise features ## Managing invoices (Enterprise) Enterprise accounts receive monthly invoices based on their service agreement. Invoices include: * API usage (requests and tokens) * Feature access (Data Engine, Search, Item APIs) * Any custom services or add-ons Invoices are sent to your designated billing contact at the end of each billing period. For questions about your invoice, contact your account manager. Enterprise invoices include detailed usage breakdowns. You can also monitor real-time usage in the [API Usage](/studio/api-usage) dashboard. ## Legacy plans Two kinds of organization predate the product model above. Nothing changes for them until they choose to add a product. ### Pro Pro is no longer sold to new organizations. If you are on it, it continues to work as it does today: everything in pay-as-you-go, plus Data Engine, Content Library and Search access, billed monthly to your card. * **Spend limit:** \$800/month by default. Adjust it from the **Spend Limit** card on the Billing page, up to your organization's ceiling (\$800 unless it has been raised on request). When you reach it, API access pauses until the next billing period or until support raises your ceiling. * **Adding Gloo Code:** the upgrade checkout moves the organization onto a combined plan and carries your Pro entitlements across, so you keep Data Engine and Content Library access. Details below. #### Adding Gloo Code to a Pro organization Admins on a Pro plan see an **Add Gloo Code to your Pro plan** card on the Billing page, and the same option on the products page. The upgrade checkout moves the organization onto a combined plan: * **You keep Data Engine, Content Library and Search.** Pro appears under **Your products** as *Pro · \$25/mo, includes Data Engine ingestion*, billed together with everything else. * **Your \$25 Pro fee continues.** It is not charged again at upgrade. The next Pro charge moves to your new monthly billing date. * **API usage moves to prepaid credits.** The checkout includes an opening credit purchase of at least \$10, and from then on API calls draw down your balance instead of being billed in arrears. See [Prepaid Credits](/studio/prepaid-credits). * **The editable spend limit goes away.** Prepaid API usage has the fixed \$100 weekly usage limit instead. A higher ceiling that support raised for your Pro plan does not carry over; contact support if you still need it. * **Gloo Code is billed from today.** The first month of your tier is charged at checkout, then it renews on the same date each month with the rest of your plan. Members, publishers, content and existing API keys are unaffected. #### If your team used Gloo Code before with a Client ID and Secret Earlier builds of Gloo Code authenticated with API client credentials and billed their usage to the organization's plan. Gloo Code now signs in through the browser and needs a Gloo Code plan plus a seat for each user, so a team that was set up the old way is re-enabled by an organization admin: 1. **Add Gloo Code.** On the [Billing page](https://studio.ai.gloo.com/billing), click **Add to Pro** on the *Add Gloo Code to your Pro plan* card. Pick a tier, choose **My team**, set the seat count to the number of people who will use Gloo Code (2 to 25), and complete checkout with your opening credits. Those credits fund the organization's direct API usage only; Gloo Code work is covered by the tier's monthly allowance and never draws on them. For a single user, pick the single-seat tier instead. 2. **Assign seats.** Back on the Billing page, open **Manage seats** on the Gloo Code product and assign a seat to each member. Anyone not yet in the organization must be [invited](/studio/manage-users) first. On a single-seat tier, assign the seat to yourself. 3. **Each member signs in again.** In the terminal, run `gloocode` and then `/login`; in Gloo Code Desktop, use the sign-in dialog. Choose this organization when the browser asks and approve access. Gloo Code no longer uses the Client ID and Secret. Leave those credentials in place if other applications still use them, or revoke them from the [API Keys](/studio/manage-api-credentials) page. A member who sees *Your account has no organizations eligible for Gloo Code* does not have a seat yet. See [Troubleshooting](/gloocode/installation#troubleshooting). ### Weekly arrears pay-as-you-go Organizations created before prepaid credits are billed weekly for what they used. They set their own weekly limit from the **Spend Limit** card, up to their organization's ceiling, and they keep doing so. New organizations are prepaid instead; see [Prepaid Credits](/studio/prepaid-credits). Admins on weekly billing see an **Upgrade to Gloo Code and prepaid credits** card on the Billing page. Upgrading: * charges usage since your last weekly bill to your card now * ends weekly billing and sets your new monthly billing date to today * moves API usage to prepaid credits, funded by an opening purchase of at least \$10, with the fixed \$100 weekly usage limit in place of the limit you set yourself. A ceiling support raised for you does not carry over. There is no self-serve path back to weekly billing afterwards. ### How the plans compare | Feature | Inference (prepaid) | Pro (legacy) | Enterprise | | - | - | - | - | | **Studio Playground Access** | Yes | Yes | Yes | | **API Access** | Yes | Yes | Yes | | **Spend Limits** | \$100/week usage limit | \$800/month standard ceiling | Managed via agreement | | **Data Engine Access** | No | Yes | Yes | | **Content Library** | No | Yes | Yes | | **Search Access** | No | Yes | Yes | | **Item Recommendations API** | No | Yes | Yes | | **Private Transcription** | No | No | Paid add-on | | **Billing Method** | Prepaid credits, card on file | Credit card (monthly) | Monthly invoicing | | **Analytics Access** | Usage & Cost | Usage & Cost | Extended (Ingestion) | | **Support** | Community | Priority | Shared Slack | ## Frequently asked questions **Can I have two Gloo Code tiers at once?** No. One tier per organization. Moving between tiers is a swap, not an addition. **Do I need credits if I only use Gloo Code?** No. A Gloo Code plan on its own is enough for Gloo Code. Add credits only if you also call the Gloo AI API directly. **Does my Gloo Code allowance roll over?** The monthly allowance does not. It is for the billing period and renews with your subscription. Top-up units do carry over until they are used. **What happens to my credits if I cancel a Gloo Code plan?** Nothing. They are separate. Credits stay on your organization. **What happens if I exceed my spend limit?** For prepaid Inference, API access pauses at the \$100 weekly usage limit and resumes when the window resets; the Wallet card shows when. For legacy Pro, access pauses at the monthly cap and resumes at the start of the next billing period. In both cases support can raise the ceiling. **Can I change my plan?** * **Add, change or remove a product:** self-serve from the [products page](https://studio.ai.gloo.com/products) or the Billing page, with the timing described in [Changing your plan](#changing-your-plan) * **From legacy Pro or weekly pay-as-you-go to the product model:** self-serve from the upgrade card on the Billing page; see [Legacy plans](#legacy-plans) * **To Enterprise:** [contact us](https://studio.ai.gloo.com/contact) * **Downgrade from a legacy plan:** contact support to discuss your options **Can I still get an invoice instead of paying by card?** Invoicing is an Enterprise arrangement. See [Upgrading to Enterprise](#upgrading-to-enterprise). **What payment methods are accepted?** We accept all major credit cards (Visa, MasterCard, American Express, Discover), Apple Pay, and U.S. bank accounts (ACH) for self-serve organizations. Enterprise accounts are invoiced monthly and can arrange payment terms with our billing team. **Can I pay with my bank account (ACH)?** Yes. When you add a U.S. bank account, you can either connect it instantly by signing in to your bank, or verify it manually with two small deposits. Instant connections are ready to use right away. Manual (micro-deposit) verification takes 1-2 business days, and until you confirm the deposit amounts the payment stays pending. **How do I stop paying?** Remove every product from the products page; each one ends at your next billing date and nothing further is charged. Credits you have already bought stay on your organization and keep working until they are used. Removing your last card from **Payment Methods** stops auto-refill and any future charges, though Studio asks you to settle any outstanding charges first. # Ingestion Analytics Source: https://docs.gloo.com/studio/ingestion-analytics Monitor Data Engine content ingestion and processing metrics. The Ingestion Analytics dashboard is only available on Pro and Enterprise plans with Data Engine access. [Upgrade your plan](/studio/billing#upgrading-to-enterprise) to access this feature. ## Overview The **Ingestion Analytics** dashboard helps you monitor your Data Engine content ingestion and processing. ## Accessing Ingestion Analytics 1. Log in to [Gloo AI Studio](https://studio.ai.gloo.com) 2. Select your organization from the organization switcher 3. Click **Ingestion Analytics** in the ANALYZE section of the main navigation ## Available Metrics The Ingestion Analytics dashboard tracks: **Content Totals:** * **Total Files:** Number of files uploaded to the Data Engine * **Total Chunks:** Number of content chunks processed **Content by Type:** * Total Articles * Total Books * Total Podcasts * Total Videos * Total Audio files * Total Webpages * Total RSS Events **Trends Over Time:** * Historical charts showing files and chunks processed over time * Filter by publisher to track individual content sources * View ingestion patterns and identify peak processing periods ## Filtering Use the **Filter by Publishers** dropdown to view ingestion metrics for specific publishers, and select date ranges using the **All Time** selector to analyze historical trends. ## Frequently Asked Questions **How often are ingestion metrics updated?** Ingestion Analytics metrics update as content is processed, typically within a few minutes of ingestion completing. **How far back can I view historical data?** Enterprise accounts have access to extended historical data for the duration of your account. # Generate API Keys Source: https://docs.gloo.com/studio/manage-api-credentials How to generate and manage API keys. Gloo AI Studio provides a secure and straightforward interface for managing the API keys used to authenticate with the Gloo AI API. This guide will walk you through creating keys and using them in your applications. ## Generating an API Key Follow these steps to create a new API key: 1. **Navigate to API Keys:** From the main Studio sidebar, click on "API Keys". 2. **Know the permissions:** Any member of your organization can create an API key. Only organization **Admins** can delete or revoke keys. If you need a key removed, ask an org admin. 3. **Create the key:** Click the "Create New Key" button. Enter a name for the key and click "Create". Studio will generate a new **API key**. **Treat your API key like a password!** It will only be shown once — copy it immediately and store it in a secure location. After creation, only the obfuscated value (e.g., `sk_...3456`) is displayed in Studio. 4. **Use the key in API calls:** Once you have your key, send it directly in the `Authorization` header: ```bash theme={null} export GLOO_API_KEY="sk_your_api_key_here" curl https://platform.ai.gloo.com/ai/v2/guarded/responses \ -H "Authorization: Bearer $GLOO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gloo-openai-gpt-5-mini", "input": "Hello!" }' ``` Your API key is sent directly as a bearer token. ## Key Lifecycle * **Revocation:** Revoked keys stop working within 60 seconds. Use this for compromised or unused keys. * **Deactivation:** Temporarily disable a key without deleting it. * **Activation:** Re-enable a previously deactivated key. **Existing Client ID / Client Secret credentials are being deprecated** and will be sunset by December 2026. They continue to work during the transition period. To migrate, create a new API key above and update your applications to use it directly (no token exchange needed). See the [Authentication Tutorial](/tutorials/authentication) for details. **Gloo Code** doesn't use the keys on this page. You sign in to it from the browser, and it manages its own credential for you — nothing to create, copy, or paste. See the [Gloo Code installation guide](/gloocode/installation). # Discovery Source: https://docs.gloo.com/studio/manage-components How to create, customize, and embed ready-to-use AI Components. Gloo AI Components are ready-to-use widgets that you can integrate into your own applications to leverage the power of the platform. The primary component available in Studio is **AI Discovery**. The **AI Discovery** component is an embeddable widget that enhances your content with intelligent search capabilities. It allows your users to search by topic or question, sourcing results from your ingested and enriched data. This guide will walk you through the process of building and deploying the Discovery widget. Please note that this is a beta version of the AI Discovery component which is still undergoing final testing before its official release. *** ## The Widget Builder The entire creation and customization process is handled in the five-step Widget Builder. AI Discovery Widget Builder ### Step 1: Access the Widget Builder Discovery is available on Pro and Enterprise plans. 1. Open the **Discovery** section in Studio. 2. Tap on an existing widget card to manage its configuration or select **"Create New Widget"** to start building a new one. ### Step 2: Customize Your Widget The builder will guide you through four configuration tabs: 1. **Layout:** The default grid layout is not configurable at this time. 2. **Data Sources:** Configure your data sources including: * Publisher to use for searching content * If you don't have an existing publisher, a prompt will guide you to create one via Organizations. * Typesense collection 3. **Styles:** Customize your widget by adding your logo. 4. **API Key:** * Select an API key from the dropdown. * Define the allowed domains where the widget can be embedded. If there are no API keys available, click **"Create an API key"** and you'll be redirected to the API Credentials page. See our guide on [Managing API Credentials](/studio/manage-api-credentials). Ensure you save your widget configuration in this step—it will then appear in your widget dashboard. ### Step 3: Embed Your Widget When you save a new widget configuration, the Widget Builder will automatically bring you to the fifth and final step, where it will generate an embed code. Simply copy this code and paste it into the HTML of your website where you want the widget to appear. *** ## Editing Widgets You can manage your existing widgets at any time. From the Discovery page, select the widget you wish to modify from the dashboard. You can then change any of the four steps (Layout, Data Sources, Styles, API Key) and save the updated configuration by clicking "Update Configuration" on the API Key step. # Manage Publishers Source: https://docs.gloo.com/studio/manage-publishers Create and configure Publishers within your organization. [Publishers](/getting-started/glossary#publisher) are the distinct content entities within your [organization](/getting-started/glossary#organization). When you upload data, it is associated with a Publisher. This allows you to manage rights and identity within your organization by author, division, or brand. You must create at least one Publisher before you can upload data with Data Engine. ## How Organizations and Publishers relate The structure supports both individual creators and large enterprises. ```mermaid theme={null} graph TD O1["Organization 1
Independent Podcast Content Creator (John Smith)"] O2["Organization 2
Large Book Publisher"] P1["Publisher
John Smith"] P2A["Publisher
John Doe"] P2B["Publisher
Jane Doe"] O1 --> P1 O2 --> P2A O2 --> P2B ``` * **The Independent Creator:** A podcast host or individual content creator uses a single Organization with a single Publisher. The Organization and Publisher serve the same person, providing a simple but secure container. * **The Large Enterprise:** A book publisher operates as the Organization, with individual authors or imprints acting as Publishers underneath it. *** To get started, click on your user account in the bottom-left of the screen and select **Manage Organizations** from the menu. Find your organization and click **View Publishers**. *** ## Creating a New Publisher Click **Create New Publisher** to open the creation form. It has four sections: 1. **Basic:** The `Name` of the publisher is required. 2. **Logos:** Upload logos that will be stored securely and used in products like Gloo AI Chat. 3. **Hierarchy:** Denotes a sub-publisher within a parent publisher. Leave blank if not applicable. 4. **DRM (Digital Rights Management):** * Checking **Aspen** allows your data to be used in Chat's Sources. * ~~Checking "Kalm" enables it to be used for Gloo AI's LLM training.~~ **The "Kalm" option is deprecated and no longer supported.** Click **Create Publisher** when done. *** ## Editing an Existing Publisher 1. Navigate to the **Manage Organizations** page. 2. In your organization's row, click **View Publishers**. 3. Find the publisher you want to edit and click the **Edit** icon on the far right of its row. You can update any of the fields set during creation. # Manage Users Source: https://docs.gloo.com/studio/manage-users Invite team members and assign roles within your organization. You can invite team members to join your organization and grant them permissions to perform specific tasks — uploading content, analyzing ingestion data, creating API credentials, and more. To get started, click on your user account in the bottom-left of the screen and select **Manage Organizations** from the menu. Find your organization and click **Manage Users**. *** ## User Roles When inviting a user, you assign them one of three [roles](/getting-started/glossary#user-roles): | Role | Permissions | | - | - | | **Admin** | Can invite users, create and manage publishers, upload content in Data Engine, and view all analytics (including ingestion analytics). | | **Editor** | Can upload content in Data Engine. | | **Viewer** | Can browse the Content Library and use Tools/components. | *** ## Inviting a New User 1. From the **Manage Users** screen, click **Invite User**. 2. Enter the user's email address. 3. Select the appropriate role (Admin, Editor, or Viewer). 4. Click **Send Invitation**. *** ## The Invitation Process 1. The invited user receives an email from `no-reply@gloo.ai`. 2. Clicking the link in the email takes them to the Sign In page. 3. After signing in, they'll see a prompt to accept the invitation. Once accepted, they'll have access to Studio based on the role you assigned. Invited users can follow the [Quickstart: Invited Users](/getting-started/quickstart-invited-users) guide to get oriented. # Chat Playground Source: https://docs.gloo.com/studio/playground-chat Test chat completions, compare models, and experiment with settings in the browser. ## Overview The Chat Playground lets you experiment with Gloo AI's chat capabilities directly in your browser. Test different models, adjust parameters, and see responses in real-time before implementing in your application. ## Availability & Access The Chat Playground is available on all plans (Pay-as-you-go, Pro, and Enterprise). Playground messages are routed through your organization's API credentials, so usage is billed at standard model rates and counts against your spend limit. See [Billing & Plans](/studio/billing) to get started. ## Getting Started Access the Chat Playground and start testing: 1. Log in to [Gloo AI Studio](https://studio.ai.gloo.com) 2. Click **Playground** in the main navigation 3. Select the **Chat** tab 4. Type your message in the text input 5. Press **Enter** or click **Send** to submit Use the quick-start action buttons (Write, Learn, Summarize, Compare, Life Stuff) to jump-start conversations with pre-configured prompts. ## Selecting Models The Chat Playground supports multiple AI models. Choose the model that best fits your testing needs: 1. Click the gear icon next to the chat interface 2. Click the **Model** dropdown at the top of the chat interface 3. Select from three categories: * **AI Core:** Gloo's auto-routing system that selects the best model for your request * **Specific Model:** such as Claude Opus 4.5 or GPT 5.2 * **Model Family:** for example Anthropic or OpenAI, letting Gloo pick the best model from that family Different models have different capabilities, speeds, and costs. See [Supported Models](/api-guides/supported-models) for detailed model comparisons. Model dropdown showing AI Core, Anthropic, and OpenAI categories ## Advanced Settings Customize model behavior using advanced settings: 1. Click the gear icon next to the chat interface 2. Open the **Advanced Settings** dropdown 3. Modify one or more settings **Religious Denomination** Select a religious tradition to align responses with specific theological perspectives. Choose from denominations like Catholic, Protestant, Orthodox, and others. **Temperature** Control response creativity and randomness: * **Precise (0.0):** Deterministic, focused responses * **Balanced (0.5-0.7):** Mix of consistency and creativity * **Creative (1.0):** More varied, imaginative responses **Max Tokens** Toggle to limit the maximum length of model responses. When enabled, set a specific token count to control response size. Start with default settings for general testing. Adjust temperature when you need more creative responses (higher) or more consistent outputs (lower). Use max tokens to control costs in production. Advanced settings panel showing denomination, temperature, and max tokens controls ## Comparing Models Side-by-Side Test the same prompt across multiple models simultaneously: 1. Click the **Compare Models** button in the top right 2. Select models for each comparison pane 3. **Sync Chats** is enabled by default which sends the same message to all models at once 4. Type your message and see how different models respond **When to use model comparison:** * Evaluating model quality for your use case * Testing response consistency across models * Choosing the right model before API implementation * Understanding pricing vs. performance trade-offs Split-screen comparison showing responses from two different models ## Understanding Temporary Sessions Chat Playground conversations are temporary and not saved. When you close or refresh the page, your conversation history is lost. To preserve important content: * Copy valuable responses to a text editor or note-taking app * Take screenshots of useful examples * Document successful prompts for later API implementation ## Transitioning to API Once you've tested your prompts in the Playground, transition to the API: **Parameter Mapping:** | Playground Setting | API Parameter | Example Value | | - | - | - | | Model dropdown | `model` | `"ai-core"`, `"claude-sonnet-4"`, `"gpt-4"` | | Temperature | `temperature` | `0.7` | | Max Tokens | `max_tokens` | `1024` | | Denomination | `tradition` | `"mainline"` | Your Playground experiments map directly to API requests. Use your successful prompts and settings to build production implementations. Implement chat completions in your application Generate credentials for API access View all available models and capabilities Monitor API requests and costs ## Frequently Asked Questions **Why aren't my conversations saved?** The Playground is designed for quick testing and experimentation, not persistent storage. This keeps the interface fast and prevents sensitive test data from being stored. For saved conversations, implement the API in your application with your own storage. **Do Playground messages count against my rate limits and spend?** Yes. Playground messages are routed through your organization's API credentials, so they count toward your spend limit and are subject to the same rate limits as direct API calls. Monitor your consumption in the [API Usage](/studio/api-usage) dashboard. **Do I need API credentials to use the Playground?** Yes. The Chat Playground routes requests through your organization's API credentials, so at least one active API key must exist before you can send messages. See [Generate API Keys](/studio/manage-api-credentials) to create them. **How do I choose the right model?** Start with **AI Core** for automatic model selection. If you need specific capabilities, use model comparison to test options side-by-side. Consider factors like response quality, speed, and cost for your use case. **What's the difference between Chat and Search Playground?** Chat Playground tests conversational AI interactions. Search Playground (Pro and Enterprise) tests semantic search across your Data Engine content library. Use Chat for general AI testing, Search for content discovery features. **Can I access the Playground on mobile?** Yes. The Chat Playground works on mobile browsers, though the experience is optimized for desktop screens. Mobile access is useful for quick tests when away from your computer. # Search Playground Source: https://docs.gloo.com/studio/playground-search Test semantic search across your Data Engine content library. ## Overview The Search Playground lets you test semantic search across your organization's content library. Experiment with queries, understand how semantic ranking works, and preview search results before implementing the Search API. ## Availability & Prerequisites The Search Playground is available to **Pro and Enterprise plan customers**. [Upgrade your plan](/studio/billing#upgrading-to-enterprise) to access this feature. **Requirements:** * Active Pro or Enterprise subscription * Data Engine access * Content uploaded to your content library You must upload content before using the Search Playground. See [Manage Content](/studio/manage-content) to add content to your library. ## Getting Started Access and use the Search Playground: 1. Log in to [Gloo AI Studio](https://studio.ai.gloo.com) 2. Click **Playground** in the main navigation 3. Select the **Search** tab 4. Choose a content library from the **Publisher** dropdown 5. Enter a search query in the text input 6. Press **Enter** or click **Search** to see results Only Publishers where you have access will appear in the dropdown. If no Publishers are visible, verify that content has been uploaded to your organization. Publisher dropdown showing available content libraries ## Understanding Search Results Search results display as cards, each containing: **Title:** The content item's headline or name **Part Number:** Sequential identifier for multi-part content (e.g., chapters, episodes) **Source:** The publisher or origin of the content **Snippet:** A preview of relevant text from the content Results are ranked by semantic relevance, not keyword matching. The most contextually relevant content appears first, even if it doesn't contain your exact search terms. Semantic search understands meaning and context. A search for "dealing with anxiety" might return content about "managing stress" or "finding peace" because they're semantically related. Search results showing cards with titles, sources, and snippets ## Exploring Key Insights Some search results include expandable Key Insights sections: 1. Look for the **Key Insights** section below search results 2. Click to expand and view AI-generated summaries 3. Review insights to understand common themes across results Key Insights are automatically generated when available. Not all searches will include insights depending on result quality and content type. Expanded Key Insights panel showing AI-generated summaries ## Refining Your Search Get better results with these best practices: **Use Natural Language** Write queries as questions or phrases, not just keywords: * Good: "How can I improve my prayer life?" * Also Good: "tips for consistent prayer" * Less Effective: "prayer" **Be Specific** Add context to narrow results: * "marriage advice for newlyweds" instead of "marriage" * "understanding the book of Romans" instead of "Romans" **Test Variations** Try different phrasings to see how semantic search interprets your query: * "coping with loss" vs. "grief support" vs. "dealing with death" **Reset and Retry** Click **Reset** to clear your search and start fresh with a new query. ## Transitioning to API Once you've tested search queries in the Playground, implement them via the Search API: **Parameter Mapping:** | Playground Setting | API Parameter | Example Value | | - | - | - | | Publisher selection | `tenant` and `collection` | Organization and publisher IDs | | Search query | `search` | `"How to pray effectively"` | | Result count | `limit` | `10` (default) | Your Playground experiments translate directly to API requests. Use successful queries to guide your production search implementation. Implement semantic search in your application Upload and organize content in Studio Generate credentials for API access Learn about content ingestion and management ## Understanding Semantic Search Semantic search differs from traditional keyword search by understanding meaning and context: **Keyword Search:** * Matches exact words in your query * Ranks by word frequency * Misses synonyms and related concepts **Semantic Search:** * Understands intent and meaning * Ranks by contextual relevance * Finds related concepts even without exact matches **Example Comparisons:** | Query | Keyword Search Finds | Semantic Search Finds | | - | - | - | | "finding purpose" | Content with exact phrase "finding purpose" | Also: "discovering your calling", "life meaning", "God's plan for you" | | "dealing with fear" | Only "fear" mentions | Also: "overcoming anxiety", "courage", "trusting God in uncertainty" | | "healthy relationships" | Exact match only | Also: "building strong marriages", "biblical friendship", "communication skills" | Think about what users mean, not just what they say. Semantic search finds content that answers the underlying question, even if the wording differs. ## Frequently Asked Questions **Why can't I access the Search Playground?** Search Playground requires a Pro or Enterprise plan with Data Engine access. Pay-as-you-go plans do not include search capabilities. [Upgrade your plan](/studio/billing#upgrading-to-enterprise) to access this feature. **Why don't I see any Publishers in the dropdown?** Publishers only appear after content has been uploaded. Upload content via [Manage Content](/studio/manage-content) or the Content API first, then return to the Search Playground. **How many results does the Playground return?** The Playground returns up to 10 results by default. The Search API lets you customize the result limit using the `limit` parameter (up to 100 results per request). **Why are my results irrelevant?** Try these troubleshooting steps: * Make your query more specific with additional context * Verify your content library contains relevant material * Test different query phrasings * Ensure content has been fully processed after upload **What's the difference between semantic and keyword search?** Semantic search understands meaning and context, finding conceptually related content even without exact word matches. Keyword search only matches specific words. Semantic search provides better results for natural language queries. **Can I filter results by content type?** The Playground shows all results from the selected Publisher. Use the Search API to implement custom filtering by content type, date, author, or other metadata in your application. **How do I use search results for RAG (Retrieval Augmented Generation)?** Search results include text snippets perfect for RAG implementations. The snippets preview the exact content you'll receive via the API, which you can pass to the Completions API as context for generating answers. # Prepaid Credits Source: https://docs.gloo.com/studio/prepaid-credits Buy credits up front, watch your balance, and keep it topped up automatically. ## Overview Pay-as-you-go API access is funded with **prepaid credits**. You buy credit before you use it, every request draws down the balance at the model's published rate, and there is no invoice at the end of the week. You have already paid. Prepaid credits pay for API usage only. [Gloo Code](/studio/billing#the-one-rule-that-surprises-people) is billed separately, through its own monthly allowance and top-ups, and does not draw from these credits. Enterprise organizations are invoiced under their agreement and do not use prepaid credits. Buy \$10 to \$100 at a time from the Billing page. Credit is available within seconds of payment. Optionally let Gloo top you up automatically before you run dry, up to \$100 of automatic charges per week. Credits belong to your organization, not to a product. Adding or removing Gloo Code, or changing tiers, leaves your balance untouched. ## Adding credits You need to fund your credits at least once before you can create an API key. Until then, the [API Keys](/studio/manage-api-credentials) page sends you here to add credits first. Signing in to Gloo Code from the terminal is separate: it needs a Gloo Code plan, not credits. 1. Go to the [Studio Billing page](https://studio.ai.gloo.com/billing) 2. On the **Overview** tab, click **Add credits** 3. Choose \$10, \$25, \$50 or \$100, or enter a custom amount between \$10 and \$100 4. Complete payment on the checkout page New organizations choose their first credit amount during onboarding. The card you pay with is saved so it can be used for auto-refill later. You can remove it at any time from the **Payment Methods** tab. **When the credit appears.** Fulfilment happens when your payment confirms, not when your browser returns. In practice credit lands within a few seconds and the page updates on its own. If it takes longer than about 90 seconds the page will say so and offer **Continue anyway**. Your payment went through, and the credit will appear without you doing anything. Gloo does not email a purchase receipt. Your receipt is the invoice from our payment processor, and every purchase is also listed in **Billing History** in the Studio with the amount and a link to the invoice. ## Auto-refill Auto-refill charges your saved card automatically when your balance drops below **\$5**, or when a request would need more credit than you have left, so requests do not start failing mid-workload. The charge is your chosen amount, applied to the card on file. **Turning it on:** 1. On the Billing page, find the **Auto-refill** switch on the Wallet card 2. Set the amount to add each time (\$10 to \$100) 3. Flip the switch on and confirm in the dialog Nothing is saved until you confirm. Closing the dialog leaves auto-refill off. Turning it off later takes effect immediately. **How it decides to charge.** Auto-refill is not on a fixed schedule. It checks your balance on every request and charges as soon as either condition above is met. **The weekly charge cap.** Auto-refill will charge at most **\$100 per rolling 7 days**. This is a cap on money charged to your card, not on usage. When you reach it: * Auto-refill pauses and the Wallet card shows **Paused, weekly cap reached** * You get one email telling you auto-refill is paused * **Manual top-ups still work.** The cap applies only to automatic charges * Headroom returns gradually as older charges age past 7 days If the amount you configured would not fit in the remaining headroom, Gloo charges the part that fits rather than skipping the top-up entirely. This charge cap is separate from the **\$100 weekly usage limit** covered below, which pauses your API access rather than your card. Adding credits manually is always allowed, even at the usage limit. The purchase raises your balance, but access still resumes only when the window resets. ## When you run out There are two different limits, they produce two different errors, and the fix for one does nothing for the other. | | Out of credits | Weekly usage limit | | - | - | - | | **Error** | `402 INSUFFICIENT_CREDIT` | `429 SPENDING_LIMIT_EXCEEDED` | | **What happened** | Your balance reached zero | You used \$100 of API usage this week | | **Can you still have a balance?** | No | **Yes**. This fires even with credit remaining | | **What fixes it** | Add credits; access resumes immediately | Wait for your weekly window to reset | | **Can you add credits now?** | Yes | Yes. Your balance goes up, but access still resumes only when the window resets | The weekly usage limit is not the same as the auto-refill charge cap. One limits how much you can **spend**; the other limits how much Gloo will **charge your card automatically**. It is possible to be stopped by one while the other has room. **When the weekly limit resets.** Your weekly window is anchored to when your account was first funded, not to Monday and not to UTC midnight. Usage drops to zero all at once at the boundary rather than rolling off gradually, so access returns in one step. The Wallet card and the weekly limit banner both show the date and time your window resets. Only usage paid from credit counts toward the weekly limit. Work done inside Gloo Code draws on your Gloo Code allowance and does not count. If your organization needs a higher weekly limit, contact support. ## If your card is declined If an automatic top-up is declined: * **No credit is added, and your existing balance is untouched.** Nothing is lost. * You get one email, *"Your card was declined"*, with a link to update your payment method. * Gloo waits before retrying rather than re-attempting immediately, so a declined card does not generate repeated charges. * Your existing credit keeps working until it runs out, at which point requests return `402`. Update your card on the **Payment Methods** tab and auto-refill resumes on the next top-up. Decline emails are limited to one per organization per day, and are sent to the email address on your payment profile. ## If you also use Gloo Code A Gloo Code plan includes its own usage allowance, and **that allowance and your credits are separate pools that cannot substitute for each other**. Gloo Code work draws on the Gloo Code allowance; direct API calls draw on your credits. See [Billing & Plans](/studio/billing#the-one-rule-that-surprises-people). ## Promotional credits If your organization has promotional credit, it is shown alongside your paid balance and is **always spent first**. Only once promotional credit reaches zero does usage begin drawing on credit you paid for. ## Who can see and change this Billing surfaces are restricted to members who can manage billing. Everyone else sees *"Billing is managed by your organization admins"* in place of the Wallet card, payment methods and plan changes. ## Frequently asked questions **Do my credits expire?** No expiry is applied to purchased credit today. Credit is consumed by usage and survives plan changes. If that changes, it will be announced and reflected in the [Terms of Service](https://gloo.com/legal/ai-studio-supplemental-terms-of-service) before it takes effect. **Can I get a refund on unused credits?** Credit purchases are final. Buy in the amount you expect to use. You can always add more. If something went wrong with a purchase, contact support. **Why was my top-up refused?** A manual top-up is only refused if the amount is outside the \$10 to \$100 range. Being at the weekly usage limit does not stop it: the purchase goes through and raises your balance, though your access will not resume until the window resets. Auto-refill is different: it pauses once it reaches its own weekly charge cap, but you can still add credits manually while it is paused. **Why did auto-refill not fire before I ran out?** Auto-refill charges when your balance drops below \$5 or a request needs more credit than you have left, and the charge takes a few seconds to land. A burst of large requests arriving in that window can briefly outrun it. If that happens regularly, raise your auto-refill amount so each top-up lasts longer. # Authentication with Gloo AI Source: https://docs.gloo.com/tutorials/authentication Learn how to authenticate with the Gloo AI API using an API key. This tutorial covers how to authenticate with the Gloo AI API. Authentication is required for all API endpoints and is done with a single API key passed in the `Authorization` header. ## Overview The Gloo AI API uses API key authentication. This process involves: 1. **Get an API Key** - Obtain your API key from the Gloo AI Studio 2. **Set Your API Key** - Store it securely in an environment variable 3. **Use Your API Key in API Calls** - Include it in the `Authorization` header of every request ## Prerequisites Before starting, ensure you have: * A Gloo AI Studio account * Your API key from the [API Credentials page](/studio/manage-api-credentials) ## Step 1: Environment Setup First, set up your environment variable to securely store your API key: ### Environment Variables Create a `.env` file in your project root: ```bash theme={null} GLOO_API_KEY=your_api_key_here ``` For Go and Java, you can also export it directly: ```bash theme={null} export GLOO_API_KEY="your_api_key_here" ``` ## Step 2: Using Your API Key in API Calls Include your API key in the `Authorization` header of every API request: ```http theme={null} Authorization: Bearer YOUR_API_KEY ``` ### Example API Request The examples below call Completions V2 (`/ai/v2/guarded/chat/completions`). The same API key works on the [Responses API](/api-guides/responses) (`/ai/v2/guarded/responses`), Gloo's recommended endpoint for new integrations — only the URL and request shape differ. ```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") def make_authenticated_request(endpoint, payload=None): """Make an authenticated API request.""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } if payload: response = requests.post(endpoint, headers=headers, json=payload) else: response = requests.get(endpoint, headers=headers) response.raise_for_status() return response.json() # Example usage result = make_authenticated_request( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { "auto_routing": True, "messages": [{"role": "user", "content": "Hello!"}] } ) ``` ```javascript JavaScript theme={null} import axios from 'axios'; require('dotenv').config(); const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; async function makeAuthenticatedRequest(endpoint, payload = null) { const config = { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }; if (payload) { const response = await axios.post(endpoint, payload, config); return response.data; } else { const response = await axios.get(endpoint, config); return response.data; } } // Example usage const result = await makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { auto_routing: true, messages: [{ role: "user", content: "Hello!" }] } ); ``` ```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'; async function makeAuthenticatedRequest(endpoint: string, payload?: any): Promise { const config = { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }; if (payload) { const response = await axios.post(endpoint, payload, config); return response.data; } else { const response = await axios.get(endpoint, config); return response.data; } } // Example usage const result = await makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { auto_routing: true, messages: [{ role: "user", content: "Hello!" }] } ); ``` ```php PHP theme={null} load(); $API_KEY = getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY'; function makeAuthenticatedRequest($endpoint, $payload = null) { global $API_KEY; $headers = [ 'Authorization: Bearer ' . $API_KEY, 'Content-Type: application/json' ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); if ($payload) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); } $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); return json_decode($result, true); } // Example usage $result = makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", [ 'auto_routing' => true, 'messages' => [['role' => 'user', 'content' => 'Hello!']] ] ); ``` ```go Go theme={null} package main import ( "bytes" "fmt" "io/ioutil" "net/http" "os" ) func getEnv(key, fallback string) string { if value, ok := os.LookupEnv(key); ok { return value } return fallback } var apiKey = getEnv("GLOO_API_KEY", "YOUR_API_KEY") func makeAuthenticatedRequest(endpoint string, payload []byte) ([]byte, error) { var req *http.Request var err error if payload != nil { req, err = http.NewRequest("POST", endpoint, bytes.NewBuffer(payload)) } else { req, err = http.NewRequest("GET", endpoint, nil) } 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() return ioutil.ReadAll(resp.Body) } // Example usage func main() { payload := []byte(`{ "auto_routing": true, "messages": [{"role": "user", "content": "Hello!"}] }`) result, err := makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", payload, ) if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Println(string(result)) } ``` ```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; public class AuthManager { private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY"); private static final HttpClient httpClient = HttpClient.newHttpClient(); public static String makeAuthenticatedRequest(String endpoint, String payload) throws IOException, InterruptedException { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(endpoint)) .header("Authorization", "Bearer " + API_KEY) .header("Content-Type", "application/json"); if (payload != null) { requestBuilder.POST(HttpRequest.BodyPublishers.ofString(payload)); } else { requestBuilder.GET(); } HttpRequest request = requestBuilder.build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IOException("API call failed: " + response.body()); } return response.body(); } // Example usage public static void main(String[] args) { String payload = """ { "auto_routing": true, "messages": [{"role": "user", "content": "Hello!"}] } """; try { String result = makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", payload ); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } } ``` ## Security Best Practices ### 1. Environment Variables * Never hardcode your API key in source code * Use environment variables or secure credential storage * Add `.env` files to your `.gitignore` ### 2. Network Security * Always use HTTPS for API calls * Implement proper error handling * Use secure HTTP client configurations ### 3. Error Handling * Handle authentication failures gracefully * Implement retry logic for transient failures * Log authentication events securely ## Common Issues and Solutions ### Issue: 401 Unauthorized **Cause**: Invalid or missing API key **Solution**: Verify your `GLOO_API_KEY` is set correctly and has not been revoked ### Issue: 403 Forbidden **Cause**: Insufficient permissions **Solution**: Check your API access levels in the Studio ## Testing Your Implementation Create a simple test to verify your authentication setup: ```python Python theme={null} def test_authentication(): """Test authentication flow.""" try: result = make_authenticated_request( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { "auto_routing": True, "messages": [{"role": "user", "content": "Hello!"}] } ) print("✓ Authentication successful") print(result) return True except Exception as e: print(f"✗ Authentication failed: {e}") return False if __name__ == "__main__": test_authentication() ``` ```javascript JavaScript theme={null} async function testAuthentication() { try { const result = await makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { auto_routing: true, messages: [{ role: "user", content: "Hello!" }] } ); console.log("✓ Authentication successful"); console.log(result); return true; } catch (error) { console.error("✗ Authentication failed:", error.message); return false; } } testAuthentication(); ``` ```typescript TypeScript theme={null} async function testAuthentication(): Promise { try { const result = await makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", { auto_routing: true, messages: [{ role: "user", content: "Hello!" }] } ); console.log("✓ Authentication successful"); console.log(result); return true; } catch (error: any) { console.error("✗ Authentication failed:", error.message); return false; } } testAuthentication(); ``` ```php PHP theme={null} function testAuthentication() { try { $result = makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", [ 'auto_routing' => true, 'messages' => [['role' => 'user', 'content' => 'Hello!']] ] ); echo "✓ Authentication successful\n"; print_r($result); return true; } catch (Exception $e) { echo "✗ Authentication failed: " . $e->getMessage() . "\n"; return false; } } testAuthentication(); ``` ```go Go theme={null} func testAuthentication() bool { fmt.Println("Testing authentication...") payload := []byte(`{ "auto_routing": true, "messages": [{"role": "user", "content": "Hello!"}] }`) result, err := makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", payload, ) if err != nil { fmt.Printf("✗ Authentication failed: %v\n", err) return false } fmt.Println("✓ Authentication successful") fmt.Println(string(result)) return true } func main() { testAuthentication() } ``` ```java Java theme={null} public static boolean testAuthentication() { String payload = """ { "auto_routing": true, "messages": [{"role": "user", "content": "Hello!"}] } """; try { String result = makeAuthenticatedRequest( "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions", payload ); System.out.println("✓ Authentication successful"); System.out.println(result); return true; } catch (Exception e) { System.err.println("✗ Authentication failed: " + e.getMessage()); return false; } } public static void main(String[] args) { testAuthentication(); } ``` ## 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 authentication set up, you can use it in other Gloo AI tutorials: 1. **[Responses API](/api-guides/responses)** - Build on Gloo's recommended API surface (text, vision, image generation, tool use) 2. **[Building Interactive Chat](/tutorials/chat)** - Create conversational experiences 3. **[Using the Completions API](/tutorials/completions-v2)** - Generate text completions (includes intelligent routing, model\_family, and grounded completions) 4. **[API Reference](/api-reference/general/overview)** - Explore all available endpoints *** ## Deprecated: OAuth2 Client Credentials The OAuth2 client credentials flow described below is deprecated. New integrations should use the API key authentication shown above. This section is preserved only for existing integrations that have not yet migrated. If you are maintaining an existing integration that still uses OAuth2 client credentials, you can exchange your Client ID and Client Secret for a temporary access token using the token endpoint. ### Environment Variables ```bash theme={null} GLOO_CLIENT_ID=your_actual_client_id_here GLOO_CLIENT_SECRET=your_actual_client_secret_here ``` ### Token Exchange ```python Python theme={null} import requests import time import os from dotenv import load_dotenv load_dotenv() CLIENT_ID = os.getenv("GLOO_CLIENT_ID", "YOUR_CLIENT_ID") CLIENT_SECRET = os.getenv("GLOO_CLIENT_SECRET", "YOUR_CLIENT_SECRET") TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token" def get_access_token(): """Retrieve a new access token from the Gloo AI API.""" headers = {"Content-Type": "application/x-www-form-urlencoded"} data = {"grant_type": "client_credentials", "scope": "api/access"} response = requests.post(TOKEN_URL, headers=headers, data=data, auth=(CLIENT_ID, CLIENT_SECRET)) response.raise_for_status() token_data = response.json() token_data['expires_at'] = int(time.time()) + token_data['expires_in'] return token_data # Example usage token_info = get_access_token() print(f"Access token: {token_info['access_token']}") print(f"Expires in: {token_info['expires_in']} seconds") ``` ```javascript JavaScript theme={null} const axios = require('axios'); require('dotenv').config(); const CLIENT_ID = process.env.GLOO_CLIENT_ID || "YOUR_CLIENT_ID"; const CLIENT_SECRET = process.env.GLOO_CLIENT_SECRET || "YOUR_CLIENT_SECRET"; const TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token"; async function getAccessToken() { const body = 'grant_type=client_credentials&scope=api/access'; const response = await axios.post(TOKEN_URL, body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, auth: { username: CLIENT_ID, password: CLIENT_SECRET } }); const tokenData = response.data; tokenData.expires_at = Math.floor(Date.now() / 1000) + tokenData.expires_in; return tokenData; } // Example usage getAccessToken().then(tokenInfo => { console.log(`Access token: ${tokenInfo.access_token}`); console.log(`Expires in: ${tokenInfo.expires_in} seconds`); }); ``` ```typescript TypeScript theme={null} import axios from 'axios'; import * as dotenv from 'dotenv'; dotenv.config(); const CLIENT_ID = process.env.GLOO_CLIENT_ID || "YOUR_CLIENT_ID"; const CLIENT_SECRET = process.env.GLOO_CLIENT_SECRET || "YOUR_CLIENT_SECRET"; const TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token"; interface TokenInfo { access_token: string; expires_in: number; expires_at: number; token_type: string; } async function getAccessToken(): Promise { const body = 'grant_type=client_credentials&scope=api/access'; const response = await axios.post(TOKEN_URL, body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, auth: { username: CLIENT_ID, password: CLIENT_SECRET } }); const tokenData = response.data; (tokenData as any).expires_at = Math.floor(Date.now() / 1000) + tokenData.expires_in; return tokenData; } // Example usage getAccessToken().then(tokenInfo => { console.log(`Access token: ${tokenInfo.access_token}`); console.log(`Expires in: ${tokenInfo.expires_in} seconds`); }); ``` ```php PHP theme={null} load(); $CLIENT_ID = getenv('GLOO_CLIENT_ID') ?: 'YOUR_CLIENT_ID'; $CLIENT_SECRET = getenv('GLOO_CLIENT_SECRET') ?: 'YOUR_CLIENT_SECRET'; $TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token'; function getAccessToken($client_id, $client_secret, $token_url) { $post_data = 'grant_type=client_credentials&scope=api/access'; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $token_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); curl_setopt($ch, CURLOPT_USERPWD, $client_id . ':' . $client_secret); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']); $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); $token_data = json_decode($result, true); $token_data['expires_at'] = time() + $token_data['expires_in']; return $token_data; } // Example usage $token_info = getAccessToken($CLIENT_ID, $CLIENT_SECRET, $TOKEN_URL); echo "Access token: " . $token_info['access_token'] . "\n"; echo "Expires in: " . $token_info['expires_in'] . " seconds\n"; ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "os" "strings" "time" ) var ( clientID = getEnv("GLOO_CLIENT_ID", "YOUR_CLIENT_ID") clientSecret = getEnv("GLOO_CLIENT_SECRET", "YOUR_CLIENT_SECRET") tokenURL = "https://platform.ai.gloo.com/oauth2/token" ) type TokenInfo struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` ExpiresAt int64 `json:"expires_at"` TokenType string `json:"token_type"` } func getAccessToken() (*TokenInfo, error) { data := strings.NewReader("grant_type=client_credentials&scope=api/access") req, err := http.NewRequest("POST", tokenURL, data) if err != nil { return nil, err } req.SetBasicAuth(clientID, clientSecret) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := ioutil.ReadAll(resp.Body) return nil, fmt.Errorf("failed to get token: %s - %s", resp.Status, string(bodyBytes)) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var token TokenInfo if err := json.Unmarshal(body, &token); err != nil { return nil, err } token.ExpiresAt = time.Now().Unix() + int64(token.ExpiresIn) return &token, nil } func getEnv(key, fallback string) string { if value, ok := os.LookupEnv(key); ok { return value } return fallback } // Example usage func main() { tokenInfo, err := getAccessToken() if err != nil { fmt.Printf("Error: %v\n", err) return } fmt.Printf("Access token: %s\n", tokenInfo.AccessToken) fmt.Printf("Expires in: %d seconds\n", tokenInfo.ExpiresIn) } ``` ```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.Base64; public class AuthManager { private static final String CLIENT_ID = System.getenv().getOrDefault("GLOO_CLIENT_ID", "YOUR_CLIENT_ID"); private static final String CLIENT_SECRET = System.getenv().getOrDefault("GLOO_CLIENT_SECRET", "YOUR_CLIENT_SECRET"); private static final String TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token"; private static final HttpClient httpClient = HttpClient.newHttpClient(); private static final Gson gson = new Gson(); public static class TokenInfo { public String access_token; public int expires_in; public long expires_at; public String token_type; } public static TokenInfo getAccessToken() throws IOException, InterruptedException { String auth = CLIENT_ID + ":" + CLIENT_SECRET; String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes()); String requestBody = "grant_type=client_credentials&scope=api/access"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(TOKEN_URL)) .header("Content-Type", "application/x-www-form-urlencoded") .header("Authorization", "Basic " + encodedAuth) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IOException("Failed to get access token: " + response.body()); } TokenInfo token = gson.fromJson(response.body(), TokenInfo.class); token.expires_at = Instant.now().getEpochSecond() + token.expires_in; return token; } // Example usage public static void main(String[] args) { try { TokenInfo tokenInfo = getAccessToken(); System.out.println("Access token: " + tokenInfo.access_token); System.out.println("Expires in: " + tokenInfo.expires_in + " seconds"); } catch (Exception e) { e.printStackTrace(); } } } ``` ### Token Management Access tokens obtained through the OAuth2 flow expire after one hour. Implement token management to handle expiration in legacy integrations: ```python Python theme={null} # Global token storage access_token_info = {} def is_token_expired(token_info): """Check if the token is expired or close to expiring.""" if not token_info or 'expires_at' not in token_info: return True return time.time() > (token_info['expires_at'] - 60) def ensure_valid_token(): """Ensure we have a valid access token.""" global access_token_info if is_token_expired(access_token_info): print("Getting new access token...") access_token_info = get_access_token() return access_token_info['access_token'] # Usage in API calls def make_api_call(): token = ensure_valid_token() headers = {"Authorization": f"Bearer {token}"} # Make your API call here ``` ```javascript JavaScript theme={null} // Global token storage let tokenInfo = {}; function isTokenExpired(token) { if (!token || !token.expires_at) return true; return (Date.now() / 1000) > (token.expires_at - 60); } async function ensureValidToken() { if (isTokenExpired(tokenInfo)) { console.log("Getting new access token..."); tokenInfo = await getAccessToken(); } return tokenInfo.access_token; } // Usage in API calls async function makeApiCall() { const token = await ensureValidToken(); const headers = { 'Authorization': `Bearer ${token}` }; // Make your API call here } ``` ```typescript TypeScript theme={null} // Global token storage let tokenInfo: TokenInfo | null = null; function isTokenExpired(token: TokenInfo | null): boolean { if (!token || !(token as any).expires_at) return true; return (Date.now() / 1000) > ((token as any).expires_at - 60); } async function ensureValidToken(): Promise { if (isTokenExpired(tokenInfo)) { console.log("Getting new access token..."); tokenInfo = await getAccessToken(); } return tokenInfo!.access_token; } // Usage in API calls async function makeApiCall(): Promise { const token = await ensureValidToken(); const headers = { 'Authorization': `Bearer ${token}` }; // Make your API call here } ``` ```php PHP theme={null} // Global token storage $token_info = []; function isTokenExpired($token) { if (empty($token) || !isset($token['expires_at'])) { return true; } return time() > ($token['expires_at'] - 60); } function ensureValidToken() { global $token_info, $CLIENT_ID, $CLIENT_SECRET, $TOKEN_URL; if (isTokenExpired($token_info)) { echo "Getting new access token...\n"; $token_info = getAccessToken($CLIENT_ID, $CLIENT_SECRET, $TOKEN_URL); } return $token_info['access_token']; } // Usage in API calls function makeApiCall() { $token = ensureValidToken(); $headers = ['Authorization: Bearer ' . $token]; // Make your API call here } ``` ```go Go theme={null} var tokenInfo *TokenInfo func isTokenExpired(token *TokenInfo) bool { if token == nil || token.ExpiresAt == 0 { return true } return time.Now().Unix() > (token.ExpiresAt - 60) } func ensureValidToken() (string, error) { if isTokenExpired(tokenInfo) { fmt.Println("Getting new access token...") var err error tokenInfo, err = getAccessToken() if err != nil { return "", err } } return tokenInfo.AccessToken, nil } // Usage in API calls func makeApiCall() error { token, err := ensureValidToken() if err != nil { return err } // Use token in Authorization header // Make your API call here return nil } ``` ```java Java theme={null} private static TokenInfo tokenInfo; public static boolean isTokenExpired(TokenInfo token) { if (token == null || token.expires_at == 0) { return true; } return Instant.now().getEpochSecond() > (token.expires_at - 60); } public static String ensureValidToken() throws IOException, InterruptedException { if (isTokenExpired(tokenInfo)) { System.out.println("Getting new access token..."); tokenInfo = getAccessToken(); } return tokenInfo.access_token; } // Usage in API calls public static void makeApiCall() throws IOException, InterruptedException { String token = ensureValidToken(); // Use token in Authorization header // Make your API call here } ``` The authentication patterns shown here work across all Gloo AI API endpoints, providing a secure foundation for your applications. # Building Interactive Chat (Deprecated) Source: https://docs.gloo.com/tutorials/chat Learn how to create meaningful conversations using Gloo AI. **The Chat API is deprecated.** It will be replaced with a new tutorial using Completions V2 API. This tutorial demonstrates how to build interactive chat experiences using Gloo AI APIs. You'll learn to create chat sessions, send messages, continue conversations, and retrieve chat history. The Chat and Message APIs allow for: * **Persistent conversations** with session management * **Contextual responses** that build on previous interactions * **Source integration** for grounded, authoritative answers * **Streaming responses** for real-time conversation flow *** ## 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 Chat and Message APIs require an API key for authentication. If you haven't set up authentication yet, follow the [Authentication Tutorial](/tutorials/authentication). *** ## Step 1: Creating a Chat Session The Message API can either create a new chat session or continue an existing one. When you don't provide a `chat_id`, it automatically creates a new session. ```python Python theme={null} MESSAGE_API_URL = "https://platform.ai.gloo.com/ai/v1/message" def create_chat_session(initial_message): """Create a new chat session with an initial message.""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } #- Request body for new chat (no chat_id provided) payload = { "query": initial_message, "character_limit": 1000, "sources_limit": 5, "stream": False, "publishers": [], "enable_suggestions": 1 #- Enable suggested follow-up questions } response = requests.post(MESSAGE_API_URL, headers=headers, json=payload) response.raise_for_status() return response.json() #- Example usage initial_question = "How can I find meaning and purpose when facing life's greatest challenges?" chat_response = create_chat_session(initial_question) print("Chat created successfully!") print(f"Chat ID: {chat_response['chat_id']}") print(f"Message ID: {chat_response['message_id']}") print(f"Response: {chat_response['message']}") #- Show suggested follow-up questions if chat_response.get('suggestions'): print("\nSuggested follow-up questions:") for i, suggestion in enumerate(chat_response['suggestions'], 1): print(f"{i}. {suggestion}") ``` ```javascript JavaScript theme={null} const MESSAGE_API_URL = 'https://platform.ai.gloo.com/ai/v1/message'; async function createChatSession(initialMessage) { const payload = { query: initialMessage, character_limit: 1000, sources_limit: 5, stream: false, publishers: [], enable_suggestions: 1, // Enable suggested follow-up questions }; const response = await axios.post(MESSAGE_API_URL, payload, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } // Example usage const initialQuestion = "How can I find meaning and purpose when facing life's greatest challenges?"; const chatResponse = await createChatSession(initialQuestion); console.log('Chat created successfully!'); console.log(`Chat ID: ${chatResponse.chat_id}`); console.log(`Message ID: ${chatResponse.message_id}`); console.log(`Response: ${chatResponse.message}`); // Show suggested follow-up questions if (chatResponse.suggestions) { console.log('\nSuggested follow-up questions:'); chatResponse.suggestions.forEach((suggestion, index) => { console.log(`${index + 1}. ${suggestion}`); }); } ``` ```typescript TypeScript theme={null} const MESSAGE_API_URL = 'https://platform.ai.gloo.com/ai/v1/message'; interface MessageResponse { chat_id: string; query_id: string; message_id: string; message: string; timestamp: string; success: boolean; suggestions?: string[]; sources?: any[]; } interface MessageRequest { query: string; character_limit?: number; sources_limit?: number; stream?: boolean; publishers?: string[]; chat_id?: string; enable_suggestions?: number; } async function createChatSession( initialMessage: string, ): Promise { const payload: MessageRequest = { query: initialMessage, character_limit: 1000, sources_limit: 5, stream: false, publishers: [], enable_suggestions: 1, // Enable suggested follow-up questions }; const response = await axios.post(MESSAGE_API_URL, payload, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } // Example usage const initialQuestion = "How can I find meaning and purpose when facing life's greatest challenges?"; const chatResponse = await createChatSession(initialQuestion); console.log('Chat created successfully!'); console.log(`Chat ID: ${chatResponse.chat_id}`); console.log(`Message ID: ${chatResponse.message_id}`); console.log(`Response: ${chatResponse.message}`); // Show suggested follow-up questions if (chatResponse.suggestions) { console.log('\nSuggested follow-up questions:'); chatResponse.suggestions.forEach((suggestion, index) => { console.log(`${index + 1}. ${suggestion}`); }); } ``` ```php PHP theme={null} $MESSAGE_API_URL = 'https://platform.ai.gloo.com/ai/v1/message'; function createChatSession($initial_message) { $payload = json_encode([ 'query' => $initial_message, 'character_limit' => 1000, 'sources_limit' => 5, 'stream' => false, 'publishers' => [], 'enable_suggestions' => 1 // Enable suggested follow-up questions ]); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $GLOBALS['MESSAGE_API_URL']); 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 ' . $GLOBALS['API_KEY'] ]); $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); return json_decode($result, true); } // Example usage $initial_question = "How can I find meaning and purpose when facing life's greatest challenges?"; $chat_response = createChatSession($initial_question); echo "Chat created successfully!\n"; echo "Chat ID: " . $chat_response['chat_id'] . "\n"; echo "Message ID: " . $chat_response['message_id'] . "\n"; echo "Response: " . $chat_response['message'] . "\n"; // Show suggested follow-up questions if (!empty($chat_response['suggestions'])) { echo "\nSuggested follow-up questions:\n"; foreach ($chat_response['suggestions'] as $index => $suggestion) { echo ($index + 1) . ". " . $suggestion . "\n"; } } ``` ```go Go theme={null} const messageAPIURL = "https://platform.ai.gloo.com/ai/v1/message" type MessageRequest struct { Query string `json:"query"` CharacterLimit int `json:"character_limit,omitempty"` SourcesLimit int `json:"sources_limit,omitempty"` Stream bool `json:"stream,omitempty"` Publishers []string `json:"publishers,omitempty"` ChatID string `json:"chat_id,omitempty"` EnableSuggestions int `json:"enable_suggestions,omitempty"` } type MessageResponse struct { ChatID string `json:"chat_id"` QueryID string `json:"query_id"` MessageID string `json:"message_id"` Message string `json:"message"` Timestamp string `json:"timestamp"` Success bool `json:"success"` Suggestions []string `json:"suggestions"` Sources []any `json:"sources"` } func createChatSession(initialMessage string) (*MessageResponse, error) { payload := MessageRequest{ Query: initialMessage, CharacterLimit: 1000, SourcesLimit: 5, Stream: false, Publishers: []string{}, EnableSuggestions: 1, // Enable suggested follow-up questions } jsonPayload, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest("POST", messageAPIURL, 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, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body)) } var response MessageResponse if err := json.Unmarshal(body, &response); err != nil { return nil, err } return &response, nil } // Example usage initialQuestion := "How can I find meaning and purpose when facing life's greatest challenges?" chatResponse, err := createChatSession(initialQuestion) if err != nil { fmt.Printf("Error creating chat: %v\n", err) return } fmt.Println("Chat created successfully!") fmt.Printf("Query ID: %s\n", chatResponse.QueryID) fmt.Printf("Message ID: %s\n", chatResponse.MessageID) fmt.Printf("Response: %s\n", chatResponse.Message) // Show suggested follow-up questions if len(chatResponse.Suggestions) > 0 { fmt.Println("\nSuggested follow-up questions:") for i, suggestion := range chatResponse.Suggestions { fmt.Printf("%d. %s\n", i+1, suggestion) } } ``` ```java Java theme={null} import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.List; public class ChatManager { private static final String MESSAGE_API_URL = "https://platform.ai.gloo.com/ai/v1/message"; private static final HttpClient httpClient = HttpClient.newHttpClient(); private static final Gson gson = new Gson(); public static class MessageRequest { public String query; public Integer character_limit; public Integer sources_limit; public Boolean stream; public List publishers; public String chat_id; public Integer enable_suggestions; } public static class MessageResponse { public String chat_id; public String query_id; public String message_id; public String message; public String timestamp; public Boolean success; public List suggestions; public List sources; } public static MessageResponse createChatSession(String initialMessage) throws IOException, InterruptedException { MessageRequest payload = new MessageRequest(); payload.query = initialMessage; payload.character_limit = 1000; payload.sources_limit = 5; payload.stream = false; payload.publishers = List.of(); payload.enable_suggestions = 1; // Enable suggested follow-up questions String jsonPayload = gson.toJson(payload); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(MESSAGE_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(), MessageResponse.class); } // Example usage public static void main(String[] args) { try { String initialQuestion = "How can I find meaning and purpose when facing life's greatest challenges?"; MessageResponse chatResponse = createChatSession(initialQuestion); System.out.println("Chat created successfully!"); System.out.println("Chat ID: " + chatResponse.chat_id); System.out.println("Message ID: " + chatResponse.message_id); System.out.println("Response: " + chatResponse.message); // Show suggested follow-up questions if (chatResponse.suggestions != null && !chatResponse.suggestions.isEmpty()) { System.out.println("\nSuggested follow-up questions:"); for (int i = 0; i < chatResponse.suggestions.size(); i++) { System.out.println((i + 1) + ". " + chatResponse.suggestions.get(i)); } } } catch (Exception e) { e.printStackTrace(); } } } ``` *** ## Step 2: Continuing the Conversation Once you have a chat session, you can continue the conversation by including the `chat_id` from the previous response in your next message request. You don't need to retrieve chat history to continue the conversation - the API maintains context automatically. ```python Python theme={null} def continue_conversation(chat_id, follow_up_message): """Continue an existing chat conversation.""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } #- Include chat_id to continue the conversation payload = { "chat_id": chat_id, "query": follow_up_message, "character_limit": 1000, "sources_limit": 5, "stream": False, "publishers": [], "enable_suggestions": 1 #- Continue getting suggestions } response = requests.post(MESSAGE_API_URL, headers=headers, json=payload) response.raise_for_status() return response.json() #- Example: Continue the conversation using a suggested response #- Use the first suggestion from the initial response if chat_response.get('suggestions') and len(chat_response['suggestions']) > 0: follow_up = chat_response['suggestions'][0] #- Use first suggested question else: follow_up = "Can you give me practical steps I can take today to begin this journey?" follow_up_response = continue_conversation(chat_response['chat_id'], follow_up) print("\nFollow-up Response:") print(f"Question: {follow_up}") print(f"Response: {follow_up_response['message']}") ``` ```javascript JavaScript theme={null} async function continueConversation(chatId, followUpMessage) { const payload = { chat_id: chatId, query: followUpMessage, character_limit: 1000, sources_limit: 5, stream: false, publishers: [], enable_suggestions: 1, // Continue getting suggestions }; const response = await axios.post(MESSAGE_API_URL, payload, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } // Example: Continue the conversation with a follow-up const followUpQuestion = chatResponse.suggestions && chatResponse.suggestions.length > 0 ? chatResponse.suggestions[0] : 'Can you give me practical steps I can take today to begin this journey?'; const followUpResponse = await continueConversation( chatResponse.chat_id, followUpQuestion, ); console.log('\nFollow-up Response:'); console.log(`Response: ${followUpResponse.message}`); ``` ```typescript TypeScript theme={null} async function continueConversation( chatId: string, followUpMessage: string, ): Promise { const payload: MessageRequest = { chat_id: chatId, query: followUpMessage, character_limit: 1000, sources_limit: 5, stream: false, publishers: [], }; const response = await axios.post(MESSAGE_API_URL, payload, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } // Example: Continue the conversation with a follow-up const followUpQuestion = chatResponse.suggestions && chatResponse.suggestions.length > 0 ? chatResponse.suggestions[0] : 'Can you give me practical steps I can take today to begin this journey?'; const followUpResponse = await continueConversation( chatResponse.chat_id, followUpQuestion, ); console.log('\nFollow-up Response:'); console.log(`Response: ${followUpResponse.message}`); ``` ```php PHP theme={null} function continueConversation($chat_id, $follow_up) { $payload = json_encode([ 'chat_id' => $chat_id, 'query' => $follow_up, 'character_limit' => 1000, 'sources_limit' => 5, 'stream' => false, 'publishers' => [] ]); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $GLOBALS['MESSAGE_API_URL']); 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 ' . $GLOBALS['API_KEY'] ]); $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); return json_decode($result, true); } // Example: Continue the conversation with a follow-up $follow_up = !empty($chat_response['suggestions']) ? $chat_response['suggestions'][0] : "Can you give me practical steps I can take today to begin this journey?"; $follow_up_response = continueConversation($chat_response['chat_id'], $follow_up); echo "\nFollow-up Response:\n"; echo "Response: " . $follow_up_response['message'] . "\n"; ``` ```go Go theme={null} func continueConversation(chatID, followUpMessage string) (*MessageResponse, error) { payload := MessageRequest{ ChatID: chatID, Query: followUpMessage, CharacterLimit: 1000, SourcesLimit: 5, Stream: false, Publishers: []string{}, } jsonPayload, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest("POST", messageAPIURL, 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, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body)) } var response MessageResponse if err := json.Unmarshal(body, &response); err != nil { return nil, err } return &response, nil } // Example: Continue the conversation with a follow-up var followUp string if len(chatResponse.Suggestions) > 0 { followUp = chatResponse.Suggestions[0] } else { followUp = "Can you give me practical steps I can take today to begin this journey?" } followUpResponse, err := continueConversation(chatResponse.ChatID, followUp) if err != nil { fmt.Printf("Error continuing conversation: %v\n", err) return } fmt.Println("\nFollow-up Response:") fmt.Printf("Response: %s\n", followUpResponse.Message) ``` ```java Java theme={null} public static MessageResponse continueConversation(String chatId, String followUpMessage) throws IOException, InterruptedException { MessageRequest payload = new MessageRequest(); payload.chat_id = chatId; payload.query = followUpMessage; payload.character_limit = 1000; payload.sources_limit = 5; payload.stream = false; payload.publishers = List.of(); String jsonPayload = gson.toJson(payload); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(MESSAGE_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(), MessageResponse.class); } // Modified main public static void main(String[] args) { try { // ... Code from previous step String followUp = (chatResponse.suggestions != null && !chatResponse.suggestions.isEmpty()) ? chatResponse.suggestions.get(0) : "Can you give me practical steps I can take today to begin this journey?"; MessageResponse followUpResponse = continueConversation(chatId, followUp); System.out.println("\nFollow-up Response:"); System.out.println("Response: " + followUpResponse.message); } catch (Exception e) { e.printStackTrace(); } } ``` *** ## Step 3: Retrieving Chat History (Optional) If you want to view the complete conversation history, you can retrieve it using the Chat API. This is useful for displaying conversation logs or analyzing chat patterns. ```python Python theme={null} def get_chat_history(chat_id): """Retrieve the complete chat history for a conversation.""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } #- Use the chat endpoint to get full conversation history params = {"chat_id": chat_id} response = requests.get(CHAT_API_URL, headers=headers, params=params) response.raise_for_status() return response.json() #- Example: Get chat history after the follow-up conversation print("\n" + "="*50) print("COMPLETE CONVERSATION HISTORY") print("="*50) chat_history = get_chat_history(chat_response['chat_id']) #- Display the conversation history for i, message in enumerate(chat_history.get('messages', []), 1): role = message.get('role', 'unknown') content = message.get('content', '') timestamp = message.get('timestamp', '') print(f"\n{i}. {role.upper()}: {content}") if timestamp: print(f" Time: {timestamp}") ``` ```javascript JavaScript theme={null} async function getChatHistory(chatId) { const response = await axios.get(CHAT_API_URL, { params: { chat_id: chatId }, headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } // Example: Get chat history after the follow-up conversation console.log('\n' + '='.repeat(50)); console.log('COMPLETE CONVERSATION HISTORY'); console.log('='.repeat(50)); const chatHistory = await getChatHistory(chatResponse.chat_id); // Display the conversation history chatHistory.messages?.forEach((message, index) => { const role = message.role || 'unknown'; const content = message.content || ''; const timestamp = message.timestamp || ''; console.log(`\n${index + 1}. ${role.toUpperCase()}: ${content}`); if (timestamp) { console.log(` Time: ${timestamp}`); } }); ``` ```typescript TypeScript theme={null} async function getChatHistory(chatId: string): Promise { const response = await axios.get(CHAT_API_URL, { params: { chat_id: chatId }, headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } // Example: Get chat history after the follow-up conversation console.log('\n' + '='.repeat(50)); console.log('COMPLETE CONVERSATION HISTORY'); console.log('='.repeat(50)); const chatHistory = await getChatHistory(chatResponse.chat_id); // Display the conversation history chatHistory.messages?.forEach((message, index) => { const role = message.role || 'unknown'; const content = message.content || ''; const timestamp = message.timestamp || ''; console.log(`\n${index + 1}. ${role.toUpperCase()}: ${content}`); if (timestamp) { console.log(` Time: ${timestamp}`); } }); ``` ```php PHP theme={null} function getChatHistory($chat_id) { $url = $GLOBALS['CHAT_API_URL'] . '?' . http_build_query(['chat_id' => $chat_id]); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $GLOBALS['API_KEY'] ]); $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); return json_decode($result, true); } // Example: Get chat history after the follow-up conversation echo "\n" . str_repeat("=", 50) . "\n"; echo "COMPLETE CONVERSATION HISTORY\n"; echo str_repeat("=", 50) . "\n"; $chat_history = getChatHistory($chat_response['chat_id']); // Display the conversation history foreach ($chat_history['messages'] ?? [] as $index => $message) { $role = $message['role'] ?? 'unknown'; $content = $message['content'] ?? ''; $timestamp = $message['timestamp'] ?? ''; echo "\n" . ($index + 1) . ". " . strtoupper($role) . ": " . $content . "\n"; if (!empty($timestamp)) { echo " Time: " . $timestamp . "\n"; } } ``` ```go Go theme={null} func getChatHistory(chatID string) (*ChatHistoryResponse, error) { // Build URL with query parameters chatURL := fmt.Sprintf("%s?chat_id=%s", chatAPIURL, chatID) req, err := http.NewRequest("GET", chatURL, nil) 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, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body)) } var response ChatHistoryResponse if err := json.Unmarshal(body, &response); err != nil { return nil, err } return &response, nil } // Example: Get chat history after the follow-up conversation fmt.Println("\n" + strings.Repeat("=", 50)) fmt.Println("COMPLETE CONVERSATION HISTORY") fmt.Println(strings.Repeat("=", 50)) chatHistory, err := getChatHistory(chatResponse.QueryID) if err != nil { fmt.Printf("Error getting chat history: %v\n", err) return } // Display the conversation history for i, message := range chatHistory.Messages { role := message.Role if role == "" { role = "unknown" } content := message.Content timestamp := message.Timestamp fmt.Printf("\n%d. %s: %s\n", i+1, strings.ToUpper(role), content) if timestamp != "" { fmt.Printf(" Time: %s\n", timestamp) } } ``` ```java Java theme={null} public static ChatHistoryResponse getChatHistory(String chatId) throws IOException, InterruptedException { String chatUrl = CHAT_API_URL + "?chat_id=" + chatId; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(chatUrl)) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + API_KEY) .GET() .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(), ChatHistoryResponse.class); } // Example: Get chat history after the follow-up conversation public static void displayChatHistory(String chatId) throws IOException, InterruptedException { System.out.println("\n" + "=".repeat(50)); System.out.println("COMPLETE CONVERSATION HISTORY"); System.out.println("=".repeat(50)); ChatHistoryResponse chatHistory = getChatHistory(chatId); // Display the conversation history List messages = chatHistory.messages != null ? chatHistory.messages : new ArrayList<>(); for (int i = 0; i < messages.size(); i++) { ChatMessage message = messages.get(i); String role = message.role != null ? message.role : "unknown"; String content = message.content != null ? message.content : ""; String timestamp = message.timestamp != null ? message.timestamp : ""; System.out.printf("\n%d. %s: %s\n", (i + 1), role.toUpperCase(), content); if (!timestamp.isEmpty()) { System.out.printf(" Time: %s\n", timestamp); } } } ``` *** ## Complete Working Examples Here are complete, runnable examples that demonstrate the full chat flow: ```python Python theme={null} #!/usr/bin/env python3 """ Complete Chat Example - Python Demonstrates creating a chat session and continuing the conversation. """ import requests import os from dotenv import load_dotenv #- Load environment variables load_dotenv() #- Configuration API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY") MESSAGE_API_URL = "https://platform.ai.gloo.com/ai/v1/message" CHAT_API_URL = "https://platform.ai.gloo.com/ai/v1/chat" def send_message(message_text, chat_id=None): """Send a message to the chat API.""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } payload = { "query": message_text, "character_limit": 1000, "sources_limit": 5, "stream": False, "publishers": [] } if chat_id: payload["chat_id"] = chat_id response = requests.post(MESSAGE_API_URL, headers=headers, json=payload) response.raise_for_status() return response.json() def get_chat_history(chat_id): """Retrieve the full chat history for a given chat ID.""" headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } params = {"chat_id": chat_id} response = requests.get(CHAT_API_URL, headers=headers, params=params) response.raise_for_status() return response.json() def main(): """Main function demonstrating the complete chat flow.""" try: #- Start with a deep, meaningful question initial_question = "How can I find meaning and purpose when facing life's greatest challenges?" print("=== Starting New Chat Session ===") print(f"Question: {initial_question}") print() #- Create new chat session chat_response = send_message(initial_question) chat_id = chat_response['chat_id'] print("AI Response:") print(chat_response['message']) print() #- Show suggested follow-up questions if chat_response['suggestions']: print("Suggested follow-up questions:") for i, suggestion in enumerate(chat_response['suggestions'], 1): print(f"{i}. {suggestion}") print() #- Use the first suggested question for follow-up, or fallback if chat_response['suggestions']: follow_up_question = chat_response['suggestions'][0] else: follow_up_question = "Can you give me practical steps I can take today to begin this journey?" print("=== Continuing the Conversation ===") print(f"Using suggested question: {follow_up_question}") print() #- Send follow-up message follow_up_response = send_message(follow_up_question, chat_id) print("AI Response:") print(follow_up_response.message) print() #- Display final chat history print("=== Complete Chat History ===") chat_history = get_chat_history(chat_id) for message in chat_history['messages']: role = message['role'].upper() content = message['message'] print(f"{role}: {content}") print() print("Chat session completed successfully!") except requests.exceptions.RequestException as e: print(f"API Error: {e}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": main() ``` ```javascript JavaScript theme={null} #!/usr/bin/env node /** * Complete Chat Example - JavaScript/Node.js * Demonstrates creating a chat session and continuing the conversation. */ const axios = require('axios'); require('dotenv').config(); // Configuration const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; const MESSAGE_API_URL = 'https://platform.ai.gloo.com/ai/v1/message'; const CHAT_API_URL = 'https://platform.ai.gloo.com/ai/v1/chat'; async function sendMessage(messageText, chatId = null) { const payload = { query: messageText, character_limit: 1000, sources_limit: 5, stream: false, publishers: [], enable_suggestions: 1, }; if (chatId) { payload.chat_id = chatId; } const response = await axios.post(MESSAGE_API_URL, payload, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } async function getChatHistory(chatId) { const response = await axios.get(CHAT_API_URL, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, params: { chat_id: chatId, }, }); return response.data; } async function main() { try { // Start with a deep, meaningful question const initialQuestion = "How can I find meaning and purpose when facing life's greatest challenges?"; console.log('=== Starting New Chat Session ==='); console.log(`Question: ${initialQuestion}`); console.log(); // Create new chat session const chatResponse = await sendMessage(initialQuestion); const chatId = chatResponse.chat_id; console.log('AI Response:'); console.log(chatResponse.message); console.log(); // Show suggested follow-up questions if (chatResponse.suggestions && chatResponse.suggestions.length > 0) { console.log('Suggested follow-up questions:'); chatResponse.suggestions.forEach((suggestion, index) => { console.log(`${index + 1}. ${suggestion}`); }); console.log(); } // Use the first suggested question for follow-up, or fallback const followUpQuestion = chatResponse.suggestions && chatResponse.suggestions.length > 0 ? chatResponse.suggestions[0] : 'Can you give me practical steps I can take today to begin this journey?'; console.log('=== Continuing the Conversation ==='); console.log(`Using suggested question: ${followUpQuestion}`); console.log(); // Send follow-up message const followUpResponse = await sendMessage(followUpQuestion, chatId); console.log('AI Response:'); console.log(followUpResponse.message); console.log(); // Display final chat history (optional) console.log('=== Complete Chat History (Optional) ==='); console.log( 'This shows how to retrieve the complete conversation history:', ); console.log(); const chatHistory = await getChatHistory(chatId); chatHistory.messages.forEach((message, index) => { const role = message.role.toUpperCase(); const content = message.message; console.log(`${index + 1}. ${role}: ${content}`); console.log(); }); console.log('Chat session completed successfully!'); } catch (error) { console.error( 'Error:', error.response ? error.response.data : error.message, ); } } // Run the main function main(); ``` The `kallm` message role is deprecated and no longer supported. ```typescript TypeScript theme={null} #!/usr/bin/env tsx /** * Complete Chat Example - TypeScript * Demonstrates creating a chat session and continuing the conversation. */ import axios from 'axios'; import * as dotenv from 'dotenv'; dotenv.config(); // Configuration const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; const MESSAGE_API_URL = 'https://platform.ai.gloo.com/ai/v1/message'; const CHAT_API_URL = 'https://platform.ai.gloo.com/ai/v1/chat'; // Type definitions interface MessageResponse { chat_id: string; query_id: string; message_id: string; message: string; timestamp: string; success: boolean; suggestions?: string[]; sources?: any[]; } interface MessageRequest { query: string; character_limit?: number; sources_limit?: number; stream?: boolean; publishers?: string[]; chat_id?: string; enable_suggestions?: number; } interface ChatMessage { query_id: string; message_id: string; timestamp: string; role: 'user' | 'kallm'; message: string; character_limit?: number; } interface ChatHistory { chat_id: string; created_at: string; messages: ChatMessage[]; } async function sendMessage( messageText: string, chatId?: string, ): Promise { const payload: MessageRequest = { query: messageText, character_limit: 1000, sources_limit: 5, stream: false, publishers: [], enable_suggestions: 1, }; if (chatId) { payload.chat_id = chatId; } const response = await axios.post(MESSAGE_API_URL, payload, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); return response.data; } async function getChatHistory(chatId: string): Promise { const response = await axios.get(CHAT_API_URL, { headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, params: { chat_id: chatId, }, }); return response.data; } async function main(): Promise { try { // Start with a deep, meaningful question const initialQuestion = "How can I find meaning and purpose when facing life's greatest challenges?"; console.log('=== Starting New Chat Session ==='); console.log(`Question: ${initialQuestion}`); console.log(); // Create new chat session const chatResponse = await sendMessage(initialQuestion); const chatId = chatResponse.chat_id; console.log('AI Response:'); console.log(chatResponse.message); console.log(); // Show suggested follow-up questions if (chatResponse.suggestions && chatResponse.suggestions.length > 0) { console.log('Suggested follow-up questions:'); chatResponse.suggestions.forEach((suggestion, index) => { console.log(`${index + 1}. ${suggestion}`); }); console.log(); } // Use the first suggested question for follow-up, or fallback const followUpQuestion = chatResponse.suggestions && chatResponse.suggestions.length > 0 ? chatResponse.suggestions[0] : 'Can you give me practical steps I can take today to begin this journey?'; console.log('=== Continuing the Conversation ==='); console.log(`Using suggested question: ${followUpQuestion}`); console.log(); // Send follow-up message const followUpResponse = await sendMessage(followUpQuestion, chatId); console.log('AI Response:'); console.log(followUpResponse.message); console.log(); // Display final chat history console.log('=== Complete Chat History ==='); const chatHistory = await getChatHistory(chatId); chatHistory.messages.forEach((message) => { const role = message.role.toUpperCase(); const content = message.message; console.log(`${role}: ${content}`); console.log(); }); console.log('Chat session completed successfully!'); } catch (error: any) { console.error( 'Error:', error.response ? error.response.data : error.message, ); } } // Run the main function main(); ``` ```php PHP theme={null} #!/usr/bin/env php load(); // Configuration $API_KEY = getenv('GLOO_API_KEY') ?: 'YOUR_API_KEY'; $MESSAGE_API_URL = 'https://platform.ai.gloo.com/ai/v1/message'; $CHAT_API_URL = 'https://platform.ai.gloo.com/ai/v1/chat'; function sendMessage($messageText, $chatId = null) { global $MESSAGE_API_URL, $API_KEY; $payload = [ 'query' => $messageText, 'character_limit' => 1000, 'sources_limit' => 5, 'stream' => false, 'publishers' => [] ]; if ($chatId) { $payload['chat_id'] = $chatId; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $MESSAGE_API_URL); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $API_KEY ]); $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); return json_decode($result, true); } function getChatHistory($chatId) { global $CHAT_API_URL, $API_KEY; $url = $CHAT_API_URL . '?' . http_build_query(['chat_id' => $chatId]); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $API_KEY, 'Content-Type: application/json' ]); $result = curl_exec($ch); if (curl_errno($ch)) { throw new Exception(curl_error($ch)); } curl_close($ch); return json_decode($result, true); } function main() { try { // Start with a deep, meaningful question about human flourishing $initial_question = "How can I find meaning and purpose when facing life's greatest challenges?"; echo "=== Starting New Chat Session ===\n"; echo "Question: $initial_question\n\n"; // Create new chat session $chat_response = sendMessage($initial_question); $chat_id = $chat_response->chat_id; echo "AI Response:\n"; echo $chat_response->message . "\n\n"; // Show suggested follow-up questions if (!empty($chat_response->suggestions)) { echo "Suggested follow-up questions:\n"; foreach ($chat_response->suggestions as $index => $suggestion) { echo ($index + 1) . ". " . $suggestion . "\n"; } echo "\n"; } // Use the first suggested question for follow-up, or a predefined fallback $follow_up_question = !empty($chat_response->suggestions) ? $chat_response->suggestions[0] : "Can you give me practical steps I can take today to begin this journey?"; echo "=== Continuing the Conversation ===\n"; echo "Using suggested question: $follow_up_question\n\n"; // Send the follow-up message $follow_up_response = sendMessage($follow_up_question, $chat_id); echo "AI Response:\n"; echo $follow_up_response->message . "\n\n"; // Display final chat history echo "=== Complete Chat History ===\n"; $chat_history = getChatHistory($chat_id); foreach ($chat_history->messages as $i => $message) { displayMessage($message, $i); } echo "Chat session completed successfully!\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } } // Run the main function main(); ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "os" "strings" ) // Configuration var ( apiKey = getEnv("GLOO_API_KEY", "YOUR_API_KEY") messageURL = "https://platform.ai.gloo.com/ai/v1/message" chatURL = "https://platform.ai.gloo.com/ai/v1/chat" ) // Data structures type MessageRequest struct { Query string `json:"query"` CharacterLimit int `json:"character_limit,omitempty"` SourcesLimit int `json:"sources_limit,omitempty"` Stream bool `json:"stream,omitempty"` Publishers []string `json:"publishers,omitempty"` ChatID string `json:"chat_id,omitempty"` } type MessageResponse struct { QueryID string `json:"query_id"` MessageID string `json:"message_id"` Message string `json:"message"` Timestamp string `json:"timestamp"` } type ChatMessage struct { QueryID string `json:"query_id"` MessageID string `json:"message_id"` Timestamp string `json:"timestamp"` Role string `json:"role"` Message string `json:"message"` CharacterLimit int `json:"character_limit,omitempty"` } type ChatHistory struct { ChatID string `json:"chat_id"` CreatedAt string `json:"created_at"` Messages []ChatMessage `json:"messages"` } func getEnv(key, fallback string) string { if value, ok := os.LookupEnv(key); ok { return value } return fallback } func sendMessage(messageText string, chatID string) (*MessageResponse, error) { payload := MessageRequest{ Query: messageText, CharacterLimit: 1000, SourcesLimit: 5, Stream: false, Publishers: []string{}, ChatID: chatID, } jsonPayload, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest("POST", messageURL, 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, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body)) } var response MessageResponse if err := json.Unmarshal(body, &response); err != nil { return nil, err } return &response, nil } func getChatHistory(chatID string) (*ChatHistory, error) { params := url.Values{} params.Add("chat_id", chatID) requestURL := fmt.Sprintf("%s?%s", chatURL, params.Encode()) req, err := http.NewRequest("GET", requestURL, nil) 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, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API call failed: %s - %s", resp.Status, string(body)) } var history ChatHistory if err := json.Unmarshal(body, &history); err != nil { return nil, err } return &history, nil } func main() { // Start with a deep, meaningful question initialQuestion := "How can I find meaning and purpose when facing life's greatest challenges?" fmt.Println("=== Starting New Chat Session ===") fmt.Printf("Question: %s\n\n", initialQuestion) // Create new chat session chatResponse, err := sendMessage(initialQuestion, "") if err != nil { fmt.Printf("Error creating chat: %v\n", err) return } chatID := chatResponse.ChatID fmt.Println("AI Response:") fmt.Printf("%s\n\n", chatResponse.Message) // Continue with follow-up questions followUpQuestions := []string{ "Can you give me practical steps I can take today to begin this journey?", "How do I maintain hope when circumstances seem overwhelming?", "What role does community play in finding purpose?", } for i, question := range followUpQuestions { fmt.Printf("=== Follow-up Question %d ===\n", i+1) fmt.Printf("Question: %s\n\n", question) // Send follow-up message followUpResponse, err := sendMessage(question, chatID) if err != nil { fmt.Printf("Error sending follow-up: %v\n", err) continue } fmt.Println("AI Response:") fmt.Printf("%s\n\n", followUpResponse.Message) } // Display final chat history fmt.Println("=== Complete Chat History ===") chatHistory, err := getChatHistory(chatID) if err != nil { fmt.Printf("Error getting chat history: %v\n", err) return } for _, message := range chatHistory.Messages { role := strings.ToUpper(message.Role) content := message.Message fmt.Printf("%s: %s\n\n", role, content) } fmt.Println("Chat session completed successfully!") } ``` ```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; /** * Complete Chat Example - Java * Demonstrates creating a chat session and continuing the conversation. */ public class ChatExample { // Configuration private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY"); private static final String MESSAGE_API_URL = "https://platform.ai.gloo.com/ai/v1/message"; private static final String CHAT_API_URL = "https://platform.ai.gloo.com/ai/v1/chat"; // HTTP client and JSON parser private static final HttpClient httpClient = HttpClient.newHttpClient(); private static final Gson gson = new Gson(); // Data classes public static class MessageRequest { public String query; public Integer character_limit; public Integer sources_limit; public Boolean stream; public List publishers; public String chat_id; } public static class MessageResponse { public String chat_id; public String query_id; public String message_id; public String message; public String timestamp; public Boolean success; public List suggestions; public List sources; } public static class ChatMessage { public String query_id; public String message_id; public String timestamp; public String role; public String message; public Integer character_limit; } public static class ChatHistory { public String chat_id; public String created_at; public List messages; } // Chat methods private static MessageResponse sendMessage(String messageText, String chatId) throws IOException, InterruptedException { MessageRequest payload = new MessageRequest(); payload.query = messageText; payload.character_limit = 1000; payload.sources_limit = 5; payload.stream = false; payload.publishers = List.of(); payload.chat_id = chatId; String jsonPayload = gson.toJson(payload); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(MESSAGE_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(), MessageResponse.class); } private static ChatHistory getChatHistory(String chatId) throws IOException, InterruptedException { String url = CHAT_API_URL + "?chat_id=" + chatId; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Authorization", "Bearer " + API_KEY) .header("Content-Type", "application/json") .GET() .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(), ChatHistory.class); } // Main method public static void main(String[] args) { try { // Start with a deep, meaningful question String initialQuestion = "How can I find meaning and purpose when facing life's greatest challenges?"; System.out.println("=== Starting New Chat Session ==="); System.out.println("Question: " + initialQuestion); System.out.println(); // Create new chat session MessageResponse chatResponse = sendMessage(initialQuestion, null); String chatId = chatResponse.chat_id; System.out.println("AI Response:"); System.out.println(chatResponse.message); System.out.println(); // Show suggested follow-up questions List suggestions = chatResponse.getSuggestions(); if (suggestions != null && !suggestions.isEmpty()) { System.out.println("Suggested follow-up questions:"); for (int i = 0; i < suggestions.size(); i++) { System.out.println((i + 1) + ". " + suggestions.get(i)); } System.out.println(); } // Use the first suggested question for follow-up, or fallback String followUpQuestion = (suggestions != null && !suggestions.isEmpty()) ? suggestions.get(0) : "Can you give me practical steps I can take today to begin this journey?"; System.out.println("=== Continuing the Conversation ==="); System.out.println("Using suggested question: " + followUpQuestion); System.out.println(); // Send follow-up message MessageResponse followUpResponse = sendMessage(followUpQuestion, chatId); System.out.println("AI Response:"); System.out.println(followUpResponse.getMessage()); System.out.println(); // Display final chat history System.out.println("=== Complete Chat History ==="); ChatHistory chatHistory = getChatHistory(chatId); for (ChatMessage message : chatHistory.messages) { String role = message.role.toUpperCase(); String content = message.message; System.out.println(role + ": " + content); System.out.println(); } System.out.println("Chat session completed successfully!"); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); e.printStackTrace(); } } } ``` *** ## Error Handling Best Practices When working with the Message API, implement proper error handling for common scenarios: ### Authentication Errors * **401 Unauthorized**: Token expired or invalid * **403 Forbidden**: Insufficient permissions ### Request Errors * **400 Bad Request**: Invalid request parameters * **422 Unprocessable Entity**: Validation errors ### Rate Limiting * **429 Too Many Requests**: Implement backoff strategies ### Network Issues * **Connection timeouts**: Implement retry logic * **Network failures**: Handle gracefully with user feedback *** ## Advanced Features ### Streaming Responses Set `stream: true` in your request for real-time response streaming: ```json theme={null} { "query": "Tell me about hope in difficult times", "stream": true, "chat_id": "your-chat-id" } ``` ### Publisher Filtering Restrict responses to specific content publishers: ```json theme={null} { "query": "What does Scripture say about finding purpose?", "publishers": ["Bible", "Christian Literature"], "chat_id": "your-chat-id" } ``` ### Response Customization Control response length and source count: ```json theme={null} { "query": "How can I grow spiritually?", "character_limit": 500, "sources_limit": 3, "chat_id": "your-chat-id" } ``` *** ## 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 Message API, consider exploring: 1. **[Authentication Tutorial](/tutorials/authentication)** - For detailed authentication setup 2. **[Messages API](/api-reference/chat/post-message)** - For additional API information 3. **[Completions Tutorial](/tutorials/completions)** - For stateless chat interactions 4. **[Tool Use](/api-guides/tool-use)** - For enhanced AI capabilities The Message API provides a powerful foundation for building meaningful, contextual conversations that can help users explore deep questions about life, purpose, and human flourishing. # Building a Knowledge-Grounded Chatbot with RAG Source: https://docs.gloo.com/tutorials/completions-grounded Learn how to ground AI responses in your own content using Gloo AI Grounded Completions with Retrieval-Augmented Generation (RAG). From time immemorial, well let's say late 2022, we've all had the experience of AI models confidently responding to a question with nonsense or made-up facts. That's a term known as "hallucinating". It happens when models lack the knowledge needed and are incentivized to try and be helpful. At other times, their responses are simply vague and unhelpful. The good news? There's a fix! This tutorial walks you through how to use Gloo AI's Grounded Completions API with RAG (Retrieval-Augmented Generation). You'll see how giving your AI the right context makes all the difference. **Key Problem**: Without grounding, AI models may hallucinate answers about your organization, products, or services. **The Solution**: Grounded Completions uses RAG to retrieve relevant content from YOUR publisher before generating responses. This tutorial shows how grounding on your own content transforms generic AI into an accurate, source-backed assistant. ## 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 created with content uploaded in [Gloo Studio](https://studio.ai.gloo.com) **New to Publishers?** Check out the [Upload Files to Data Engine](/tutorials/upload-files) recipe to learn how to create a Publisher and upload your documents using Gloo AI APIs. *** ## What You'll Build A 2-step comparison that shows you how RAG grounding works: 1. **Non-grounded (Step 1)**: Generic model knowledge, may hallucinate 2. **Grounded on your publisher (Step 2)**: Your specific content, accurate and source-backed This comparison shows the dramatic difference grounding on your own content makes. ### Example: Bezalel Ministries For this tutorial, we've cooked up a fictional organization called Bezalel Ministries. They're a faith-based creative group who produce biblically-accurate artwork and educational resources. They're delightful. We've created 5 sample documents that cover everything from their hiring process to their educational programs and research methodology. Why use a made-up org? Because it lets us show you exactly how the API works without any real-world baggage. Plus, Bezalel Ministries is more interesting than "Company X" or "Acme Corp". **What the comparison reveals**: * **Step 1 (Non-grounded)**: Generic hiring advice with no knowledge of Bezalel * **Step 2 (Publisher grounded)**: Accurate details about Bezalel's 3-phase selection journey from their actual docs Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java). Includes the 5 Bezalel sample content files. Setup and testing instructions are provided later. *** ## Step 1: Understanding the Hallucination Problem Without grounding, when you ask about specific organizational information, the model relies on general knowledge and may: * Provide generic answers that don't match your reality * Make up plausible-sounding but incorrect information * Say "I don't have information" even though you've uploaded relevant content Grounding narrows this problem substantially, but it does not close it. A grounded answer can still contain a quotation or a resource title that appears in none of the retrieved sources. Production applications add a verification step between generation and display. See [Building Trustworthy Grounded Applications](/best-practices/trustworthy-grounded-applications). ### The Non-Grounded Request This is a standard Completions V2 call: ```python Python theme={null} import requests import os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY") def make_non_grounded_request(query, api_key=API_KEY): """Standard completion WITHOUT RAG - may hallucinate.""" 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": query}], "auto_routing": True, "max_tokens": 500 } response = requests.post(api_url, headers=headers, json=payload) response.raise_for_status() return response.json() # Example query query = "What is Bezalel Ministries' hiring process?" result = make_non_grounded_request(query) print(result['choices'][0]['message']['content']) # Result: Generic hiring advice, not specific to Bezalel ``` ```javascript JavaScript theme={null} const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; async function makeNonGroundedRequest(query, apiKey = API_KEY) { // Standard completion WITHOUT RAG - may hallucinate const apiUrl = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions'; const payload = { messages: [{ role: 'user', content: query }], auto_routing: true, max_tokens: 500, }; const response = await fetch(apiUrl, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); return response.json(); } // Example query const query = "What is Bezalel Ministries' hiring process?"; const result = await makeNonGroundedRequest(query); console.log(result.choices[0].message.content); // Result: Generic hiring advice, not specific to Bezalel ``` ```typescript TypeScript theme={null} const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; interface CompletionResponse { choices: Array<{ message: { content: string; }; }>; sources_returned?: boolean; model?: string; } async function makeNonGroundedRequest( query: string, apiKey: string, ): Promise { // Standard completion WITHOUT RAG - may hallucinate const apiUrl = 'https://platform.ai.gloo.com/ai/v2/guarded/chat/completions'; const payload = { messages: [{ role: 'user', content: query }], auto_routing: true, max_tokens: 500, }; const response = await fetch(apiUrl, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); return response.json(); } // Example query const query = "What is Bezalel Ministries' hiring process?"; const result = await makeNonGroundedRequest(query); console.log(result.choices[0].message.content); // Result: Generic hiring advice, not specific to Bezalel ``` ```php PHP theme={null} [["role" => "user", "content" => $query]], "auto_routing" => true, "max_tokens" => 500 ]; $ch = curl_init($apiUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . $apiKey, "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); } // Example query $query = "What is Bezalel Ministries' hiring process?"; $result = makeNonGroundedRequest($query); echo $result['choices'][0]['message']['content']; // Result: Generic hiring advice, not specific to Bezalel ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) var apiKey string func init() { apiKey = os.Getenv("GLOO_API_KEY") if apiKey == "" { apiKey = "YOUR_API_KEY" } } type CompletionRequest struct { Messages []Message `json:"messages"` AutoRouting bool `json:"auto_routing"` MaxTokens int `json:"max_tokens"` } type Message struct { Role string `json:"role"` Content string `json:"content"` } type CompletionResponse struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` SourcesReturned bool `json:"sources_returned,omitempty"` Model string `json:"model,omitempty"` } func makeNonGroundedRequest(query string) (*CompletionResponse, error) { // Standard completion WITHOUT RAG - may hallucinate apiURL := "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions" payload := CompletionRequest{ Messages: []Message{ {Role: "user", Content: query}, }, AutoRouting: true, MaxTokens: 500, } jsonData, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result CompletionResponse json.NewDecoder(resp.Body).Decode(&result) return &result, nil } // Example usage func main() { query := "What is Bezalel Ministries' hiring process?" result, _ := makeNonGroundedRequest(query) fmt.Println(result.Choices[0].Message.Content) // Result: Generic hiring advice, not specific to Bezalel } ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; import com.google.gson.*; public class NonGroundedRequest { private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY"); public static JsonObject makeNonGroundedRequest(String query) throws Exception { // Standard completion WITHOUT RAG - may hallucinate String apiUrl = "https://platform.ai.gloo.com/ai/v2/guarded/chat/completions"; JsonObject message = new JsonObject(); message.addProperty("role", "user"); message.addProperty("content", query); JsonArray messages = new JsonArray(); messages.add(message); JsonObject payload = new JsonObject(); payload.add("messages", messages); payload.addProperty("auto_routing", true); payload.addProperty("max_tokens", 500); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("Authorization", "Bearer " + API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload.toString())) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); return JsonParser.parseString(response.body()).getAsJsonObject(); } // Example query public static void main(String[] args) throws Exception { String query = "What is Bezalel Ministries' hiring process?"; JsonObject result = makeNonGroundedRequest(query); String content = result.getAsJsonArray("choices") .get(0).getAsJsonObject() .getAsJsonObject("message") .get("content").getAsString(); System.out.println(content); // Result: Generic hiring advice, not specific to Bezalel } } ``` **What you'll see**: A generic response about hiring processes, or possibly "I don't have specific information about Bezalel Ministries." The model doesn't know about your organization because it wasn't trained on your content. *** ## Step 2: Grounding on Your Publisher Now let's use the `/grounded` endpoint with the `rag_publisher` parameter to ground on YOUR specific content. ### The Publisher Grounded Request This adds two key changes from Step 1: the `/grounded` endpoint and the `rag_publisher` parameter: ```python Python theme={null} import requests import os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("GLOO_API_KEY", "YOUR_API_KEY") def make_publisher_grounded_request(query, publisher_name, api_key=API_KEY, sources_limit=3): """Grounded on YOUR publisher - accurate, source-backed.""" api_url = "https://platform.ai.gloo.com/ai/v2/grounded/chat/completions" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "messages": [{"role": "user", "content": query}], "auto_routing": True, "rag_publisher": publisher_name, # KEY: This grounds on YOUR content "sources_limit": sources_limit, "max_tokens": 500 } response = requests.post(api_url, headers=headers, json=payload) response.raise_for_status() return response.json() # Same query, now grounded on YOUR publisher query = "What is Bezalel Ministries' hiring process?" result = make_publisher_grounded_request(query, "Bezalel") print(result['choices'][0]['message']['content']) print(f"Sources used: {result.get('sources_returned')}") # Result: Detailed answer about Bezalel's 3-phase selection journey # Sources used: True ``` ```javascript JavaScript theme={null} const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; async function makePublisherGroundedRequest( query, publisherName, apiKey = API_KEY, sourcesLimit = 3, ) { // Grounded on YOUR publisher - accurate, source-backed const apiUrl = 'https://platform.ai.gloo.com/ai/v2/grounded/chat/completions'; const payload = { messages: [{ role: 'user', content: query }], auto_routing: true, rag_publisher: publisherName, // KEY: This grounds on YOUR content sources_limit: sourcesLimit, max_tokens: 500, }; const response = await fetch(apiUrl, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); return response.json(); } // Same query, now grounded on YOUR publisher const query = "What is Bezalel Ministries' hiring process?"; const result = await makePublisherGroundedRequest( query, 'Bezalel', ); console.log(result.choices[0].message.content); console.log(`Sources used: ${result.sources_returned}`); // Result: Detailed answer about Bezalel's 3-phase selection journey // Sources used: true ``` ```typescript TypeScript theme={null} const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; interface PublisherGroundedRequest { messages: Array<{ role: string; content: string }>; auto_routing: boolean; rag_publisher: string; sources_limit: number; max_tokens: number; } async function makePublisherGroundedRequest( query: string, publisherName: string, apiKey: string = API_KEY, sourcesLimit: number = 3, ): Promise { // Grounded on YOUR publisher - accurate, source-backed const apiUrl = 'https://platform.ai.gloo.com/ai/v2/grounded/chat/completions'; const payload: PublisherGroundedRequest = { messages: [{ role: 'user', content: query }], auto_routing: true, rag_publisher: publisherName, // KEY: This grounds on YOUR content sources_limit: sourcesLimit, max_tokens: 500, }; const response = await fetch(apiUrl, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); return response.json(); } // Same query, now grounded on YOUR publisher const query = "What is Bezalel Ministries' hiring process?"; const result = await makePublisherGroundedRequest( query, 'Bezalel', ); console.log(result.choices[0].message.content); console.log(`Sources used: ${result.sources_returned}`); // Result: Detailed answer about Bezalel's 3-phase selection journey // Sources used: true ``` ```php PHP theme={null} [["role" => "user", "content" => $query]], "auto_routing" => true, "rag_publisher" => $publisherName, // KEY: This grounds on YOUR content "sources_limit" => $sourcesLimit, "max_tokens" => 500 ]; $ch = curl_init($apiUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . $apiKey, "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); } // Same query, now grounded on YOUR publisher $query = "What is Bezalel Ministries' hiring process?"; $result = makePublisherGroundedRequest($query, "Bezalel"); echo $result['choices'][0]['message']['content'] . "\n"; echo "Sources used: " . ($result['sources_returned'] ? 'true' : 'false'); // Result: Detailed answer about Bezalel's 3-phase selection journey // Sources used: true ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "net/http" ) type PublisherGroundedRequest struct { Messages []Message `json:"messages"` AutoRouting bool `json:"auto_routing"` RagPublisher string `json:"rag_publisher"` // KEY: This grounds on YOUR content SourcesLimit int `json:"sources_limit"` MaxTokens int `json:"max_tokens"` } func makePublisherGroundedRequest(query, publisherName string, sourcesLimit int) (*GroundedResponse, error) { // Grounded on YOUR publisher - accurate, source-backed apiURL := "https://platform.ai.gloo.com/ai/v2/grounded/chat/completions" payload := PublisherGroundedRequest{ Messages: []Message{ {Role: "user", Content: query}, }, AutoRouting: true, RagPublisher: publisherName, SourcesLimit: sourcesLimit, MaxTokens: 500, } jsonData, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result GroundedResponse json.NewDecoder(resp.Body).Decode(&result) return &result, nil } // Example usage func main() { query := "What is Bezalel Ministries' hiring process?" result, _ := makePublisherGroundedRequest(query, "Bezalel", 3) fmt.Println(result.Choices[0].Message.Content) fmt.Printf("Sources used: %v\n", result.SourcesReturned) // Result: Detailed answer about Bezalel's 3-phase selection journey // Sources used: true } ``` ```java Java theme={null} import java.net.http.*; import java.net.URI; import com.google.gson.*; public class PublisherGroundedRequest { private static final String API_KEY = System.getenv().getOrDefault("GLOO_API_KEY", "YOUR_API_KEY"); public static JsonObject makePublisherGroundedRequest( String query, String publisherName, int sourcesLimit) throws Exception { // Grounded on YOUR publisher - accurate, source-backed String apiUrl = "https://platform.ai.gloo.com/ai/v2/grounded/chat/completions"; JsonObject message = new JsonObject(); message.addProperty("role", "user"); message.addProperty("content", query); JsonArray messages = new JsonArray(); messages.add(message); JsonObject payload = new JsonObject(); payload.add("messages", messages); payload.addProperty("auto_routing", true); payload.addProperty("rag_publisher", publisherName); // KEY: This grounds on YOUR content payload.addProperty("sources_limit", sourcesLimit); payload.addProperty("max_tokens", 500); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("Authorization", "Bearer " + API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload.toString())) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); return JsonParser.parseString(response.body()).getAsJsonObject(); } // Same query, now grounded on YOUR publisher public static void main(String[] args) throws Exception { String query = "What is Bezalel Ministries' hiring process?"; JsonObject result = makePublisherGroundedRequest(query, "Bezalel", 3); String content = result.getAsJsonArray("choices") .get(0).getAsJsonObject() .getAsJsonObject("message") .get("content").getAsString(); boolean sourcesReturned = result.get("sources_returned").getAsBoolean(); System.out.println(content); System.out.println("Sources used: " + sourcesReturned); // Result: Detailed answer about Bezalel's 3-phase selection journey // Sources used: true } } ``` **What you'll see**: A detailed, accurate response about Bezalel's specific 3-phase selection journey, pulled directly from their uploaded content. The `sources_returned: true` flag confirms RAG found and used relevant sources. **Why this query works**: The Bezalel publisher contains a document titled "bezalel\_hiring\_process.txt" that describes their selection journey in detail. When you ground on this publisher, the RAG system retrieves this document before generating the response. For your own use case, design queries that match your uploaded content. If you upload product documentation, ask product questions. If you upload HR policies, ask HR questions. ### Side-by-Side Comparison | Step | Endpoint | rag\_publisher | sources\_returned | Response Quality | | - | - | - | - | - | | 1. Non-grounded | `/ai/v2/guarded/chat/completions` | N/A | N/A | Generic/may hallucinate | | 2. Publisher grounded | `/ai/v2/grounded/chat/completions` | Your publisher | `true` | Accurate, specific | *** ## Quick Reference: Publisher & Query Examples Different content types require different query approaches: | Content Type | Publisher Example | Good Query Examples | Why It Works | | - | - | - | - | | **Organizational Docs** | "Bezalel" | "What is the hiring process?"
"What benefits do employees get?" | Documents contain policy/process descriptions | | **Bible Study Resources** | "FaithLibrary" | "What study guides are available on the Book of Romans?"
"How do I start a small group Bible study?" | Study materials answer how-to and topical questions | | **Pastoral Care Support** | "CareMinistry" | "What counseling services are available?"
"How do I request prayer support?" | Ministry docs answer member care questions | | **Educational Content** | "CourseCatalog" | "What courses are available on Python?"
"What are the prerequisites for Course 101?" | Course descriptions match query topics | **Key Pattern**: Your queries should directly relate to the information in your uploaded documents. *** ## Step 3: Comparing Side-by-Side The most powerful way to demonstrate RAG's value is to compare both approaches. Here's how to build a comparison function: ```python Python theme={null} def compare_responses(query, publisher_name, api_key=API_KEY): """Compare non-grounded vs publisher-grounded responses.""" print(f"\nQuery: {query}") print("=" * 80) # Step 1: Non-grounded print("\n🔹 STEP 1: NON-GROUNDED Response (Generic Model Knowledge):") print("-" * 80) non_grounded = make_non_grounded_request(query, api_key) print(non_grounded['choices'][0]['message']['content']) print(f"\n📊 Sources used: {non_grounded.get('sources_returned', False)}") print("\n" + "=" * 80 + "\n") # Step 2: Publisher grounded print("🔹 STEP 2: GROUNDED on Your Publisher (Your Specific Content):") print("-" * 80) publisher_grounded = make_publisher_grounded_request(query, publisher_name, api_key) print(publisher_grounded['choices'][0]['message']['content']) print(f"\n📊 Sources used: {publisher_grounded.get('sources_returned', False)}") print(f"📊 Model: {publisher_grounded.get('model')}") # Run comparison compare_responses( "What is Bezalel Ministries' hiring process?", "Bezalel", ) ``` ```javascript JavaScript theme={null} const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; async function compareResponses(query, publisherName, apiKey = API_KEY) { // Compare non-grounded vs publisher-grounded responses console.log(`\nQuery: ${query}`); console.log('='.repeat(80)); // Step 1: Non-grounded console.log('\n🔹 STEP 1: NON-GROUNDED Response (Generic Model Knowledge):'); console.log('-'.repeat(80)); const nonGrounded = await makeNonGroundedRequest(query, apiKey); console.log(nonGrounded.choices[0].message.content); console.log(`\n📊 Sources used: ${nonGrounded.sources_returned || false}`); console.log('\n' + '='.repeat(80) + '\n'); // Step 2: Publisher grounded console.log('🔹 STEP 2: GROUNDED on Your Publisher (Your Specific Content):'); console.log('-'.repeat(80)); const publisherGrounded = await makePublisherGroundedRequest( query, publisherName, apiKey, ); console.log(publisherGrounded.choices[0].message.content); console.log(`\n📊 Sources used: ${publisherGrounded.sources_returned}`); console.log(`📊 Model: ${publisherGrounded.model}`); } // Run comparison await compareResponses( "What is Bezalel Ministries' hiring process?", 'Bezalel', ); ``` ```typescript TypeScript theme={null} const API_KEY = process.env.GLOO_API_KEY || 'YOUR_API_KEY'; async function compareResponses( query: string, publisherName: string, apiKey: string = API_KEY, ): Promise { // Compare non-grounded vs publisher-grounded responses console.log(`\nQuery: ${query}`); console.log('='.repeat(80)); // Step 1: Non-grounded console.log('\n🔹 STEP 1: NON-GROUNDED Response (Generic Model Knowledge):'); console.log('-'.repeat(80)); const nonGrounded = await makeNonGroundedRequest(query, apiKey); console.log(nonGrounded.choices[0].message.content); console.log(`\n📊 Sources used: ${nonGrounded.sources_returned || false}`); console.log('\n' + '='.repeat(80) + '\n'); // Step 2: Publisher grounded console.log('🔹 STEP 2: GROUNDED on Your Publisher (Your Specific Content):'); console.log('-'.repeat(80)); const publisherGrounded = await makePublisherGroundedRequest( query, publisherName, apiKey, ); console.log(publisherGrounded.choices[0].message.content); console.log(`\n📊 Sources used: ${publisherGrounded.sources_returned}`); console.log(`📊 Model: ${publisherGrounded.model}`); } // Run comparison await compareResponses( "What is Bezalel Ministries' hiring process?", 'Bezalel', ); ``` ```php PHP theme={null} ### Example Output ``` Query: What is Bezalel Ministries' hiring process? ================================================================================ 🔹 STEP 1: NON-GROUNDED Response (Generic Model Knowledge): -------------------------------------------------------------------------------- I cannot provide specific details about the internal hiring process for Bezalel Ministries, as that information is not publicly available... 📊 Sources used: false ================================================================================ 🔹 STEP 2: GROUNDED on Your Publisher (Your Specific Content): -------------------------------------------------------------------------------- Bezalel Ministries views its hiring process as discerning a divine calling, modeling their approach on God's selection of Bezalel in Exodus. Their process includes three phases: calling alignment through structured interviews about spiritual gifts, skills assessment with portfolio reviews, and a discernment period where candidates spend time with the team... 📊 Sources used: true 📊 Model: gloo-google-gemini-2.5-pro ``` Notice the difference: * **Step 1 (Non-grounded)**: Generic or "I don't have specific information" * **Step 2 (Publisher grounded)**: Detailed, accurate answer from Bezalel's actual documentation *** ## Testing Your Implementation This tutorial provides two testing paths: 1. **Quick Start (Recommended)**: Test the Bezalel example to validate your setup 2. **Your Own Content**: Adapt the code to use your publisher and content *** ### Phase 1: Quick Start with Bezalel Example First, validate your setup works by running the provided Bezalel example. #### Setup 1. **Install dependencies** for your language (see cookbook READMEs) 2. **Configure `.env`** with your credentials: ```bash theme={null} GLOO_API_KEY=your_api_key PUBLISHER_NAME=Bezalel ``` 3. **Create the Bezalel publisher** in [Gloo Studio](https://studio.ai.gloo.com): * Create a new publisher named "Bezalel" (exact name, case-sensitive) * Upload the 5 sample content files from the [cookbook's `content/` directory](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/completions-grounded/content) * Wait for all documents to show "Completed" status before proceeding #### Run the Comparison Each language implementation includes a ready-to-run comparison demo: ```bash Python theme={null} python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt python main.py ``` ```bash JavaScript theme={null} npm install npm start ``` ```bash TypeScript theme={null} npm install npm start ``` ```bash PHP theme={null} composer install php index.php ``` ```bash Go theme={null} go mod download go run main.go ``` ```bash Java theme={null} mvn clean install mvn exec:java ``` #### What to Look For When testing with ANY publisher (Bezalel or your own): ✅ **Step 1 (Non-grounded)**: * Generic response not specific to your content * Example: "A typical hiring process involves..." (for any hiring query) ✅ **Step 2 (Publisher grounded)**: * Specific, accurate information from YOUR uploaded documents * `sources_returned: true` (if this is false, see troubleshooting) * Example: For Bezalel → "3-phase selection journey with calling alignment..." * Example: For your product docs → "The enterprise plan is \$X/month and includes..." If Step 2 fails or returns generic responses, see the [Troubleshooting](#troubleshooting) section. *** ### Phase 2: Adapt to Your Own Content Once the Bezalel example works, adapt it to your own use case. #### Step 1: Prepare Your Content Before modifying code, ensure you have: 1. **Created a Publisher** in [Gloo Studio](https://studio.ai.gloo.com) * See [Upload Files to Data Engine](/tutorials/upload-files) recipe for detailed instructions 2. **Uploaded relevant documents** (PDFs, text files, Word docs, etc.) * Upload content that answers the questions your users will ask * Example: HR policies for HR questions, product docs for product questions 3. **Note your Publisher name** (you'll need this for the code) **Content Quality Matters**: The more relevant and well-structured your content, the better your grounded responses will be. Each document should focus on a specific topic area. #### Step 2: Update the Code Modify two areas in the code: **A. Update Environment Variables** In your `.env` file: ```bash theme={null} # Before PUBLISHER_NAME=Bezalel # After PUBLISHER_NAME=MyCompanyDocs ``` **B. Update Queries** Design queries that match your uploaded content: ```python theme={null} # Before (Bezalel example) queries = [ "What is Bezalel Ministries' hiring process?", "What educational resources does Bezalel Ministries provide?", ] # After (your content - example: e-commerce) queries = [ "What is the return policy for damaged items?", "How do I track my order?", ] # After (your content - example: technical docs) queries = [ "How do I configure SSL certificates?", "What are the system requirements for installation?", ] ``` #### Step 3: Run and Validate Run your modified code using the same commands from Phase 1. **Interpreting Results**: ✅ **Success indicators**: * Step 2 shows `sources_returned: true` * Step 2 response contains specific information from your documents * Step 2 is noticeably more accurate than Step 1 ⚠️ **If Step 2 is generic or wrong**: * Check that your publisher name matches exactly (case-sensitive) * Verify content is uploaded and indexed in Studio * Ensure your queries are relevant to your content * Try increasing `sources_limit` (change 3 to 5 or 10) #### Step 3: Refine Your Queries If results aren't accurate: 1. **Make queries more specific**: "What is the pricing?" → "What is the enterprise plan pricing for annual subscriptions?" 2. **Match content structure**: If your document is titled "Installation Guide," ask installation questions 3. **Test incrementally**: Start with one query, verify it works, then add more 4. **Check sources**: If available in the API response, inspect which documents were retrieved **Iterative Testing**: RAG quality improves with iteration. Test different queries, refine your content, and adjust `sources_limit` until you get optimal results. *** ## Working Code Sample Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions and Bezalel content files. *** ## Next Steps Now that you understand grounded completions: ### Use Your Own Content See [Phase 2: Adapt to Your Own Content](#phase-2-adapt-to-your-own-content) above for detailed instructions on: * Preparing and uploading your content * Updating the code for your publisher * Designing effective queries * Validating and refining your results ### Build a Production Chatbot Extend this recipe: * Add streaming for real-time responses * Implement conversation history * Extract and display source citations * [Verify answers against their sources](/best-practices/trustworthy-grounded-applications) before displaying them * Handle edge cases (no sources found, publisher not found) * Add user feedback collection ### Explore Advanced Features * **Adjust source limits**: Use `sources_limit` to control how many documents are retrieved * **Search without generation**: Use the [Search API](/api-reference/data/search-publisher-data) for RAG without completion * **Multi-turn conversations**: Maintain context across multiple grounded queries * **Hybrid approaches**: Mix grounded and non-grounded based on query type *** ## Troubleshooting ### Publisher Not Found Error ``` Error: Publisher 'YourPublisher' not found ``` **Solutions**: * Verify publisher name is **exact match** (case-sensitive) to Studio * Confirm publisher exists at [Gloo Studio](https://studio.ai.gloo.com) * Check you're using the correct account/credentials ### No Sources Returned ``` sources_returned: false (in grounded request) ``` **Solutions**: * Verify content is uploaded to the publisher in Studio * Check that your query is relevant to uploaded content * Increase `sources_limit` (try 5-10 for complex queries) * Ensure content has finished indexing in Studio ### Authentication Errors ``` 401 Unauthorized ``` **Solutions**: * Verify `GLOO_API_KEY` is correct * Get a fresh API key from the [API Credentials page](https://studio.ai.gloo.com/settings/api-keys) ### Generic Responses in Grounded Mode If Step 2 (publisher grounded) responses still seem generic: * Check `sources_returned` flag—should be `true` for Step 2 * Verify you're using the `rag_publisher` parameter * Confirm the right content is uploaded to your publisher in Studio * Try more specific queries that match your content * Review content in Studio to ensure it's indexed ### Step 2 Isn't Using My Content If Step 2 returns `sources_returned: false` or generic responses: **Check Publisher Name**: * Publisher names are **case-sensitive**: "MyPublisher" ≠ "mypublisher" * Verify exact name in [Gloo Studio](https://studio.ai.gloo.com) * Check for typos in your code or `.env` file **Verify Content is Indexed**: * Log into Gloo Studio * Navigate to your Publisher * Confirm documents show "Completed" status (not "Pending") * Wait 5-10 minutes after upload before testing **Query-Content Mismatch**: * Your query might not match your content * Example: Asking "What is the pricing?" when you only uploaded a "User Guide" * Solution: Ask questions your documents can answer **Increase sources\_limit**: * Default is 3 sources * Try 5-10 for complex queries: `sources_limit: 10` * More sources = broader retrieval, better coverage **Content Quality Issues**: * Scanned PDFs may have poor OCR * Very short documents (\<100 words) may not be retrieved * Highly technical jargon may need more context *** ## Additional Resources * [Completions V2 API Guide](/api-guides/completions-v2) - Complete API documentation * [Grounded Completions API Reference](/api-reference/completions/v2/grounded) - Full parameter details * [Upload Files Tutorial](/tutorials/upload-files) - Setting up Publishers and uploading documents * [Search API Reference](/api-reference/data/search-publisher-data) - RAG without generation * [Authentication Tutorial](/tutorials/authentication) - API key setup details # Streaming AI Responses in Real Time Source: https://docs.gloo.com/tutorials/completions-streaming Learn how to stream Gloo AI completions token-by-token using Server-Sent Events, with patterns for terminal rendering and a server-side proxy. ## Overview The Gloo AI completions API supports streaming responses, so instead of waiting for the full answer, your application receives tokens one at a time as the model generates them. This creates a faster, more interactive user experience and is the standard pattern for chat and content-generation products. In this tutorial you'll build a streaming client from scratch: parsing the SSE wire protocol, accumulating tokens, handling errors, and rendering output as it arrives. You'll also build a server-side proxy that shields your API credentials from the browser. ## What You'll Build By the end of this tutorial, you'll have a complete streaming implementation featuring: * **SSE stream parser** that reads tokens as they arrive from the API * **Token accumulator** that assembles the full response with timing and token count * **Streaming-aware error handler** that catches auth and rate-limit errors before reading the stream * **Terminal renderer** that displays tokens in real time with a typing effect * **Server-side proxy** that relays the stream to browser clients without exposing your credentials ## Understanding Server-Sent Events When you set `"stream": true` in a completions request, the API switches from a single JSON response to an SSE stream. Each token arrives as a line formatted `data: `, 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>() {}.getType()); } catch (Exception e) { return null; } } ``` 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> choices = (List>) chunk.get("choices"); if (choices == null || choices.isEmpty()) return ""; Map delta = (Map) choices.get(0).get("delta"); if (delta == null) return ""; Object content = delta.get("content"); return content != null ? content.toString() : ""; } catch (Exception e) { return ""; } } ``` 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. Gloo AI Streaming Demo browser page showing a streamed response to "What is my purpose in life" #### 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. The end-to-end Gloo journey in three phases. Set up your content: 1 create a publisher in Studio, 2 ingest content into the Data Engine, 3 attach metadata. Answer questions (at request time): 4 semantic search finds matching snippets, 5 grounded completions answer from your content, 6 your app displays the sources. Keep it current (ongoing): 7 update or delete items, 8 monitor status and debug errors. Purple stages are handled by you and your app; green stages are handled by Gloo. The end-to-end Gloo journey in three phases. Set up your content: 1 create a publisher in Studio, 2 ingest content into the Data Engine, 3 attach metadata. Answer questions (at request time): 4 semantic search finds matching snippets, 5 grounded completions answer from your content, 6 your app displays the sources. Keep it current (ongoing): 7 update or delete items, 8 monitor status and debug errors. Purple stages are handled by you and your app; green stages are handled by Gloo. 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**. Your Data in Studio *** ### 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: Publisher recommendation results showing content cards with relevance scores and optional snippet previews ### Affiliate Network Discovery The **Explore More** panel fires automatically alongside your publisher results, surfacing related content from across the Gloo network: Affiliate recommendation results showing resources from across the Gloo publisher 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: Search results showing content cards with titles, types, and relevance percentages ### 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: AI response generated from search results with source citations 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. Authenticated Gloo AI API flow Authenticated Gloo AI API flow 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: Example 1: Streaming Chat in Action - Lovable
Example 1: Streaming Chat in Action - Codex 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 2: Tool Use in Action - Bolt Example 2: Tool Use in Action - Claude Code ### 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: Example 3: Search in Action - Replit Agent Example 3: Search in Action - Antigravity ### 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?