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:
+19
-3
@@ -143,6 +143,11 @@ class MemoCreate(BaseModel):
|
|||||||
content: str
|
content: str
|
||||||
title: str | None = None # 선택적 제목 (없으면 첫 줄 자동 생성)
|
title: str | None = None # 선택적 제목 (없으면 첫 줄 자동 생성)
|
||||||
ask_includable: bool = True
|
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):
|
class MemoUpdate(BaseModel):
|
||||||
@@ -175,7 +180,8 @@ class MemoResponse(BaseModel):
|
|||||||
# Memo Intake Upgrade PR-2B — AI 추천 분류 (사용자 1-click promote 의 hint)
|
# Memo Intake Upgrade PR-2B — AI 추천 분류 (사용자 1-click promote 의 hint)
|
||||||
ai_event_kind: str | None = None
|
ai_event_kind: str | None = None
|
||||||
ai_event_confidence: float | 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_type: str | None = None # audio (voice 메모) vs note (text 메모)
|
||||||
file_path: str | None = None # voice 메모의 NAS audio 경로 (audio player 용)
|
file_path: str | None = None # voice 메모의 NAS audio 경로 (audio player 용)
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
@@ -210,6 +216,7 @@ def _to_memo_response(doc: Document) -> MemoResponse:
|
|||||||
ai_event_kind=doc.ai_event_kind,
|
ai_event_kind=doc.ai_event_kind,
|
||||||
ai_event_confidence=doc.ai_event_confidence,
|
ai_event_confidence=doc.ai_event_confidence,
|
||||||
source_channel=doc.source_channel,
|
source_channel=doc.source_channel,
|
||||||
|
source_metadata=dict(doc.source_metadata or {}),
|
||||||
file_type=doc.file_type,
|
file_type=doc.file_type,
|
||||||
file_path=doc.file_path,
|
file_path=doc.file_path,
|
||||||
created_at=doc.created_at,
|
created_at=doc.created_at,
|
||||||
@@ -231,6 +238,13 @@ async def create_memo(
|
|||||||
if not content:
|
if not content:
|
||||||
raise HTTPException(status_code=400, detail="메모 내용이 비어있습니다")
|
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(
|
doc = Document(
|
||||||
file_path=None,
|
file_path=None,
|
||||||
file_hash=_content_hash(content),
|
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),
|
title=body.title.strip() if body.title and body.title.strip() else _auto_title(content),
|
||||||
extracted_text=content,
|
extracted_text=content,
|
||||||
review_status="approved",
|
review_status="approved",
|
||||||
source_channel="memo",
|
source_channel=channel,
|
||||||
|
source_metadata=body.source_metadata or {},
|
||||||
user_tags=_parse_hashtags(content),
|
user_tags=_parse_hashtags(content),
|
||||||
pinned=False,
|
pinned=False,
|
||||||
archived=False,
|
archived=False,
|
||||||
@@ -273,9 +288,10 @@ async def list_memos(
|
|||||||
PR-2C: source_channel='voice' (음성 메모) 도 포함. 사용자 의도 = 메모는 모든 입력의 inbox.
|
PR-2C: source_channel='voice' (음성 메모) 도 포함. 사용자 의도 = 메모는 모든 입력의 inbox.
|
||||||
voice 메모는 file_type='immutable' + category='audio' + source_channel='voice' 패턴.
|
voice 메모는 file_type='immutable' + category='audio' + source_channel='voice' 패턴.
|
||||||
source_channel 만으로 분리 (file_type 필터는 immutable 다른 binary 까지 끌어옴 — 회피).
|
source_channel 만으로 분리 (file_type 필터는 immutable 다른 binary 까지 끌어옴 — 회피).
|
||||||
|
PR-Hermes-Docsrv-Bridge-1: source_channel='hermes' (Hermes Discord 등 외부 채널 진입) 도 inbox 포함.
|
||||||
"""
|
"""
|
||||||
base = select(Document).where(
|
base = select(Document).where(
|
||||||
Document.source_channel.in_(("memo", "voice")),
|
Document.source_channel.in_(("memo", "voice", "hermes")),
|
||||||
Document.deleted_at == None, # noqa: E711
|
Document.deleted_at == None, # noqa: E711
|
||||||
Document.archived == archived,
|
Document.archived == archived,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,9 +104,12 @@ class Document(Base):
|
|||||||
source_channel: Mapped[str | None] = mapped_column(
|
source_channel: Mapped[str | None] = mapped_column(
|
||||||
Enum("law_monitor", "devonagent", "email", "web_clip",
|
Enum("law_monitor", "devonagent", "email", "web_clip",
|
||||||
"tksafety", "inbox_route", "manual", "drive_sync", "news", "memo",
|
"tksafety", "inbox_route", "manual", "drive_sync", "news", "memo",
|
||||||
"voice",
|
"voice", "hermes",
|
||||||
name="source_channel")
|
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(
|
data_origin: Mapped[str | None] = mapped_column(
|
||||||
Enum("work", "external", name="data_origin")
|
Enum("work", "external", name="data_origin")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user