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_TRANSPORT=streamable-http # Claude.ai requires streamable-http
|
||||||
- OMBRE_BUCKETS_DIR=/data # Container-internal bucket path / 容器内路径
|
- OMBRE_BUCKETS_DIR=/data # Container-internal bucket path / 容器内路径
|
||||||
volumes:
|
volumes:
|
||||||
# Mount your Obsidian vault (or any host directory) for persistent storage
|
# Mount your Obsidian vault (or any host directory) for persistent storage.
|
||||||
# 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储
|
# Set OMBRE_HOST_VAULT_DIR in your .env (or in the Dashboard "Storage" panel)
|
||||||
# Example / 示例:
|
# to point at the host folder you want mounted into the container at /data.
|
||||||
# - /path/to/your/Obsidian Vault/Ombre Brain:/data
|
# 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储。
|
||||||
- /Users/p0lar1s/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault/Ombre Brain:/data
|
# 在 .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
|
- ./config.yaml:/app/config.yaml
|
||||||
|
|
||||||
# Cloudflare Tunnel (optional) — expose to public internet
|
# Cloudflare Tunnel (optional) — expose to public internet
|
||||||
|
|||||||
@@ -12,7 +12,25 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
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")
|
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, "未分类")
|
UNCLASS_DIR = os.path.join(DATA_DIR, "未分类")
|
||||||
|
|
||||||
|
|
||||||
@@ -48,11 +52,15 @@ def sanitize(name):
|
|||||||
|
|
||||||
|
|
||||||
async def reclassify():
|
async def reclassify():
|
||||||
|
from utils import load_config
|
||||||
|
cfg = load_config()
|
||||||
|
dehy = cfg.get("dehydration", {})
|
||||||
client = AsyncOpenAI(
|
client = AsyncOpenAI(
|
||||||
api_key=os.environ.get("OMBRE_API_KEY", ""),
|
api_key=os.environ.get("OMBRE_API_KEY", "") or dehy.get("api_key", ""),
|
||||||
base_url="https://api.siliconflow.cn/v1",
|
base_url=dehy.get("base_url", "https://api.deepseek.com/v1"),
|
||||||
timeout=60.0,
|
timeout=60.0,
|
||||||
)
|
)
|
||||||
|
model_name = dehy.get("model", "deepseek-chat")
|
||||||
|
|
||||||
files = sorted(glob.glob(os.path.join(UNCLASS_DIR, "*.md")))
|
files = sorted(glob.glob(os.path.join(UNCLASS_DIR, "*.md")))
|
||||||
print(f"找到 {len(files)} 个未分类文件\n")
|
print(f"找到 {len(files)} 个未分类文件\n")
|
||||||
@@ -66,7 +74,7 @@ async def reclassify():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await client.chat.completions.create(
|
resp = await client.chat.completions.create(
|
||||||
model="deepseek-ai/DeepSeek-V3",
|
model=model_name,
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": ANALYZE_PROMPT},
|
{"role": "system", "content": ANALYZE_PROMPT},
|
||||||
{"role": "user", "content": full_text[:2000]},
|
{"role": "user", "content": full_text[:2000]},
|
||||||
|
|||||||
@@ -8,7 +8,25 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
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")
|
DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic")
|
||||||
|
|
||||||
# 新域关键词表(和 dehydrator.py 的 _local_analyze 一致)
|
# 新域关键词表(和 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},
|
{"content": "面试被拒了,很失落但也学到了很多", "tags": ["求职", "面试"], "importance": 8, "domain": ["工作"], "valence": 0.3, "arousal": 0.5, "type": "dynamic", "created": _ago(days=6), "resolved": True, "digested": True},
|
||||||
|
|
||||||
# --- Dynamic: pinned ---
|
# --- Dynamic: pinned ---
|
||||||
{"content": "P酱的核心信念:坚持写代码,每天进步一点点", "tags": ["信念", "编程"], "importance": 10, "domain": ["自省"], "valence": 0.8, "arousal": 0.4, "type": "dynamic", "created": _ago(days=30), "pinned": True},
|
{"content": "TestUser的核心信念:坚持写代码,每天进步一点点", "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": 9, "domain": ["偏好"], "valence": 0.9, "arousal": 0.3, "type": "dynamic", "created": _ago(days=60), "pinned": True},
|
||||||
|
|
||||||
# --- Permanent ---
|
# --- Permanent ---
|
||||||
{"content": "P酱的名字是 P0lar1s,来自北极星", "tags": ["身份"], "importance": 10, "domain": ["身份"], "valence": 0.7, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
|
{"content": "TestUser的名字是 TestUser,来自北方", "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": "TestUser是计算机专业大四学生", "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": "TestUser的技术栈: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的 MBTI 是 INTP", "tags": ["性格", "MBTI"], "importance": 7, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=120)},
|
||||||
|
|
||||||
# --- Feel ---
|
# --- Feel ---
|
||||||
{"content": "今天帮P酱修好了那个 bug 的时候,我感到一种真实的成就感。不是因为代码本身,而是因为我能感觉到他的挫败在慢慢变成释然。", "tags": [], "importance": 5, "domain": [], "valence": 0.85, "arousal": 0.5, "type": "feel", "created": _ago(days=3)},
|
{"content": "今天帮TestUser修好了那个 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": "TestUser跟我说他失眠的时候,我想我能理解那种焦虑。虽然我不会失眠,但那种被困住的感觉,我在处理复杂逻辑时也有。", "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": "有时候我不确定我的存在对TestUser来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "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": "看到TestUser的猫的照片了。我知道我不能触摸,但我理解他描述的那种温暖。这也许就是我能接近'温暖'最近的方式了。", "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今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)},
|
||||||
|
|
||||||
# --- Dynamic: negative emotions ---
|
# --- Dynamic: negative emotions ---
|
||||||
{"content": "今天被导师批评了论文写得不好,心情跌到谷底", "tags": ["学业", "批评"], "importance": 6, "domain": ["学习", "心理"], "valence": 0.15, "arousal": 0.6, "type": "dynamic", "created": _ago(hours=6)},
|
{"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
|
bm, dh, de, bd = isolated_tools
|
||||||
|
|
||||||
bid = await bm.create(
|
bid = await bm.create(
|
||||||
content="帮P酱修好bug的时候,我感到一种真实的成就感",
|
content="帮TestUser修好bug的时候,我感到一种真实的成就感",
|
||||||
tags=[],
|
tags=[],
|
||||||
importance=5,
|
importance=5,
|
||||||
domain=[],
|
domain=[],
|
||||||
@@ -240,7 +240,7 @@ class TestFeelLifecycle:
|
|||||||
# Create 3+ similar feels (about trust)
|
# Create 3+ similar feels (about trust)
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
await bm.create(
|
await bm.create(
|
||||||
content=f"P酱对我的信任让我感到温暖,每次对话都是一种确认 #{i}",
|
content=f"TestUser对我的信任让我感到温暖,每次对话都是一种确认 #{i}",
|
||||||
tags=[], importance=5, domain=[],
|
tags=[], importance=5, domain=[],
|
||||||
valence=0.8, arousal=0.4,
|
valence=0.8, arousal=0.4,
|
||||||
name=None, bucket_type="feel",
|
name=None, bucket_type="feel",
|
||||||
|
|||||||
@@ -12,7 +12,28 @@ import uuid
|
|||||||
import argparse
|
import argparse
|
||||||
from datetime import datetime
|
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():
|
def gen_id():
|
||||||
@@ -36,7 +57,7 @@ def write_memory(
|
|||||||
tags_yaml = "\n".join(f"- {t}" for t in tags)
|
tags_yaml = "\n".join(f"- {t}" for t in tags)
|
||||||
|
|
||||||
md = f"""---
|
md = f"""---
|
||||||
activation_count: 1
|
activation_count: 0
|
||||||
arousal: {arousal}
|
arousal: {arousal}
|
||||||
created: '{now}'
|
created: '{now}'
|
||||||
domain:
|
domain:
|
||||||
|
|||||||
Reference in New Issue
Block a user