Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate DSPy into the form generation #68

Merged
merged 38 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
94ddfe7
feat: use basic authentication scheme
snakedye Jun 6, 2024
131c13f
doc: to-do user check
snakedye Jun 6, 2024
e2954f4
feat: /upload now accepts multiple images at the same time
snakedye Jun 6, 2024
502694e
fix: return label_id after analysis
snakedye Jun 6, 2024
b37d0e4
feat: /new_label route
snakedye Jun 7, 2024
8fdec16
docs: update workflow
snakedye Jun 10, 2024
320616e
fix: lint errors
snakedye Jun 10, 2024
f565b99
fix: return only the form
snakedye Jun 10, 2024
e7a4396
fix: only support one image per request for /upload
snakedye Jun 10, 2024
578a68a
feat: support sending images throught analyze
snakedye Jun 10, 2024
6d09639
test: set response format for GPT
snakedye Jun 10, 2024
cef13df
doc: display the new workflow in diagram
snakedye Jun 12, 2024
10b66d7
Merge branch 'main' into 46-analyze-route
snakedye Jun 14, 2024
4c7d6ea
fix: support json return format in GPT
snakedye Jun 17, 2024
6ef7b0a
doc: s/GET/POST in workflow
snakedye Jun 17, 2024
aaa4d33
fix: remove try/catch on generate_form
snakedye Jun 17, 2024
2825b06
fix: improve the prompt
snakedye Jun 17, 2024
4e3b1dc
feat: use dspy to improve the prompt
snakedye Jun 18, 2024
c0549b9
Merge branch 'main' into dspy
snakedye Jun 18, 2024
d688816
fix: git merge
snakedye Jun 18, 2024
b5d3abb
feat: remove the routes
snakedye Jun 18, 2024
34cb091
fix: remove sessions
snakedye Jun 18, 2024
24c3691
Merge branch '59-deprecate-legacy-api' into dspy
snakedye Jun 18, 2024
1376034
fix: log the form
snakedye Jun 18, 2024
afcbd4b
refactor: prompt in a more natural format
snakedye Jun 19, 2024
429a397
fix: tweak the prompt
snakedye Jun 19, 2024
c73e786
fix: s/quantity/percentage and split weight by kg and lb
snakedye Jun 19, 2024
5e1e1ef
fix: test errors
snakedye Jun 20, 2024
3e31c2a
fix: log doc intelligence too
snakedye Jun 21, 2024
11da601
Merge branch 'main' into dspy
snakedye Jun 21, 2024
3ba634b
deps: dspy
snakedye Jun 21, 2024
3a7aba8
refactor: gpt test more exhaustive
snakedye Jun 23, 2024
83584e5
fix: reduce max_token for gpt4
snakedye Jun 25, 2024
1c4a78a
fix: eof rule
snakedye Jun 25, 2024
f5aaa07
Sanity checks with Pydantic (#70)
snakedye Jun 25, 2024
0892f3b
Merge branch 'main' into dspy
snakedye Jun 25, 2024
1e0b9f7
feat: exception coverage for /analyze
snakedye Jun 26, 2024
7c705e2
test: verify conformity of the fertiliser form
snakedye Jun 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ samples/*
# Logs
.logs

# Logs
.logs

# VS Code
.vscode

Expand Down
120 changes: 69 additions & 51 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import os
from http import HTTPStatus
import json
from http import HTTPStatus
from dotenv import load_dotenv
from auth import Token
from backend.form import FertiliserForm
from azure.core.exceptions import HttpResponseError
from werkzeug.utils import secure_filename
from backend import OCR, GPT, LabelStorage
from flask import Flask, request, render_template
from backend import OCR, GPT, LabelStorage, save_text_to_file
from datetime import datetime
from flask import Flask, request, render_template, jsonify
from flask_cors import CORS

# Load environment variables
Expand All @@ -19,8 +23,9 @@
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# CORS configuration limited to the frontend URL
cors = CORS(app,resources={"*",FRONTEND_URL})
cors = CORS(app, resources={"*", FRONTEND_URL})
app.config['CORS_HEADERS'] = 'Content-Type'

# Configuration for Azure Form Recognizer
API_ENDPOINT = os.getenv('AZURE_API_ENDPOINT')
API_KEY = os.getenv('AZURE_API_KEY')
Expand All @@ -35,54 +40,67 @@
def main_page():
return render_template('index.html')

# Example request
# curl -X POST http://localhost:5000/analyze \
# -H "Authorization: Basic <your_encoded_credentials>" \
# -F "images=@/path/to/image1.jpg" \
# -F "images=@/path/to/image2.jpg"
@app.route('/analyze', methods=['POST'])
def analyze_document():
files = request.files.getlist('images')

# The authorization scheme is still unsure.
#
# Current format: user_id:session_id
# Initialize a token instance from the request authorization header
auth_header = request.headers.get("Authorization")
# Currently we are not using the token. It might change in the future.
Token(auth_header) if request.authorization else Token()

# Initialize the storage for the user
label_storage = LabelStorage()

for file in files:
if file:
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)

# Add image to label storage
label_storage.add_image(file_path)

document = label_storage.get_document()
if not document:
return "No documents to analyze", HTTPStatus.BAD_REQUEST

result = ocr.extract_text(document=document)

# Generate form from extracted text
# Send the JSON if we have more token.
# form = language_model.generate_form(result_json)
form = language_model.generate_form(result.content)

# Clear the label cache
label_storage.clear()

return app.response_class(
response=form,
status=HTTPStatus.OK,
mimetype="application/json"
)
try:
files = request.files.getlist('images')

if not files:
raise ValueError("No files provided for analysis")

# The authorization scheme is still unsure.
auth_header = request.headers.get("Authorization")
Token(auth_header) if request.authorization else Token()

# Initialize the storage for the user
label_storage = LabelStorage()

for file in files:
if file:
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
label_storage.add_image(file_path)

document = label_storage.get_document()
result = ocr.extract_text(document=document)

# Logs the results from document intelligence
now = datetime.now()
if not os.path.exists('./.logs'):
os.mkdir('./.logs')
save_text_to_file(result.content, f"./.logs/{now}.md")

# Generate form from extracted text
raw_form = language_model.generate_form(result.content)

# Logs the results from GPT
save_text_to_file(raw_form, f"./.logs/{now}.json")

# Clear the label cache
label_storage.clear()

# Check the conformity of the JSON
form = FertiliserForm(**json.loads(raw_form))
return app.response_class(
response=form.model_dump_json(indent=2),
status=HTTPStatus.OK,
mimetype="application/json"
)
except ValueError as err:
return jsonify(error=str(err)), HTTPStatus.BAD_REQUEST
except HttpResponseError as err:
return jsonify(error=err.message), err.status_code
except Exception as err:
return jsonify(error=str(err)), HTTPStatus.INTERNAL_SERVER_ERROR

@app.errorhandler(404)
def not_found(error):
return jsonify(error="Not Found"), HTTPStatus.NOT_FOUND

@app.errorhandler(500)
def internal_error(error):
return jsonify(error=str(error)), HTTPStatus.INTERNAL_SERVER_ERROR

if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
app.run(host='0.0.0.0', debug=True)
1 change: 1 addition & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .label import LabelStorage # noqa: F401
from .ocr import OCR # noqa: F401
from .gpt import GPT # noqa: F401
from .form import FertiliserForm # noqa: F401
import requests

def curl_file(url:str, path: str):
Expand Down
58 changes: 58 additions & 0 deletions backend/form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import List, Optional
from pydantic import BaseModel, Field

class NutrientAnalysis(BaseModel):
nutrient: str
percentage: str

class Micronutrient(BaseModel):
name: str
percentage: str

class OrganicIngredient(BaseModel):
name: str
percentage: str

class Specification(BaseModel):
humidity: Optional[str] = Field(..., alias='humidity')
ph: Optional[str] = Field(..., alias='ph')
solubility: str

class FertiliserForm(BaseModel):
company_name: Optional[str] = ""
snakedye marked this conversation as resolved.
Show resolved Hide resolved
company_address: Optional[str] = ""
company_website: Optional[str] = ""
company_phone_number: Optional[str] = ""
manufacturer_name: Optional[str] = ""
manufacturer_address: Optional[str] = ""
manufacturer_website: Optional[str] = ""
manufacturer_phone_number: Optional[str] = ""
fertiliser_name: Optional[str] = ""
fertiliser_registration_number: Optional[str] = ""
fertiliser_lot_number: Optional[str] = ""
fertiliser_weight_kg: Optional[str] = None
fertiliser_weight_lb: Optional[str] = None
fertiliser_density: Optional[str] = None
fertiliser_volume: Optional[str] = None
warranty: Optional[str] = ""
fertiliser_npk: str = Field(..., pattern=r'^(\d+-\d+-\d+)?$')
precautions_en: Optional[str] = None
instructions_en: List[str] = []
micronutrients_en: List[Micronutrient] = []
organic_ingredients_en: List[OrganicIngredient] = []
inert_ingredients_en: List[str] = []
specifications_en: List[Specification] = []
cautions_en: Optional[str] = None
first_aid_en: Optional[str] = None
precautions_fr: Optional[str] = None
instructions_fr: List[str] = []
micronutrients_fr: List[Micronutrient] = []
organic_ingredients_fr: List[OrganicIngredient] = []
inert_ingredients_fr: List[str] = []
specifications_fr: List[Specification] = []
cautions_fr: Optional[str] = None
first_aid_fr: Optional[str] = None
fertiliser_guaranteed_analysis: List[NutrientAnalysis] = []

class Config:
populate_by_name = True
54 changes: 34 additions & 20 deletions backend/gpt.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import os
from openai import AzureOpenAI
import dspy
from openai.types.chat.completion_create_params import ResponseFormat

# Constants
MODELS_WITH_RESPONSE_FORMAT = [
"ailab-llm"
] # List of models that support the response_format option

class ProduceLabelForm(dspy.Signature):
"""
You are a fertilizer label inspector working for the Canadian Food Inspection Agency.
Your task is to classify all information present in the provided text using the specified keys.
Your response should be accurate, formatted in JSON, and contain all the text from the provided text without modifications.
"""

text = dspy.InputField(desc="The text of the fertilizer label extracted using OCR.")
specification = dspy.InputField(desc="The specification containing the fields to highlight and their requirements.")
form = dspy.OutputField(desc="A complete JSON with all fields occupied. Do not return any note or additional text that isn't in the JSON.")

class GPT:
def __init__(self, api_endpoint, api_key, deployment="ailab-gpt-35-turbo-16k"):
if not api_endpoint or not api_key:
raise ValueError(
"API endpoint and key are required to instantiate the GPT class."
)
raise ValueError("API endpoint and key are required to instantiate the GPT class.")

# self.model = deployment

response_format = None
if deployment in MODELS_WITH_RESPONSE_FORMAT:
response_format = ResponseFormat(type='json_object')

max_token = 12000
if deployment == 'ailab-llm':
max_token = 3500

self.model = deployment
self.client = AzureOpenAI(

self.dspy_client = dspy.AzureOpenAI(
api_base=api_endpoint,
api_key=api_key,
azure_endpoint=api_endpoint, # Your Azure OpenAI resource's endpoint value.
deployment_id=deployment,
api_version="2024-02-01",
max_tokens=max_token,
response_format=response_format,
)

def generate_form(self, prompt):
Expand All @@ -27,17 +49,9 @@ def generate_form(self, prompt):
system_prompt = prompt_file.read()
prompt_file.close()

response_format = None
if self.model in MODELS_WITH_RESPONSE_FORMAT:
response_format = ResponseFormat(type='json_object')
dspy.configure(lm=self.dspy_client)
signature = dspy.ChainOfThought(ProduceLabelForm)
prediction = signature(specification=system_prompt, text=prompt)

response = self.client.chat.completions.create(
model=self.model, # model = "deployment_name".
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
response_format=response_format,
temperature=0,
)
return response.choices[0].message.content
# print(prediction)
return prediction.form
Binary file modified out/docs/analyze_dss/Analyze DSS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified out/docs/domain_model/Model Diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed out/docs/upload_dss/Upload DSS.png
Binary file not shown.
Loading
Loading