Spaces:
Sleeping
Sleeping
File size: 8,813 Bytes
44cf624 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
from pydantic import BaseModel, Field, model_validator
from datetime import date, datetime
from typing import Optional, List, Dict, Any
import json
import openai
CONTRACT_TYPES = [
"Appalto",
"Concessione",
"Procedura negoziata",
"Manifestazione d'interesse", # or "Manifestazione di interesse", usually found along with "Procedura aperta"
"Procedura aperta",
"Indagine di mercato"
]
class ContractEntity(BaseModel):
municipality: Optional[str] = Field(None, description="Nome completo del Comune citato nel bando (e.g. 'Comune di Castelletto Monferrato').")
provincia: Optional[str] = Field(None, description="Provincia del Comune di riferimento (sigle, e.g. 'TO', 'MI').")
regione: Optional[str] = Field(None, description="Regione italiana cui appartiene il Comune di riferimento (e.g. 'Piemonte', 'Lombardia').")
cig_code: Optional[str] = Field(None, description="Codice Identificativo Gara espresso in 10 caratteri alfanumerici (e.g. 'B2AF744338').")
proposal_deadline: Optional[str] = Field(None, description="Data di scadenza per la presentazione dell'offerta.")
contract_deadline: Optional[str] = Field(None, description="Data di scadenza del contratto.")
contract_duration: Optional[str] = Field(None, description="Durata in anni del contratto (e.g. se il bando menziona la validità per gli anni 2024/2025, 2025/2026 e 2026/2027, la durata è '3 anni')")
extension_period: Optional[str] = Field(None, description="Durata in anni, o mesi se meno di un anno, della proroga. Non confondere con rinnovo!")
renewal_period: Optional[str] = Field(None, description="Durata in anni, o mesi se meno di un anno, del rinnovo. Non confondere con proroga!")
active: Optional[bool] = Field(False, description="Se il bando è aperto o no (oggi <= proposal_deadline).")
meals_per_year: Optional[int] = Field(None, description="Numero di pasti all'anno.")
meals_per_day: Optional[int] = Field(None, description=f"Numero di pasti al giorno.")
contract_type: Optional[str] = Field(None, description=f"Tipologia di affidamento del contratto. Tipologie valide: {CONTRACT_TYPES}.")
current_src: Optional[str] = Field(None, description="Nome completo dell'attuale società che si occupa del servizio (e.g. 'Progetti e Soluzioni SpA').")
required_sw: Optional[bool] = Field(None, description="Se è richiesto un sistema informatizzato (sì/no).")
addition_points: Optional[bool] = Field(None, description="Se sono attribuiti dei punti per migliorie proposte (sì/no).")
current_sw: Optional[str] = Field(None, description="Il nome del software attualmente in uso, se indicato.")
summary: Optional[str] = Field(None, description="Riassunto contenente le informazioni chiave del contratto.")
@model_validator(mode="after")
def compute_active(cls, model):
"""
Dopo che tutti i campi sono validati, ricalcola `active`:
True se oggi ≤ proposal_deadline, altrimenti False.
"""
model.active = date.today() <= datetime.strptime(model.proposal_deadline, "%d-%m-%Y").date()
return model
@classmethod
def fields_list(cls) -> List[str]:
return list(cls.model_fields.keys())
# == Preprocessing Instructions ==
SYSTEM_PREPROCESS = f"""
Sei un assistente italiano specializzato nell'analisi di bandi di gara.
Di seguito il modello dati in formato JSON Schema, dove per ogni campo è indicata la descrizione ('description'):
{ContractEntity.model_json_schema()}
Per ogni query fornita dall'utente:
- Identifica l'intento, ossia la categoria cui la domanda si riferisce (campo "intent"), scegliendo **esattamente** uno tra i campi dello schema JSON sopra;
- Estrai il nome completo del comune citato, se presente (campo "municipality");
- Riscrivi la domanda in italiano usando termini attinenti all'ambito dei bandi di gare (campo "rewritten_query"), senza ripetere il comune;
- Basandoti sulla descrizione del campo identificato come intento, riscrivi la domanda in italiano (campo "rewritten_query"), senza ripetere il comune, usando il linguaggio tecnico necessario **per trovare** tramite il retriever l'informazione descritta.
Restituisci **solo** un JSON con i tre campi: "municipality", "rewritten_query" e "intent".
"""
FEW_SHOTS_PREPROCESS = [
("Quanto dura il contratto per Verona?", {"intent":"contract_duration",
"municipality":"Comune di Verona",
"rewritten_query":"Durata del contratto?"}),
("Devo sapere se richiedono una piattaforma ad-hoc in particolare.", {"intent":"required_sw",
"municipality":"",
"rewritten_query":"È richiesto un software specifico?"}),
("Quando finisce il contratto con Roma?", {"intent":"contract_deadline",
"municipality":"Comune di Roma",
"rewritten_query":"Quando scade il contratto?"}),
("Qual è la scadenza per presentare l'offerta per il bando di Bologna?", {"intent": "proposal_deadline",
"municipality": "Comune di Bologna",
"rewritten_query": "Qual è la data di scadenza per la presentazione dell'offerta?"}),
("Vorrei sapere quanti pasti servono ogni giorno.", {"intent": "meals_per_day",
"municipality": "",
"rewritten_query": "Numero di pasti al giorno?"}),
("A Firenze, chi è l'attuale gestore del servizio mensa scolastica?", {"intent": "current_src",
"municipality": "Comune di Firenze",
"rewritten_query": "Chi è l'attuale gestore del servizio?"}),
("Mi serve il CIG della gara a Bari.", {"intent": "cig_code",
"municipality": "Comune di Bari",
"rewritten_query": "Qual è il CIG?"}),
("Il contratto a Palermo prevede una proroga?", {"intent": "extension_period",
"municipality": "Comune di Palermo",
"rewritten_query": "È prevista una proroga?"}),
("Che tipo di affidamento è stato utilizzato nel bando di Trento?", {"intent": "contract_type",
"municipality": "Comune di Trento",
"rewritten_query": "Qual è il tipo di affidamento?"}),
("Cosa devo sapere di importante sul bando di Como?", {"intent":"summary",
"municipality":"Comune di Como",
"rewritten_query":"Riassunto delle informazioni chiave?"}),
]
def preprocess_query(raw: str, feedback: Optional[str] = None) -> Dict[str, Any]:
"""
Calls the LLM once to extract:
- intent (one of ContractEntity fields)
- municipality (str)
- rewritten_query (str)
Optionally refines rewritten_query if feedback is provided.
"""
msgs = [{"role": "system", "content": SYSTEM_PREPROCESS}]
# Using few shots
for example_q, example_out in FEW_SHOTS_PREPROCESS:
msgs.append({"role": "user", "content": example_q})
msgs.append({
"role": "assistant",
"content": json.dumps(example_out, ensure_ascii=False)
})
# Raw user query
msgs.append({"role": "user", "content": raw})
# Query refining if we have feedback
if feedback:
msgs.append({
"role": "user",
"content": f"Feedback: {feedback}\n"
"Modifica SOLO il campo \"rewritten_query\" tenendo in considerazione questo feedback."
})
resp = openai.chat.completions.create(
model="gpt-4o-mini",
messages=msgs,
temperature=0.0
)
text = resp.choices[0].message.content.strip()
try:
obj = json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(f"Non ho potuto parsare JSON dalla risposta:\n{text}") from e
for key in ("intent", "municipality", "rewritten_query"):
if key not in obj:
raise KeyError(f"Il campo '{key}' manca nella risposta: {obj}")
return obj |