Di seguito mostreremo l'implementazione di una semplice web app che, usando le API pubbliche di OpenAI, permetterà di interagire con ChatGPT. Lo sviluppo lato server farà uso del framework Python Flask e la parte lato client verrà gestita con la libreria JavaScript React e farà uso dello standard JSON Web Token (JWT) per effettuare chiamate autenticate alle API del backend in Python.
Per quanto possa sembrare alquanto assurdo oggi, Python nei contesti ufficiali delle aziende e delle PA italiane non viene considerato come un'alternativa valida a soluzioni basate su Java o .NET. Motivo? Non è "affidabile".
Alla base di questa motivazione vi è il pregiudizio ancora molto radicato secondo cui il mondo Open Source è formato per lo più da amatori poco seri che smanettano nel loro garage. E se il loro codice non viene gestito da aziende come Oracle o Microsoft, usarlo è un "rischio".
JavaScript è tollerato in tal senso in quanto ormai la sua diffusione lo ha reso un must e anche
se è uno standard del Web consolidatissimo, sentirete spesso confonderlo con Java da quelle stesse
persone che confondono React (una libreria JS) con un linguaggio di programmazione. Poco importa quindi
se un candidato sa cos'è l'hook useCallback()
di React ma ignora il significato e l'uso
dei callback in JavaScript.
map()
, filter()
...)import
server
├── flaskr
│ ├── __init__.py
│ ├── api.py
│ ├── cache.py
│ └── utils.py
└── requirements.txt
flask
flask-cors
flask-jwt-extended
requests
python-dotenv
redis
waitress
python-is-python3
python3-venv
/path/to/server/flaskr/.env
API_KEY=api-key-di-open-ai
JWT_SECRET_KEY=secret-del-json-web-token
USERNAME=username-per-il-client-react
PASSWORD=password-per-il-client-react
REDIS_PORT=6379
REDIS_HOST=0.0.0.0
upstream chatapi{ server 127.0.0.1:8080; } server { server_name chatapi.example.com; location / { proxy_pass http://chatapi; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Prefix /; } }
certbot
python3-certbot-nginx
import requests
def send_request_to_api(api_key, text):
completions_endpoint = 'https://api.openai.com/v1/chat/completions'
post_data = {
'model': 'gpt-3.5-turbo',
'messages': [{'role': 'user', 'content': text}]
}
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
try:
r = requests.post(completions_endpoint, json=post_data, headers=headers)
return r.json()
except requests.exceptions.RequestException:
return None
def get_choices_text_from_api(choices):
choices_text = []
if not choices or not isinstance(choices, list) or len(choices) == 0:
return choices_text
for choice in choices:
message = choice.get('message')
content = message.get('content') if message and message.get('content') else ''
choices_text.append(content)
return choices_text
from flask import (
Blueprint, jsonify, request, current_app
)
from flask_jwt_extended import jwt_required
from flaskr.utils import send_request_to_api, get_choices_text_from_api
from flaskr.cache import redis_connection
bp = Blueprint('api', __name__, url_prefix='/v1')
@bp.route('/question', methods=['POST'])
@jwt_required()
def question():
text = request.json.get('question', '')
if not text:
return jsonify({'error': 'Invalid request.'})
cached_question = redis_connection.get(text)
if cached_question:
return jsonify({'response': cached_question })
req = send_request_to_api(current_app.config['API_KEY'], text)
if not req:
return jsonify({'error': 'Request failed or timed out.'})
choices_text = get_choices_text_from_api(req.get('choices', []))
answer = '\n'.join(choices_text)
redis_connection.set(text, answer)
return jsonify({'response': answer})
from flask_jwt_extended import create_access_token
@bp.route('/login', methods=['POST'])
def login():
username = request.json.get('username', '')
password = request.json.get('password', '')
if username != current_app.config['USERNAME'] or password != current_app.config['PASSWORD']:
return jsonify({'error': 'Invalid login.'})
access_token = create_access_token(identity=username)
return jsonify({'token': access_token})
import os
from flask import Flask
from flask_cors import CORS
from flask_jwt_extended import JWTManager
from dotenv import load_dotenv
load_dotenv()
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
CORS(app)
app.config.from_mapping(
API_KEY=os.getenv('API_KEY'),
JWT_SECRET_KEY=os.getenv('JWT_SECRET_KEY'),
USERNAME=os.getenv('USERNAME'),
PASSWORD=os.getenv('PASSWORD'),
REDIS_PORT=os.getenv('REDIS_PORT'),
REDIS_HOST=os.getenv('REDIS_HOST')
)
jwt = JWTManager(app)
from . import api
app.register_blueprint(api.bp)
if test_config is None:
app.config.from_pyfile('config.py', silent=True)
else:
app.config.from_mapping(test_config)
try:
os.makedirs(app.instance_path)
except OSError:
pass
return app
client
├── node_modules
├── package-lock.json
├── package.json
├── public
└── src
import axios from "axios";
const APIURL = 'https://chatapi.example.com/v1/';
export async function doLogin(data) {
try {
const resp = await axios.post(`${APIURL}login`, data);
return resp.data;
} catch(err) {
return { error: err.message };
}
}
export async function sendQuestion(text) {
const config = {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
};
const data = {
question: text
};
try {
const resp = await axios.post(`${APIURL}question`, data, config);
return resp.data;
} catch(err) {
return { error: err.message };
}
}
import '../styles/Login.css';
import { useRef, useState } from 'react';
import { doLogin } from '../api';
export default function Login({ setLoggedIn }) {
const [error, setError] = useState('');
const usernameRef = useRef('');
const passwordRef = useRef('');
document.title = 'Login | ChatGPT App';
const handleLogin = () => {
setError('');
const data = {
username: usernameRef.current.value,
password: passwordRef.current.value
};
doLogin(data).then(res => {
if(res.error) {
return setError(res.error);
}
localStorage.setItem('token', res.token);
setLoggedIn(true);
});
};
return (
<div className="login-wrap">
<form>
<div className="form-group mb-3">
<label htmlFor="username" className="mb-2">Username</label>
<input type="text" ref={usernameRef} className="form-control" />
</div>
<div className="form-group mb-3">
<label htmlFor="password" className="mb-2">Password</label>
<input type="password" ref={passwordRef} className="form-control" />
</div>
<div>
<button onClick={handleLogin} type="button" className="btn btn-primary btn-block w-100">
Login
</button>
{error && <div className="alert alert-danger mt-4">{error}</div> }
</div>
</form>
</div>
)
}
import '../styles/FormMessage.css';
import { useRef } from 'react';
import SendMessageIcon from './ui/SendMessageIcon';
export default function FormMessage({ addUserMessage }) {
const msgRef = useRef('');
const handleSubmitByKey = evt => {
const code = evt.code;
if(code === 'Enter') {
evt.preventDefault();
return addUserMessage(null, msgRef.current.value);
}
};
return (
<form className="form-message mt-auto" onSubmit={(e) => addUserMessage(e, msgRef.current.value)}>
<div className="form-group">
<textarea onKeyDown={(e) => handleSubmitByKey(e)} placeholder="Send a message." className="form-control" ref={msgRef}></textarea>
<button type="submit">
<SendMessageIcon />
</button>
</div>
</form>
);
}
import '../styles/Messages.css';
import ReactMarkdown from 'react-markdown';
import Loader from './Loader';
export default function Messages({ messages, isLoading }) {
return (
<ol className="messages">
{messages.map((msg, i) => (
<li key={i} className={`message ${msg.type}`}>
{msg.type === 'chat' ? <ReactMarkdown>{msg.text}</ReactMarkdown> : msg.text}
</li>
))}
{isLoading && <Loader />}
</ol>
)
}
import { useState, useEffect } from 'react';
import '../styles/App.css';
import Login from './Login';
import FormMessage from './FormMessage';
import Messages from './Messages';
import Refresh from './Refresh';
import { sendQuestion } from '../api';
export default function App() {
const [loggedIn, setLoggedIn] = useState(false);
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const addChatMessage = text => {
setIsLoading(true);
sendQuestion(text).then(resp => {
if(resp.response) {
setMessages(previous => {
return [...previous, { text: resp.response, type: 'chat' }];
});
} else {
setMessages(previous => {
return [...previous, { text: 'Oops, something went wrong!', type: 'error' }];
});
}
setIsLoading(false);
}).catch(err => console.log(err));
};
const addUserMessage = (evt = null, text = '') => {
if(evt) {
evt.preventDefault();
}
setMessages(previous => {
return [...previous, { text, type: 'user' }];
});
addChatMessage(text);
};
const refreshChat = () => {
setMessages([]);
localStorage.removeItem('token');
window.location = window.location.href;
};
document.title = 'ChatGPT App';
useEffect(() => {
setLoggedIn(localStorage.getItem('token') !== null);
setIsLoading(false);
setMessages([]);
}, []);
return (
<main className="app">
{!loggedIn && <Login setLoggedIn={setLoggedIn} /> }
{loggedIn &&
<div className="chat-wrap">
<Messages messages={messages} isLoading={isLoading} />
<FormMessage addUserMessage={addUserMessage} />
<Refresh refreshChat={refreshChat} />
</div> }
</main>
);
}
# Installazione package
npm install
# Server live di sviluppo
npm run start
# Build di produzione
npm run build
Le API offerte da ChatGPT aprono infiniti scenari di applicazione dove il semplice sviluppo di una chat di base è solo una delle tante possibilità.