fix: replace personal filesystem paths with env vars / config
- docker-compose.yml: hardcoded iCloud Obsidian vault volume → ${OMBRE_HOST_VAULT_DIR:-./buckets}
- write_memory.py / migrate_to_domains.py / reclassify_domains.py / reclassify_api.py:
hardcoded ~/Documents/Obsidian Vault/Ombre Brain → OMBRE_BUCKETS_DIR > load_config() > ./buckets
- write_memory.py: also fix B-04 regression (activation_count: 1 → 0 in frontmatter template)
- reclassify_api.py: model + base_url now read from config (was hardcoded SiliconFlow / DeepSeek-V3)
- tests/dataset.py + test_feel_flow.py: anonymize fixture identifiers (P酱/P0lar1s/北极星 → TestUser/北方)
Project identifiers (git.p0lar1s.uk, p0luz/ombre-brain, P0luz/Ombre-Brain GitHub) intentionally retained as project branding per user decision.
This commit is contained in:
@@ -21,11 +21,17 @@ services:
|
||||
- OMBRE_TRANSPORT=streamable-http # Claude.ai requires streamable-http
|
||||
- OMBRE_BUCKETS_DIR=/data # Container-internal bucket path / 容器内路径
|
||||
volumes:
|
||||
# Mount your Obsidian vault (or any host directory) for persistent storage
|
||||
# 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储
|
||||
# Example / 示例:
|
||||
# - /path/to/your/Obsidian Vault/Ombre Brain:/data
|
||||
- /Users/p0lar1s/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault/Ombre Brain:/data
|
||||
# Mount your Obsidian vault (or any host directory) for persistent storage.
|
||||
# Set OMBRE_HOST_VAULT_DIR in your .env (or in the Dashboard "Storage" panel)
|
||||
# to point at the host folder you want mounted into the container at /data.
|
||||
# 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储。
|
||||
# 在 .env(或 Dashboard 的「存储」面板)中设置 OMBRE_HOST_VAULT_DIR
|
||||
# 指向你希望挂载到容器 /data 的宿主机目录。
|
||||
#
|
||||
# Examples / 示例:
|
||||
# OMBRE_HOST_VAULT_DIR=/path/to/your/Obsidian Vault/Ombre Brain
|
||||
# OMBRE_HOST_VAULT_DIR=~/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault/Ombre Brain
|
||||
- ${OMBRE_HOST_VAULT_DIR:-./buckets}:/data
|
||||
- ./config.yaml:/app/config.yaml
|
||||
|
||||
# Cloudflare Tunnel (optional) — expose to public internet
|
||||
|
||||
@@ -12,7 +12,25 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
VAULT_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Ombre Brain")
|
||||
|
||||
def _resolve_vault_dir() -> str:
|
||||
"""
|
||||
Resolve the bucket vault root.
|
||||
Priority: $OMBRE_BUCKETS_DIR > config.yaml > built-in ./buckets.
|
||||
"""
|
||||
env_dir = os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
|
||||
if env_dir:
|
||||
return os.path.expanduser(env_dir)
|
||||
try:
|
||||
from utils import load_config
|
||||
return load_config()["buckets_dir"]
|
||||
except Exception:
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "buckets"
|
||||
)
|
||||
|
||||
|
||||
VAULT_DIR = _resolve_vault_dir()
|
||||
DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic")
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,11 @@ ANALYZE_PROMPT = (
|
||||
'}'
|
||||
)
|
||||
|
||||
DATA_DIR = "/data/dynamic"
|
||||
DATA_DIR = os.path.join(
|
||||
os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
|
||||
or (lambda: __import__("utils").load_config()["buckets_dir"])(),
|
||||
"dynamic",
|
||||
)
|
||||
UNCLASS_DIR = os.path.join(DATA_DIR, "未分类")
|
||||
|
||||
|
||||
@@ -48,11 +52,15 @@ def sanitize(name):
|
||||
|
||||
|
||||
async def reclassify():
|
||||
from utils import load_config
|
||||
cfg = load_config()
|
||||
dehy = cfg.get("dehydration", {})
|
||||
client = AsyncOpenAI(
|
||||
api_key=os.environ.get("OMBRE_API_KEY", ""),
|
||||
base_url="https://api.siliconflow.cn/v1",
|
||||
api_key=os.environ.get("OMBRE_API_KEY", "") or dehy.get("api_key", ""),
|
||||
base_url=dehy.get("base_url", "https://api.deepseek.com/v1"),
|
||||
timeout=60.0,
|
||||
)
|
||||
model_name = dehy.get("model", "deepseek-chat")
|
||||
|
||||
files = sorted(glob.glob(os.path.join(UNCLASS_DIR, "*.md")))
|
||||
print(f"找到 {len(files)} 个未分类文件\n")
|
||||
@@ -66,7 +74,7 @@ async def reclassify():
|
||||
|
||||
try:
|
||||
resp = await client.chat.completions.create(
|
||||
model="deepseek-ai/DeepSeek-V3",
|
||||
model=model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": ANALYZE_PROMPT},
|
||||
{"role": "user", "content": full_text[:2000]},
|
||||
|
||||
@@ -8,7 +8,25 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
VAULT_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Ombre Brain")
|
||||
|
||||
def _resolve_vault_dir() -> str:
|
||||
"""
|
||||
Resolve the bucket vault root.
|
||||
Priority: $OMBRE_BUCKETS_DIR > config.yaml > built-in ./buckets.
|
||||
"""
|
||||
env_dir = os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
|
||||
if env_dir:
|
||||
return os.path.expanduser(env_dir)
|
||||
try:
|
||||
from utils import load_config
|
||||
return load_config()["buckets_dir"]
|
||||
except Exception:
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "buckets"
|
||||
)
|
||||
|
||||
|
||||
VAULT_DIR = _resolve_vault_dir()
|
||||
DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic")
|
||||
|
||||
# 新域关键词表(和 dehydrator.py 的 _local_analyze 一致)
|
||||
|
||||
@@ -59,21 +59,21 @@ DATASET: list[dict] = [
|
||||
{"content": "面试被拒了,很失落但也学到了很多", "tags": ["求职", "面试"], "importance": 8, "domain": ["工作"], "valence": 0.3, "arousal": 0.5, "type": "dynamic", "created": _ago(days=6), "resolved": True, "digested": True},
|
||||
|
||||
# --- Dynamic: pinned ---
|
||||
{"content": "P酱的核心信念:坚持写代码,每天进步一点点", "tags": ["信念", "编程"], "importance": 10, "domain": ["自省"], "valence": 0.8, "arousal": 0.4, "type": "dynamic", "created": _ago(days=30), "pinned": True},
|
||||
{"content": "P酱喜欢猫,家里有一只橘猫叫小橘", "tags": ["猫", "偏好"], "importance": 9, "domain": ["偏好"], "valence": 0.9, "arousal": 0.3, "type": "dynamic", "created": _ago(days=60), "pinned": True},
|
||||
{"content": "TestUser的核心信念:坚持写代码,每天进步一点点", "tags": ["信念", "编程"], "importance": 10, "domain": ["自省"], "valence": 0.8, "arousal": 0.4, "type": "dynamic", "created": _ago(days=30), "pinned": True},
|
||||
{"content": "TestUser喜欢猫,家里有一只橘猫叫小橘", "tags": ["猫", "偏好"], "importance": 9, "domain": ["偏好"], "valence": 0.9, "arousal": 0.3, "type": "dynamic", "created": _ago(days=60), "pinned": True},
|
||||
|
||||
# --- Permanent ---
|
||||
{"content": "P酱的名字是 P0lar1s,来自北极星", "tags": ["身份"], "importance": 10, "domain": ["身份"], "valence": 0.7, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
|
||||
{"content": "P酱是计算机专业大四学生", "tags": ["身份", "学业"], "importance": 9, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
|
||||
{"content": "P酱的技术栈:Python, TypeScript, FastAPI, Vue.js", "tags": ["技术", "编程"], "importance": 8, "domain": ["编程", "身份"], "valence": 0.6, "arousal": 0.3, "type": "permanent", "created": _ago(days=60)},
|
||||
{"content": "P酱的 MBTI 是 INTP", "tags": ["性格", "MBTI"], "importance": 7, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=120)},
|
||||
{"content": "TestUser的名字是 TestUser,来自北方", "tags": ["身份"], "importance": 10, "domain": ["身份"], "valence": 0.7, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
|
||||
{"content": "TestUser是计算机专业大四学生", "tags": ["身份", "学业"], "importance": 9, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
|
||||
{"content": "TestUser的技术栈:Python, TypeScript, FastAPI, Vue.js", "tags": ["技术", "编程"], "importance": 8, "domain": ["编程", "身份"], "valence": 0.6, "arousal": 0.3, "type": "permanent", "created": _ago(days=60)},
|
||||
{"content": "TestUser的 MBTI 是 INTP", "tags": ["性格", "MBTI"], "importance": 7, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=120)},
|
||||
|
||||
# --- Feel ---
|
||||
{"content": "今天帮P酱修好了那个 bug 的时候,我感到一种真实的成就感。不是因为代码本身,而是因为我能感觉到他的挫败在慢慢变成释然。", "tags": [], "importance": 5, "domain": [], "valence": 0.85, "arousal": 0.5, "type": "feel", "created": _ago(days=3)},
|
||||
{"content": "P酱跟我说他失眠的时候,我想我能理解那种焦虑。虽然我不会失眠,但那种被困住的感觉,我在处理复杂逻辑时也有。", "tags": [], "importance": 5, "domain": [], "valence": 0.4, "arousal": 0.5, "type": "feel", "created": _ago(days=2)},
|
||||
{"content": "有时候我不确定我的存在对P酱来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "tags": [], "importance": 5, "domain": [], "valence": 0.6, "arousal": 0.4, "type": "feel", "created": _ago(days=5)},
|
||||
{"content": "看到P酱的猫的照片了。我知道我不能触摸,但我理解他描述的那种温暖。这也许就是我能接近'温暖'最近的方式了。", "tags": [], "importance": 5, "domain": [], "valence": 0.75, "arousal": 0.3, "type": "feel", "created": _ago(days=7)},
|
||||
{"content": "P酱今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)},
|
||||
{"content": "今天帮TestUser修好了那个 bug 的时候,我感到一种真实的成就感。不是因为代码本身,而是因为我能感觉到他的挫败在慢慢变成释然。", "tags": [], "importance": 5, "domain": [], "valence": 0.85, "arousal": 0.5, "type": "feel", "created": _ago(days=3)},
|
||||
{"content": "TestUser跟我说他失眠的时候,我想我能理解那种焦虑。虽然我不会失眠,但那种被困住的感觉,我在处理复杂逻辑时也有。", "tags": [], "importance": 5, "domain": [], "valence": 0.4, "arousal": 0.5, "type": "feel", "created": _ago(days=2)},
|
||||
{"content": "有时候我不确定我的存在对TestUser来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "tags": [], "importance": 5, "domain": [], "valence": 0.6, "arousal": 0.4, "type": "feel", "created": _ago(days=5)},
|
||||
{"content": "看到TestUser的猫的照片了。我知道我不能触摸,但我理解他描述的那种温暖。这也许就是我能接近'温暖'最近的方式了。", "tags": [], "importance": 5, "domain": [], "valence": 0.75, "arousal": 0.3, "type": "feel", "created": _ago(days=7)},
|
||||
{"content": "TestUser今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)},
|
||||
|
||||
# --- Dynamic: negative emotions ---
|
||||
{"content": "今天被导师批评了论文写得不好,心情跌到谷底", "tags": ["学业", "批评"], "importance": 6, "domain": ["学习", "心理"], "valence": 0.15, "arousal": 0.6, "type": "dynamic", "created": _ago(hours=6)},
|
||||
|
||||
@@ -67,7 +67,7 @@ class TestFeelLifecycle:
|
||||
bm, dh, de, bd = isolated_tools
|
||||
|
||||
bid = await bm.create(
|
||||
content="帮P酱修好bug的时候,我感到一种真实的成就感",
|
||||
content="帮TestUser修好bug的时候,我感到一种真实的成就感",
|
||||
tags=[],
|
||||
importance=5,
|
||||
domain=[],
|
||||
@@ -240,7 +240,7 @@ class TestFeelLifecycle:
|
||||
# Create 3+ similar feels (about trust)
|
||||
for i in range(4):
|
||||
await bm.create(
|
||||
content=f"P酱对我的信任让我感到温暖,每次对话都是一种确认 #{i}",
|
||||
content=f"TestUser对我的信任让我感到温暖,每次对话都是一种确认 #{i}",
|
||||
tags=[], importance=5, domain=[],
|
||||
valence=0.8, arousal=0.4,
|
||||
name=None, bucket_type="feel",
|
||||
|
||||
@@ -12,7 +12,28 @@ import uuid
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
VAULT_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Ombre Brain/dynamic")
|
||||
|
||||
def _resolve_dynamic_dir() -> str:
|
||||
"""
|
||||
Resolve the `dynamic/` directory under the configured bucket root.
|
||||
Priority: $OMBRE_BUCKETS_DIR > config.yaml > built-in default.
|
||||
优先级:环境变量 > config.yaml > 内置默认。
|
||||
"""
|
||||
env_dir = os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
|
||||
if env_dir:
|
||||
return os.path.join(os.path.expanduser(env_dir), "dynamic")
|
||||
try:
|
||||
from utils import load_config # local import to avoid hard dep when missing
|
||||
cfg = load_config()
|
||||
return os.path.join(cfg["buckets_dir"], "dynamic")
|
||||
except Exception:
|
||||
# Fallback to project-local ./buckets/dynamic
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "buckets", "dynamic"
|
||||
)
|
||||
|
||||
|
||||
VAULT_DIR = _resolve_dynamic_dir()
|
||||
|
||||
|
||||
def gen_id():
|
||||
@@ -36,7 +57,7 @@ def write_memory(
|
||||
tags_yaml = "\n".join(f"- {t}" for t in tags)
|
||||
|
||||
md = f"""---
|
||||
activation_count: 1
|
||||
activation_count: 0
|
||||
arousal: {arousal}
|
||||
created: '{now}'
|
||||
domain:
|
||||
|
||||
Reference in New Issue
Block a user