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
|
|
@ -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