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)