Skip to main content

Django + Django REST Framework

This example creates a POST /api/assistant/ask/ endpoint in your Django backend. The browser only talks to it; Django calls Datafala and runs the SQL on a read-only database alias.

Reuse the client from the Python page in assistant/datafala.py, reading DATAFALA_API_KEY and DATAFALA_END_USER_SECRET from the environment variables.

Read-only database​

settings.py
DATABASES["readonly"] = {
"ENGINE": "django.db.backends.postgresql",
"NAME": env("READONLY_DB_NAME"),
"USER": env("READONLY_DB_USER"), # the datafala_ro role
"PASSWORD": env("READONLY_DB_PASSWORD"),
"HOST": env("READONLY_DB_HOST"),
"PORT": "5432",
"OPTIONS": {"options": "-c statement_timeout=5000 -c default_transaction_read_only=on"},
"TEST": {"MIRROR": "default"},
}

The alias is only used with a manual cursor; migrate and the models keep using default.

Execution​

assistant/runner.py
from django.db import connections


def run_readonly(sql: str, params: dict, max_rows: int = 1000):
# params ALWAYS, even empty: without it the driver does not handle the %% of the literals.
with connections["readonly"].cursor() as cursor:
cursor.execute(sql, params)
columns = [column[0] for column in cursor.description]
rows = cursor.fetchmany(max_rows)
return columns, rows

View​

assistant/views.py
from django.db import DatabaseError
from rest_framework import serializers, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from . import datafala
from .runner import run_readonly

# Error codes whose (already translated) message can be shown to the user.
USER_FACING = {"no_answer", "sql_rejected", "question_too_long", "rate_limited"}


class AskSerializer(serializers.Serializer):
question = serializers.CharField(max_length=2000)


class AskView(APIView):
permission_classes = [IsAuthenticated]

def post(self, request):
body = AskSerializer(data=request.data)
body.is_valid(raise_exception=True)
try:
answer = datafala.ask(body.validated_data["question"], user_id=request.user.pk)
except datafala.DatafalaError as error:
if error.code in USER_FACING:
code = status.HTTP_429_TOO_MANY_REQUESTS if error.code == "rate_limited" else 422
return Response({"code": error.code, "message": error.message}, status=code)
return Response(
{"code": "assistant_unavailable"}, status=status.HTTP_503_SERVICE_UNAVAILABLE
)

try:
columns, rows = run_readonly(answer["sql"], answer["params"])
except DatabaseError:
return Response(
{"code": "query_failed", "request_id": answer["request_id"]}, status=422
)

return Response(
{
"request_id": answer["request_id"],
"explanation": answer["explanation"],
"sql": answer["sql"],
"columns": columns,
"rows": rows,
}
)


class FeedbackView(APIView):
permission_classes = [IsAuthenticated]

def post(self, request):
datafala.feedback(
request.data["request_id"], request.data["rating"], request.data.get("comment")
)
return Response(status=status.HTTP_204_NO_CONTENT)

Points of attention​

  • Web server timeout: the LLM call can take tens of seconds. Adjust the Gunicorn timeout (--timeout 90) and the reverse proxy for this route.
  • Permission: release the route only to people who may see the data in the registered tables. In multi-company systems, read Data scope.
  • Audit: store in your database who asked, the question and the request_id. Datafala's history belongs to the whole tenant, not to each user.