File size: 7,107 Bytes
19e6827
f94f7e6
 
 
19e6827
4bca401
 
26568c8
4bca401
950e491
4bca401
0a1acad
 
fe613d9
0a1acad
 
 
 
 
 
 
4bca401
 
1ca0f8c
fe613d9
 
 
 
4bca401
177593d
 
1ca0f8c
 
4bca401
 
 
 
 
 
177593d
43689f7
 
 
 
 
 
 
 
f94f7e6
43689f7
f94f7e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43689f7
 
f94f7e6
 
 
 
 
 
4bca401
0a1acad
4bca401
 
fe613d9
4bca401
 
0a1acad
4bca401
 
 
 
 
 
0a1acad
4bca401
 
0a1acad
4bca401
 
 
 
 
0a1acad
 
 
 
 
 
43689f7
0a1acad
 
 
 
 
 
 
fe613d9
0a1acad
fe613d9
0a1acad
4bca401
 
 
 
 
2b040fa
0a1acad
fc5e86b
f94f7e6
 
4bca401
 
2b040fa
0a1acad
4bca401
f94f7e6
4bca401
 
 
2b040fa
f94f7e6
 
26568c8
f94f7e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26568c8
0a1acad
bdb14b5
4bca401
 
2b040fa
4bca401
0a1acad
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
163
164
165
166
167
168
169
170
171
172
"""
MODULE JASMINE AGENT - V FINAL (AIP LOGIC KERNEL + GROQ)
========================================================
Responsabilité : Traduire le langage naturel en actions ontologiques précises.
"""
import google.generativeai as genai
from groq import Groq
import json
import streamlit as st
import os
import re
import sys

# Gestion des imports
try:
    from src.core.schema_extractor import SchemaExtractor
    from src.agent.query_planner import QueryPlanner
except ImportError:
    sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
    from src.core.schema_extractor import SchemaExtractor
    from src.agent.query_planner import QueryPlanner

class JasmineAgent:
    def __init__(self, rdf_store, ontology_rules):
        self.google_key = os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY")
        if self.google_key: genai.configure(api_key=self.google_key)

        self.groq_key = os.environ.get("GROQ_API_KEY") or st.secrets.get("GROQ_API_KEY")
        self.groq_client = Groq(api_key=self.groq_key) if self.groq_key else None
        
        self.rdf_store = rdf_store
        self.ontology_rules = ontology_rules
        
        self.MODEL_CASCADE = [
            "gemini-2.0-flash-exp",
            "llama-3.3-70b-versatile",
            "gemini-2.0-flash-lite",
            "llama-3.1-8b-instant"
        ]

    def _get_prefixes(self):
        return """
PREFIX vortex: <http://vortex.ai/ontology#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
"""

    def build_base_system_prompt(self):
        return f"""
ROLE: Tu es le Cerveau Numérique (AIP Logic) de VORTEX.
MISSION: Répondre aux questions en interrogeant le Knowledge Graph via SPARQL.

--- TON TERRITOIRE (ONTOLOGIE) ---
1. Prêts : vortex:Pret (liés via 'a_emprunteur' à Client, 'est_garanti_par' à Garant).
2. Clients : vortex:Client (propriété 'nom' ou 'rdfs:label' pour le nom complet).
3. Montants : 
   - 'vortex:montant' : Donnée brute (peut contenir des doublons).
   - 'vortex:montant_net' : GOLDEN RECORD (Valeur unique et fiable pour les calculs).

--- RÈGLES DE NAVIGATION (CRITIQUES) ---
Pour chercher un dossier par NOM (ex: "Koulnan" ou "Aichatou") :
NE JAMAIS faire : ?pret vortex:a_emprunteur 'Koulnan'. (Car a_emprunteur pointe vers un ID technique).
TU DOIS FAIRE UNE JOINTURE :
   ?pret vortex:a_emprunteur ?client_id .
   ?client_id rdfs:label ?nom_client .
   FILTER(CONTAINS(LCASE(STR(?nom_client)), 'koulnan'))

--- RÈGLES DE CALCUL ---
Pour une SOMME, un MONTANT TOTAL ou un SEUIL (> 100000) :
1. Utilise TOUJOURS 'vortex:montant_net' (le Golden Record).
2. Utilise TOUJOURS l'outil 'execute_sparql'.
3. NE JAMAIS utiliser 'search_semantic' pour des filtres numériques.

--- RÈGLES TECHNIQUES ---
- Namespace UNIQUE : <http://vortex.ai/ontology#> (prefix: vortex).
- Préfixes OBLIGATOIRES :
{self._get_prefixes()}

CONTRAINTES DE SORTIE (JSON STRICT) :
{{
  "thought_trace": "1. ANALYSE: ... 2. EXTRACTION: ... 3. NAVIGATION: ... 4. OUTIL: ...",
  "tool": "execute_sparql" | "search_semantic" | "none",
  "args": {{ "query": "..." }}
}}
"""

    def _format_messages_for_groq(self, system_prompt, chat_history, user_message):
        msgs = [{"role": "system", "content": system_prompt}]
        for m in chat_history[-4:]:
            role = "assistant" if m["role"] in ["model", "assistant"] else "user"
            content = str(m.get("content", ""))
            if "🛠️" not in content and content.strip(): 
                msgs.append({"role": role, "content": content})
        msgs.append({"role": "user", "content": user_message})
        return msgs

    def _format_messages_for_gemini(self, system_prompt, chat_history, user_message):
        msgs = [{"role": "user", "parts": [system_prompt]}]
        for m in chat_history[-4:]:
            role = "user" if m["role"] == "user" else "model"
            content = str(m.get("content", ""))
            if "🛠️" not in content and content.strip(): 
                msgs.append({"role": role, "parts": [content]})
        msgs.append({"role": "user", "parts": [user_message]})
        return msgs

    def ask(self, user_message, chat_history):
        extractor = SchemaExtractor(self.rdf_store)
        real_schema = extractor.get_real_schema()
        schema_prompt = extractor.generate_prompt_schema()

        planner = QueryPlanner(self.rdf_store, real_schema)
        plan = planner.analyze_and_plan(user_message)
        planning_log = planner.get_planning_logs(plan) 

        full_system_prompt = f"""
{self.build_base_system_prompt()}

--- 🗺️ TERRITOIRE (SCHÉMA RDF RÉEL) ---
{schema_prompt}

--- 🎯 PLAN TACTIQUE ---
STRATÉGIE: {plan['strategy']}
CONTEXTE: {plan['reason']}
"""
        last_error = None
        for model_name in self.MODEL_CASCADE:
            try:
                response_text = ""
                if "gemini" in model_name:
                    if not self.google_key: continue
                    msgs = self._format_messages_for_gemini(full_system_prompt, chat_history, user_message)
                    model = genai.GenerativeModel(model_name)
                    # Force JSON mode for Gemini
                    res = model.generate_content(msgs, generation_config={"response_mime_type": "application/json"})
                    response_text = res.text
                else:
                    if not self.groq_client: continue
                    msgs = self._format_messages_for_groq(full_system_prompt, chat_history, user_message)
                    completion = self.groq_client.chat.completions.create(
                        model=model_name, messages=msgs, temperature=0.0, response_format={"type": "json_object"}
                    )
                    response_text = completion.choices[0].message.content

                clean_text = response_text.strip()
                # Nettoyage au cas où le modèle ajoute du markdown
                clean_text = re.sub(r'```json\s*|\s*```', '', clean_text)
                
                try:
                    action = json.loads(clean_text)
                    
                    # Sécurité Préfixes SPARQL
                    if action.get("tool") == "execute_sparql":
                        raw_query = action["args"].get("query", "")
                        if "PREFIX vortex:" not in raw_query:
                            action["args"]["query"] = self._get_prefixes() + "\n" + raw_query
                    
                    # Récupération du Chain of Thought
                    thought_trace = action.get("thought_trace", planning_log)
                    
                    return "", action, thought_trace 
                except json.JSONDecodeError:
                    pass
                
                return clean_text, None, planning_log

            except Exception as e:
                last_error = str(e)
                continue

        return f"⚠️ ERREUR SYSTEME : {last_error}", None, planning_log