feat(memo): Hermes input gateway — source_channel='hermes' + source_metadata jsonb

PR-Hermes-Docsrv-Bridge-1 v1. Hermes Agent (Mac mini Discord) 를 Document Server
입력 게이트웨이로 reframe — 코딩 executor X, Claude Code 변동 0.

변경:
- migration 267: source_channel enum 에 'hermes' 추가
- migration 268: documents.source_metadata jsonb NOT NULL DEFAULT '{}' 추가
- Document model: source_metadata 컬럼 ORM 매핑 + enum 'hermes' 노출
- MemoCreate: source_channel + source_metadata 필드 수용 (default='memo' 호환)
- create_memo: channel allowlist (memo/voice/hermes) + metadata jsonb 저장
- list_memos: IN tuple 에 'hermes' 추가 (inbox 노출)
- MemoResponse + _to_memo_response: source_metadata 노출 (UI 배지 준비)

LLM 호출 0 — Hermes 의 HTTP POST 만. 분류/요약은 classify_worker 비동기 처리.
promote-to-event guard (562/664) 변경 0 — v1 = hermes 메모 promote 차단 유지.

plan: ~/.claude/plans/idempotent-seeking-hollerith.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-05-16 13:44:15 +09:00
parent 3627060d2a
commit 19bf5b1e38
4 changed files with 32 additions and 4 deletions
+19 -3
View File
@@ -143,6 +143,11 @@ class MemoCreate(BaseModel):
content: str
title: str | None = None # 선택적 제목 (없으면 첫 줄 자동 생성)
ask_includable: bool = True
# PR-Hermes-Docsrv-Bridge-1: 외부 채널 진입점 식별. default='memo' (web UI 호환).
# 허용 값: memo / voice / hermes / ... (app/models/document.py source_channel enum).
source_channel: str | None = None
# PR-Hermes-Docsrv-Bridge-1: channel/user/message_id/timestamp 등 채널 메타.
source_metadata: dict | None = None
class MemoUpdate(BaseModel):
@@ -175,7 +180,8 @@ class MemoResponse(BaseModel):
# Memo Intake Upgrade PR-2B — AI 추천 분류 (사용자 1-click promote 의 hint)
ai_event_kind: str | None = None
ai_event_confidence: float | None = None
source_channel: str | None = None # voice/memo 등 진입점 식별 (UI 배지)
source_channel: str | None = None # voice/memo/hermes 등 진입점 식별 (UI 배지)
source_metadata: dict = {} # PR-Hermes-Docsrv-Bridge-1: channel/user/message_id/timestamp
file_type: str | None = None # audio (voice 메모) vs note (text 메모)
file_path: str | None = None # voice 메모의 NAS audio 경로 (audio player 용)
created_at: datetime
@@ -210,6 +216,7 @@ def _to_memo_response(doc: Document) -> MemoResponse:
ai_event_kind=doc.ai_event_kind,
ai_event_confidence=doc.ai_event_confidence,
source_channel=doc.source_channel,
source_metadata=dict(doc.source_metadata or {}),
file_type=doc.file_type,
file_path=doc.file_path,
created_at=doc.created_at,
@@ -231,6 +238,13 @@ async def create_memo(
if not content:
raise HTTPException(status_code=400, detail="메모 내용이 비어있습니다")
# PR-Hermes-Docsrv-Bridge-1: source_channel/metadata override 가능. default='memo' (기존 web UI 호환).
channel = body.source_channel or "memo"
if channel not in ("memo", "voice", "hermes"):
raise HTTPException(
status_code=400,
detail=f"source_channel '{channel}' 허용 안 됨 (memo/voice/hermes 만)",
)
doc = Document(
file_path=None,
file_hash=_content_hash(content),
@@ -240,7 +254,8 @@ async def create_memo(
title=body.title.strip() if body.title and body.title.strip() else _auto_title(content),
extracted_text=content,
review_status="approved",
source_channel="memo",
source_channel=channel,
source_metadata=body.source_metadata or {},
user_tags=_parse_hashtags(content),
pinned=False,
archived=False,
@@ -273,9 +288,10 @@ async def list_memos(
PR-2C: source_channel='voice' (음성 메모) 도 포함. 사용자 의도 = 메모는 모든 입력의 inbox.
voice 메모는 file_type='immutable' + category='audio' + source_channel='voice' 패턴.
source_channel 만으로 분리 (file_type 필터는 immutable 다른 binary 까지 끌어옴 — 회피).
PR-Hermes-Docsrv-Bridge-1: source_channel='hermes' (Hermes Discord 등 외부 채널 진입) 도 inbox 포함.
"""
base = select(Document).where(
Document.source_channel.in_(("memo", "voice")),
Document.source_channel.in_(("memo", "voice", "hermes")),
Document.deleted_at == None, # noqa: E711
Document.archived == archived,
)
+4 -1
View File
@@ -104,9 +104,12 @@ class Document(Base):
source_channel: Mapped[str | None] = mapped_column(
Enum("law_monitor", "devonagent", "email", "web_clip",
"tksafety", "inbox_route", "manual", "drive_sync", "news", "memo",
"voice",
"voice", "hermes",
name="source_channel")
)
# 외부 채널 (Hermes Discord 등) 의 channel/user/message_id/timestamp 메타.
# extract_meta (OCR 전용) 와 분리.
source_metadata: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
data_origin: Mapped[str | None] = mapped_column(
Enum("work", "external", name="data_origin")
)
+4
View File
@@ -0,0 +1,4 @@
-- 2026-05-16 PR-Hermes-Docsrv-Bridge-1: source_channel enum 에 'hermes' 추가.
-- Hermes Agent (Mac mini) 가 Discord 등 채널에서 받은 텍스트를 Document Server memo 로
-- 저장할 때 source_channel='hermes' 로 표시. 기존 'memo'/'voice' 와 동등 inbox 진입점.
ALTER TYPE source_channel ADD VALUE IF NOT EXISTS 'hermes';
@@ -0,0 +1,5 @@
-- 2026-05-16 PR-Hermes-Docsrv-Bridge-1: documents.source_metadata jsonb 컬럼 추가.
-- 외부 채널 (Hermes Discord 등) 에서 들어온 입력의 channel/user/message_id/timestamp
-- 메타데이터 보존. 기존 extract_meta (OCR 전용) 와 분리 — semantically 다른 도메인.
-- DEFAULT '{}'::jsonb 라 백필 X, 빠른 ADD COLUMN.
ALTER TABLE documents ADD COLUMN IF NOT EXISTS source_metadata jsonb DEFAULT '{}'::jsonb NOT NULL;