·17 min

Complete Full-Stack AI Persona Generator: Django REST API + React Frontend with Ollama Integration

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.

DK

Daniel Kliewer

Author, Sovereign AI

DjangoReactAIOllamaLLMPersonaTutorialPythonTypeScriptREST APIFull-StackWeb Development
Sovereign AI book cover

From the Book

This is from Sovereign AI: Building Local-First Intelligent Systems.

Get the Book — $88
Complete Full-Stack AI Persona Generator: Django REST API + React Frontend with Ollama Integration

Image

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

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
1django-admin startproject backend
2cd backend
3python 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
1# backend/settings.py
2
3INSTALLED_APPS = [
4 # ...
5 'rest_framework',
6 'core',
7 'corsheaders',
8]
9
10MIDDLEWARE = [
11 'corsheaders.middleware.CorsMiddleware',
12 # ...
13]
14
15CORS_ALLOWED_ORIGINS = [
16 'http://localhost:3000', # Frontend URL
17]

Defining Models

Create models for Persona and BlogPost in core/models.py:

python
1# core/models.py
2
3from django.db import models
4
5class Persona(models.Model):
6 name = models.CharField(max_length=100)
7 data = models.JSONField()
8
9 def __str__(self):
10 return self.name
11
12class BlogPost(models.Model):
13 persona = models.ForeignKey(Persona, on_delete=models.CASCADE, related_name='blog_posts')
14 title = models.CharField(max_length=200, blank=True, null=True)
15 content = models.TextField()
16 created_at = models.DateTimeField(auto_now_add=True)
17
18 def __str__(self):
19 return self.title or f"BlogPost {self.id}"

Apply the migrations:

bash
1python manage.py makemigrations
2python manage.py migrate

Creating Serializers

Define serializers to convert model instances to JSON and vice versa in core/serializers.py:

python
1# core/serializers.py
2
3from rest_framework import serializers
4from .models import Persona, BlogPost
5from .utils import analyze_writing_sample
6import logging
7
8logger = logging.getLogger(__name__)
9
10class PersonaSerializer(serializers.ModelSerializer):
11 writing_sample = serializers.CharField(write_only=True)
12
13 class Meta:
14 model = Persona
15 fields = ['id', 'name', 'writing_sample', 'data']
16 read_only_fields = ['id', 'data']
17
18 def create(self, validated_data):
19 writing_sample = validated_data.pop('writing_sample')
20 logger.debug(f"Writing sample received: {writing_sample[:100]}...")
21 analyzed_data = analyze_writing_sample(writing_sample)
22 logger.debug(f"Analyzed data: {analyzed_data}")
23 if not analyzed_data:
24 logger.error("Failed to analyze the writing sample.")
25 raise serializers.ValidationError({"writing_sample": "Analysis failed."})
26 validated_data['data'] = analyzed_data
27 return Persona.objects.create(**validated_data)
28
29class BlogPostSerializer(serializers.ModelSerializer):
30 persona = serializers.StringRelatedField()
31
32 class Meta:
33 model = BlogPost
34 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
1# core/utils.py
2
3import logging
4import requests
5import json
6import re
7from decouple import config
8
9logger = logging.getLogger(__name__)
10OLLAMA_API_URL = config('OLLAMA_API_URL', default='http://localhost:11434/api/generate')
11
12def extract_json(response_text):
13 decoder = json.JSONDecoder()
14 pos = 0
15 while pos < len(response_text):
16 try:
17 obj, pos = decoder.raw_decode(response_text, pos)
18 return obj
19 except json.JSONDecodeError:
20 pos += 1
21 return None
22
23def analyze_writing_sample(writing_sample):
24 encoding_prompt = f'''
25Please 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.
26
27
28 "name": "[Author/Character Name]",
29 "vocabulary_complexity": [1-10],
30 "sentence_structure": "[simple/complex/varied]",
31 "paragraph_organization": "[structured/loose/stream-of-consciousness]",
32 "idiom_usage": [1-10],
33 "metaphor_frequency": [1-10],
34 "simile_frequency": [1-10],
35 "tone": "[formal/informal/academic/conversational/etc.]",
36 "punctuation_style": "[minimal/heavy/unconventional]",
37 "contraction_usage": [1-10],
38 "pronoun_preference": "[first-person/third-person/etc.]",
39 "passive_voice_frequency": [1-10],
40 "rhetorical_question_usage": [1-10],
41 "list_usage_tendency": [1-10],
42 "personal_anecdote_inclusion": [1-10],
43 "pop_culture_reference_frequency": [1-10],
44 "technical_jargon_usage": [1-10],
45 "parenthetical_aside_frequency": [1-10],
46 "humor_sarcasm_usage": [1-10],
47 "emotional_expressiveness": [1-10],
48 "emphatic_device_usage": [1-10],
49 "quotation_frequency": [1-10],
50 "analogy_usage": [1-10],
51 "sensory_detail_inclusion": [1-10],
52 "onomatopoeia_usage": [1-10],
53 "alliteration_frequency": [1-10],
54 "word_length_preference": "[short/long/varied]",
55 "foreign_phrase_usage": [1-10],
56 "rhetorical_device_usage": [1-10],
57 "statistical_data_usage": [1-10],
58 "personal_opinion_inclusion": [1-10],
59 "transition_usage": [1-10],
60 "reader_question_frequency": [1-10],
61 "imperative_sentence_usage": [1-10],
62 "dialogue_inclusion": [1-10],
63 "regional_dialect_usage": [1-10],
64 "hedging_language_frequency": [1-10],
65 "language_abstraction": "[concrete/abstract/mixed]",
66 "personal_belief_inclusion": [1-10],
67 "repetition_usage": [1-10],
68 "subordinate_clause_frequency": [1-10],
69 "verb_type_preference": "[active/stative/mixed]",
70 "sensory_imagery_usage": [1-10],
71 "symbolism_usage": [1-10],
72 "digression_frequency": [1-10],
73 "formality_level": [1-10],
74 "reflection_inclusion": [1-10],
75 "irony_usage": [1-10],
76 "neologism_frequency": [1-10],
77 "ellipsis_usage": [1-10],
78 "cultural_reference_inclusion": [1-10],
79 "stream_of_consciousness_usage": [1-10],
80 "openness_to_experience": [1-10],
81 "conscientiousness": [1-10],
82 "extraversion": [1-10],
83 "agreeableness": [1-10],
84 "emotional_stability": [1-10],
85 "dominant_motivations": "[achievement/affiliation/power/etc.]",
86 "core_values": "[integrity/freedom/knowledge/etc.]",
87 "decision_making_style": "[analytical/intuitive/spontaneous/etc.]",
88 "empathy_level": [1-10],
89 "self_confidence": [1-10],
90 "risk_taking_tendency": [1-10],
91 "idealism_vs_realism": "[idealistic/realistic/mixed]",
92 "conflict_resolution_style": "[assertive/collaborative/avoidant/etc.]",
93"relationship_orientation": "[independent/communal/mixed]",
94"emotional_response_tendency": "[calm/reactive/intense]",
95"creativity_level": [1-10],
96"age": "[age or age range]",
97 "gender": "[gender]",
98 "education_level": "[highest level of education]",
99 "professional_background": "[brief description]",
100 "cultural_background": "[brief description]",
101 "primary_language": "[language]",
102 "language_fluency": "[native/fluent/intermediate/beginner]",
103 "background": "[A brief paragraph describing the author's context, major influences, and any other relevant information not captured above]"
104
105
106Writing Sample:
107{writing_sample}
108'''
109
110 payload = {
111 'model': 'llama3.2', # Replace with your Ollama model name
112 'prompt': encoding_prompt,
113 'stream': False
114 }
115 headers = {'Content-Type': 'application/json'}
116
117 try:
118 response = requests.post(OLLAMA_API_URL, json=payload, headers=headers)
119 response.raise_for_status()
120 json_str = re.search(r'\{.*?\}', response.text, re.DOTALL).group()
121 analyzed_data = extract_json(response.text)
122 if analyzed_data is None:
123 logger.error("No JSON object found in the response.")
124 return None
125 return analyzed_data
126 except (requests.RequestException, json.JSONDecodeError, AttributeError) as e:
127 logger.error(f"Error during analyze_writing_sample: {str(e)}")
128 return None
129
130def generate_content(persona_data, prompt):
131 decoding_prompt = f'''
132You are to write a blog post in the style of {persona_data.get('name', 'Unknown Author')}, a writer with the following characteristics:
133
134{json.dumps(persona_data, indent=2)}
135
136Now, please write a response in this style about the following topic:
137"{prompt}"
138Begin with a compelling title that reflects the content of the post.
139'''
140
141 payload = {
142 'model': 'llama3.2', # Replace with your Ollama model name
143 'prompt': decoding_prompt,
144 'stream': False
145 }
146 headers = {'Content-Type': 'application/json'}
147
148 try:
149 logger.info(f"Sending request to OLLAMA API at {OLLAMA_API_URL} with payload: {payload}")
150 response = requests.post(OLLAMA_API_URL, json=payload, headers=headers)
151 logger.info(f"Received response from OLLAMA API: Status Code {response.status_code}")
152
153 response.raise_for_status()
154
155 response_json = response.json()
156 response_content = response_json.get('response', '').strip()
157 if not response_content:
158 logger.error("OLLAMA API response 'response' field is empty.")
159 return ''
160
161 return response_content
162
163 except requests.RequestException as e:
164 logger.error(f"Error during generate_content: {e}")
165 if hasattr(e, 'response') and e.response:
166 logger.error(f"Ollama Response Status: {e.response.status_code}")
167 logger.error(f"Ollama Response Body: {e.response.text}")
168 return ''
169
170def save_blog_post(blog_post, title):
171 # Implement if needed
172 pass

Building Views

Create views to handle API requests in core/views.py:

python
1from django.shortcuts import render
2
3# Create your views here.
4import logging
5from rest_framework.views import APIView
6from rest_framework.response import Response
7from rest_framework import status, generics
8from .serializers import PersonaSerializer, BlogPostSerializer
9from .models import Persona, BlogPost
10from .utils import generate_content
11
12logger = logging.getLogger(__name__)
13
14class AnalyzeWritingSampleView(APIView):
15 def post(self, request, *args, **kwargs):
16 logger.debug(f"Request data: {request.data}")
17 serializer = PersonaSerializer(data=request.data)
18 if serializer.is_valid():
19 persona = serializer.save()
20 return Response(PersonaSerializer(persona).data, status=status.HTTP_201_CREATED)
21 else:
22 logger.error(f"Serializer validation failed: {serializer.errors}")
23 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
24
25class GenerateContentView(APIView):
26 def post(self, request):
27 persona_id = request.data.get('persona_id')
28 prompt = request.data.get('prompt')
29
30 if not persona_id:
31 logger.warning('persona_id is required.')
32 return Response({'error': 'persona_id is required.'}, status=status.HTTP_400_BAD_REQUEST)
33
34 if not prompt:
35 logger.warning('prompt is required.')
36 return Response({'error': 'prompt is required.'}, status=status.HTTP_400_BAD_REQUEST)
37
38 try:
39 persona = Persona.objects.get(id=persona_id)
40 except Persona.DoesNotExist:
41 logger.warning(f"Persona with ID {persona_id} not found.")
42 return Response({'error': f'Persona with ID {persona_id} not found'}, status=status.HTTP_404_NOT_FOUND)
43
44 blog_post_content = generate_content(persona.data, prompt)
45
46 if not blog_post_content:
47 logger.error('Failed to generate blog post.')
48 return Response({'error': 'Failed to generate blog post.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
49
50 # Create BlogPost object
51 lines = blog_post_content.strip().split('\n')
52 title = lines[0] if lines else 'Untitled'
53 content = '\n'.join(lines[1:]) if len(lines) > 1 else ''
54
55 blog_post = BlogPost.objects.create(
56 persona=persona,
57 title=title,
58 content=content
59 )
60
61 return Response(BlogPostSerializer(blog_post).data, status=status.HTTP_201_CREATED)
62
63class PersonaListView(generics.ListAPIView):
64 queryset = Persona.objects.all()
65 serializer_class = PersonaSerializer
66
67class PersonaDetailView(APIView):
68 def get(self, request, persona_id):
69 try:
70 persona = Persona.objects.get(id=persona_id)
71 except Persona.DoesNotExist:
72 logger.warning(f"Persona with ID {persona_id} not found.")
73 return Response({'error': 'Persona not found'}, status=status.HTTP_404_NOT_FOUND)
74
75 serializer = PersonaSerializer(persona)
76 return Response(serializer.data, status=status.HTTP_200_OK)
77
78class BlogPostView(generics.ListAPIView):
79 queryset = BlogPost.objects.all().order_by('-created_at')
80 serializer_class = BlogPostSerializer

Setting Up URLs

Define API endpoints in core/urls.py:

python
1# core/urls.py
2
3from django.urls import path
4from .views import (
5 AnalyzeWritingSampleView,
6 GenerateContentView,
7 PersonaListView,
8 PersonaDetailView,
9 BlogPostView
10)
11
12urlpatterns = [
13 path('analyze/', AnalyzeWritingSampleView.as_view(), name='analyze-writing-sample'),
14 path('generate/', GenerateContentView.as_view(), name='generate-content'),
15 path('personas/', PersonaListView.as_view(), name='persona-list'),
16 path('personas/<int:persona_id>/', PersonaDetailView.as_view(), name='persona-detail'),
17 path('blog-posts/', BlogPostView.as_view(), name='blog-posts'),
18]

Include the core app's URLs in the project's urls.py:

python
1# backend/urls.py
2
3from django.contrib import admin
4from django.urls import path, include
5
6urlpatterns = [
7 path('admin/', admin.site.urls),
8 path('api/', include('core.urls')),
9]

Setting Up the Frontend with React

Creating a React App

Ensure you have Node.js and npm installed. Create a new React application:

bash
1npx create-react-app frontend --template typescript
2cd frontend

Update package.json to include necessary dependencies:

json
1// frontend/package.json
2
3{
4 "name": "frontend",
5 "version": "0.1.0",
6 "private": true,
7 "dependencies": {
8 // ...
9 "axios": "^1.7.7",
10 "react-router-dom": "^6.27.0"
11 },
12 // ...
13}

Install the new dependencies:

bash
1npm install

Configuring Axios

Create an Axios instance for consistent API calls in src/axiosConfig.ts:

typescript
1// src/axiosConfig.ts
2
3import axios from 'axios';
4
5const instance = axios.create({
6 baseURL: 'http://localhost:8000/api/', // Backend URL
7});
8
9export default instance;

Building Components

Create the following components:

UploadSample Component

Allows users to upload a writing sample.

typescript
1import React, { useState } from 'react';
2import axios from '../axiosConfig'; // Adjust the path if necessary
3
4const UploadSample: React.FC = () => {
5 const [name, setName] = useState('');
6 const [writingSample, setWritingSample] = useState('');
7 const [error, setError] = useState<string | null>(null);
8 const [success, setSuccess] = useState<string | null>(null);
9
10 const handleSubmit = async (event: React.FormEvent) => {
11 event.preventDefault();
12
13 const payload = {
14 name: name.trim(),
15 writing_sample: writingSample.trim(),
16 };
17
18 try {
19 console.log('Payload being sent:', payload);
20 const response = await axios.post('analyze/', payload);
21 console.log('Response received:', response.data);
22 setSuccess(`Persona "${response.data.name}" created successfully!`);
23 setError(null);
24 setName('');
25 setWritingSample('');
26 } catch (error: any) {
27 console.error('Error uploading writing sample:', error);
28 console.log('Error response:', error.response);
29 if (error.response && error.response.data) {
30 setError(JSON.stringify(error.response.data));
31 } else {
32 setError('An error occurred while uploading the writing sample.');
33 }
34 setSuccess(null);
35 }
36 };
37
38 return (
39 <div>
40 <h2>Upload Writing Sample</h2>
41 {error && <div style={{ color: 'red' }}>Error: {error}</div>}
42 {success && <div style={{ color: 'green' }}>{success}</div>}
43 <form onSubmit={handleSubmit}>
44 <div>
45 <label htmlFor="name">Persona Name:</label>
46 <input
47 type="text"
48 id="name"
49 value={name}
50 onChange={(e) => setName(e.target.value)}
51 required
52 maxLength={100}
53 />
54 </div>
55 <div>
56 <label htmlFor="writingSample">Writing Sample:</label>
57 <textarea
58 id="writingSample"
59 value={writingSample}
60 onChange={(e) => setWritingSample(e.target.value)}
61 required
62 rows={10}
63 cols={50}
64 ></textarea>
65 </div>
66 <button type="submit">Submit</button>
67 </form>
68 </div>
69 );
70};
71
72export default UploadSample;

PersonaList Component

Displays a list of saved personas.

typescript
1// src/components/PersonaList.tsx
2
3import React, { useEffect, useState } from 'react';
4import axios from '../axiosConfig';
5import { useNavigate } from 'react-router-dom';
6
7const PersonaList: React.FC = () => {
8 import React, { useEffect, useState } from 'react';
9import axios from '../axiosConfig'; // Adjust the path if necessary
10import { useNavigate } from 'react-router-dom';
11
12interface Persona {
13 id: number;
14 name: string;
15 data: Record<string, any>;
16}
17
18const PersonaList: React.FC = () => {
19 const [personas, setPersonas] = useState<Persona[]>([]);
20 const [loading, setLoading] = useState<boolean>(true);
21 const [error, setError] = useState<string | null>(null);
22 const navigate = useNavigate();
23
24 useEffect(() => {
25 const fetchPersonas = async () => {
26 try {
27 const response = await axios.get('personas/');
28 setPersonas(response.data);
29 } catch (err) {
30 console.error('Error fetching personas:', err);
31 setError('Failed to load personas.');
32 } finally {
33 setLoading(false);
34 }
35 };
36
37 fetchPersonas();
38 }, []);
39
40 const handleSelectPersona = (personaId: number) => {
41 navigate(`/generate?personaId=${personaId}`);
42 };
43
44 if (loading) return <div className="loading">Loading...</div>;
45 if (error) return <div className="error">{error}</div>;
46
47 return (
48 <div>
49 <h2>Saved Personas</h2>
50 {personas.length === 0 ? (
51 <p>No personas found.</p>
52 ) : (
53 <ul>
54 {personas.map((persona) => (
55 <li key={persona.id}>
56 {persona.name}
57 <button onClick={() => handleSelectPersona(persona.id)}>
58 Generate Content
59 </button>
60 </li>
61 ))}
62 </ul>
63 )}
64 </div>
65 );
66};
67
68export default PersonaList;

GenerateContent Component

Allows generating content based on a selected persona.

typescript
1// src/components/GenerateContent.tsx
2
3import React, { useState } from 'react';
4import axios from '../axiosConfig';
5import { useSearchParams } from 'react-router-dom';
6
7const GenerateContent: React.FC = () => {
8import React, { useState } from 'react';
9import axios from '../axiosConfig'; // Adjust the path if necessary
10import { useSearchParams } from 'react-router-dom';
11
12interface BlogPost {
13 id: number;
14 persona: string;
15 title: string;
16 content: string;
17 created_at: string;
18}
19
20const GenerateContent: React.FC = () => {
21 const [searchParams] = useSearchParams();
22 const personaIdParam = searchParams.get('personaId');
23 const personaId = personaIdParam ? Number(personaIdParam) : null;
24 const [prompt, setPrompt] = useState<string>('');
25 const [content, setContent] = useState<BlogPost | null>(null);
26 const [loading, setLoading] = useState<boolean>(false);
27 const [error, setError] = useState<string | null>(null);
28
29 const handleGenerate = async () => {
30 if (!prompt) {
31 setError('Please enter a prompt.');
32 return;
33 }
34 if (!personaId) {
35 setError('Invalid Persona ID.');
36 return;
37 }
38 setLoading(true);
39 setError(null);
40
41 try {
42 const response = await axios.post('generate/', {
43 persona_id: personaId,
44 prompt: prompt,
45 });
46 setContent(response.data);
47 setError(null);
48 setPrompt('');
49 } catch (err: any) {
50 console.error('Error generating content:', err);
51 if (err.response && err.response.data) {
52 setError(JSON.stringify(err.response.data));
53 } else {
54 setError('Failed to generate content.');
55 }
56 } finally {
57 setLoading(false);
58 }
59 };
60
61 return (
62 <div>
63 <h2>Generate Content</h2>
64 <div>
65 <label htmlFor="prompt">Prompt:</label>
66 <textarea
67 id="prompt"
68 value={prompt}
69 onChange={(e) => setPrompt(e.target.value)}
70 placeholder="Enter a topic or prompt..."
71 rows={4}
72 cols={50}
73 required
74 />
75 </div>
76 <button onClick={handleGenerate} disabled={loading}>
77 {loading ? 'Generating...' : 'Generate Content'}
78 </button>
79 {error && <p className="error">Error: {error}</p>}
80 {content && (
81 <div>
82 <h3>{content.title}</h3>
83 <p>{content.content}</p>
84 </div>
85 )}
86 </div>
87 );
88};
89
90export default GenerateContent;

BlogPosts Component

Displays generated blog posts.

typescript
1import React, { useEffect, useState } from 'react';
2import axios from '../axiosConfig'; // Adjust the path if necessary
3interface BlogPost {
4 id: number;
5 persona: string;
6 title: string;
7 content: string;
8 created_at: string;
9}
10
11const BlogPosts: React.FC = () => {
12 const [blogPosts, setBlogPosts] = useState<BlogPost[]>([]);
13 const [loading, setLoading] = useState<boolean>(true);
14 const [error, setError] = useState<string | null>(null);
15
16 useEffect(() => {
17 const fetchBlogPosts = async () => {
18 try {
19 const response = await axios.get('blog-posts/');
20 setBlogPosts(response.data);
21 } catch (err) {
22 console.error('Error fetching blog posts:', err);
23 setError('Failed to load blog posts.');
24 } finally {
25 setLoading(false);
26 }
27 };
28
29 fetchBlogPosts();
30 }, []);
31
32 if (loading) return <p>Loading...</p>;
33 if (error) return <p className="error">{error}</p>;
34
35 return (
36 <div>
37 <h2>Blog Posts</h2>
38 {blogPosts.length === 0 ? (
39 <p>No blog posts found.</p>
40 ) : (
41 <ul>
42 {blogPosts.map((post) => (
43 <li key={post.id}>
44 <h3>{post.title || 'Untitled'}</h3>
45 <p>{post.content}</p>
46 <small>
47 By: {post.persona} on{' '}
48 {new Date(post.created_at).toLocaleString()}
49 </small>
50 </li>
51 ))}
52 </ul>
53 )}
54 </div>
55 );
56};
57
58export default BlogPosts;

Integrating React Router

Set up routing in src/App.tsx:

typescript
1// src/App.tsx
2
3import React from 'react';
4import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
5import UploadSample from './components/UploadSample';
6import PersonaList from './components/PersonaList';
7import GenerateContent from './components/GenerateContent';
8import BlogPosts from './components/BlogPosts';
9
10const App: React.FC = () => {
11 return (
12 <Router>
13 <nav>
14 <ul>
15 <li><Link to="/">Upload Sample</Link></li>
16 <li><Link to="/personas">Personas</Link></li>
17 <li><Link to="/blog-posts">Blog Posts</Link></li>
18 </ul>
19 </nav>
20 <Routes>
21 <Route path="/" element={<UploadSample />} />
22 <Route path="/personas" element={<PersonaList />} />
23 <Route path="/generate" element={<GenerateContent />} />
24 <Route path="/blog-posts" element={<BlogPosts />} />
25 </Routes>
26 </Router>
27 );
28};
29
30export default App;

Setting Up Ollama and llama3.2

To analyze the writing samples and generate content, we'll use Ollama, a tool for running AI language models locally. We'll be using the llama3.2 model in this guide.

Installing Ollama

First, install Ollama on your machine. Ollama currently supports macOS.

For macOS:

If you have Homebrew installed, you can install Ollama by running:

bash
1brew install ollama

If you don't have Homebrew, install it from here and then run the above command.

Downloading the llama3.2 Model

Once Ollama is installed, you can download the llama3.2 model:

bash
1ollama pull llama3.2

This command will download and install the llama3.2 model locally.

Note: If llama3.2 is not available, replace it with the latest version of the Llama model supported by Ollama, such as llama2.

Running Ollama

Ollama runs as a background service. Start the Ollama server:

bash
1ollama serve

This will start the server on http://localhost:11434, which is the default API endpoint for Ollama.

Testing the Model

To ensure everything is set up correctly, test the model using the Ollama CLI:

bash
1ollama generate llama3.2 "Hello, how are you?"

You should see the model generate a response in your terminal.

Integrating Ollama with Django

Now that Ollama is running with the llama3.2 model, we'll integrate it into our Django application.

Installing python-decouple

We need python-decouple to manage environment variables. Install it using:

bash
1pip install python-decouple

Configuring Environment Variables

Create a .env file in your backend directory to store sensitive information and environment variables:

bash
1touch .env

Add the following line to your .env file:

OLLAMA_API_URL=http://localhost:11434/api/generate

This sets the API URL for Ollama.

Updating settings.py

Ensure that python-decouple is set up in your Django settings:

python
1# backend/settings.py
2
3from decouple import config
4
5# ... rest of your settings ...
6
7OLLAMA_API_URL = config('OLLAMA_API_URL', default='http://localhost:11434/api/generate')

Updating utils.py

Modify your analyze_writing_sample and generate_content functions in backend/core/utils.py to use Ollama and the llama3.2 model.

python
1# core/utils.py
2
3import logging
4import requests
5import json
6from decouple import config
7
8logger = logging.getLogger(__name__)
9OLLAMA_API_URL = config('OLLAMA_API_URL', default='http://localhost:11434/api/generate')
10
11def analyze_writing_sample(writing_sample):
12 encoding_prompt = f'''
13Please 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.
14
15"vocabulary_complexity": [1-10],
16"sentence_structure": "[simple/complex/varied]",
17# ... [rest of your JSON template] ...
18
19Writing Sample:
20{writing_sample}
21'''
22
23 payload = {
24 'model': 'llama3.2', # Using llama3.2 model
25 'prompt': encoding_prompt,
26 'stream': False
27 }
28 headers = {'Content-Type': 'application/json'}
29
30 try:
31 response = requests.post(OLLAMA_API_URL, json=payload, headers=headers)
32 response.raise_for_status()
33 response_text = response.json().get('response', '')
34 analyzed_data = extract_json(response_text)
35 if analyzed_data is None:
36 logger.error("No JSON object found in the response.")
37 return None
38 return analyzed_data
39 except (requests.RequestException, json.JSONDecodeError, AttributeError) as e:
40 logger.error(f"Error during analyze_writing_sample: {str(e)}")
41 return None
42
43def generate_content(persona_data, prompt):
44 decoding_prompt = f'''
45You are to write a blog post in the style of {persona_data.get('name', 'Unknown Author')}, a writer with the following characteristics:
46
47{json.dumps(persona_data, indent=2)}
48
49Now, please write a response in this style about the following topic:
50"{prompt}"
51Begin with a compelling title that reflects the content of the post.
52'''
53
54 payload = {
55 'model': 'llama3.2', # Using llama3.2 model
56 'prompt': decoding_prompt,
57 'stream': False
58 }
59 headers = {'Content-Type': 'application/json'}
60
61 try:
62 logger.info(f"Sending request to Ollama API with payload: {payload}")
63 response = requests.post(OLLAMA_API_URL, json=payload, headers=headers)
64 response.raise_for_status()
65 response_json = response.json()
66 response_content = response_json.get('response', '').strip()
67 if not response_content:
68 logger.error("Ollama API response 'response' field is empty.")
69 return ''
70 return response_content
71 except requests.RequestException as e:
72 logger.error(f"Error during generate_content: {e}")
73 if e.response:
74 logger.error(f"Ollama Response Status: {e.response.status_code}")
75 logger.error(f"Ollama Response Body: {e.response.text}")
76 return ''

Updating the extract_json Function

Modify the extract_json function to parse the JSON data correctly:

python
1def extract_json(response_text):
2 try:
3 # If the response contains extra text, extract the JSON object using regex
4 json_str = re.search(r'\{.*\}', response_text, re.DOTALL).group()
5 json_data = json.loads(json_str)
6 return json_data
7 except (json.JSONDecodeError, AttributeError) as e:
8 logger.error(f"JSON decoding failed: {e}")
9 return None

This function uses regular expressions to find the JSON object within the response text.

Testing the Integration

Restart the Django development server to apply the changes:

bash
1python manage.py runserver

Ensure that Ollama is running and the llama3.2 model is loaded.

Using the Application

Now, when you use the frontend to upload a writing sample, the backend will:

  1. Send the writing sample to Ollama's API with the llama3.2 model.
  2. Receive the analysis in JSON format.
  3. Store the analysis in the Persona model.
  4. Generate content based on the persona data when prompted.

Running and Testing the Application

Starting the Backend

In the backend directory, start the Django development server:

bash
1python manage.py runserver

Starting the Frontend

In the frontend directory, start the React development server:

bash
1npm start

Testing the Application

  1. Upload a Writing Sample:

    • Navigate to http://localhost:3000/.
    • Fill in the persona name and paste a writing sample.
    • Submit the form to create a new persona.
  2. View Saved Personas:

    • Navigate to http://localhost:3000/personas.
    • See the list of personas you've created.
  3. Generate Content:

    • From the personas list, click "Generate Content" next to a persona.
    • Enter a prompt or topic.
    • Generate content styled after the selected persona.
  4. View Blog Posts:

    • Navigate to http://localhost:3000/blog-posts.
    • Read the generated blog posts.

Conclusion

By setting up Ollama and integrating the llama3.2 model, your application can now analyze writing samples and generate content using AI capabilities locally. This enhances the functionality of your application, allowing for personalized content generation.

Final Steps:

  • Ensure Ollama Starts on Boot: Consider configuring Ollama to start automatically when your system boots if you plan to use it frequently.
  • Model Updates: Keep an eye on updates to Ollama and available models to enhance your application's capabilities.
  • Resource Management: Running AI models locally can consume significant resources. Monitor system performance and adjust as necessary.

References:


Note: Replace 'llama3.2' with the appropriate model name if llama3.2 is not available or if you are using a different model supported by Ollama.

Let me know if you have any questions or need further assistance!

Sovereign AI book cover

Sovereign AI: Building Local-First Intelligent Systems

by Daniel Kliewer · Paperback · 72 pages

The hands-on guide to building AI that runs on your hardware, keeps your data private, and eliminates cloud dependence. Working code included.