Skip to content

Commit d849ebe

Browse files
committed
Merge branch 'dev' into fix/cleanup-pre-release-v0.1.0
2 parents 892b1b2 + dafe563 commit d849ebe

30 files changed

+4440
-2024
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
2+
import React, { useState } from 'react';
3+
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';
4+
5+
interface ObsidianIngestProps {
6+
backendUrl: string;
7+
jwtToken: string | null;
8+
}
9+
10+
export const ObsidianIngest: React.FC<ObsidianIngestProps> = ({
11+
backendUrl,
12+
jwtToken,
13+
}) => {
14+
const [vaultPath, setVaultPath] = useState('/app/data/obsidian_vault');
15+
const [loading, setLoading] = useState(false);
16+
17+
const handleIngest = async () => {
18+
if (!backendUrl) {
19+
Alert.alert("Error", "Backend URL not set");
20+
return;
21+
}
22+
23+
if (!jwtToken) {
24+
Alert.alert("Authentication Required", "Please login to ingest Obsidian vault.");
25+
return;
26+
}
27+
28+
setLoading(true);
29+
try {
30+
let baseUrl = backendUrl.trim();
31+
// Handle different URL formats
32+
if (baseUrl.startsWith('ws://')) {
33+
baseUrl = baseUrl.replace('ws://', 'http://');
34+
} else if (baseUrl.startsWith('wss://')) {
35+
baseUrl = baseUrl.replace('wss://', 'https://');
36+
}
37+
baseUrl = baseUrl.split('/ws')[0];
38+
39+
const response = await fetch(`${baseUrl}/api/obsidian/ingest`, {
40+
method: 'POST',
41+
headers: {
42+
'Content-Type': 'application/json',
43+
'Authorization': `Bearer ${jwtToken}`
44+
},
45+
body: JSON.stringify({ vault_path: vaultPath })
46+
});
47+
48+
if (response.ok) {
49+
Alert.alert("Success", "Ingestion started in background.");
50+
} else {
51+
const errorText = await response.text();
52+
Alert.alert("Error", `Ingestion failed: ${response.status} - ${errorText}`);
53+
}
54+
} catch (e) {
55+
Alert.alert("Error", `Network request failed: ${e}`);
56+
} finally {
57+
setLoading(false);
58+
}
59+
};
60+
61+
return (
62+
<View style={styles.section}>
63+
<Text style={styles.sectionTitle}>Obsidian Ingestion</Text>
64+
65+
<Text style={styles.inputLabel}>Vault Path (Backend Container):</Text>
66+
<TextInput
67+
style={styles.textInput}
68+
value={vaultPath}
69+
onChangeText={setVaultPath}
70+
placeholder="/app/data/obsidian_vault"
71+
autoCapitalize="none"
72+
autoCorrect={false}
73+
/>
74+
75+
<TouchableOpacity
76+
style={[styles.button, loading ? styles.buttonDisabled : null]}
77+
onPress={handleIngest}
78+
disabled={loading}
79+
>
80+
<Text style={styles.buttonText}>
81+
{loading ? 'Starting Ingestion...' : 'Ingest to Neo4j'}
82+
</Text>
83+
</TouchableOpacity>
84+
85+
<Text style={styles.helpText}>
86+
Enter the absolute path to the Obsidian vault INSIDE the backend container.
87+
Ensure the folder is mounted to the container.
88+
</Text>
89+
</View>
90+
);
91+
};
92+
93+
const styles = StyleSheet.create({
94+
section: {
95+
marginBottom: 25,
96+
padding: 15,
97+
backgroundColor: 'white',
98+
borderRadius: 10,
99+
shadowColor: '#000',
100+
shadowOffset: { width: 0, height: 1 },
101+
shadowOpacity: 0.1,
102+
shadowRadius: 3,
103+
elevation: 2,
104+
},
105+
sectionTitle: {
106+
fontSize: 18,
107+
fontWeight: '600',
108+
marginBottom: 15,
109+
color: '#333',
110+
},
111+
inputLabel: {
112+
fontSize: 14,
113+
color: '#333',
114+
marginBottom: 5,
115+
fontWeight: '500',
116+
},
117+
textInput: {
118+
backgroundColor: '#f0f0f0',
119+
borderWidth: 1,
120+
borderColor: '#ddd',
121+
borderRadius: 6,
122+
padding: 10,
123+
fontSize: 14,
124+
width: '100%',
125+
marginBottom: 15,
126+
color: '#333',
127+
},
128+
button: {
129+
backgroundColor: '#9b59b6', // Purple for Obsidian
130+
paddingVertical: 12,
131+
paddingHorizontal: 20,
132+
borderRadius: 8,
133+
alignItems: 'center',
134+
marginBottom: 10,
135+
elevation: 2,
136+
},
137+
buttonDisabled: {
138+
backgroundColor: '#A0A0A0',
139+
opacity: 0.7,
140+
},
141+
buttonText: {
142+
color: 'white',
143+
fontSize: 16,
144+
fontWeight: '600',
145+
},
146+
helpText: {
147+
fontSize: 12,
148+
color: '#666',
149+
textAlign: 'center',
150+
fontStyle: 'italic',
151+
},
152+
});
153+
154+
export default ObsidianIngest;

app/app/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import DeviceListItem from './components/DeviceListItem';
2828
import DeviceDetails from './components/DeviceDetails';
2929
import AuthSection from './components/AuthSection';
3030
import BackendStatus from './components/BackendStatus';
31+
import ObsidianIngest from './components/ObsidianIngest';
3132
import PhoneAudioButton from './components/PhoneAudioButton';
3233

3334
export default function App() {
@@ -538,6 +539,14 @@ export default function App() {
538539
onAuthStatusChange={handleAuthStatusChange}
539540
/>
540541

542+
{/* Obsidian Ingestion - Only when authenticated */}
543+
{isAuthenticated && (
544+
<ObsidianIngest
545+
backendUrl={webSocketUrl}
546+
jwtToken={jwtToken}
547+
/>
548+
)}
549+
541550
{/* Phone Audio Streaming Button */}
542551
<PhoneAudioButton
543552
isRecording={phoneAudioRecorder.isRecording || isPhoneAudioMode}

backends/advanced/docker-compose.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ services:
3838
condition: service_healthy
3939
redis:
4040
condition: service_healthy
41-
# neo4j-mem0:
42-
# condition: service_started
4341
healthcheck:
4442
test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"]
4543
interval: 30s
@@ -175,8 +173,6 @@ services:
175173
timeout: 3s
176174
retries: 5
177175

178-
## Additional
179-
180176
neo4j-mem0:
181177
image: neo4j:5.15-community
182178
hostname: neo4j-mem0

backends/advanced/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ dependencies = [
1010
"fastmcp>=0.5.0", # MCP server for conversation access
1111
"mem0ai", # Using main branch with PR #3250 AsyncMemory fix
1212
"langchain_neo4j",
13+
"neo4j>=5.0.0,<6.0.0",
1314
"motor>=3.7.1",
1415
"ollama>=0.4.8",
1516
"friend-lite-sdk",
1617
"python-dotenv>=1.1.0",
1718
"uvicorn>=0.34.2",
1819
"wyoming>=1.6.1",
1920
"aiohttp>=3.8.0",
21+
"httpx>=0.28.0,<1.0.0",
2022
"fastapi-users[beanie]>=14.0.1",
2123
"PyYAML>=6.0.1",
2224
"langfuse>=3.3.0",
@@ -54,7 +56,7 @@ profile = "black"
5456
line-length = 100
5557

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

5961
[tool.poetry.dependencies]
6062
robotframework = "^6.1.1"

backends/advanced/src/advanced_omi_backend/chat_service.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
from advanced_omi_backend.database import get_database
2424
from advanced_omi_backend.llm_client import get_llm_client
2525
from advanced_omi_backend.services.memory import get_memory_service
26+
from advanced_omi_backend.services.memory.base import MemoryEntry
27+
from advanced_omi_backend.services.obsidian_service import (
28+
get_obsidian_service,
29+
ObsidianSearchError,
30+
)
2631
from advanced_omi_backend.users import User
2732

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

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

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

303308
# Get relevant memories
304309
memories = await self.get_relevant_memories(current_message, user_id)
305-
memory_ids = [memory.get("id", "") for memory in memories if memory.get("id")]
310+
memory_ids = [memory.id for memory in memories if memory.id]
306311

307312
# Build context string
308313
context_parts = []
@@ -311,11 +316,34 @@ async def format_conversation_context(
311316
if memories:
312317
context_parts.append("# Relevant Personal Memories:")
313318
for i, memory in enumerate(memories, 1):
314-
memory_text = memory.get("memory", memory.get("text", ""))
319+
memory_text = memory.content
315320
if memory_text:
316321
context_parts.append(f"{i}. {memory_text}")
317322
context_parts.append("")
318323

324+
# Add Obsidian context if requested
325+
if include_obsidian_memory:
326+
try:
327+
obsidian_service = get_obsidian_service()
328+
obsidian_result = await obsidian_service.search_obsidian(current_message)
329+
obsidian_context = obsidian_result["results"]
330+
if obsidian_context:
331+
context_parts.append("# Relevant Obsidian Notes:")
332+
for entry in obsidian_context:
333+
context_parts.append(entry)
334+
context_parts.append("")
335+
logger.info(f"Added {len(obsidian_context)} Obsidian notes to context")
336+
except ObsidianSearchError as exc:
337+
logger.error(
338+
"Failed to get Obsidian context (%s stage): %s",
339+
exc.stage,
340+
exc,
341+
)
342+
raise
343+
except Exception as e:
344+
logger.error(f"Failed to get Obsidian context: {e}")
345+
raise e
346+
319347
# Add conversation history
320348
if messages:
321349
context_parts.append("# Recent Conversation:")
@@ -332,7 +360,7 @@ async def format_conversation_context(
332360
return context, memory_ids
333361

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

352380
# Format context with memories
353381
context, memory_ids = await self.format_conversation_context(
354-
session_id, user_id, message_content
382+
session_id, user_id, message_content, include_obsidian_memory=include_obsidian_memory
355383
)
356384

357385
# Send memory context used
@@ -552,4 +580,4 @@ async def cleanup_chat_service():
552580
if _chat_service:
553581
_chat_service._initialized = False
554582
_chat_service = None
555-
logger.info("Chat service cleaned up")
583+
logger.info("Chat service cleaned up")

backends/advanced/src/advanced_omi_backend/controllers/system_controller.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
)
1818
from advanced_omi_backend.model_registry import _find_config_path, load_models_config
1919
from advanced_omi_backend.models.user import User
20-
from advanced_omi_backend.task_manager import get_task_manager
2120

2221
logger = logging.getLogger(__name__)
2322
audio_logger = logging.getLogger("audio_processing")

backends/advanced/src/advanced_omi_backend/llm_client.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import logging
1010
import os
1111
from abc import ABC, abstractmethod
12-
from typing import Dict
12+
from typing import Dict, Any, Optional
13+
14+
from advanced_omi_backend.services.memory.config import load_config_yml as _load_root_config
15+
from advanced_omi_backend.services.memory.config import resolve_value as _resolve_value
1316

1417
from advanced_omi_backend.model_registry import get_models_registry
1518

@@ -53,10 +56,10 @@ def __init__(
5356
temperature: float = 0.1,
5457
):
5558
super().__init__(model, temperature)
56-
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
57-
self.base_url = base_url or os.getenv("OPENAI_BASE_URL")
58-
self.model = model or os.getenv("OPENAI_MODEL")
59-
59+
# Do not read from environment here; values are provided by config.yml
60+
self.api_key = api_key
61+
self.base_url = base_url
62+
self.model = model
6063
if not self.api_key or not self.base_url or not self.model:
6164
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'}")
6265

backends/advanced/src/advanced_omi_backend/routers/api_router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
client_router,
1616
conversation_router,
1717
memory_router,
18+
obsidian_router,
1819
queue_router,
1920
system_router,
2021
user_router,
@@ -34,6 +35,7 @@
3435
router.include_router(client_router)
3536
router.include_router(conversation_router)
3637
router.include_router(memory_router)
38+
router.include_router(obsidian_router)
3739
router.include_router(system_router)
3840
router.include_router(queue_router)
3941
router.include_router(health_router) # Also include under /api for frontend compatibility

backends/advanced/src/advanced_omi_backend/routers/modules/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .conversation_routes import router as conversation_router
2121
from .health_routes import router as health_router
2222
from .memory_routes import router as memory_router
23+
from .obsidian_routes import router as obsidian_router
2324
from .queue_routes import router as queue_router
2425
from .system_routes import router as system_router
2526
from .user_routes import router as user_router
@@ -32,6 +33,7 @@
3233
"conversation_router",
3334
"health_router",
3435
"memory_router",
36+
"obsidian_router",
3537
"queue_router",
3638
"system_router",
3739
"user_router",

0 commit comments

Comments
 (0)