DEV Community

shashank ms
shashank ms

Posted on

Integrating LLM into Web Applications

We're going to build a customer support chat backend that drops into any existing web frontend. It streams responses from an LLM hosted on Oxlo.ai, using flat per-request pricing so long user messages and conversation history do not inflate costs. By the end you will have a working FastAPI endpoint and a minimal HTML page to exercise it.

What you'll need

You will need Python 3.10 or newer, an Oxlo.ai API key from https://portal.oxlo.ai, and the OpenAI SDK. Install the dependencies with pip.

pip install openai fastapi uvicorn python-dotenv

Step 1: Scaffold the project

Create a directory for the service and a virtual environment to keep things isolated. I also create a .env file to hold the Oxlo.ai key so it is not hard-coded.

mkdir support-agent && cd support-agent
python -m venv .venv
source .venv/bin/activate  # on Windows use .venv\Scripts\activate

# .env
OXLO_API_KEY=YOUR_OXLO_API_KEY

Step 2: Configure the Oxlo.ai client and system prompt

Create main.py and initialize the OpenAI-compatible client pointing at Oxlo.ai. I load the key from the environment and define a system prompt that keeps the agent focused on our fictional store policies.

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    base_url="https://api.oxlo.ai/v1",
    api_key=os.getenv("OXLO_API_KEY"),
)

The system prompt below governs tone and boundaries. Edit it to match your actual product.

SYSTEM_PROMPT = """You are a customer support agent for Acme Widgets.
- Answer questions about shipping, returns, and warranty.
- Be concise. Ask for clarification if a request is ambiguous.
- Do not make up policy. If you do not know, offer to escalate.
- Current policies: Free shipping over $50. Returns accepted within 30 days.
"""

Step 3: Build the chat endpoint with conversation history

I use a simple in-memory dictionary to track sessions by a browser-generated ID. The endpoint appends the latest user message to the history and sends the full list to Oxlo.ai so the model retains context.

import os
from dotenv import load_dotenv
from openai import OpenAI
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Dict

load_dotenv()

client = OpenAI(
    base_url="https://api.oxlo.ai/v1",
    api_key=os.getenv("OXLO_API_KEY"),
)

SYSTEM_PROMPT = """You are a customer support agent for Acme Widgets.
- Answer questions about shipping, returns, and warranty.
- Be concise. Ask for clarification if a request is ambiguous.
- Do not make up policy. If you do not know, offer to escalate.
- Current policies: Free shipping over $50. Returns accepted within 30 days.
"""

app = FastAPI()

sessions: Dict[str, List[dict]] = {}

class ChatRequest(BaseModel):
    session_id: str
    message: str

@app.post("/chat")
async def chat(req: ChatRequest):
    if req.session_id not in sessions:
        sessions[req.session_id] = [
            {"role": "system", "content": SYSTEM_PROMPT}
        ]

    sessions[req.session_id].append(
        {"role": "user", "content": req.message}
    )

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=sessions[req.session_id],
    )

    assistant_msg = response.choices[0].message.content
    sessions[req.session_id].append(
        {"role": "assistant", "content": assistant_msg}
    )

    return {"reply": assistant_msg}

Step 4: Add streaming for real-time tokens

Waiting for the full completion feels slow in a browser, so I switch on streaming. The endpoint now returns a FastAPI StreamingResponse that yields text chunks as Oxlo.ai generates them.

import os
from dotenv import load_dotenv
from openai import OpenAI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List, Dict

load_dotenv()

client = OpenAI(
    base_url="https://api.oxlo.ai/v1",
    api_key=os.getenv("OXLO_API_KEY"),
)

SYSTEM_PROMPT = """You are a customer support agent for Acme Widgets.
- Answer questions about shipping, returns, and warranty.
- Be concise. Ask for clarification if a request is ambiguous.
- Do not make up policy. If you do not know, offer to escalate.
- Current policies: Free shipping over $50. Returns accepted within 30 days.
"""

app = FastAPI()

sessions: Dict[str, List[dict]] = {}

class ChatRequest(BaseModel):
    session_id: str
    message: str

@app.post("/chat")
async def chat(req: ChatRequest):
    if req.session_id not in sessions:
        sessions[req.session_id] = [
            {"role": "system", "content": SYSTEM_PROMPT}
        ]

    sessions[req.session_id].append(
        {"role": "user", "content": req.message}
    )

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=sessions[req.session_id],
        stream=True,
    )

    def event_generator():
        assistant_buffer = ""
        for chunk in response:
            delta = chunk.choices[0].delta.content or ""
            assistant_buffer += delta
            yield delta

        sessions[req.session_id].append(
            {"role": "assistant", "content": assistant_buffer}
        )

    return StreamingResponse(event_generator(), media_type="text/plain")

Step 5: Create a test frontend

I add a static file mount so FastAPI serves an HTML test page. This file posts to /chat and appends each chunk to the page as it arrives, giving you a working demo without React or Webpack.

import os
from fastapi.staticfiles import StaticFiles

os.makedirs("static", exist_ok=True)
app.mount("/static", StaticFiles(directory="static"), name="static")

Save this as static/index.html.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Support Agent</title>
  <style>
    body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
    #log { border: 1px solid #ccc; padding: 12px; height: 300px; overflow-y: auto; margin-bottom: 12px; }
    .msg { margin: 4px 0; }
    .user { color: #0366d6; }
    .bot { color: #22863a; }
  </style>
</head>
<body>
  <h2>Acme Widgets Support</h2>
  <div id="log"></div>
  <input id="inp" type="text" placeholder="Ask about shipping or returns..." style="width: 70%;" />
  <button onclick="send()">Send</button>

  <script>
    const sessionId = Math.random().toString(36).slice(2);
    const log = document.getElementById('log');
    const inp = document.getElementById('inp');

    function append(cls, text) {
      const d = document.createElement('div');
      d.className = 'msg ' + cls;
      d.textContent = text;
      log.appendChild(d);
      log.scrollTop = log.scrollHeight;
    }

    async function send() {
      const text = inp.value.trim();
      if (!text) return;
      append('user', 'You: ' + text);
      inp.value = '';

      const res = await fetch('/chat', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({session_id: sessionId, message: text})
      });

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let botText = '';
      const botEl = document.createElement('div');
      botEl.className = 'msg bot';
      botEl.textContent = 'Agent: ';
      log.appendChild(botEl);

      while (true) {
        const {done, value} = await reader.read();
        if (done) break;
        botText += decoder.decode(value, {stream: true});
        botEl.textContent = 'Agent: ' + botText;
        log.scrollTop = log.scrollHeight;
      }
    }

    inp.addEventListener('keydown', e => { if (e.key === 'Enter') send(); });
  </script>
</body>
</html>

Run it

Start the server with Uvicorn and open http://localhost:8000/static/index.html in your browser.

uvicorn main:app --reload

Try pasting a long message with order details. Because Oxlo.ai charges per request rather than per token, a multi-paragraph user message costs the same as a one-word message. The agent should answer policy questions and remember prior turns.

Example interaction after asking "What is your return policy?" and then "Does that include opened items?":

Agent: We accept returns within 30 days of delivery.
Agent: Yes, opened items can be returned within the same 30-day window as long as they are not damaged.

Wrap-up

The endpoint is ready to integrate into your actual frontend. Two concrete next steps: wire the session store to Redis so history survives restarts, and add a tool-calling loop so the agent can look up real order IDs via your existing API. Oxlo.ai supports function calling on models like Llama 3.3 70B and Qwen 3 32B, so you can expand this into a fully agentic workflow without changing your integration code.

Top comments (0)