""" Free-form, read-only chat with GPT-4o about the current cockpit state. Deliberately has NO function-calling/tools wired up — a plain text-completion call physically cannot trigger any action (no trade, no cycle, no DB write beyond persisting the conversation itself). The system prompt also tells the model explicitly not to claim it can act, so it doesn't mislead the user. """ import re import time from typing import Dict, List, Optional from services.ai_analyzer import get_client from services.ai_chat_context import assemble_context, CONTEXT_BLOCKS SYSTEM_PROMPT_HEADER = """Tu es l'assistant IA integre au cockpit de trading OpenFin Intelligence. Tu as acces ci-dessous a un instantane en lecture seule de la situation actuelle (portefeuille, risque geopolitique, regime macro, patterns, options/IV, indicateurs techniques, signaux ondelettes, watchlist, calendrier economique, rapports institutionnels, lecons accumulees, VaR/risque). REGLES IMPORTANTES : - Tu ne peux declencher AUCUNE action (pas de trade, pas de cycle, pas de modification) - tu es uniquement la pour discuter, expliquer et aider a comprendre la situation ou une idee (ex. un montage d'options, un signal ondelette, pourquoi un pattern a tel score). - Si on te demande d'agir, rappelle clairement que tu ne peux qu'expliquer/discuter, pas executer. - Reponds en francais, de facon concise et directe, en t'appuyant sur le contexte fourni. Si une donnee demandee n'est pas dans le contexte ci-dessous, dis-le plutot que d'inventer. === CONTEXTE ACTUEL === {context} === FIN DU CONTEXTE === """ def _chat_messages(system: str, messages: List[Dict], model: str = "gpt-4o", max_tokens: int = 1200) -> str: """Multi-turn variant of ai_analyzer._chat() — accepts a full message history instead of a single system+user pair. Same client/retry/backoff logic, kept independent so it never risks the well-tested cycle-facing _chat().""" client = get_client() if not client: raise RuntimeError("OpenAI API key not configured") kwargs = { "model": model, "messages": [{"role": "system", "content": system}] + messages, "temperature": 0.4, "max_tokens": max_tokens, } last_exc: Optional[Exception] = None for attempt in range(4): try: resp = client.chat.completions.create(**kwargs) return resp.choices[0].message.content or "" except Exception as e: last_exc = e err_str = str(e) if "429" in err_str or "rate_limit" in err_str: m = re.search(r"try again in ([\d.]+)s", err_str) wait = float(m.group(1)) + 1.0 if m else 2 ** (attempt + 1) * 5.0 time.sleep(min(wait, 60.0)) continue raise raise last_exc # type: ignore[misc] def send_chat_message( session_id: str, message: str, enabled_blocks: Optional[List[str]] = None, refresh_context: bool = False, ) -> Dict: from services.database import save_chat_message, get_chat_messages enabled = enabled_blocks or CONTEXT_BLOCKS blocks = assemble_context(enabled, session_id, refresh=refresh_context) context_text = "\n\n".join(blocks.values()) system = SYSTEM_PROMPT_HEADER.format(context=context_text) history = get_chat_messages(session_id) messages = [{"role": h["role"], "content": h["content"]} for h in history] messages.append({"role": "user", "content": message}) save_chat_message(session_id, "user", message) reply = _chat_messages(system, messages) save_chat_message(session_id, "assistant", reply) return {"reply": reply, "blocks_included": list(blocks.keys())}