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,284 changes: 247 additions & 1,037 deletions Docs/audio-pipeline-architecture.md

Large diffs are not rendered by default.

47 changes: 0 additions & 47 deletions backends/advanced/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -74,50 +74,3 @@ GALILEO_LOG_STREAM=default

# Tailscale auth key (for remote service access)
TS_AUTHKEY=

# ========================================
# Plugin Configuration
# ========================================
# Plugin-specific configuration is in: backends/advanced/src/advanced_omi_backend/plugins/{plugin_id}/config.yml
# Plugin orchestration (enabled, events) is in: config/plugins.yml
# This section contains ONLY plugin secrets

# ---------------------------------------
# Home Assistant Plugin
# ---------------------------------------
# Enable in config/plugins.yml
# Configure in backends/advanced/src/advanced_omi_backend/plugins/homeassistant/config.yml

# Home Assistant server URL
HA_URL=http://homeassistant.local:8123

# Home Assistant long-lived access token
# Get from: Profile → Security → Long-Lived Access Tokens
HA_TOKEN=

# Wake word for voice commands (optional, default: vivi)
HA_WAKE_WORD=vivi

# Request timeout in seconds (optional, default: 30)
HA_TIMEOUT=30

# ---------------------------------------
# Email Summarizer Plugin
# ---------------------------------------
# Enable in config/plugins.yml
# Configure in backends/advanced/src/advanced_omi_backend/plugins/email_summarizer/config.yml

# SMTP server configuration
# For Gmail: Use App Password (requires 2FA enabled)
# 1. Go to Google Account → Security → 2-Step Verification
# 2. Scroll to "App passwords" → Generate password for "Mail"
# 3. Use the 16-character password below (no spaces)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password-here
SMTP_USE_TLS=true

# Email sender information
FROM_EMAIL=noreply@chronicle.ai
FROM_NAME=Chronicle AI
284 changes: 215 additions & 69 deletions backends/advanced/init.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,16 @@ async def mark_session_complete(
"""
session_key = f"audio:session:{session_id}"
mark_time = time.time()
mapping = {
"status": "finished",
"completed_at": str(mark_time),
"completion_reason": reason,
}
if reason == "websocket_disconnect":
mapping["websocket_connected"] = "false"
await redis_client.hset(
session_key,
mapping={
"status": "finished",
"completed_at": str(mark_time),
"completion_reason": reason,
},
mapping=mapping,
)
logger.info(
f"✅ Session {session_id[:12]} marked finished: {reason} [TIME: {mark_time:.3f}]"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pathlib import Path
from typing import Optional

from dotenv import set_key as dotenv_set_key
from fastapi import HTTPException
from ruamel.yaml import YAML

Expand Down Expand Up @@ -880,35 +881,13 @@ async def set_memory_provider(provider: str):
if not os.path.exists(env_path):
raise FileNotFoundError(f".env file not found at {env_path}")

# Read current .env file
with open(env_path, "r") as file:
lines = file.readlines()

# Update or add MEMORY_PROVIDER line
provider_found = False
updated_lines = []

for line in lines:
if line.strip().startswith("MEMORY_PROVIDER="):
updated_lines.append(f"MEMORY_PROVIDER={provider}\n")
provider_found = True
else:
updated_lines.append(line)

# If MEMORY_PROVIDER wasn't found, add it
if not provider_found:
updated_lines.append(
f"\n# Memory Provider Configuration\nMEMORY_PROVIDER={provider}\n"
)

# Create backup
backup_path = f"{env_path}.bak"
shutil.copy2(env_path, backup_path)
logger.info(f"Created .env backup at {backup_path}")

# Write updated .env file
with open(env_path, "w") as file:
file.writelines(updated_lines)
# Update key using python-dotenv (handles add-or-update automatically)
dotenv_set_key(env_path, "MEMORY_PROVIDER", provider, quote_mode="never")

# Update environment variable for current process
os.environ["MEMORY_PROVIDER"] = provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,18 @@ async def cleanup_client_state(client_id: str):
for key in keys:
# Check if this session belongs to this client
client_id_bytes = await async_redis.hget(key, "client_id")
if client_id_bytes and client_id_bytes.decode() == client_id:
is_match = client_id_bytes and client_id_bytes.decode() == client_id
if not is_match:
# Fallback: session hash may have lost client_id (e.g., partial resurrection).
# Session IDs are formatted as "{client_id}-{uuid}", so check prefix.
session_id_from_key = key.decode().replace("audio:session:", "")
if session_id_from_key.startswith(client_id):
is_match = True
logger.warning(
f"⚠️ Session {session_id_from_key[:12]} missing client_id field — "
f"matched via session_id prefix fallback"
)
if is_match:
session_id = key.decode().replace("audio:session:", "")

# Check session status
Expand Down
76 changes: 48 additions & 28 deletions backends/advanced/src/advanced_omi_backend/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def generate(
pass

@abstractmethod
def health_check(self) -> Dict:
async def health_check(self) -> Dict:
"""Check if the LLM service is available and healthy."""
pass

Expand Down Expand Up @@ -120,34 +120,55 @@ def chat_with_tools(
params["tools"] = tools
return self.client.chat.completions.create(**params)

def health_check(self) -> Dict:
"""Check OpenAI-compatible service health."""
async def health_check(self) -> Dict:
"""Check OpenAI-compatible service health by calling models.list()."""
import openai as _openai

if not self.api_key or not self.base_url or not self.model:
return {
"status": "⚠️ Configuration incomplete",
"healthy": False,
"base_url": self.base_url,
"default_model": self.model,
}

try:
# For OpenAI API, check if we have valid configuration
# Avoid calling /models endpoint as it can be unreliable
if self.api_key and self.api_key != "dummy" and self.model:
return {
"status": "✅ Connected",
"base_url": self.base_url,
"default_model": self.model,
"api_key_configured": bool(
self.api_key and self.api_key != "dummy"
),
}
else:
return {
"status": "⚠️ Configuration incomplete",
"base_url": self.base_url,
"default_model": self.model,
"api_key_configured": bool(
self.api_key and self.api_key != "dummy"
),
}
async_client = create_openai_client(
api_key=self.api_key, base_url=self.base_url, is_async=True
)
await asyncio.wait_for(async_client.models.list(), timeout=5.0)
return {
"status": "✅ Connected",
"healthy": True,
"base_url": self.base_url,
"default_model": self.model,
}
except _openai.AuthenticationError:
return {
"status": "❌ Auth Failed — check API key",
"healthy": False,
"base_url": self.base_url,
"default_model": self.model,
}
except asyncio.TimeoutError:
return {
"status": "❌ Connection Timeout",
"healthy": False,
"base_url": self.base_url,
"default_model": self.model,
}
except _openai.APIConnectionError:
return {
"status": "❌ Connection Failed — service unreachable",
"healthy": False,
"base_url": self.base_url,
"default_model": self.model,
}
except Exception as e:
self.logger.error(f"Health check failed: {e}")
return {
"status": "❌ Failed",
"error": str(e),
"status": f"❌ Error: {e}",
"healthy": False,
"base_url": self.base_url,
"default_model": self.model,
}
Expand Down Expand Up @@ -280,7 +301,6 @@ async def async_chat_with_tools(


async def async_health_check() -> Dict:
"""Async wrapper for LLM health check."""
"""Async LLM health check."""
client = get_llm_client()
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, client.health_check)
return await client.health_check()
Loading
Loading