DEV Community

shashank ms
shashank ms

Posted on

Integrating LLM into Mobile Apps

We are building a FastAPI backend that turns unstructured field notes into structured JSON for a mobile technician app. The agent extracts customer names, issue summaries, and follow-up tasks so crews can close jobs without paperwork. It is a pattern you can reuse for any mobile form-filling workflow.

What you'll need

Prerequisites are minimal. You will need Python 3.10 or newer, the OpenAI SDK, and an API key.

  • Python 3.10+
  • pip install fastapi uvicorn openai
  • An Oxlo.ai API key from https://portal.oxlo.ai (the free tier includes deepseek-v3.2 and 15+ other models)

Step 1: Scaffold the backend

I start with a minimal FastAPI app and a Pydantic model to accept the raw note from the mobile client.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class FieldNote(BaseModel):
    raw_text: str

@app.post("/structure-note")
async def structure_note(note: FieldNote):
    return {"status": "pending", "raw": note.raw_text}

Step 2: Define the system prompt

The prompt is the contract between the mobile app and the model. It tells the LLM exactly what JSON schema to return and how to behave when a field is missing.

SYSTEM_PROMPT = """You are a field-note parser for a mobile technician app.
Read the user's messy note and emit a single JSON object with these keys:
- customer_name: string, infer if possible else "Unknown"
- site_address: string, infer if possible else "Unknown"
- issue_summary: string, one sentence
- parts_used: list of strings, empty list if none mentioned
- follow_up_required: boolean
- urgency: one of "low", "medium", "high", "critical"

Rules:
- Output ONLY valid JSON. No markdown, no explanation.
- Use null for unknown values only when inference is impossible."""

Step 3: Connect to Oxlo.ai

Now I wire in the OpenAI SDK pointed at Oxlo.ai. I use qwen-3-32b because it handles agentic extraction reliably. Because Oxlo.ai charges per request instead of per token, a long voice transcript costs the same as a short one. That keeps the backend cost predictable as note length varies.

import os
from openai import OpenAI

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

@app.post("/structure-note")
async def structure_note(note: FieldNote):
    response = client.chat.completions.create(
        model="qwen-3-32b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": note.raw_text},
        ],
    )
    return {"structured": response.choices[0].message.content}

Step 4: Enforce JSON mode

Mobile clients crash on malformed JSON, so I enable JSON mode in the API call and parse the result server-side before returning it.

import json
from fastapi.responses import JSONResponse

@app.post("/structure-note")
async def structure_note(note: FieldNote):
    response = client.chat.completions.create(
        model="qwen-3-32b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": note.raw_text},
        ],
        response_format={"type": "json_object"},
    )

    raw_content = response.choices[0].message.content
    parsed = json.loads(raw_content)

    return JSONResponse(content={"data": parsed})

Step 5: Add fallback logic

Voice-to-text on job sites can produce garbage. I wrap the call in a fallback that retries with deepseek-v3.2 if the first model returns broken JSON. With Oxlo.ai's flat per-request pricing, a retry does not balloon the bill based on transcript length the way token-based providers would.

from fastapi import HTTPException

def extract_note(raw_text: str, attempt: int = 0) -> dict:
    model = "qwen-3-32b" if attempt == 0 else "deepseek-v3.2"
    try:
        resp = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": raw_text},
            ],
            response_format={"type": "json_object"},
        )
        return json.loads(resp.choices[0].message.content)
    except (json.JSONDecodeError, KeyError):
        if attempt == 0:
            return extract_note(raw_text, attempt=1)
        raise HTTPException(status_code=422, detail="Unparseable note after retry")

@app.post("/structure-note")
async def structure_note(note: FieldNote):
    data = extract_note(note.raw_text)
    return JSONResponse(content={"data": data})

Run it

Start the server locally.

uvicorn main:app --reload --port 8000

Send a test payload that mimics a messy voice-to-text transcript from a phone.

curl -X POST http://localhost:8000/structure-note \
  -H "Content-Type: application/json" \
  -d '{"raw_text": "Customer bob at 123 main st AC blowing warm air replaced capacitor fan still not spinning right needs callback tomorrow urgent"}'

The backend returns structured JSON ready for your mobile UI.

{
  "data": {
    "customer_name": "Bob",
    "site_address": "123 main st",
    "issue_summary": "AC blowing warm air, replaced capacitor, fan still not spinning correctly",
    "parts_used": ["capacitor"],
    "follow_up_required": true,
    "urgency": "high"
  }
}

Next steps

Wire this endpoint into your iOS or Android networking layer. If you want to speed things up, add an Oxlo.ai embeddings call with bge-large to autocomplete site addresses from past notes before the extraction step. You can also swap in kimi-k2.6 if you need vision support for photos attached to the ticket.

Top comments (0)