Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions backend/utils/believe_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
43 changes: 38 additions & 5 deletions backend/utils/llm/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
#
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions backend/utils/llm/conversation_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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}.
Expand Down
5 changes: 4 additions & 1 deletion backend/utils/llm/external_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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.
Expand Down