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
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,8 @@ config-docker: ## Generate Docker Compose configuration files
@CONFIG_FILE=config.env.dev python3 scripts/generate-docker-configs.py
@echo "✅ Docker Compose configuration files generated"

config-k8s: ## Generate Kubernetes configuration files (Skaffold env + ConfigMap/Secret)
config-k8s: ## Generate Kubernetes configuration files (ConfigMap/Secret only - no .env files)
@echo "☸️ Generating Kubernetes configuration files..."
@python3 scripts/generate-docker-configs.py
@python3 scripts/generate-k8s-configs.py
@echo "📦 Applying ConfigMap and Secret to Kubernetes..."
@kubectl apply -f k8s-manifests/configmap.yaml -n $(APPLICATION_NAMESPACE) 2>/dev/null || echo "⚠️ ConfigMap not applied (cluster not available?)"
Expand Down
5 changes: 4 additions & 1 deletion backends/advanced/Dockerfile.k8s
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ COPY memory_config.yaml ./
COPY start-k8s.sh start-workers.sh ./
RUN chmod +x start-k8s.sh start-workers.sh

# Activate virtual environment in PATH
ENV PATH="/app/.venv/bin:$PATH"

# Run the application with workers
# K8s startup script starts both FastAPI backend and RQ workers with --no-sync
# K8s startup script starts both FastAPI backend and RQ workers
CMD ["./start-k8s.sh"]
2 changes: 1 addition & 1 deletion backends/advanced/Docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ src/advanced_omi_backend/
- `GET /api/conversations/{conversation_id}/versions` - Get version history
- `POST /api/conversations/{conversation_id}/activate-transcript` - Switch transcript version
- `POST /api/conversations/{conversation_id}/activate-memory` - Switch memory version
- `POST /api/process-audio-files` - Batch audio file processing
- `POST /api/audio/upload` - Batch audio file upload and processing
- WebSocket `/ws_omi` - Real-time Opus audio streaming with Wyoming protocol (OMI devices)
- WebSocket `/ws_pcm` - Real-time PCM audio streaming with Wyoming protocol (all apps)

Expand Down
4 changes: 2 additions & 2 deletions backends/advanced/Docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,13 @@ The system supports processing existing audio files through the file upload API.
export USER_TOKEN="your-jwt-token"

# Upload single WAV file
curl -X POST "http://localhost:8000/api/process-audio-files" \
curl -X POST "http://localhost:8000/api/audio/upload" \
-H "Authorization: Bearer $USER_TOKEN" \
-F "files=@/path/to/audio.wav" \
-F "device_name=file_upload"

# Upload multiple WAV files
curl -X POST "http://localhost:8000/api/process-audio-files" \
curl -X POST "http://localhost:8000/api/audio/upload" \
-H "Authorization: Bearer $USER_TOKEN" \
-F "files=@/path/to/recording1.wav" \
-F "files=@/path/to/recording2.wav" \
Expand Down
43 changes: 42 additions & 1 deletion backends/advanced/docker-compose-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ services:
# Use test database name to ensure isolation
command: mongod --dbpath /data/db --bind_ip_all
healthcheck:
test: ["CMD", "mongo", "--eval", "db.runCommand('ping').ok", "--quiet"]
test: ["CMD", "mongosh", "--eval", "db.runCommand('ping').ok", "--quiet"]
interval: 5s
timeout: 5s
retries: 10
Expand All @@ -114,6 +114,47 @@ services:
timeout: 3s
retries: 5

workers-test:
build:
context: .
dockerfile: Dockerfile
command: ./start-workers.sh
volumes:
- ./src:/app/src
- ./data/test_audio_chunks:/app/audio_chunks
- ./data/test_debug_dir:/app/debug_dir
- ./data/test_data:/app/data
environment:
# Same environment as backend
- MONGODB_URI=mongodb://mongo-test:27017/test_db
- QDRANT_BASE_URL=qdrant-test
- QDRANT_PORT=6333
- REDIS_URL=redis://redis-test:6379/0
- DEBUG_DIR=/app/debug_dir
- DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- LLM_PROVIDER=${LLM_PROVIDER:-openai}
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o-mini}
- AUTH_SECRET_KEY=test-jwt-signing-key-for-integration-tests
- ADMIN_PASSWORD=test-admin-password-123
- ADMIN_EMAIL=test-admin@example.com
- TRANSCRIPTION_PROVIDER=${TRANSCRIPTION_PROVIDER:-deepgram}
- MEMORY_PROVIDER=${MEMORY_PROVIDER:-friend_lite}
- OPENMEMORY_MCP_URL=${OPENMEMORY_MCP_URL:-http://host.docker.internal:8765}
- OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory}
- DISABLE_SPEAKER_RECOGNITION=false
- SPEAKER_SERVICE_URL=https://localhost:8085
depends_on:
friend-backend-test:
condition: service_healthy
mongo-test:
condition: service_healthy
redis-test:
condition: service_started
qdrant-test:
condition: service_started
restart: unless-stopped

# caddy:
# image: caddy:2-alpine
# ports:
Expand Down
32 changes: 23 additions & 9 deletions backends/advanced/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,23 +394,37 @@ def setup_https(self):
# Generate Caddyfile from template
self.console.print("[blue][INFO][/blue] Creating Caddyfile configuration...")
caddyfile_template = script_dir / "Caddyfile.template"
caddyfile_path = script_dir / "Caddyfile"

if caddyfile_template.exists():
try:
with open(caddyfile_template, 'r') as f:
caddyfile_content = f.read()
# Check if Caddyfile exists as a directory (common issue)
if caddyfile_path.exists() and caddyfile_path.is_dir():
self.console.print("[red]❌ ERROR: 'Caddyfile' exists as a directory![/red]")
self.console.print("[yellow] Please remove it manually:[/yellow]")
self.console.print(f"[yellow] rm -rf {caddyfile_path}[/yellow]")
self.console.print("[red] HTTPS will NOT work without a proper Caddyfile![/red]")
self.config["HTTPS_ENABLED"] = "false"
else:
with open(caddyfile_template, 'r') as f:
caddyfile_content = f.read()

# Replace TAILSCALE_IP with server_ip
caddyfile_content = caddyfile_content.replace('TAILSCALE_IP', server_ip)
# Replace TAILSCALE_IP with server_ip
caddyfile_content = caddyfile_content.replace('TAILSCALE_IP', server_ip)

with open('Caddyfile', 'w') as f:
f.write(caddyfile_content)
with open(caddyfile_path, 'w') as f:
f.write(caddyfile_content)

self.console.print(f"[green][SUCCESS][/green] Caddyfile created for: {server_ip}")
self.console.print(f"[green][SUCCESS][/green] Caddyfile created for: {server_ip}")

except Exception as e:
self.console.print(f"[yellow][WARNING][/yellow] Caddyfile generation failed: {e}")
self.console.print(f"[red]❌ ERROR: Caddyfile generation failed: {e}[/red]")
self.console.print("[red] HTTPS will NOT work without a proper Caddyfile![/red]")
self.config["HTTPS_ENABLED"] = "false"
else:
self.console.print("[yellow][WARNING][/yellow] Caddyfile.template not found")
self.console.print("[red]❌ ERROR: Caddyfile.template not found[/red]")
self.console.print("[red] HTTPS will NOT work without a proper Caddyfile![/red]")
self.config["HTTPS_ENABLED"] = "false"
else:
self.config["HTTPS_ENABLED"] = "false"

Expand Down
6 changes: 4 additions & 2 deletions backends/advanced/src/advanced_omi_backend/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ async def websocket_auth(websocket, token: Optional[str] = None) -> Optional[Use

# Try JWT token from query parameter first
if token:
logger.debug("Attempting WebSocket auth with query token.")
logger.info(f"Attempting WebSocket auth with query token (first 20 chars): {token[:20]}...")
try:
user_db_gen = get_user_db()
user_db = await user_db_gen.__anext__()
Expand All @@ -192,8 +192,10 @@ async def websocket_auth(websocket, token: Optional[str] = None) -> Optional[Use
if user and user.is_active:
logger.info(f"WebSocket auth successful for user {user.user_id} using query token.")
return user
else:
logger.warning(f"Token validated but user inactive or not found: user={user}")
except Exception as e:
logger.warning(f"WebSocket auth with query token failed: {e}")
logger.error(f"WebSocket auth with query token failed: {type(e).__name__}: {e}", exc_info=True)

# Try cookie authentication
logger.debug("Attempting WebSocket auth with cookie.")
Expand Down
39 changes: 12 additions & 27 deletions backends/advanced/src/advanced_omi_backend/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from advanced_omi_backend.conversation_manager import get_conversation_manager
from advanced_omi_backend.database import AudioChunksRepository
from advanced_omi_backend.task_manager import get_task_manager
from wyoming.audio import AudioChunk
Expand Down Expand Up @@ -133,33 +132,19 @@ async def close_current_conversation(self):
audio_logger.info(f"🔒 No active conversation to close for client {self.client_id}")
return

# Debug logging for memory processing investigation
audio_logger.info(f"🔍 ClientState close_current_conversation debug for {self.client_id}:")
audio_logger.info(f" - current_audio_uuid: {self.current_audio_uuid}")
audio_logger.info(f" - user_id: {self.user_id}")
audio_logger.info(f" - user_email: {self.user_email}")
audio_logger.info(f" - client_id: {self.client_id}")

# Use ConversationManager for clean separation of concerns
conversation_manager = get_conversation_manager()
success = await conversation_manager.close_conversation(
client_id=self.client_id,
audio_uuid=self.current_audio_uuid,
user_id=self.user_id,
user_email=self.user_email,
conversation_start_time=self.conversation_start_time,
speech_segments=self.speech_segments,
chunk_dir=self.chunk_dir,
)
# NOTE: ClientState is legacy V1 code. In V2 architecture, conversation closure
# is handled by the websocket controllers using RQ jobs directly.
# This method is kept minimal for backward compatibility.

if success:
# Clean up speech segments for this conversation
if self.current_audio_uuid in self.speech_segments:
del self.speech_segments[self.current_audio_uuid]
if self.current_audio_uuid in self.current_speech_start:
del self.current_speech_start[self.current_audio_uuid]
else:
audio_logger.warning(f"⚠️ Conversation closure had issues for {self.current_audio_uuid}")
audio_logger.info(f"🔒 Closing conversation for client {self.client_id}, audio_uuid: {self.current_audio_uuid}")

# Clean up speech segments for this conversation
if self.current_audio_uuid in self.speech_segments:
del self.speech_segments[self.current_audio_uuid]
if self.current_audio_uuid in self.current_speech_start:
del self.current_speech_start[self.current_audio_uuid]

audio_logger.info(f"✅ Cleaned up state for {self.current_audio_uuid}")

async def start_new_conversation(self):
"""Start a new conversation by closing current and resetting state."""
Expand Down
Loading
Loading