167 lines
6.4 KiB
Python
167 lines
6.4 KiB
Python
"""LLM integration for Superviseur bot."""
|
|
|
|
import json
|
|
import aiohttp
|
|
import logging
|
|
import re
|
|
from typing import Optional, Dict, Any
|
|
|
|
logger = logging.getLogger('Superviseur')
|
|
|
|
|
|
class LLMManager:
|
|
"""Manages LLM interactions with Ollama."""
|
|
|
|
def __init__(
|
|
self,
|
|
ollama_api_url: str,
|
|
ollama_model: str,
|
|
ollama_timeout: int = 60,
|
|
ollama_temperature: float = 0.7
|
|
):
|
|
self.ollama_api_url = ollama_api_url
|
|
self.ollama_model = ollama_model
|
|
self.ollama_timeout = ollama_timeout
|
|
self.ollama_temperature = ollama_temperature
|
|
self.http_session: Optional[aiohttp.ClientSession] = None
|
|
|
|
async def get_session(self) -> aiohttp.ClientSession:
|
|
"""Get or create HTTP session."""
|
|
if not self.http_session or self.http_session.closed:
|
|
self.http_session = aiohttp.ClientSession()
|
|
return self.http_session
|
|
|
|
async def close_session(self) -> None:
|
|
"""Close HTTP session."""
|
|
if self.http_session and not self.http_session.closed:
|
|
await self.http_session.close()
|
|
|
|
def build_payload(
|
|
self,
|
|
prompt: str,
|
|
system_prompt: Optional[str] = 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}
|
|
}
|
|
|
|
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
|
|
|
|
return payload
|
|
|
|
async def call_ollama(self, payload: Dict[str, Any]) -> str:
|
|
"""Call Ollama API and accumulate response."""
|
|
session = await self.get_session()
|
|
|
|
async with session.post(
|
|
self.ollama_api_url,
|
|
json=payload,
|
|
timeout=aiohttp.ClientTimeout(total=self.ollama_timeout)
|
|
) 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()
|
|
if not line:
|
|
continue
|
|
|
|
try:
|
|
data = json.loads(line)
|
|
|
|
if 'thinking' in data and data['thinking']:
|
|
print(f"🤔 Le superviseur réfléchit : {data['thinking']}", flush=True)
|
|
continue
|
|
|
|
if 'response' in data:
|
|
chunk = data['response']
|
|
accumulated_reply += chunk
|
|
if chunk.strip():
|
|
print(f"🤖 Réponse du Superviseur (fragment) : {chunk}", flush=True)
|
|
|
|
elif 'action' in data:
|
|
accumulated_reply = line
|
|
break
|
|
|
|
except json.JSONDecodeError:
|
|
accumulated_reply += line
|
|
|
|
return accumulated_reply
|
|
|
|
def extract_json_actions(self, text: str) -> Optional[str]:
|
|
"""Extract JSON action objects from text more robustly."""
|
|
actions = []
|
|
|
|
# 1. 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]
|
|
|
|
for candidate_text in candidates:
|
|
# Try to parse the whole text as a list or dict first
|
|
try:
|
|
parsed = json.loads(candidate_text.strip())
|
|
if isinstance(parsed, list):
|
|
for item in parsed:
|
|
if isinstance(item, dict) and 'action' in item:
|
|
if item not in actions:
|
|
actions.append(item)
|
|
if actions: return json.dumps(actions)
|
|
elif isinstance(parsed, dict) and 'action' in parsed:
|
|
if parsed not in actions:
|
|
actions.append(parsed)
|
|
return json.dumps(actions)
|
|
except:
|
|
pass
|
|
|
|
# Fallback: scan for { ... } using brace matching
|
|
start_indexes = [i for i, c in enumerate(candidate_text) if c == '{']
|
|
for start in start_indexes:
|
|
brace_count = 0
|
|
for i in range(start, len(candidate_text)):
|
|
if candidate_text[i] == '{':
|
|
brace_count += 1
|
|
elif candidate_text[i] == '}':
|
|
brace_count -= 1
|
|
if brace_count == 0:
|
|
json_str = candidate_text[start:i+1]
|
|
try:
|
|
parsed = json.loads(json_str)
|
|
if isinstance(parsed, dict) and 'action' in parsed:
|
|
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
|
|
break # Move to next start brace
|
|
|
|
if actions:
|
|
return json.dumps(actions)
|
|
return None
|
|
|