Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8d7f262
obsidian support
0xrushi Dec 20, 2025
8f1579e
neo4j comment
0xrushi Dec 20, 2025
bd0a316
cleanup code
0xrushi Dec 20, 2025
65c3fe6
Merge github.com:0xrushi/friend-lite into feat/add-obsidian
0xrushi Dec 21, 2025
c9fcd1e
unused line
0xrushi Dec 21, 2025
cb3c725
unused line
0xrushi Dec 21, 2025
f09c8ff
Fix MemoryEntry object usage in chat service
0xrushi Dec 21, 2025
9b37c03
comment
0xrushi Dec 21, 2025
39e8bfc
feat(obsidian): add obsidian memory search integration to chat
0xrushi Dec 21, 2025
ecfa5c3
unit test
0xrushi Dec 21, 2025
4b72466
use rq
0xrushi Dec 21, 2025
874f49e
Merge branch 'feat/add-obsidian' of github.com:0xrushi/friend-lite in…
0xrushi Dec 21, 2025
1f2e1d2
neo4j service
0xrushi Dec 21, 2025
a75217e
typefix
0xrushi Dec 21, 2025
0dfe250
test fix
0xrushi Dec 21, 2025
fa7c532
Merge branch 'dev' into feat/add-obsidian
0xrushi Dec 23, 2025
e1011c8
cleanup
0xrushi Dec 23, 2025
fe04565
Merge branch 'feat/add-obsidian' of github.com:0xrushi/friend-lite in…
0xrushi Dec 23, 2025
4572bb7
Merge branch 'dev' into feat/add-obsidian
0xrushi Dec 23, 2025
ff48228
cleanup
0xrushi Dec 23, 2025
1bb02c1
Merge branch 'feat/add-obsidian' of github.com:0xrushi/friend-lite in…
0xrushi Dec 23, 2025
38ea008
version changes
0xrushi Dec 23, 2025
d3cbf57
profile
0xrushi Dec 24, 2025
2001baa
remove unused imports
AnkushMalaker Dec 24, 2025
02dfe89
Refactor memory configuration validation endpoints
AnkushMalaker Dec 24, 2025
322088c
Refactor health check model configuration loading
AnkushMalaker Dec 24, 2025
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
154 changes: 154 additions & 0 deletions app/app/components/ObsidianIngest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@

import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';

interface ObsidianIngestProps {
backendUrl: string;
jwtToken: string | null;
}

export const ObsidianIngest: React.FC<ObsidianIngestProps> = ({
backendUrl,
jwtToken,
}) => {
const [vaultPath, setVaultPath] = useState('/app/data/obsidian_vault');
const [loading, setLoading] = useState(false);

const handleIngest = async () => {
if (!backendUrl) {
Alert.alert("Error", "Backend URL not set");
return;
}

if (!jwtToken) {
Alert.alert("Authentication Required", "Please login to ingest Obsidian vault.");
return;
}

setLoading(true);
try {
let baseUrl = backendUrl.trim();
// Handle different URL formats
if (baseUrl.startsWith('ws://')) {
baseUrl = baseUrl.replace('ws://', 'http://');
} else if (baseUrl.startsWith('wss://')) {
baseUrl = baseUrl.replace('wss://', 'https://');
}
baseUrl = baseUrl.split('/ws')[0];

const response = await fetch(`${baseUrl}/api/obsidian/ingest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({ vault_path: vaultPath })
});

if (response.ok) {
Alert.alert("Success", "Ingestion started in background.");
} else {
const errorText = await response.text();
Alert.alert("Error", `Ingestion failed: ${response.status} - ${errorText}`);
}
} catch (e) {
Alert.alert("Error", `Network request failed: ${e}`);
} finally {
setLoading(false);
}
};

return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Obsidian Ingestion</Text>

<Text style={styles.inputLabel}>Vault Path (Backend Container):</Text>
<TextInput
style={styles.textInput}
value={vaultPath}
onChangeText={setVaultPath}
placeholder="/app/data/obsidian_vault"
autoCapitalize="none"
autoCorrect={false}
/>

<TouchableOpacity
style={[styles.button, loading ? styles.buttonDisabled : null]}
onPress={handleIngest}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Starting Ingestion...' : 'Ingest to Neo4j'}
</Text>
</TouchableOpacity>

<Text style={styles.helpText}>
Enter the absolute path to the Obsidian vault INSIDE the backend container.
Ensure the folder is mounted to the container.
</Text>
</View>
);
};

const styles = StyleSheet.create({
section: {
marginBottom: 25,
padding: 15,
backgroundColor: 'white',
borderRadius: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 15,
color: '#333',
},
inputLabel: {
fontSize: 14,
color: '#333',
marginBottom: 5,
fontWeight: '500',
},
textInput: {
backgroundColor: '#f0f0f0',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 6,
padding: 10,
fontSize: 14,
width: '100%',
marginBottom: 15,
color: '#333',
},
button: {
backgroundColor: '#9b59b6', // Purple for Obsidian
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: 'center',
marginBottom: 10,
elevation: 2,
},
buttonDisabled: {
backgroundColor: '#A0A0A0',
opacity: 0.7,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
helpText: {
fontSize: 12,
color: '#666',
textAlign: 'center',
fontStyle: 'italic',
},
});

export default ObsidianIngest;
9 changes: 9 additions & 0 deletions app/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import DeviceListItem from './components/DeviceListItem';
import DeviceDetails from './components/DeviceDetails';
import AuthSection from './components/AuthSection';
import BackendStatus from './components/BackendStatus';
import ObsidianIngest from './components/ObsidianIngest';
import PhoneAudioButton from './components/PhoneAudioButton';

export default function App() {
Expand Down Expand Up @@ -538,6 +539,14 @@ export default function App() {
onAuthStatusChange={handleAuthStatusChange}
/>

{/* Obsidian Ingestion - Only when authenticated */}
{isAuthenticated && (
<ObsidianIngest
backendUrl={webSocketUrl}
jwtToken={jwtToken}
/>
)}

{/* Phone Audio Streaming Button */}
<PhoneAudioButton
isRecording={phoneAudioRecorder.isRecording || isPhoneAudioMode}
Expand Down
40 changes: 22 additions & 18 deletions backends/advanced/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
# neo4j-mem0:
# condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"]
interval: 30s
Expand Down Expand Up @@ -175,22 +173,28 @@ services:
timeout: 3s
retries: 5

## Additional

# neo4j-mem0:
# image: neo4j:5.15-community
# ports:
# - "7474:7474" # HTTP
# - "7687:7687" # Bolt
# environment:
# - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password}
# - NEO4J_PLUGINS=["apoc"]
# - NEO4J_dbms_security_procedures_unrestricted=apoc.*
# - NEO4J_dbms_security_procedures_allowlist=apoc.*
# volumes:
# - ./data/neo4j_data:/data
# - ./data/neo4j_logs:/logs
# restart: unless-stopped
neo4j-mem0:
image: neo4j:5.15-community
hostname: neo4j-mem0
ports:
- "7474:7474" # HTTP
- "7687:7687" # Bolt
environment:
- NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password}
- NEO4J_PLUGINS=["apoc"]
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
- NEO4J_dbms_security_procedures_allowlist=apoc.*
- NEO4J_server_default__listen__address=0.0.0.0
- NEO4J_server_bolt_listen__address=0.0.0.0:7687
- NEO4J_server_http_listen__address=0.0.0.0:7474
- NEO4J_dbms_memory_heap_initial__size=512m
- NEO4J_dbms_memory_heap_max__size=2G
volumes:
- ./data/neo4j_data:/data
- ./data/neo4j_logs:/logs
restart: unless-stopped
profiles:
- obsidian

# ollama:
# image: ollama/ollama:latest
Expand Down
4 changes: 3 additions & 1 deletion backends/advanced/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ dependencies = [
"fastmcp>=0.5.0", # MCP server for conversation access
"mem0ai", # Using main branch with PR #3250 AsyncMemory fix
"langchain_neo4j",
"neo4j>=5.0.0,<6.0.0",
"motor>=3.7.1",
"ollama>=0.4.8",
"friend-lite-sdk",
"python-dotenv>=1.1.0",
"uvicorn>=0.34.2",
"wyoming>=1.6.1",
"aiohttp>=3.8.0",
"httpx>=0.28.0,<1.0.0",
"fastapi-users[beanie]>=14.0.1",
"PyYAML>=6.0.1",
"langfuse>=3.3.0",
Expand Down Expand Up @@ -54,7 +56,7 @@ profile = "black"
line-length = 100

[tool.uv.sources]
mem0ai = { git = "https://github.com/AnkushMalaker/mem0.git", rev = "async-client-unbound-var-fix" }
mem0ai = { git = "https://github.com/AnkushMalaker/mem0.git", rev = "main" }

[tool.poetry.dependencies]
robotframework = "^6.1.1"
Expand Down
42 changes: 35 additions & 7 deletions backends/advanced/src/advanced_omi_backend/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
from advanced_omi_backend.database import get_database
from advanced_omi_backend.llm_client import get_llm_client
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 (
get_obsidian_service,
ObsidianSearchError,
)
from advanced_omi_backend.users import User

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -279,7 +284,7 @@ async def add_message(self, message: ChatMessage) -> bool:
logger.error(f"Failed to add message to session {message.session_id}: {e}")
return False

async def get_relevant_memories(self, query: str, user_id: str) -> List[Dict]:
async def get_relevant_memories(self, query: str, user_id: str) -> List[MemoryEntry]:
"""Get relevant memories for the user's query."""
try:
memories = await self.memory_service.search_memories(
Expand All @@ -294,15 +299,15 @@ async def get_relevant_memories(self, query: str, user_id: str) -> List[Dict]:
return []

async def format_conversation_context(
self, session_id: str, user_id: str, current_message: str
self, session_id: str, user_id: str, current_message: str, include_obsidian_memory: bool = False
) -> Tuple[str, List[str]]:
"""Format conversation context with memory integration."""
# Get recent conversation history
messages = await self.get_session_messages(session_id, user_id, MAX_CONVERSATION_HISTORY)

# Get relevant memories
memories = await self.get_relevant_memories(current_message, user_id)
memory_ids = [memory.get("id", "") for memory in memories if memory.get("id")]
memory_ids = [memory.id for memory in memories if memory.id]

# Build context string
context_parts = []
Expand All @@ -311,11 +316,34 @@ async def format_conversation_context(
if memories:
context_parts.append("# Relevant Personal Memories:")
for i, memory in enumerate(memories, 1):
memory_text = memory.get("memory", memory.get("text", ""))
memory_text = memory.content
if memory_text:
context_parts.append(f"{i}. {memory_text}")
context_parts.append("")

# Add Obsidian context if requested
if include_obsidian_memory:
try:
obsidian_service = get_obsidian_service()
obsidian_result = await obsidian_service.search_obsidian(current_message)
obsidian_context = obsidian_result["results"]
if obsidian_context:
context_parts.append("# Relevant Obsidian Notes:")
for entry in obsidian_context:
context_parts.append(entry)
context_parts.append("")
logger.info(f"Added {len(obsidian_context)} Obsidian notes to context")
except ObsidianSearchError as exc:
logger.error(
"Failed to get Obsidian context (%s stage): %s",
exc.stage,
exc,
)
raise
except Exception as e:
logger.error(f"Failed to get Obsidian context: {e}")
raise e

# Add conversation history
if messages:
context_parts.append("# Recent Conversation:")
Expand All @@ -332,7 +360,7 @@ async def format_conversation_context(
return context, memory_ids

async def generate_response_stream(
self, session_id: str, user_id: str, message_content: str
self, session_id: str, user_id: str, message_content: str, include_obsidian_memory: bool = False
) -> AsyncGenerator[Dict, None]:
"""Generate streaming response with memory context."""
if not self._initialized:
Expand All @@ -351,7 +379,7 @@ async def generate_response_stream(

# Format context with memories
context, memory_ids = await self.format_conversation_context(
session_id, user_id, message_content
session_id, user_id, message_content, include_obsidian_memory=include_obsidian_memory
)

# Send memory context used
Expand Down Expand Up @@ -552,4 +580,4 @@ async def cleanup_chat_service():
if _chat_service:
_chat_service._initialized = False
_chat_service = None
logger.info("Chat service cleaned up")
logger.info("Chat service cleaned up")
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
)
from advanced_omi_backend.model_registry import _find_config_path, load_models_config
from advanced_omi_backend.models.user import User
from advanced_omi_backend.task_manager import get_task_manager

logger = logging.getLogger(__name__)
audio_logger = logging.getLogger("audio_processing")
Expand Down
13 changes: 8 additions & 5 deletions backends/advanced/src/advanced_omi_backend/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import logging
import os
from abc import ABC, abstractmethod
from typing import Dict
from typing import Dict, Any, Optional

from advanced_omi_backend.services.memory.config import load_config_yml as _load_root_config
from advanced_omi_backend.services.memory.config import resolve_value as _resolve_value

from advanced_omi_backend.model_registry import get_models_registry

Expand Down Expand Up @@ -53,10 +56,10 @@ def __init__(
temperature: float = 0.1,
):
super().__init__(model, temperature)
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
self.base_url = base_url or os.getenv("OPENAI_BASE_URL")
self.model = model or os.getenv("OPENAI_MODEL")

# Do not read from environment here; values are provided by config.yml
self.api_key = api_key
self.base_url = base_url
self.model = model
if not self.api_key or not self.base_url or not self.model:
raise ValueError(f"LLM configuration incomplete: api_key={'set' if self.api_key else 'MISSING'}, base_url={'set' if self.base_url else 'MISSING'}, model={'set' if self.model else 'MISSING'}")

Expand Down
Loading
Loading