Skip to content

lyric2249/secondbrain-public

Repository files navigation

secondbrain

AI chat 로그를 Obsidian vault와 SQLite 기반 지식 그래프로 변환하는 Python 배치 처리기입니다.

assistant 응답 로그를 JSONL에서 읽고, Ollama로 문서 제목/본문/엔티티를 추출한 뒤 markdown 문서로 저장합니다. 동시에 선택한 embedding provider로 벡터를 만들고, SQLite에 문서와 엔티티 노드 및 sqlite-vec 임베딩 테이블을 관리해 이후 로그가 기존 문서에 append될지 새 문서가 될지 판단합니다.

주요 기능

  • JSONL chat 로그에서 assistant 메시지만 추출
  • 여러 text fragment를 하나의 답변 본문으로 결합
  • Ollama 기반 요약, 문서 본문, append 본문, 엔티티 추출
  • Ollama/Voyage 기반 embedding provider 선택
  • Obsidian markdown 문서 생성 및 기존 문서 append
  • frontmatter에 created, modified, aliases, entities, source_file, source_line 기록
  • 엔티티를 [[Entity]] wikilink로 기록
  • SQLite Nodes, Edges, ProcessedLogssqlite-vec 기반 vec_docs 관리
  • ProcessedLogs.line_hash 전역 unique 기준으로 동일한 로그 라인 내용 중복 처리 방지
  • UNIQUE(type, name), document path unique index, Edges foreign key 기반 graph schema
  • vector similarity와 graph relationship을 결합한 hybrid search
  • markdown 파일 변경 후 DB 작업 실패 시 파일 rollback

프로젝트 구조

  • src/secondbrain/main.py: CLI 진입점
  • src/secondbrain/commands.py: CLI/API 요청을 실제 작업 단위로 실행하는 command service
  • src/secondbrain/config/settings.py: 환경변수 기반 런타임 설정
  • src/secondbrain/ingestion/logs.py: JSONL 로그 해석과 assistant 메시지 추출
  • src/secondbrain/pipeline/: 로그 한 줄 처리 파이프라인
  • src/secondbrain/services/llm_client.py: Ollama generate client와 Ollama/Voyage embedding client
  • src/secondbrain/core/search.py: hybrid scoring과 문서 검색
  • src/secondbrain/db/: SQLite 연결, schema, repository query
  • src/secondbrain/storage/: Obsidian markdown 생성, 파일 변경, vault/graph 갱신
  • src/secondbrain/api/: FastAPI job API, job store, worker, callback 처리
  • tests/: pytest 테스트

개발 환경

이 프로젝트는 Python 3.13만 대상으로 합니다.

uv를 사용할 수 있으면 다음 명령으로 의존성을 설치하고 테스트를 실행합니다.

uv sync
uv run pytest

uv가 없으면 Python 3.13 환경에서 editable install 후 pytest를 실행합니다.

python -m pip install -e . pytest
python -m pytest

로컬 실행

.env.example을 참고해 .env를 만들고 필요한 값을 채운 뒤 실행합니다.

uv run --env-file .env secondbrain --log "/path/to/logs/*.jsonl"

의존성이 이미 설치되어 있고 src가 import 가능한 환경이라면 다음처럼 실행할 수도 있습니다.

python -m secondbrain --log "/path/to/logs/*.jsonl"

기존 markdown vault를 DB/graph에 색인하려면:

uv run --env-file .env secondbrain --index-vault

현재 markdown 본문 기준으로 문서 임베딩을 다시 만들려면:

uv run --env-file .env secondbrain --refresh-embeddings

LangGraph 처리 파이프라인을 PNG로 저장하려면:

uv run --env-file .env secondbrain --draw-pipeline-graph "./docs/mermaid-graph.png"

이 명령은 app.get_graph().draw_mermaid_png()를 호출하며, Mermaid.ink API를 사용해 PNG를 생성합니다. 따라서 실행 환경에서 Mermaid.ink에 접근할 수 있어야 합니다.

Command Service

CLI와 API는 최종적으로 src/secondbrain/commands.pyrun_secondbrain_command()를 호출합니다. 이 함수는 SecondbrainCommandOptions를 받아 아래 작업들을 순서대로 실행하고, 실행 결과를 operations 배열로 반환합니다.

SecondbrainCommandOptions 필드는 다음과 같습니다.

  • log_patterns: 처리할 JSONL 파일 또는 glob pattern 목록
  • index_vault: 기존 markdown vault를 DB/graph에 색인할지 여부
  • refresh_embeddings: 색인된 document embedding을 현재 markdown 본문 기준으로 갱신할지 여부
  • markdown_dir: index_vault 실행 시 스캔할 markdown 디렉터리
  • draw_pipeline_graph: LangGraph 처리 파이프라인 PNG 출력 경로

주요 내부 operation:

Operation 실행 조건 역할
draw_pipeline_graph draw_pipeline_graph 값이 있을 때 DB를 초기화하지 않고 LangGraph processor graph를 PNG로 렌더링한 뒤 종료
process_logs log_patterns가 있거나, 다른 작업 옵션이 없을 때 JSONL assistant 로그를 처리해 vault markdown, SQLite graph, vector index를 갱신
index_vault index_vault=True 실제 markdown 파일이 없는 document row와 embedding을 먼저 정리한 뒤, 아직 DB에 색인되지 않은 markdown 파일을 document node, embedding, entity edge로 추가
refresh_embeddings refresh_embeddings=True DB에 색인된 document의 현재 markdown 본문을 읽어 vec_docs embedding을 교체

draw_pipeline_graph는 단독 작업입니다. 이 값이 있으면 로그 처리, vault 색인, embedding refresh는 실행하지 않습니다.

process_logs는 기본 작업입니다. log_patterns, index_vault, refresh_embeddings가 모두 비어 있으면 LOG_PATH를 사용해 로그 처리를 실행합니다. 반대로 --index-vault만 실행하거나 --refresh-embeddings만 실행할 때는 로그 처리를 건너뜁니다.

index_vault는 시작 시 DB의 document path를 확인합니다. Nodes(type='document') row의 path가 비어 있거나 실제 파일이 없으면 해당 document row와 vec_docs embedding을 삭제합니다. 연결된 Edges는 foreign key cascade로 정리됩니다.

index_vaultrefresh_embeddings에서 사용할 document embedding provider는 EMBEDDING_MODEL_PROVIDER로 결정됩니다. 값이 voyage이면 Voyage embedding을 input_type="document"로 호출하고, 그 외에는 Ollama embedding을 사용합니다.

작업 조합 예시:

# 기본 LOG_PATH로 로그 처리
uv run --env-file .env secondbrain

# 로그 처리 후 vault 색인과 embedding refresh까지 순차 실행
uv run --env-file .env secondbrain \
  --log "./logs/*.jsonl" \
  --index-vault \
  --refresh-embeddings \
  --markdown-dir "./vault"

API 실행

FastAPI 서버를 실행하면 HTTP로 secondbrain 작업을 enqueue할 수 있습니다.

uv run --env-file .env uvicorn secondbrain.api.main:app --host 0.0.0.0 --port 8000

주요 endpoint:

  • GET /health: 서버 상태 확인
  • POST /jobs: secondbrain 작업 생성
  • GET /jobs/{job_id}: 단일 job 상태와 결과 조회
  • GET /jobs?limit=20: 최근 job 목록 조회

POST /jobs 요청 필드는 command service 옵션과 대응됩니다. process_logs, log_patterns, index_vault, refresh_embeddings, markdown_dir, draw_pipeline_graph, callback_url, metadata를 사용할 수 있습니다.

Docker

이미지 빌드:

docker build -f secondbrain.DockerFile -t secondbrain .

worker 컨테이너 실행:

docker run -d --name secondbrain-worker --env-file .env \
  -v "$PWD/vault:/app/vault" \
  -v "$PWD/logs:/app/logs" \
  -v "$PWD/data:/app/data" \
  secondbrain

실행 중인 컨테이너에서 작업 수행:

docker exec secondbrain-worker secondbrain --log "/app/logs/*.jsonl"
docker exec secondbrain-worker secondbrain --index-vault
docker exec secondbrain-worker secondbrain --refresh-embeddings

일회성 배치 실행:

docker run --rm --env-file .env \
  -v "$PWD/vault:/app/vault" \
  -v "$PWD/logs:/app/logs" \
  -v "$PWD/data:/app/data" \
  secondbrain secondbrain --log "/app/logs/*.jsonl"

Docker에서 Ollama를 사용할 때는 OLLAMA_ENDPOINT=localhost가 컨테이너 자기 자신을 가리킨다는 점에 주의해야 합니다. Ollama가 호스트나 다른 컨테이너에서 실행 중이면 해당 환경에서 접근 가능한 주소를 사용해야 합니다.

환경 변수

주요 설정값은 secondbrain.config.settings에서 import 시점에 읽습니다.

  • OLLAMA_ENDPOINT: Ollama base URL
  • OLLAMA_API_KEY: Ollama API key가 필요한 endpoint를 사용할 때의 optional bearer token
  • OLLAMA_TIMEOUT: Ollama HTTP timeout
  • SECONDBRAIN_API_TOKEN: FastAPI job API 인증에 사용할 optional bearer token
  • SECONDBRAIN_JOB_DB_PATH: API job 상태를 저장할 SQLite DB 경로
  • N8N_CALLBACK_URL: job 완료 후 결과를 보낼 기본 callback URL
  • N8N_CALLBACK_TIMEOUT: callback HTTP timeout
  • N8N_CALLBACK_RETRIES: callback 실패 시 추가 retry 횟수
  • VAULT_PATH: Obsidian markdown 출력 디렉터리
  • DB_PATH: SQLite database 경로
  • LOG_PATH: --log가 없을 때 사용할 기본 JSONL glob
  • THRESHOLD: 기존 문서 append 여부를 판단하는 hybrid score 기준값
  • LLM_MODEL: summary/entity extraction 모델
  • EMBEDDING_MODEL_PROVIDER: embedding provider. ollama 또는 voyage를 사용할 수 있으며 대소문자는 구분하지 않습니다.
  • EMBEDDING_MODEL: embedding 모델
  • VOYAGE_API_KEY: EMBEDDING_MODEL_PROVIDER=voyage일 때 사용할 Voyage AI API key
  • VOYAGE_ENDPOINT: Voyage AI API endpoint
  • EMBEDDING_DIMENSION: embedding vector 차원과 vec_docs 차원
  • VECTOR_SEARCH_LIMIT: vector search 후보 개수
  • VECTOR_SCORE_WEIGHT: hybrid score에서 vector score 가중치
  • GRAPH_SCORE_WEIGHT: hybrid score에서 graph score 가중치
  • EDGE_WEIGHT_INCREMENT: 같은 entity/document 관계가 반복될 때 edge weight 증가량
  • MAX_FILENAME_STEM_LENGTH: markdown 파일명 stem 최대 길이
  • MARKDOWN_SUMMARY_MAX_CHARS: markdown 색인 시 summary 최대 길이
  • SUMMARY_INSTRUCTION: 초기 문서화 prompt instruction
  • APPEND_DECISION_INSTRUCTION: 기존 문서 append 판단 prompt instruction

스코어링 방법론

새 로그가 기존 문서에 이어 붙을 수 있는지는 hybrid_search가 계산한 점수로 판단합니다. 점수는 vector score와 graph score를 가중 평균한 값입니다.

$$ \verb|hybrid_score| = \frac{\verb|(vector_score * VECTOR_SCORE_WEIGHT) + (graph_score * GRAPH_SCORE_WEIGHT)|} {\text{사용 가능한 score들의 weight 합}} $$

vector 결과만 있으면 vector score만으로 평균을 내고, graph 결과만 있으면 graph score만으로 평균을 냅니다. 둘 다 없으면 점수는 0.0입니다.

Vector Score

sqlite-vec에서 반환한 distance를 다음 방식으로 0.0 ~ 1.0 범위의 similarity score로 변환합니다.

$$ \verb|hybrid_score| = \frac{1} {1 + \max (0, \text{distance})} $$

distance가 0이면 score는 1.0입니다. distance가 커질수록 score는 완만하게 낮아집니다. 이 방식은 1.0 - distance처럼 distance 범위가 항상 0~1이라고 가정하지 않습니다.

Graph Score

graph score는 새 문서의 entity 및 alias와 기존 문서가 공유하는 entity/document edge weight를 기반으로 합니다. alias는 별도 DB 컬럼이 아니라 Nodes(type='entity')Edges를 통해 graph 신호로 저장됩니다.

먼저 DB에서 공유 엔티티를 통해 연결된 문서별 edge weight 합을 구합니다.

$$ \verb|shared_entity_weight| = \verb|SUM(Edges.weight)| $$

그 다음 로그 스케일 정규화를 적용합니다.

$$ \verb|target_weight| = \verb|entity_count| + 1.0 $$

$$ \verb|graph_score| = \min \left(1.0, \frac{\verb|log1p(shared_entity_weight|)}{\verb|log1p(target_weight|)} \right) $$

이 정책은 graph score를 0.0 ~ 1.0에 묶어 vector score와 안정적으로 결합하면서도, 반복적으로 같은 entity와 연결된 문서가 약간 더 높은 점수를 받도록 합니다. 선형 정규화처럼 edge weight가 급격히 커져 vector score를 압도하지 않고, 반복 연결의 효과는 로그 스케일로 완만하게 증가하다가 1.0에서 포화됩니다.

문서 생성과 append 기준

처리 흐름은 다음과 같습니다.

  1. 로그 한 줄이 이미 처리된 line_hash인지 확인합니다. 같은 내용의 로그 라인은 파일이 달라도 전역 중복으로 보고 처리하지 않습니다.
  2. assistant message가 아니면 무시합니다.
  3. Ollama로 제목, aliases, summary, body, append_body, entities를 추출합니다.
  4. 원문 assistant content로 document embedding을 생성합니다.
  5. vector score와 graph score를 결합해 기존 문서 후보를 찾습니다. graph search에는 entitiesaliases를 함께 사용합니다.
  6. score가 낮아도 title 또는 aliases가 기존 markdown의 title/aliases와 겹치면 같은 중심 개념의 문서 후보로 봅니다.
  7. 후보 문서가 있으면 Ollama가 기존 문서 본문과 summary를 보고 append 여부를 다시 판단합니다.
  8. THRESHOLD 이상이거나 title/alias 기반 append가 허용되면 기존 문서에 추가 기록을 붙입니다.
  9. 새 문서가 더 적절하면 unique markdown 파일을 만들고 graph에 새 document node를 추가합니다.
  10. 새 문서 생성, 기존 문서 append, vault 색인 과정에서 aliases도 graph entity edge로 연결합니다.
  11. 처리가 끝나면 ProcessedLogs에 로그 line hash를 기록합니다.

데이터 안전 규칙

실제 chat 로그, 생성된 vault, SQLite DB, .env 파일, secret은 source로 커밋하지 않습니다.

logs/, vault/, database 파일은 로컬 런타임 데이터로 취급합니다. vault 파일을 쓰는 동작은 DB 갱신 실패 시 rollback을 시도하지만, 중요한 vault를 대상으로 실행하기 전에는 별도 백업을 권장합니다.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages