Optimisation performance (30t/s --> 50t/s) & adaptation parsing json --> llama.cpp & Fix système mémoire
This commit is contained in:
parent
e27b60e60c
commit
0ba47f8363
19 changed files with 457 additions and 123 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -647,7 +647,7 @@ class Superviseur(commands.Bot):
|
|||
logger.info(f"AI interaction triggered (Monitoring: {is_monitoring})")
|
||||
|
||||
# ... (same caching logic)
|
||||
if message.guild:
|
||||
if message.guild and hasattr(message.author, 'guild_permissions'):
|
||||
perms = message.author.guild_permissions
|
||||
if check_is_admin(perms):
|
||||
self._invalidate_caches(message.guild.id)
|
||||
|
|
@ -676,13 +676,23 @@ class Superviseur(commands.Bot):
|
|||
|
||||
user_id = str(message.author.id)
|
||||
guild_name = sanitize_guild_name(message.guild.name) if message.guild else "Direct"
|
||||
history = await build_context_for_model(user_id, max_recent=12, guild_id=guild_name) or ""
|
||||
|
||||
from ia.memoire import get_structured_history
|
||||
resume, structured_history = await get_structured_history(user_id, max_recent=12, guild_id=guild_name)
|
||||
|
||||
# Prepare content
|
||||
content = self._prepare_content(message) + extra_context
|
||||
|
||||
# Build payload
|
||||
payload = self._build_payload(permissions_info, server_context, channels_list, history, content, message)
|
||||
payload = self._build_payload(
|
||||
permissions_info,
|
||||
server_context,
|
||||
channels_list,
|
||||
resume,
|
||||
structured_history,
|
||||
content,
|
||||
message
|
||||
)
|
||||
|
||||
# Execute request
|
||||
async with self._request_semaphore:
|
||||
|
|
@ -853,8 +863,8 @@ class Superviseur(commands.Bot):
|
|||
content = content.replace(f'<@{mention.id}>', f'@{mention.display_name}')
|
||||
content = content.replace(f'<@!{mention.id}>', f'@{mention.display_name}')
|
||||
|
||||
# Remove bot mention
|
||||
content = content.lstrip(f'<@{self.user.id}>').lstrip(f'<@!{self.user.id}>').strip()
|
||||
# Remove bot mention (Désactivé pour que le LLM voit qu'il est mentionné)
|
||||
# content = content.lstrip(f'<@{self.user.id}>').lstrip(f'<@!{self.user.id}>').strip()
|
||||
|
||||
# Handle attachments
|
||||
if message.attachments:
|
||||
|
|
@ -935,20 +945,35 @@ class Superviseur(commands.Bot):
|
|||
permissions_info: str,
|
||||
server_context: str,
|
||||
channels_list: str,
|
||||
history: str,
|
||||
resume: str,
|
||||
structured_history: list,
|
||||
content: str,
|
||||
message
|
||||
message,
|
||||
is_monitoring: bool = False
|
||||
) -> dict:
|
||||
"""Build LLM payload."""
|
||||
"""Build LLM payload with interaction mode context."""
|
||||
user_name_info = f"Nom d'utilisateur : {message.author.display_name} (ID: {message.author.id})"
|
||||
prompt = f"{self.system_prompt}\n{permissions_info}\n{user_name_info}{server_context}{channels_list}\n{history}\n\nUser: {content}"
|
||||
|
||||
# Indicateur de mode pour éviter que l'IA ne bégaye sur sa légitimité à répondre
|
||||
mode_indicator = "[MODE: RÉPONSE DIRECTE (Vous avez été interpellé par l'utilisateur)]"
|
||||
if is_monitoring:
|
||||
mode_indicator = "[MODE: SURVEILLANCE HEURISTIQUE (N'intervenez que si nécessaire via ALERT/INSIGHT)]"
|
||||
|
||||
import time
|
||||
current_time = time.strftime('%H:%M:%S le %d/%m/%Y')
|
||||
time_info = f"[Heure actuelle du serveur : {current_time}]"
|
||||
|
||||
prompt_system = f"{self.system_prompt}\n{time_info}\n{permissions_info}\n{mode_indicator}\n{user_name_info}{server_context}{channels_list}"
|
||||
if resume:
|
||||
prompt_system += f"\nContexte global (mémoire) :\n{resume}"
|
||||
|
||||
vision_model = any(v in self.ollama_model.lower() for v in
|
||||
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']) and message.attachments
|
||||
|
||||
return self.llm.build_payload(
|
||||
prompt=prompt,
|
||||
system_prompt=self.system_prompt,
|
||||
prompt=content,
|
||||
system_prompt=prompt_system,
|
||||
structured_history=structured_history,
|
||||
vision_model=vision_model,
|
||||
attachments=message.attachments
|
||||
)
|
||||
|
|
@ -968,7 +993,21 @@ class Superviseur(commands.Bot):
|
|||
# 2. Nettoyage des éventuels backticks de code et résidus de JSON
|
||||
clean_reply = clean_reply.replace('```json', '').replace('```', '').strip()
|
||||
|
||||
# 3. Filtrer les marqueurs internes fréquents (Leak prevention)
|
||||
# 3. Supprimer les balises de réflexion (Thinking/Thought) et leur contenu
|
||||
clean_reply = re.sub(r'<thought>.*?</thought>', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
|
||||
clean_reply = re.sub(r'<reflexion>.*?</reflexion>', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
|
||||
clean_reply = re.sub(r'\[THOUGHT\].*?\[/THOUGHT\]', '', clean_reply, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# 4. Nettoyage des tokens spéciaux de modèles (ChatML/DeepSeek etc)
|
||||
tokens_to_remove = [
|
||||
"<|end_of_sentence|>", "<|endoftext|>", "<|end|>", "<|start|>",
|
||||
"<|assistant|>", "<|user|>", "<|system|>", "<|channel|>",
|
||||
"<|constrain|>", "<|message|>", "<|thought|>"
|
||||
]
|
||||
for token in tokens_to_remove:
|
||||
clean_reply = clean_reply.replace(token, "")
|
||||
|
||||
# 5. Filtrer les marqueurs internes fréquents (Leak prevention)
|
||||
markers_to_remove = [
|
||||
"[Public]", "[Relevant]", "[Irrelevant]", "[Alerte]", "[Insight]",
|
||||
"[CRITICITÉ", "[MENACE", "[RISQUE", "[DÉTECTION", "[ANALYSE", "[ACTION"
|
||||
|
|
@ -986,10 +1025,10 @@ class Superviseur(commands.Bot):
|
|||
else:
|
||||
clean_reply = clean_reply.replace(marker, "")
|
||||
|
||||
# 4. Supprimer les résidus JSON rémanents avec un regex plus large
|
||||
# 6. Supprimer les résidus JSON rémanents avec un regex plus large
|
||||
clean_reply = re.sub(r'\{[\s\n]*[\"\']action[\"\'].*?\}', '', clean_reply, flags=re.DOTALL)
|
||||
|
||||
# 5. Supprimer les lignes vides en trop et les espaces multiples
|
||||
# 7. Supprimer les lignes vides en trop et les espaces multiples
|
||||
clean_reply = re.sub(r'\n{3,}', '\n\n', clean_reply)
|
||||
clean_reply = clean_reply.strip()
|
||||
|
||||
|
|
@ -1010,7 +1049,7 @@ class Superviseur(commands.Bot):
|
|||
|
||||
# Store interaction (Even if silent, we store the input for better context next time)
|
||||
user_id = str(message.author.id)
|
||||
guild_id = sanitize_guild_name(message.guild.name) if message.guild else None
|
||||
guild_id = sanitize_guild_name(message.guild.name) if message.guild else "Direct"
|
||||
await add_interaction(user_id, message.channel.id, message.content, clean_reply if not is_monitoring else "", guild_id)
|
||||
|
||||
# En mode monitoring, on ne répond PAS au canal public sauf si l'IA l'a explicitement demandé via une action
|
||||
|
|
@ -1088,10 +1127,10 @@ class Superviseur(commands.Bot):
|
|||
|
||||
action = item['action']
|
||||
if action == 'NONE':
|
||||
action_executed = True # Bloque le fallback text même si la réponse est vide
|
||||
resp = item.get('response', "").strip()
|
||||
if resp and not is_monitoring:
|
||||
await self.messaging.reply_with_limit(message, format_mentions_in_text(resp))
|
||||
action_executed = True
|
||||
else:
|
||||
success, result = await self.execute_action(action, item, message)
|
||||
action_logger.log_action(action, str(message.author), str(message.guild), item, success, result)
|
||||
|
|
|
|||
|
|
@ -40,34 +40,60 @@ class LLMManager:
|
|||
self,
|
||||
prompt: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
structured_history: Optional[list] = None,
|
||||
vision_model: bool = False,
|
||||
attachments: Optional[list] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Build the API payload for Ollama."""
|
||||
payload = {
|
||||
"model": self.ollama_model,
|
||||
"prompt": prompt,
|
||||
"stream": True,
|
||||
"options": {"temperature": self.ollama_temperature}
|
||||
}
|
||||
"""Build the API payload for Ollama or OpenAI Chat API (llama.cpp)."""
|
||||
is_openai = "/v1/chat/completions" in self.ollama_api_url
|
||||
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
if is_openai:
|
||||
# OpenAI Chat API format
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
|
||||
if structured_history:
|
||||
messages.extend(structured_history)
|
||||
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
payload = {
|
||||
"model": self.ollama_model,
|
||||
"messages": messages,
|
||||
"stream": True,
|
||||
"temperature": self.ollama_temperature,
|
||||
"max_tokens": 2048
|
||||
}
|
||||
else:
|
||||
# Ollama /api/generate payload (Fallback)
|
||||
full_prompt = prompt
|
||||
if structured_history:
|
||||
hist_str = "\n".join([f"{m['role'].capitalize()}: {m['content']}" for m in structured_history])
|
||||
full_prompt = f"Historique:\n{hist_str}\n\nUser: {prompt}"
|
||||
|
||||
payload = {
|
||||
"model": self.ollama_model,
|
||||
"prompt": full_prompt,
|
||||
"stream": True,
|
||||
"options": {"temperature": self.ollama_temperature}
|
||||
}
|
||||
if system_prompt:
|
||||
payload["system"] = system_prompt
|
||||
|
||||
# Vision support (simplified)
|
||||
if vision_model and attachments:
|
||||
for attachment in attachments:
|
||||
if attachment.content_type and attachment.content_type.startswith('image/'):
|
||||
if any(v in self.ollama_model.lower() for v in
|
||||
['llava', 'bakllava', 'moondream', 'llama3.2-vision', 'minicpm-v']):
|
||||
# Note: actual image handling would require download
|
||||
pass
|
||||
# actual image handling would require download and base64 for OpenAI
|
||||
pass
|
||||
|
||||
return payload
|
||||
|
||||
async def call_ollama(self, payload: Dict[str, Any]) -> str:
|
||||
"""Call Ollama API and accumulate response."""
|
||||
"""Call LLM API and accumulate response."""
|
||||
session = await self.get_session()
|
||||
is_openai = "/v1/chat/completions" in self.ollama_api_url
|
||||
|
||||
async with session.post(
|
||||
self.ollama_api_url,
|
||||
|
|
@ -76,86 +102,132 @@ class LLMManager:
|
|||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
# Si on ne streame pas, on récupère tout d'un coup
|
||||
if not payload.get("stream", True):
|
||||
data = await response.json()
|
||||
return data.get("response", "")
|
||||
|
||||
accumulated_reply = ""
|
||||
async for line in response.content:
|
||||
line = line.decode('utf-8').strip()
|
||||
accumulated_thoughts = ""
|
||||
async for line_bytes in response.content:
|
||||
line = line_bytes.decode('utf-8').strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Strip SSE prefix if present
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
elif line.startswith("event:"):
|
||||
continue
|
||||
|
||||
if line == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if 'thinking' in data and data['thinking']:
|
||||
print(f"🤔 Le superviseur réfléchit : {data['thinking']}", flush=True)
|
||||
continue
|
||||
# 1. Handle Thinking (OpenAI style reasoning or special field)
|
||||
thought = None
|
||||
if 'thinking' in data: # Custom llama.cpp field
|
||||
thought = data['thinking']
|
||||
elif 'choices' in data and len(data['choices']) > 0:
|
||||
delta = data['choices'][0].get('delta', {})
|
||||
thought = delta.get('reasoning_content')
|
||||
|
||||
if thought:
|
||||
accumulated_thoughts += thought
|
||||
print(f"🤔 Le superviseur réfléchit : {thought}", flush=True)
|
||||
|
||||
if 'response' in data:
|
||||
# 2. Handle Content
|
||||
chunk = ""
|
||||
if 'response' in data: # Ollama
|
||||
chunk = data['response']
|
||||
elif 'choices' in data and len(data['choices']) > 0: # OpenAI
|
||||
delta = data['choices'][0].get('delta', {})
|
||||
if delta.get('content') is not None:
|
||||
chunk = delta.get('content', "")
|
||||
elif 'content' in data: # llama.cpp legacy
|
||||
chunk = data['content']
|
||||
|
||||
if chunk:
|
||||
accumulated_reply += chunk
|
||||
if chunk.strip():
|
||||
print(f"🤖 Réponse du Superviseur (fragment) : {chunk}", flush=True)
|
||||
|
||||
elif 'action' in data:
|
||||
elif 'action' in data: # Action already in JSON (unlikely for chat API)
|
||||
accumulated_reply = line
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
accumulated_reply += line
|
||||
# In case of malformed line or raw text
|
||||
if not line.startswith("{"):
|
||||
accumulated_reply += line
|
||||
|
||||
# Fallback for models that put the final JSON response inside the thought block
|
||||
if not accumulated_reply.strip() and accumulated_thoughts.strip():
|
||||
# Test if the thought block contains our strict format using robust search
|
||||
if re.search(r'\{[\s\n]*[\"\']action[\"\']', accumulated_thoughts, re.IGNORECASE):
|
||||
print("⚠️ Aucun texte de contenu final mais du JSON détecté dans les pensées. Récupération.", flush=True)
|
||||
accumulated_reply = accumulated_thoughts
|
||||
|
||||
return accumulated_reply
|
||||
|
||||
def extract_json_actions(self, text: str) -> Optional[str]:
|
||||
"""Extract JSON action objects from text more robustly."""
|
||||
"""Extract JSON action objects from text more robustly using an O(N) brace matcher."""
|
||||
|
||||
# 1. First, try to extract from markdown blocks if present
|
||||
markdown_blocks = re.findall(r'```(?:json)?\s*(.*?)\s*```', text, re.DOTALL)
|
||||
candidates = markdown_blocks if markdown_blocks else [text]
|
||||
|
||||
actions = []
|
||||
|
||||
# Regex plus laxiste pour trouver des blocs JSON potentiels
|
||||
# On cherche des blocs commençant par {"action" ou {'action'
|
||||
matches = re.finditer(r'\{[\s\n]*[\"\']action[\"\'].*?\}', text, re.DOTALL)
|
||||
|
||||
for match in matches:
|
||||
json_str = match.group(0)
|
||||
for candidate_text in candidates:
|
||||
# Try to parse the whole text as a list or dict first
|
||||
try:
|
||||
# Tentative de parsing standard
|
||||
parsed = json.loads(json_str)
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
parsed = json.loads(candidate_text.strip())
|
||||
if isinstance(parsed, list):
|
||||
for item in parsed:
|
||||
if isinstance(item, dict) and 'action' in item:
|
||||
actions.append(item)
|
||||
if actions: return json.dumps(actions)
|
||||
elif isinstance(parsed, dict) and 'action' in parsed:
|
||||
actions.append(parsed)
|
||||
except json.JSONDecodeError:
|
||||
# Si erreur, on tente de remplacer les simples quotes par des doubles
|
||||
# (Cas fréquent si le LLM mélange les styles)
|
||||
try:
|
||||
# Remplacement très basique (peut échouer si contenu complexe)
|
||||
json_corrected = json_str.replace("'", '"')
|
||||
parsed = json.loads(json_corrected)
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
actions.append(parsed)
|
||||
continue
|
||||
except: pass
|
||||
return json.dumps(actions)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Méthode récursive des accolades si le bloc est tronqué ou complexe
|
||||
start = match.start()
|
||||
brace_count = 0
|
||||
for i in range(start, len(text)):
|
||||
if text[i] == '{': brace_count += 1
|
||||
elif text[i] == '}':
|
||||
# Fallback: O(N) scan for top-level { ... } blocks
|
||||
brace_count = 0
|
||||
current_block = []
|
||||
|
||||
for char in candidate_text:
|
||||
if char == '{':
|
||||
if brace_count < 0:
|
||||
brace_count = 0
|
||||
current_block = []
|
||||
brace_count += 1
|
||||
|
||||
if brace_count > 0:
|
||||
current_block.append(char)
|
||||
|
||||
if char == '}':
|
||||
if brace_count > 0:
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
if brace_count == 0 and current_block:
|
||||
json_str = "".join(current_block)
|
||||
current_block = []
|
||||
try:
|
||||
candidate = text[start:i+1]
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
except:
|
||||
parsed = json.loads(candidate.replace("'", '"'))
|
||||
|
||||
parsed = json.loads(json_str)
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
actions.append(parsed)
|
||||
break
|
||||
except: pass
|
||||
|
||||
if parsed not in actions: # avoid duplicates
|
||||
actions.append(parsed)
|
||||
except:
|
||||
# Fallback with single quote replace
|
||||
try:
|
||||
parsed = json.loads(json_str.replace("'", '"'))
|
||||
if isinstance(parsed, dict) and 'action' in parsed:
|
||||
if parsed not in actions:
|
||||
actions.append(parsed)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
brace_count = 0
|
||||
current_block = []
|
||||
|
||||
if actions:
|
||||
return json.dumps(actions)
|
||||
return None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue