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