From cea1725cfe2da923b9f3feecf6b27c414a264d41 Mon Sep 17 00:00:00 2001 From: Jbelieve Date: Thu, 9 Jul 2026 12:20:55 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat(brain):=20fanout=20de=20Omi=20embebe?= =?UTF-8?q?=20episodios=20(nomic=20768d)=20para=20b=C3=BAsqueda=20sem?= =?UTF-8?q?=C3=A1ntica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- backend/utils/believe_brain.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/backend/utils/believe_brain.py b/backend/utils/believe_brain.py index db0168aa0e4..90a83ea54bd 100644 --- a/backend/utils/believe_brain.py +++ b/backend/utils/believe_brain.py @@ -38,6 +38,7 @@ import database.memories as memories_db from models.conversation import Conversation from utils.conversations.render import conversation_to_dict +from utils.llm.clients import generate_embedding logger = logging.getLogger(__name__) @@ -102,6 +103,19 @@ def _build_episode(uid: str, conversation: Conversation) -> Optional[dict]: action_items = structured.get("action_items", []) or [] events = structured.get("events", []) or [] + # believe: embedding 768d (Ollama nomic-embed-text) para búsqueda semántica + # en el Company Brain (mc-brain-search). Fail-soft: si falla, el episodio + # entra sin vector y sigue siendo consultable por filtros. + embedding = None + try: + _embed_text = (overview or title or transcript_ref or "").strip() + if _embed_text: + _vec = generate_embedding(_embed_text[:4000]) + if isinstance(_vec, list) and len(_vec) == 768: + embedding = _vec + except Exception as _e: + logger.warning(f"believe_brain: embedding failed for {conversation.id}: {_e}") + return { "source": "omi", "external_id": conversation.id, @@ -110,6 +124,7 @@ def _build_episode(uid: str, conversation: Conversation) -> Optional[dict]: "summary": overview or title or None, "transcript_ref": transcript_ref, "channel": "omi", + "embedding": embedding, "created_at": conversation.created_at.isoformat() if conversation.created_at else None, "metadata": { "omi_uid": uid, From c5ab4c55c605b68de470e89ac243e94cdfbd4722 Mon Sep 17 00:00:00 2001 From: Jbelieve Date: Thu, 9 Jul 2026 15:25:27 -0500 Subject: [PATCH 2/3] =?UTF-8?q?fix(brain):=20forzar=20espa=C3=B1ol=20en=20?= =?UTF-8?q?title/overview=20estructurado=20de=20conversaciones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los resúmenes que fluyen al Company Brain de Believe (summary=overview, decision_summary=title en believe_brain.py) deben salir SIEMPRE en español, sin importar el idioma detectado de la conversación ni la preferencia del usuario. Algunos salían en inglés (ej. "Radio Segment Promotes Public Notices"). Se fuerza response_language='Spanish' en get_transcript_structure y get_reprocess_transcript_structure (conversation_processing.py) y en get_message_structure (external_integrations.py). summarize_experience_text no tenía instrucción de idioma; se le agrega respuesta en español. Co-Authored-By: Claude Fable 5 --- backend/utils/llm/conversation_processing.py | 8 ++++++-- backend/utils/llm/external_integrations.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/utils/llm/conversation_processing.py b/backend/utils/llm/conversation_processing.py index e46a03c2ee8..81388cedc0a 100644 --- a/backend/utils/llm/conversation_processing.py +++ b/backend/utils/llm/conversation_processing.py @@ -629,7 +629,9 @@ def get_transcript_structure( if not conversation_context: return Structured() # Should be caught by discard logic, but as a safeguard. - response_language = output_language_code or language_code + # Believe: title/overview feeding the Company Brain MUST always be in Spanish, regardless of + # the detected conversation language or the user's output preference. + response_language = 'Spanish' # First system message: task-specific instructions (static prefix enables cross-conversation caching) # NOTE: language instructions are in context_message (second message) to keep this prefix fully static. @@ -723,7 +725,9 @@ def get_reprocess_transcript_structure( return Structured() full_context = "\n\n".join(context_parts) - response_language = output_language_code or language_code + # Believe: title/overview feeding the Company Brain MUST always be in Spanish, regardless of + # the detected conversation language or the user's output preference. + response_language = 'Spanish' prompt_text = '''You are an expert content analyzer. Your task is to analyze the provided content (which could be a transcript, a series of photo descriptions from a wearable camera, or both) and provide structure and clarity. The content language is {language_code}. You MUST respond entirely in {response_language}. diff --git a/backend/utils/llm/external_integrations.py b/backend/utils/llm/external_integrations.py index 6ad9aef1a5d..cb2fae8d6b3 100644 --- a/backend/utils/llm/external_integrations.py +++ b/backend/utils/llm/external_integrations.py @@ -27,7 +27,9 @@ def get_message_structure( text_source_spec: str = None, output_language_code: str = None, ) -> Structured: - response_language = output_language_code or language_code + # Believe: title/overview feeding the Company Brain MUST always be in Spanish, regardless of + # the detected message language or the user's output preference. + response_language = 'Spanish' prompt_text = ''' You are an expert message analyzer. Your task is to analyze the message content and provide structure and clarity. The message language is {language_code}. You MUST respond entirely in {response_language}. @@ -74,6 +76,7 @@ def get_message_structure( def summarize_experience_text(text: str, text_source_spec: str = None) -> Structured: source_context = f"Source: {text_source_spec}" if text_source_spec else "their own experiences or thoughts" prompt = f'''The user sent a text of {source_context}, and wants to create a memory from it. + You MUST respond entirely in Spanish (every word of the title and overview). For the title, use the main topic of the experience or thought. For the overview, condense the descriptions into a brief summary with the main topics discussed, make sure to capture the key points and important details. For the category, classify the scenes into one of the available categories. From d94b11029af1b20f16b8951704091cca28f51fbb Mon Sep 17 00:00:00 2001 From: Jbelieve Date: Wed, 29 Jul 2026 19:06:48 -0500 Subject: [PATCH 3/3] feat: conectar Langfuse al get_llm() central via CallbackHandler de LangChain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Believe está instrumentando Langfuse (langfuse.believe-global.com) en todo el stack que consume LLM. omi-believe ya centraliza todos los clientes LangChain en get_llm()/clients.py con un usage_tracker propio, así que el menor esfuerzo es sumar el CallbackHandler oficial de Langfuse a la misma lista de callbacks que ya se pasa a cada cliente (OpenAI, Gemini, OpenRouter, BYOK, llm_mini) en vez de instrumentar cada call site. No-op si LANGFUSE_HOST/LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY no están en el entorno, y cualquier fallo al inicializar el handler se loguea pero no rompe el arranque. Co-Authored-By: Claude Sonnet 5 --- backend/requirements.txt | 1 + backend/utils/llm/clients.py | 43 +++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 233986845da..fe130b6e75a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -93,6 +93,7 @@ jsonpointer==3.0.0 jsonschema==4.23.0 jsonschema-specifications==2023.12.1 kiwisolver==1.4.5 +langfuse==4.14.1 langchain==1.2.9 langchain-community==0.4.1 langchain-core==1.3.3 diff --git a/backend/utils/llm/clients.py b/backend/utils/llm/clients.py index 3b4e8545c73..e24be920e18 100644 --- a/backend/utils/llm/clients.py +++ b/backend/utils/llm/clients.py @@ -21,6 +21,39 @@ _usage_callback = get_usage_callback() + +def _init_langfuse_callback() -> Optional[Any]: + """Create the Langfuse LangChain callback handler if Langfuse is configured. + + No-op (returns None) when LANGFUSE_HOST/LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY + aren't all set in the environment — keeps local/test setups working without + Langfuse and without touching startup behavior. + """ + if not ( + os.environ.get('LANGFUSE_HOST') + and os.environ.get('LANGFUSE_PUBLIC_KEY') + and os.environ.get('LANGFUSE_SECRET_KEY') + ): + return None + try: + from langfuse.langchain import CallbackHandler + + return CallbackHandler() + except Exception: + logger.exception('Failed to initialize Langfuse callback handler — continuing without it') + return None + + +_langfuse_callback = _init_langfuse_callback() + + +def _llm_callbacks() -> List[Any]: + """Base callback list for every LangChain LLM client: usage tracking + Langfuse (if configured).""" + if _langfuse_callback is not None: + return [_usage_callback, _langfuse_callback] + return [_usage_callback] + + # --------------------------------------------------------------------------- # BYOK (Bring Your Own Key) # @@ -90,7 +123,7 @@ def _create_byok_client( model: str, provider: str, byok_key: str, streaming: bool = False, feature: str = '' ) -> Optional[ChatOpenAI]: """Create a ChatOpenAI using the user's BYOK key. Returns None if BYOK not supported for this provider.""" - kwargs: Dict[str, Any] = {'callbacks': [_usage_callback], 'request_timeout': 120, 'max_retries': 1} + kwargs: Dict[str, Any] = {'callbacks': _llm_callbacks(), 'request_timeout': 120, 'max_retries': 1} if model == 'gpt-5.1': kwargs['extra_body'] = {"prompt_cache_retention": "24h"} if streaming: @@ -397,7 +430,7 @@ def _get_or_create_openai_llm(model_name: str, streaming: bool = False) -> ChatO key = (model_name, streaming, 'openai') if key not in _llm_cache: kwargs: Dict[str, Any] = { - 'callbacks': [_usage_callback], + 'callbacks': _llm_callbacks(), 'request_timeout': 120, 'max_retries': 1, } @@ -426,7 +459,7 @@ def _get_or_create_openrouter_llm( 'api_key': os.environ.get('OPENROUTER_API_KEY'), 'base_url': "https://openrouter.ai/api/v1", 'default_headers': {"X-Title": "Omi Chat"}, - 'callbacks': [_usage_callback], + 'callbacks': _llm_callbacks(), 'request_timeout': 120, 'max_retries': 1, } @@ -465,7 +498,7 @@ def _get_or_create_gemini_llm( use_vertex = os.environ.get('USE_VERTEX_AI', '').lower() == 'true' gcp_project = os.environ.get('GOOGLE_CLOUD_PROJECT', '') if use_vertex else '' gemini_key = os.environ.get('GEMINI_API_KEY', '') - kwargs: Dict[str, Any] = {'callbacks': [_usage_callback], 'timeout': 120, 'max_retries': 1} + kwargs: Dict[str, Any] = {'callbacks': _llm_callbacks(), 'timeout': 120, 'max_retries': 1} if streaming: kwargs['streaming'] = True if thinking_budget is not None and model_name.startswith('gemini-2.5'): @@ -616,7 +649,7 @@ def get_qos_info() -> Dict[str, Dict[str, str]]: # Legacy module-level alias (kept for test compatibility). # Production code should use get_llm(feature) exclusively. # --------------------------------------------------------------------------- -llm_mini = ChatOpenAI(model='gpt-4.1-mini', callbacks=[_usage_callback], request_timeout=120, max_retries=1) +llm_mini = ChatOpenAI(model='gpt-4.1-mini', callbacks=_llm_callbacks(), request_timeout=120, max_retries=1) # --------------------------------------------------------------------------- # Embeddings, parser, utilities