Skip to content
Merged
6 changes: 3 additions & 3 deletions backends/advanced/docker-compose-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ services:
- ./data/test_audio_chunks:/app/audio_chunks
- ./data/test_debug_dir:/app/debug_dir
- ./data/test_data:/app/data
- ${CONFIG_FILE:-../../config/config.yml}:/app/config.yml:ro # Mount config.yml for model registry and memory settings
- ${CONFIG_FILE:-../../config/config.yml}:/app/config.yml # Mount config.yml for model registry and memory settings (writable for admin config updates)
environment:
# Override with test-specific settings
- MONGODB_URI=mongodb://mongo-test:27017/test_db
Expand Down Expand Up @@ -160,7 +160,7 @@ services:
- ./data/test_audio_chunks:/app/audio_chunks
- ./data/test_debug_dir:/app/debug_dir
- ./data/test_data:/app/data
- ${CONFIG_FILE:-../../config/config.yml}:/app/config.yml:ro # Mount config.yml for model registry and memory settings
- ${CONFIG_FILE:-../../config/config.yml}:/app/config.yml # Mount config.yml for model registry and memory settings (writable for admin config updates)
environment:
# Same environment as backend
- MONGODB_URI=mongodb://mongo-test:27017/test_db
Expand Down Expand Up @@ -283,4 +283,4 @@ networks:
# - --force-recreate for clean state
# - Volume cleanup between test runs
# - Environment variables can be injected via GitHub secrets
# - Health checks ensure services are ready before tests run
# - Health checks ensure services are ready before tests run
38 changes: 31 additions & 7 deletions backends/advanced/src/advanced_omi_backend/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from advanced_omi_backend.database import get_database
from advanced_omi_backend.llm_client import get_llm_client
from advanced_omi_backend.model_registry import get_models_registry
from advanced_omi_backend.services.memory import get_memory_service
from advanced_omi_backend.services.memory.base import MemoryEntry
from advanced_omi_backend.services.obsidian_service import (
Expand Down Expand Up @@ -133,7 +134,7 @@ def from_dict(cls, data: Dict) -> "ChatSession":

class ChatService:
"""Service for managing chat sessions and memory-enhanced conversations."""

def __init__(self):
self.db = None
self.sessions_collection: Optional[AsyncIOMotorCollection] = None
Expand All @@ -142,6 +143,33 @@ def __init__(self):
self.memory_service = None
self._initialized = False

def _get_system_prompt(self) -> str:
"""
Get system prompt from config with fallback to default.

Returns:
str: System prompt for chat interactions
"""
try:
reg = get_models_registry()
if reg and hasattr(reg, 'chat'):
chat_config = reg.chat
prompt = chat_config.get('system_prompt')
if prompt:
logger.info(f"✅ Loaded chat system prompt from config (length: {len(prompt)} chars)")
logger.debug(f"System prompt: {prompt[:100]}...")
return prompt
except Exception as e:
logger.warning(f"Failed to load chat system prompt from config: {e}")

# Fallback to default
logger.info("⚠️ Using default chat system prompt (config not found)")
return """You are a helpful AI assistant with access to the user's personal memories and conversation history.

Use the provided memories and conversation context to give personalized, contextual responses. If memories are relevant, reference them naturally in your response. Be conversational and helpful.

If no relevant memories are available, respond normally based on the conversation context."""

async def initialize(self):
"""Initialize the chat service with database connections."""
if self._initialized:
Expand Down Expand Up @@ -392,12 +420,8 @@ async def generate_response_stream(
"timestamp": time.time()
}

# Create system prompt
system_prompt = """You are a helpful AI assistant with access to the user's personal memories and conversation history.

Use the provided memories and conversation context to give personalized, contextual responses. If memories are relevant, reference them naturally in your response. Be conversational and helpful.

If no relevant memories are available, respond normally based on the conversation context."""
# Get system prompt from config
system_prompt = self._get_system_prompt()

# Prepare full prompt
full_prompt = f"{system_prompt}\n\n{context}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,103 @@ async def set_memory_provider(provider: str):
except Exception as e:
logger.exception("Error setting memory provider")
raise e


# Chat Configuration Management Functions

async def get_chat_config_yaml() -> str:
"""Get chat system prompt as plain text."""
try:
config_path = _find_config_path()

default_prompt = """You are a helpful AI assistant with access to the user's personal memories and conversation history.

Use the provided memories and conversation context to give personalized, contextual responses. If memories are relevant, reference them naturally in your response. Be conversational and helpful.

If no relevant memories are available, respond normally based on the conversation context."""

if not os.path.exists(config_path):
return default_prompt

with open(config_path, 'r') as f:
full_config = yaml.safe_load(f) or {}

chat_config = full_config.get('chat', {})
system_prompt = chat_config.get('system_prompt', default_prompt)

# Return just the prompt text, not the YAML structure
return system_prompt

except Exception as e:
logger.error(f"Error loading chat config: {e}")
raise
Comment on lines +485 to +487
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve exception handling to follow project patterns.

The exception handler should use logging.exception() instead of logger.error() to capture the full stack trace, and should re-raise with from e to preserve exception context.

Based on learnings, this improves debuggability.

🔎 Proposed fix
     except Exception as e:
-        logger.error(f"Error loading chat config: {e}")
-        raise
+        logger.exception("Error loading chat config")
+        raise e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
logger.error(f"Error loading chat config: {e}")
raise
except Exception as e:
logger.exception("Error loading chat config")
raise
🧰 Tools
🪛 Ruff (0.14.10)

486-486: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In backends/advanced/src/advanced_omi_backend/controllers/system_controller.py
around lines 485 to 487, the except block currently calls logger.error and
re-raises without preserving context; replace logger.error(...) with
logger.exception(...) to log the full stack trace, and re-raise the exception
using "raise ... from e" (e.g., raise <same-exception> from e) so the original
exception context is preserved.



async def save_chat_config_yaml(prompt_text: str) -> dict:
"""Save chat system prompt from plain text."""
try:
config_path = _find_config_path()

# Validate plain text prompt
if not prompt_text or not isinstance(prompt_text, str):
raise ValueError("Prompt must be a non-empty string")

prompt_text = prompt_text.strip()
if len(prompt_text) < 10:
raise ValueError("Prompt too short (minimum 10 characters)")
if len(prompt_text) > 10000:
raise ValueError("Prompt too long (maximum 10000 characters)")

# Create chat config dict
chat_config = {'system_prompt': prompt_text}

# Load full config
if os.path.exists(config_path):
with open(config_path, 'r') as f:
full_config = yaml.safe_load(f) or {}
else:
full_config = {}

# Backup existing config
if os.path.exists(config_path):
backup_path = str(config_path) + '.backup'
shutil.copy2(config_path, backup_path)
logger.info(f"Created config backup at {backup_path}")

# Update chat section
full_config['chat'] = chat_config

# Save
with open(config_path, 'w') as f:
yaml.dump(full_config, f, default_flow_style=False, allow_unicode=True)

# Reload config in memory (hot-reload)
load_models_config(force_reload=True)

logger.info("Chat configuration updated successfully")

return {"success": True, "message": "Chat configuration updated successfully"}

except Exception as e:
logger.error(f"Error saving chat config: {e}")
raise
Comment on lines +535 to +537
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve exception handling to follow project patterns.

The exception handler should use logging.exception() instead of logger.error() to capture the full stack trace, and should re-raise with from e to preserve exception context.

Based on learnings, this improves debuggability.

🔎 Proposed fix
     except Exception as e:
-        logger.error(f"Error saving chat config: {e}")
-        raise
+        logger.exception("Error saving chat config")
+        raise e
🧰 Tools
🪛 Ruff (0.14.10)

536-536: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In backends/advanced/src/advanced_omi_backend/controllers/system_controller.py
around lines 535 to 537, the except block uses logger.error and a plain raise;
replace logger.error(...) with logger.exception(...) to record the full stack
trace, and re-raise the caught exception using "raise ... from e" (i.e., raise
the same exception with "from e") to preserve exception chaining and context.



async def validate_chat_config_yaml(prompt_text: str) -> dict:
"""Validate chat system prompt plain text."""
try:
# Validate plain text prompt
if not isinstance(prompt_text, str):
return {"valid": False, "error": "Prompt must be a string"}

prompt_text = prompt_text.strip()
if len(prompt_text) < 10:
return {"valid": False, "error": "Prompt too short (minimum 10 characters)"}
if len(prompt_text) > 10000:
return {"valid": False, "error": "Prompt too long (maximum 10000 characters)"}

return {"valid": True, "message": "Configuration is valid"}

except Exception as e:
logger.error(f"Error validating chat config: {e}")
return {"valid": False, "error": f"Validation error: {str(e)}"}
Comment on lines +555 to +557
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use logging.exception() for better error diagnostics.

While the catch-all exception handling is appropriate here (since this is a validation function that returns error details), the logging should use logging.exception() instead of logger.error() to capture the full stack trace.

Based on learnings, this improves debuggability.

🔎 Proposed fix
     except Exception as e:
-        logger.error(f"Error validating chat config: {e}")
+        logger.exception("Error validating chat config")
         return {"valid": False, "error": f"Validation error: {str(e)}"}
🧰 Tools
🪛 Ruff (0.14.10)

555-555: Do not catch blind exception: Exception

(BLE001)


556-556: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


557-557: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In backends/advanced/src/advanced_omi_backend/controllers/system_controller.py
around lines 555-557, the exception handling logs the error with logger.error
which loses the stack trace; change the call to logger.exception(...) (or
logging.exception if using module logger) to log the full traceback and keep
returning the same {"valid": False, "error": ...} payload so diagnostics include
the stack trace while behavior remains unchanged.

14 changes: 10 additions & 4 deletions backends/advanced/src/advanced_omi_backend/model_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,15 @@ def validate_model(self) -> ModelDef:

class AppModels(BaseModel):
"""Application models registry.

Contains default model selections and all available model definitions.
"""

model_config = ConfigDict(
extra='allow',
validate_assignment=True,
)

defaults: Dict[str, str] = Field(
default_factory=dict,
description="Default model names for each model_type"
Expand All @@ -185,6 +185,10 @@ class AppModels(BaseModel):
default_factory=dict,
description="Speaker recognition service configuration"
)
chat: Dict[str, Any] = Field(
default_factory=dict,
description="Chat service configuration including system prompt"
)

def get_by_name(self, name: str) -> Optional[ModelDef]:
"""Get a model by its unique name.
Expand Down Expand Up @@ -318,6 +322,7 @@ def load_models_config(force_reload: bool = False) -> Optional[AppModels]:
model_list = raw.get("models", []) or []
memory_settings = raw.get("memory", {}) or {}
speaker_recognition_cfg = raw.get("speaker_recognition", {}) or {}
chat_settings = raw.get("chat", {}) or {}

# Parse and validate models using Pydantic
models: Dict[str, ModelDef] = {}
Expand All @@ -336,7 +341,8 @@ def load_models_config(force_reload: bool = False) -> Optional[AppModels]:
defaults=defaults,
models=models,
memory=memory_settings,
speaker_recognition=speaker_recognition_cfg
speaker_recognition=speaker_recognition_cfg,
chat=chat_settings
)
return _REGISTRY

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import logging
from typing import Optional

from fastapi import APIRouter, Body, Depends, Request
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from pydantic import BaseModel

from advanced_omi_backend.auth import current_active_user, current_superuser
Expand Down Expand Up @@ -128,6 +129,53 @@ async def delete_all_user_memories(current_user: User = Depends(current_active_u
return await system_controller.delete_all_user_memories(current_user)


# Chat Configuration Management Endpoints

@router.get("/admin/chat/config", response_class=Response)
async def get_chat_config(current_user: User = Depends(current_superuser)):
"""Get chat configuration as YAML. Admin only."""
try:
yaml_content = await system_controller.get_chat_config_yaml()
return Response(content=yaml_content, media_type="text/plain")
except Exception as e:
logger.error(f"Failed to get chat config: {e}")
raise HTTPException(status_code=500, detail=str(e))
Comment on lines +137 to +142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Improve exception handling to follow project patterns.

The exception handling should use logging.exception() to capture the full stack trace and chain the raised exception with from e to preserve context.

🔎 Proposed fix
     try:
         yaml_content = await system_controller.get_chat_config_yaml()
         return Response(content=yaml_content, media_type="text/plain")
     except Exception as e:
-        logger.error(f"Failed to get chat config: {e}")
-        raise HTTPException(status_code=500, detail=str(e))
+        logger.exception("Failed to get chat config")
+        raise HTTPException(status_code=500, detail=str(e)) from e
🧰 Tools
🪛 Ruff (0.14.10)

140-140: Do not catch blind exception: Exception

(BLE001)


141-141: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


142-142: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In @backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py
around lines 137 - 142, Replace the broad logger.error call and bare raise with
logging.exception to record the full traceback and re-raise the HTTPException
chained to the original exception; specifically, in the except block catching
exceptions from system_controller.get_chat_config_yaml(), call
logger.exception("Failed to get chat config") and then raise
HTTPException(status_code=500, detail=str(e)) from e so the context is
preserved.



@router.post("/admin/chat/config")
async def save_chat_config(
request: Request,
current_user: User = Depends(current_superuser)
):
"""Save chat configuration from YAML. Admin only."""
try:
yaml_content = await request.body()
yaml_str = yaml_content.decode('utf-8')
result = await system_controller.save_chat_config_yaml(yaml_str)
return JSONResponse(content=result)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to save chat config: {e}")
raise HTTPException(status_code=500, detail=str(e))
Comment on lines +151 to +160
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Improve exception handling to follow project patterns.

Both exception handlers should chain exceptions with from e to preserve context, and the general exception handler should use logging.exception() instead of logger.error().

🔎 Proposed fix
     try:
         yaml_content = await request.body()
         yaml_str = yaml_content.decode('utf-8')
         result = await system_controller.save_chat_config_yaml(yaml_str)
         return JSONResponse(content=result)
     except ValueError as e:
-        raise HTTPException(status_code=400, detail=str(e))
+        raise HTTPException(status_code=400, detail=str(e)) from e
     except Exception as e:
-        logger.error(f"Failed to save chat config: {e}")
-        raise HTTPException(status_code=500, detail=str(e))
+        logger.exception("Failed to save chat config")
+        raise HTTPException(status_code=500, detail=str(e)) from e
🧰 Tools
🪛 Ruff (0.14.10)

157-157: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


158-158: Do not catch blind exception: Exception

(BLE001)


159-159: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


160-160: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In @backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py
around lines 151 - 160, The exception handlers in the save-chat-config route
should preserve exception context and log full trace: in the ValueError except
block re-raise HTTPException(status_code=400, detail=str(e)) using "raise ...
from e", and in the general except block replace logger.error(...) with
logging.exception("Failed to save chat config") (or logger.exception if logger
is the module logger) and re-raise HTTPException(status_code=500, detail=str(e))
using "raise ... from e"; apply these changes around the request.body()/yaml_str
handling and the call to system_controller.save_chat_config_yaml to preserve
traceback and follow project patterns.



@router.post("/admin/chat/config/validate")
async def validate_chat_config(
request: Request,
current_user: User = Depends(current_superuser)
):
"""Validate chat configuration YAML. Admin only."""
try:
yaml_content = await request.body()
yaml_str = yaml_content.decode('utf-8')
result = await system_controller.validate_chat_config_yaml(yaml_str)
return JSONResponse(content=result)
except Exception as e:
logger.error(f"Failed to validate chat config: {e}")
raise HTTPException(status_code=500, detail=str(e))
Comment on lines +169 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Improve exception handling to follow project patterns.

The exception handler should use logging.exception() instead of logger.error() and chain the raised exception with from e.

🔎 Proposed fix
     try:
         yaml_content = await request.body()
         yaml_str = yaml_content.decode('utf-8')
         result = await system_controller.validate_chat_config_yaml(yaml_str)
         return JSONResponse(content=result)
     except Exception as e:
-        logger.error(f"Failed to validate chat config: {e}")
-        raise HTTPException(status_code=500, detail=str(e))
+        logger.exception("Failed to validate chat config")
+        raise HTTPException(status_code=500, detail=str(e)) from e
🧰 Tools
🪛 Ruff (0.14.10)

174-174: Do not catch blind exception: Exception

(BLE001)


175-175: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


176-176: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In @backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py
around lines 169 - 176, Replace the current except block that logs the error and
raises HTTPException so it uses logger.exception(...) to record the full stack
trace and chain the raised HTTPException from the original exception;
specifically, in the try/except around awaiting request.body(), decoding
yaml_str and calling system_controller.validate_chat_config_yaml(yaml_str),
change logger.error(f"Failed to validate chat config: {e}") to
logger.exception("Failed to validate chat config") and change raise
HTTPException(status_code=500, detail=str(e)) to raise
HTTPException(status_code=500, detail=str(e)) from e.



@router.get("/streaming/status")
async def get_streaming_status(request: Request, current_user: User = Depends(current_superuser)):
"""Get status of active streaming sessions and Redis Streams health. Admin only."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,19 @@ async def search_memories(self, query_embedding: List[float], user_id: str, limi
# For cosine similarity, scores range from -1 to 1, where 1 is most similar
search_params = {
"collection_name": self.collection_name,
"query_vector": query_embedding,
"query": query_embedding,
"query_filter": search_filter,
"limit": limit
}

if score_threshold > 0.0:
search_params["score_threshold"] = score_threshold
memory_logger.debug(f"Using similarity threshold: {score_threshold}")
results = await self.client.search(**search_params)

response = await self.client.query_points(**search_params)

memories = []
for result in results:
for result in response.points:
memory = MemoryEntry(
id=str(result.id),
content=result.payload.get("content", ""),
Expand Down
3 changes: 2 additions & 1 deletion backends/advanced/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ echo "🚀 Starting Chronicle Backend..."
# Function to handle shutdown
shutdown() {
echo "🛑 Shutting down services..."
pkill -TERM -P $$
# Kill the backend process if running
[ -n "$BACKEND_PID" ] && kill -TERM $BACKEND_PID 2>/dev/null || true
wait
echo "✅ All services stopped"
exit 0
Expand Down
Loading