Complete Full-Stack AI Persona Generator: Django REST API + React Frontend with Ollama Integration Author: Daniel Kliewer Date: 2024-10-18 Tags: Django, React, AI, Ollama, LLM, Persona, Tutorial, Python, TypeScript, REST API, Full-Stack, Web Development Description: Comprehensive tutorial for building a sophisticated full-stack application using Django backend with REST API, React TypeScript frontend, and Ollama LLM integration for AI-powered persona-based content generation. ---![Image](/images/ComfyUI_00190_.png) # Building a Full-Stack Application with Django and React: A Step-by-Step Guide In this comprehensive guide, we'll walk through the process of building a full-stack application using Django for the backend and React for the frontend. The application allows users to upload a writing sample, analyzes it using an AI language model, and generates blog posts in the style of the uploaded sample. GitHub Repository: [kliewerdaniel/Django-React-Ollama-Integration](https://github.com/kliewerdaniel/Django-React-Ollama-Integration) ## Introduction This guide aims to help you build a full-stack application that: - **Backend (Django):** - Allows users to upload a writing sample. - Analyzes the writing sample using an AI language model. - Stores the analysis and allows generating new content based on the analysis. - **Frontend (React):** - Provides a user interface to upload writing samples. - Displays a list of saved personas (analysis results). - Allows generating and viewing blog posts in the style of the uploaded samples. --- ## Setting Up the Backend with Django ### Creating a Django Project First, ensure you have Python and Django installed. Create a new Django project and application: ```bash django-admin startproject backend cd backend python manage.py startapp core ``` ### Configuring Settings Update the `backend/settings.py` file to include the necessary configurations: - Add `rest_framework`, `core`, and `corsheaders` to `INSTALLED_APPS`. - Configure middleware to include `CorsMiddleware`. - Set up `CORS_ALLOWED_ORIGINS` to allow your frontend to communicate with the backend. ```python # backend/settings.py INSTALLED_APPS = [ # ... 'rest_framework', 'core', 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:3000', # Frontend URL ] ``` ### Defining Models Create models for `Persona` and `BlogPost` in `core/models.py`: ```python # core/models.py from django.db import models class Persona(models.Model): name = models.CharField(max_length=100) data = models.JSONField() def __str__(self): return self.name class BlogPost(models.Model): persona = models.ForeignKey(Persona, on_delete=models.CASCADE, related_name='blog_posts') title = models.CharField(max_length=200, blank=True, null=True) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.title or f"BlogPost {self.id}" ``` Apply the migrations: ```bash python manage.py makemigrations python manage.py migrate ``` ### Creating Serializers Define serializers to convert model instances to JSON and vice versa in `core/serializers.py`: ```python # core/serializers.py from rest_framework import serializers from .models import Persona, BlogPost from .utils import analyze_writing_sample import logging logger = logging.getLogger(__name__) class PersonaSerializer(serializers.ModelSerializer): writing_sample = serializers.CharField(write_only=True) class Meta: model = Persona fields = ['id', 'name', 'writing_sample', 'data'] read_only_fields = ['id', 'data'] def create(self, validated_data): writing_sample = validated_data.pop('writing_sample') logger.debug(f"Writing sample received: {writing_sample[:100]}...") analyzed_data = analyze_writing_sample(writing_sample) logger.debug(f"Analyzed data: {analyzed_data}") if not analyzed_data: logger.error("Failed to analyze the writing sample.") raise serializers.ValidationError({"writing_sample": "Analysis failed."}) validated_data['data'] = analyzed_data return Persona.objects.create(**validated_data) class BlogPostSerializer(serializers.ModelSerializer): persona = serializers.StringRelatedField() class Meta: model = BlogPost fields = ['id', 'persona', 'title', 'content', 'created_at'] ``` ### Writing Utility Functions Create utility functions in `core/utils.py` to interact with the AI language model and process responses: ```python # core/utils.py import logging import requests import json import re from decouple import config logger = logging.getLogger(__name__) OLLAMA_API_URL = config('OLLAMA_API_URL', default='http://localhost:11434/api/generate') def extract_json(response_text): decoder = json.JSONDecoder() pos = 0 while pos < len(response_text): try: obj, pos = decoder.raw_decode(response_text, pos) return obj except json.JSONDecodeError: pos += 1 return None def analyze_writing_sample(writing_sample): encoding_prompt = f''' Please analyze the writing style and personality of the given writing sample. Provide a detailed assessment of their characteristics using the following template. Rate each applicable characteristic on a scale of 1-10 where relevant, or provide a descriptive value. Return the results in a JSON format. "name": "[Author/Character Name]", "vocabulary_complexity": [1-10], "sentence_structure": "[simple/complex/varied]", "paragraph_organization": "[structured/loose/stream-of-consciousness]", "idiom_usage": [1-10], "metaphor_frequency": [1-10], "simile_frequency": [1-10], "tone": "[formal/informal/academic/conversational/etc.]", "punctuation_style": "[minimal/heavy/unconventional]", "contraction_usage": [1-10], "pronoun_preference": "[first-person/third-person/etc.]", "passive_voice_frequency": [1-10], "rhetorical_question_usage": [1-10], "list_usage_tendency": [1-10], "personal_anecdote_inclusion": [1-10], "pop_culture_reference_frequency": [1-10], "technical_jargon_usage": [1-10], "parenthetical_aside_frequency": [1-10], "humor_sarcasm_usage": [1-10], "emotional_expressiveness": [1-10], "emphatic_device_usage": [1-10], "quotation_frequency": [1-10], "analogy_usage": [1-10], "sensory_detail_inclusion": [1-10], "onomatopoeia_usage": [1-10], "alliteration_frequency": [1-10], "word_length_preference": "[short/long/varied]", "foreign_phrase_usage": [1-10], "rhetorical_device_usage": [1-10], "statistical_data_usage": [1-10], "personal_opinion_inclusion": [1-10], "transition_usage": [1-10], "reader_question_frequency": [1-10], "imperative_sentence_usage": [1-10], "dialogue_inclusion": [1-10], "regional_dialect_usage": [1-10], "hedging_language_frequency": [1-10], "language_abstraction": "[concrete/abstract/mixed]", "personal_belief_inclusion": [1-10], "repetition_usage": [1-10], "subordinate_clause_frequency": [1-10], "verb_type_preference": "[active/stative/mixed]", "sensory_imagery_usage": [1-10], "symbolism_usage": [1-10], "digression_frequency": [1-10], "formality_level": [1-10], "reflection_inclusion": [1-10], "irony_usage": [1-10], "neologism_frequency": [1-10], "ellipsis_usage": [1-10], "cultural_reference_inclusion": [1-10], "stream_of_consciousness_usage": [1-10], "openness_to_experience": [1-10], "conscientiousness": [1-10], "extraversion": [1-10], "agreeableness": [1-10], "emotional_stability": [1-10], "dominant_motivations": "[achievement/affiliation/power/etc.]", "core_values": "[integrity/freedom/knowledge/etc.]", "decision_making_style": "[analytical/intuitive/spontaneous/etc.]", "empathy_level": [1-10], "self_confidence": [1-10], "risk_taking_tendency": [1-10], "idealism_vs_realism": "[idealistic/realistic/mixed]", "conflict_resolution_style": "[assertive/collaborative/avoidant/etc.]", "relationship_orientation": "[independent/communal/mixed]", "emotional_response_tendency": "[calm/reactive/intense]", "creativity_level": [1-10], "age": "[age or age range]", "gender": "[gender]", "education_level": "[highest level of education]", "professional_background": "[brief description]", "cultural_background": "[brief description]", "primary_language": "[language]", "language_fluency": "[native/fluent/intermediate/beginner]", "background": "[A brief paragraph describing the author's context, major influences, and any other relevant information not captured above]" Writing Sample: {writing_sample} ''' payload = { 'model': 'llama3.2', # Replace with your Ollama model name 'prompt': encoding_prompt, 'stream': False } headers = {'Content-Type': 'application/json'} try: response = requests.post(OLLAMA_API_URL, json=payload, headers=headers) response.raise_for_status() json_str = re.search(r'\{.*?\}', response.text, re.DOTALL).group() analyzed_data = extract_json(response.text) if analyzed_data is None: logger.error("No JSON object found in the response.") return None return analyzed_data except (requests.RequestException, json.JSONDecodeError, AttributeError) as e: logger.error(f"Error during analyze_writing_sample: {str(e)}") return None def generate_content(persona_data, prompt): decoding_prompt = f''' You are to write a blog post in the style of {persona_data.get('name', 'Unknown Author')}, a writer with the following characteristics: {json.dumps(persona_data, indent=2)} Now, please write a response in this style about the following topic: "{prompt}" Begin with a compelling title that reflects the content of the post. ''' payload = { 'model': 'llama3.2', # Replace with your Ollama model name 'prompt': decoding_prompt, 'stream': False } headers = {'Content-Type': 'application/json'} try: logger.info(f"Sending request to OLLAMA API at {OLLAMA_API_URL} with payload: {payload}") response = requests.post(OLLAMA_API_URL, json=payload, headers=headers) logger.info(f"Received response from OLLAMA API: Status Code {response.status_code}") response.raise_for_status() response_json = response.json() response_content = response_json.get('response', '').strip() if not response_content: logger.error("OLLAMA API response 'response' field is empty.") return '' return response_content except requests.RequestException as e: logger.error(f"Error during generate_content: {e}") if hasattr(e, 'response') and e.response: logger.error(f"Ollama Response Status: {e.response.status_code}") logger.error(f"Ollama Response Body: {e.response.text}") return '' def save_blog_post(blog_post, title): # Implement if needed pass ``` ### Building Views Create views to handle API requests in `core/views.py`: ```python from django.shortcuts import render # Create your views here. import logging from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status, generics from .serializers import PersonaSerializer, BlogPostSerializer from .models import Persona, BlogPost from .utils import generate_content logger = logging.getLogger(__name__) class AnalyzeWritingSampleView(APIView): def post(self, request, *args, **kwargs): logger.debug(f"Request data: {request.data}") serializer = PersonaSerializer(data=request.data) if serializer.is_valid(): persona = serializer.save() return Response(PersonaSerializer(persona).data, status=status.HTTP_201_CREATED) else: logger.error(f"Serializer validation failed: {serializer.errors}") return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class GenerateContentView(APIView): def post(self, request): persona_id = request.data.get('persona_id') prompt = request.data.get('prompt') if not persona_id: logger.warning('persona_id is required.') return Response({'error': 'persona_id is required.'}, status=status.HTTP_400_BAD_REQUEST) if not prompt: logger.warning('prompt is required.') return Response({'error': 'prompt is required.'}, status=status.HTTP_400_BAD_REQUEST) try: persona = Persona.objects.get(id=persona_id) except Persona.DoesNotExist: logger.warning(f"Persona with ID {persona_id} not found.") return Response({'error': f'Persona with ID {persona_id} not found'}, status=status.HTTP_404_NOT_FOUND) blog_post_content = generate_content(persona.data, prompt) if not blog_post_content: logger.error('Failed to generate blog post.') return Response({'error': 'Failed to generate blog post.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Create BlogPost object lines = blog_post_content.strip().split('\n') title = lines[0] if lines else 'Untitled' content = '\n'.join(lines[1:]) if len(lines) > 1 else '' blog_post = BlogPost.objects.create( persona=persona, title=title, content=content ) return Response(BlogPostSerializer(blog_post).data, status=status.HTTP_201_CREATED) class PersonaListView(generics.ListAPIView): queryset = Persona.objects.all() serializer_class = PersonaSerializer class PersonaDetailView(APIView): def get(self, request, persona_id): try: persona = Persona.objects.get(id=persona_id) except Persona.DoesNotExist: logger.warning(f"Persona with ID {persona_id} not found.") return Response({'error': 'Persona not found'}, status=status.HTTP_404_NOT_FOUND) serializer = PersonaSerializer(persona) return Response(serializer.data, status=status.HTTP_200_OK) class BlogPostView(generics.ListAPIView): queryset = BlogPost.objects.all().order_by('-created_at') serializer_class = BlogPostSerializer ``` ### Setting Up URLs Define API endpoints in `core/urls.py`: ```python # core/urls.py from django.urls import path from .views import ( AnalyzeWritingSampleView, GenerateContentView, PersonaListView, PersonaDetailView, BlogPostView ) urlpatterns = [ path('analyze/', AnalyzeWritingSampleView.as_view(), name='analyze-writing-sample'), path('generate/', GenerateContentView.as_view(), name='generate-content'), path('personas/', PersonaListView.as_view(), name='persona-list'), path('personas//', PersonaDetailView.as_view(), name='persona-detail'), path('blog-posts/', BlogPostView.as_view(), name='blog-posts'), ] ``` Include the core app's URLs in the project's `urls.py`: ```python # backend/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('core.urls')), ] ``` --- ## Setting Up the Frontend with React ### Creating a React App Ensure you have Node.js and npm installed. Create a new React application: ```bash npx create-react-app frontend --template typescript cd frontend ``` Update `package.json` to include necessary dependencies: ```json // frontend/package.json { "name": "frontend", "version": "0.1.0", "private": true, "dependencies": { // ... "axios": "^1.7.7", "react-router-dom": "^6.27.0" }, // ... } ``` Install the new dependencies: ```bash npm install ``` ### Configuring Axios Create an Axios instance for consistent API calls in `src/axiosConfig.ts`: ```typescript // src/axiosConfig.ts import axios from 'axios'; const instance = axios.create({ baseURL: 'http://localhost:8000/api/', // Backend URL }); export default instance; ``` ### Building Components Create the following components: #### UploadSample Component Allows users to upload a writing sample. ```typescript import React, { useState } from 'react'; import axios from '../axiosConfig'; // Adjust the path if necessary const UploadSample: React.FC = () => { const [name, setName] = useState(''); const [writingSample, setWritingSample] = useState(''); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const payload = { name: name.trim(), writing_sample: writingSample.trim(), }; try { console.log('Payload being sent:', payload); const response = await axios.post('analyze/', payload); console.log('Response received:', response.data); setSuccess(`Persona "${response.data.name}" created successfully!`); setError(null); setName(''); setWritingSample(''); } catch (error: any) { console.error('Error uploading writing sample:', error); console.log('Error response:', error.response); if (error.response && error.response.data) { setError(JSON.stringify(error.response.data)); } else { setError('An error occurred while uploading the writing sample.'); } setSuccess(null); } }; return (

Upload Writing Sample

{error &&
Error: {error}
} {success &&
{success}
}
setName(e.target.value)} required maxLength={100} />
); }; export default UploadSample; ``` #### PersonaList Component Displays a list of saved personas. ```typescript // src/components/PersonaList.tsx import React, { useEffect, useState } from 'react'; import axios from '../axiosConfig'; import { useNavigate } from 'react-router-dom'; const PersonaList: React.FC = () => { import React, { useEffect, useState } from 'react'; import axios from '../axiosConfig'; // Adjust the path if necessary import { useNavigate } from 'react-router-dom'; interface Persona { id: number; name: string; data: Record; } const PersonaList: React.FC = () => { const [personas, setPersonas] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const navigate = useNavigate(); useEffect(() => { const fetchPersonas = async () => { try { const response = await axios.get('personas/'); setPersonas(response.data); } catch (err) { console.error('Error fetching personas:', err); setError('Failed to load personas.'); } finally { setLoading(false); } }; fetchPersonas(); }, []); const handleSelectPersona = (personaId: number) => { navigate(`/generate?personaId=${personaId}`); }; if (loading) return
Loading...
; if (error) return
{error}
; return (

Saved Personas

{personas.length === 0 ? (

No personas found.

) : (
    {personas.map((persona) => (
  • {persona.name}
  • ))}
)}
); }; export default PersonaList; ``` #### GenerateContent Component Allows generating content based on a selected persona. ```typescript // src/components/GenerateContent.tsx import React, { useState } from 'react'; import axios from '../axiosConfig'; import { useSearchParams } from 'react-router-dom'; const GenerateContent: React.FC = () => { import React, { useState } from 'react'; import axios from '../axiosConfig'; // Adjust the path if necessary import { useSearchParams } from 'react-router-dom'; interface BlogPost { id: number; persona: string; title: string; content: string; created_at: string; } const GenerateContent: React.FC = () => { const [searchParams] = useSearchParams(); const personaIdParam = searchParams.get('personaId'); const personaId = personaIdParam ? Number(personaIdParam) : null; const [prompt, setPrompt] = useState(''); const [content, setContent] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const handleGenerate = async () => { if (!prompt) { setError('Please enter a prompt.'); return; } if (!personaId) { setError('Invalid Persona ID.'); return; } setLoading(true); setError(null); try { const response = await axios.post('generate/', { persona_id: personaId, prompt: prompt, }); setContent(response.data); setError(null); setPrompt(''); } catch (err: any) { console.error('Error generating content:', err); if (err.response && err.response.data) { setError(JSON.stringify(err.response.data)); } else { setError('Failed to generate content.'); } } finally { setLoading(false); } }; return (

Generate Content