From f078b28cedf40c8f0f813b097a90e12c43160b57 Mon Sep 17 00:00:00 2001 From: happybell80 Date: Mon, 6 Apr 2026 07:52:15 +0900 Subject: [PATCH] =?UTF-8?q?docs:=2003=5Frag=20+=2004=5Fscheduler=20+=2005?= =?UTF-8?q?=5Fadmin=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=ED=98=84=ED=96=89=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 03_rag: - companyx_grounding_pipeline.md: 코드 SSOT 섹션 추가, 진입 조건 3단계(IC→마커 폴백) 정확히 기술, 환경변수 참조로 IP 하드코딩 제거 - companyx_incremental_indexing_workflow.md: frontmatter 표준 적용 (type, last_updated) - rag_upload_indexing_pipeline.md: 코드 SSOT·재인덱싱·업로드 경로별 진입점 테이블 추가, 환경변수 참조 04_scheduler: - scheduled_daily_briefing.md: n8n cron 전제 제거, APScheduler DB 기반 + LangGraph 워크플로우 기준 재작성 - scheduled_healthcheck_alert.md: n8n cron 전제 제거, /health 엔드포인트 + SKILL.md registry 기반 재작성 - scheduled_rag_reindex_retry.md: 현행 코드에 해당 잡 없음 → _archive 이동 05_admin: - diary_reflection_pipeline.md: n8n 전제 제거, APScheduler + diary_generator.py 기준 재작성, /api/diary/generate(존재하지 않는 엔드포인트) 제거 Refs: DOCS#8 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../03_rag/companyx_grounding_pipeline.md | 74 +++++++++-------- .../companyx_incremental_indexing_workflow.md | 4 + .../03_rag/rag_upload_indexing_pipeline.md | 32 ++++++-- .../04_scheduler/scheduled_daily_briefing.md | 69 +++++++++++----- .../scheduled_healthcheck_alert.md | 75 ++++++++++++++---- .../05_admin/diary_reflection_pipeline.md | 79 +++++++++++++------ .../scheduled_rag_reindex_retry.md | 0 7 files changed, 240 insertions(+), 93 deletions(-) rename workflow/{04_scheduler => _archive}/scheduled_rag_reindex_retry.md (100%) diff --git a/workflow/03_rag/companyx_grounding_pipeline.md b/workflow/03_rag/companyx_grounding_pipeline.md index eb1a7a8..222af20 100644 --- a/workflow/03_rag/companyx_grounding_pipeline.md +++ b/workflow/03_rag/companyx_grounding_pipeline.md @@ -1,7 +1,7 @@ --- tags: [workflow, rag, companyx, grounding, answer] type: workflow -last_updated: 2026-03-23 +last_updated: 2026-04-06 --- # Company X Grounding 파이프라인 @@ -15,56 +15,68 @@ last_updated: 2026-03-23 - [헌장.md](../../../../0_VALUE/00_Foundations/헌장.md) - [writing-principles.md](../../../../0_VALUE/20_Governance/writing-principles.md) +## 코드 SSOT +- `rb8001/app/services/companyx_grounding_service.py` + ## 입력 -- 사용자 질문 -- 사용자 ID -- 검색된 근거 문서 목록 -- 근거 문서 메타데이터 +- 사용자 질문 (`message`) +- 사용자 ID (`user_id`) +- (선택) 의도 분류 결과 (`classified_intent`, `classified_confidence`) ## 출력 -- 직접 답 -- 근거 문서명 -- 근거 문단 요약 -- 필요 시 위치 정보 -- 근거 부족 시 명시적 실패 응답 +- `CompanyXRAGOutput` (Pydantic 모델): + - `direct_answer` — 질문에 대한 직접 답변 (근거 부족 시 빈 문자열) + - `evidence_docs` — 근거 문서 파일명 목록 + - `failure_reason` — 답변 불가 사유 (문서 없음, 단정 불가 등) -## 처리 순서 -1. `team_id == COMPANYX_TEAM_ID`이고 질문이 마커(intent/domain)에 매칭되면 grounding 경로로 진입한다. team_id만으로는 진입하지 않는다. 마커 미매칭 시 일반 스킬 경로(캘린더/이메일/뉴스 등)로 fallback한다. (260323 P1-5 롤백, 전원 동의) -2. 질문 유형을 분류한다 (설명형/사실확인형/수치확인형/재정리형). -3. 멀티쿼리를 생성한다 (`_build_query_candidates()`, 7~9개 변형). -4. 하이브리드 검색(벡터+키워드 RRF, search_mode=hybrid)으로 상위 결과를 수집한다. -5. 검색 결과를 LLM에 컨텍스트로 전달하고, LLM이 질문 적합도를 판단해 답변한다. -6. LLM 응답을 Pydantic(`CompanyXRAGOutput`)으로 검증한다. -7. 근거가 부족하면 LLM이 `failure_reason`을 명시하고, 추정 답변 대신 실패 응답으로 끝낸다. +## 진입 조건 (`should_handle_companyx_grounding()`) +1. `user_id`에서 `team_id`를 조회한다 (`get_user_team_id()`). +2. `team_id != COMPANYX_TEAM_ID`이면 진입하지 않는다. +3. LLM 의도 분류가 `companyx_rag` 이외의 확정 intent를 confidence >= 0.7로 반환했으면 진입하지 않는다. +4. `companyx_rag`으로 분류된 경우(confidence >= 0.5) 진입한다. +5. 위 두 경우 모두 아닐 때, 하위 호환성을 위해 마커 기반 판단(`_looks_like_companyx_grounding_question()`)으로 진입 여부를 결정한다. + - **마커**: intent 마커(근거, 내부 문서, MOU, 계약서, 투자조합, 재무제표 등) 또는 domain 마커(오늘전통, 옐로펀치, 컴퍼니엑스 등) + +## 처리 순서 (`try_companyx_grounding()`) +1. 진입 조건 확인 (위 참조). +2. 질문 유형 분류 (`_classify_question_type()`): explanatory / fact_check / quantitative / recap. +3. 멀티쿼리 생성 (`_build_query_candidates()`): 원문 + 키워드 조합 + 문서 힌트 결합으로 중복 제거 후 다수 변형 생성. +4. 하이브리드 검색 (`_search_companyx_documents()`): + - 각 쿼리를 `$SKILL_RAG_FILE_URL/api/search`에 병렬 호출 (`asyncio.gather`). + - 페이로드: `team_id=COMPANYX_TEAM_ID`, `limit=5`, `threshold=0.35`, `search_mode=hybrid`. + - 결과를 `document_id:chunk_index` 키로 병합, 최고 점수만 유지. +5. 상위 결과 선택 (`_select_top_results()`): RRF 정규화 점수 기준 정렬, 문서 다양성(같은 document_id 중복 제거), 최대 5건(`MAX_EVIDENCE_CHUNKS`). +6. LLM 근거 답변 생성 (`_call_llm_companyx_grounding()`): + - 프롬프트: `prompts/rag/companyx_grounding.md` (render_prompt). + - 모델: `settings.DEFAULT_LLM_MODEL`, temperature=0.1, JSON 응답 형식. + - 응답을 `CompanyXRAGOutput`으로 Pydantic 검증. +7. 최종 응답 조립 (`_build_grounded_response()`): LLM 성공 시 direct_answer + 참고 문서, 실패 시 question_type별 실패 메시지. ## 검색 모드 -- 기본 검색 모드는 `hybrid`이다 (벡터+키워드 RRF 합산). +- 기본 검색 모드: `hybrid` (벡터 + 키워드 RRF 합산). - 벡터 검색: PGVector cosine similarity (Gemini Embedding 2, 768d). - 키워드 검색: PostgreSQL tsvector + GIN 인덱스, prefix 매칭(`:*`). -- 점수 합산: RRF (Reciprocal Rank Fusion, k=60), 정규화하여 0~1 범위로 반환. -- Apache AGE 그래프 점수는 hook으로 가산 가능 (현재 보조적 위치). +- 점수 합산: RRF (Reciprocal Rank Fusion, k=60), 정규화 0~1. +- Apache AGE 그래프 점수: hook으로 가산 가능 (보조적 위치). ## 근거 선별 원칙 -- 키워드 기반 룰로 검색 결과를 필터링하지 않는다 (룰베이스 절제 원칙 — 헌장.md §B.6). -- 하이브리드 검색이 반환한 RRF 정규화 점수 순서를 신뢰하고, LLM이 컨텍스트를 보고 적합도를 재판단한다. +- 키워드 기반 룰로 검색 결과를 필터링하지 않는다 (룰베이스 절제 원칙 — 헌장.md 참조). +- 하이브리드 검색 RRF 정규화 점수 순서를 신뢰하고, LLM이 컨텍스트를 보고 적합도를 재판단한다. - 근거 선별 책임은 LLM에 있으며, 코드 레벨에서는 점수 상위 결과를 문서 다양성 기준으로 선택만 한다. ## 실패 분기 -- 검색 결과가 0건이면 `try_companyx_grounding()`이 `None`을 반환하고, `message_service`가 일반 의도 분류 경로로 fallback한다. (260323 fallback 복구) +- 검색 결과가 0건이면 `try_companyx_grounding()`이 `None`을 반환 → `message_service`가 일반 의도 분류 경로로 fallback. - LLM이 컨텍스트만으로 답변 불가로 판단하면 `failure_reason`을 채우고, 성공처럼 반환하지 않는다. - 수치형 질문에서 값이 없으면 추정하지 않는다. - 내부 규정이나 최신 집계가 없으면 `문서 없음`, `미확인`, `불일치` 중 하나로 명시한다. - 메타 대화로 회피하지 않는다. -## 현재 기준 -- 이 흐름은 `rb8001` 답변 합성 규칙과 연결된다. -- 검색 결과를 그대로 붙이는 방식은 허용하지 않는다. -- 질문 유형 분류는 LLM 프롬프트 톤 조절용으로만 사용한다. -- `SKILL.md`의 Trigger 설명은 운영 의도 요약이고, 실제 진입 조건 판단은 현재 코드 기준을 우선합니다. +## 환경변수 +- `SKILL_RAG_FILE_URL` — skill-rag-file 서비스 베이스 URL (필수). ## 검증 기준 -- `오늘전통/옐로펀치` 같은 기준 질문에서 직접 답 + 근거 문서가 함께 나와야 한다. -- `투자사 수`, `휴가 규정` 같은 근거 부족 질문은 추정 없이 실패해야 한다. +- 기준 질문(오늘전통, 옐로펀치 등)에서 직접 답 + 근거 문서가 함께 나와야 한다. +- 근거 부족 질문(투자사 수, 휴가 규정 등)은 추정 없이 실패해야 한다. - Slack 실응답과 테스트 응답이 같은 기준을 따라야 한다. ## 관련 문서 diff --git a/workflow/03_rag/companyx_incremental_indexing_workflow.md b/workflow/03_rag/companyx_incremental_indexing_workflow.md index 6166e8e..300c903 100644 --- a/workflow/03_rag/companyx_incremental_indexing_workflow.md +++ b/workflow/03_rag/companyx_incremental_indexing_workflow.md @@ -1,4 +1,8 @@ +--- tags: [workflow, rag, companyx, indexing, batch, incremental] +type: workflow +last_updated: 2026-04-06 +--- # Company X 단계별 증분 인덱싱 워크플로우 diff --git a/workflow/03_rag/rag_upload_indexing_pipeline.md b/workflow/03_rag/rag_upload_indexing_pipeline.md index 4222a11..74cbc4e 100644 --- a/workflow/03_rag/rag_upload_indexing_pipeline.md +++ b/workflow/03_rag/rag_upload_indexing_pipeline.md @@ -1,7 +1,7 @@ --- tags: [workflow, rag, companyx, upload, indexing] type: workflow -last_updated: 2026-03-22 +last_updated: 2026-04-06 --- # RAG 업로드·인덱싱 파이프라인 @@ -15,6 +15,12 @@ last_updated: 2026-03-22 - [project-artifacts-ssot.md](../../../../0_VALUE/20_Governance/project-artifacts-ssot.md) - [test-principles.md](../../../../0_VALUE/20_Governance/test-principles.md) +## 코드 SSOT +- `skill-rag-file` 서비스 (업로드·인덱싱·검색 담당) +- `rb8001/app/pipelines/langgraph_document.py` (문서 파이프라인에서 업로드·재인덱싱 호출) +- `rb8001/app/router/slack_handler.py` (Slack 파일 업로드 경로) +- `rb8001/app/services/naverworks_file_processor.py` (네이버웍스 첨부 업로드 경로) + ## 입력 - `team_id` - `file_url` 또는 업로드 파일 본문 @@ -22,20 +28,37 @@ last_updated: 2026-03-22 - `metadata` ## 출력 -- 업로드 결과 -- 인덱싱 결과 +- 업로드 결과 (document_id) +- 인덱싱 결과 (청크 수, 임베딩 상태) - 저장된 문서의 메타데이터 - 실패 시 실패 원인과 재시도 가능 여부 ## 처리 순서 1. 요청 페이로드를 정규화한다. -2. 원본 문서를 `skill-rag-file` 업로드 엔드포인트로 전달한다. +2. 원본 문서를 `$SKILL_RAG_FILE_URL/api/upload`로 전달한다. 3. 텍스트 추출을 수행한다. OCR 대상(이미지 PDF 등)은 OCR 폴백 경로로 처리한다. 4. 청킹한다 (기준: 1,000자 chunk, 200자 overlap). 5. 임베딩한다 (Gemini Embedding 2, 768d). 6. DB 저장 시 `tsvector` 컬럼이 트리거로 자동 생성된다 (`simple` 설정). 7. 저장 결과를 그대로 반환한다. +## 재인덱싱 +- `$SKILL_RAG_FILE_URL/api/reindex` 엔드포인트를 통해 기존 문서 재인덱싱 가능. +- `langgraph_document.py` 파이프라인에서 업로드 후 필요 시 재인덱싱을 호출한다. +- 재인덱싱 후 `$SKILL_RAG_FILE_URL/api/text/{document_id}`로 텍스트 추출 결과를 검증한다. + +## 업로드 경로별 진입점 + +| 경로 | 코드 | 설명 | +|------|------|------| +| Slack 파일 첨부 | `slack_handler.py` → `$SKILL_RAG_FILE_URL/api/upload` | Slack에서 파일 다운로드 후 skill-rag-file로 전송 | +| 네이버웍스 메일 첨부 | `naverworks_file_processor.py` → `$SKILL_RAG_FILE_URL/api/upload` | 메일 첨부 다운로드 후 업로드 | +| IR Deck 업로드 | `ir_deck.py` → `slack_handler.upload_files_to_rag()` | IR Deck 평가를 위한 업로드 | +| 배치 인덱싱 | `skill-rag-file/scripts/` | 대량 문서 일괄 인덱싱 | + +## 환경변수 +- `SKILL_RAG_FILE_URL` — skill-rag-file 서비스 베이스 URL (필수). + ## 실패 분기 - 파일 누락이면 업로드 전에 실패한다. - 텍스트 추출이 실패하면 인덱싱하지 않는다. @@ -49,7 +72,6 @@ last_updated: 2026-03-22 - 청킹 기준: 1,000자 chunk, 200자 overlap. - 임베딩: Gemini Embedding 2, 768차원, HNSW cosine 인덱스. - `tsvector` 컬럼은 INSERT/UPDATE 트리거로 자동 생성된다 (`simple` 설정, GIN 인덱스). -- 배치 인덱싱 스크립트: `skill-rag-file/scripts/reindex_companyx_latest_200.py` (200개 기준). ## 검증 기준 - 업로드 직후 검색 API로 최소 1건 이상 적중하는지 확인한다. diff --git a/workflow/04_scheduler/scheduled_daily_briefing.md b/workflow/04_scheduler/scheduled_daily_briefing.md index 287254f..47276ee 100644 --- a/workflow/04_scheduler/scheduled_daily_briefing.md +++ b/workflow/04_scheduler/scheduled_daily_briefing.md @@ -1,31 +1,62 @@ +--- +tags: [workflow, scheduler, news, headlines, slack] +type: workflow +last_updated: 2026-04-06 +--- + # scheduled_daily_briefing 워크플로우 ## 목적 -평일 09:10에 네이버 + 동남아 스타트업 헤드라인을 수집하여 Slack 채널에 자동 게시한다. +DB에 등록된 스케줄에 따라 네이버 + 동남아 스타트업 헤드라인을 수집하여 Slack 채널에 자동 게시한다. + +## 아키텍처 +- **스케줄러**: APScheduler (DB 기반). `db_loader.py`가 `scheduler_jobs` 테이블에서 `job_type=daily_headlines` 잡을 로드하여 등록. +- **실행 래퍼**: `app/scheduler/jobs/daily_headlines.py` → `_run_headlines_with_logging(channel_id)`. +- **실제 워크플로우**: `app/services/workflows/headlines_workflow.py` — LangGraph StateGraph로 구성. +- **n8n 미사용**. systemd로 rb8001이 직접 실행. + +## 코드 SSOT +- `rb8001/app/scheduler/jobs/daily_headlines.py` — 스케줄러 잡 래퍼 +- `rb8001/app/services/skills/startup_news_skill.py` — `run_headlines_job()` 진입점 +- `rb8001/app/services/workflows/headlines_workflow.py` — LangGraph 워크플로우 본체 +- `rb8001/app/scheduler/db_loader.py` — DB 잡 로더 (`JOB_TYPE_MAP["daily_headlines"]`) ## 흐름 + ``` -09:10 Trigger (월~금) → Build Runtime Context → [병렬] Call Naver Headlines API - Call SEA Headlines API - → Describe Actual Path → APIs Reachable? → (true) Build Runtime Summary → Slack Delivery View - → (false) Build Fallback Summary → Slack Delivery View +APScheduler cron trigger + → db_loader가 등록한 guarded_job (schedule_policy 평가) + → _run_headlines_with_logging(channel_id) + → run_headlines_job(channel_id) + → run_headlines_workflow(channel_id) [LangGraph] + → fetch_naver_node (SkillCommands.fetch_naver_headlines, fmt=slack) + → fetch_sea_node (SkillCommands.fetch_sea_headlines, fmt=json) + → format_node (네이버 텍스트에 동남아 섹션 삽입) + → send_node (Slack chat.postMessage) ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| 09:10 Trigger | cron `10 9 * * 1-5` (평일 09:10) | -| Build Runtime Context | scheduler 메타 정보 설정 (channel, job wrapper, state store) | -| Call Naver Headlines API | `POST :8505/api/news/naver/startup-headlines` (format=slack) | -| Call SEA Headlines API | `POST :8505/api/news/sea/headlines` (format=json) | -| Describe Actual Path | 두 API 결과를 합산하여 실행 경로 요약 | -| APIs Reachable? | 양쪽 모두 200인지 확인 | -| Slack Delivery View | 최종 결과를 Slack 채널에 게시 | +## 스케줄 정책 +- cron 표현식은 `scheduler_jobs` DB 테이블에서 관리한다 (예: `10 9 * * 1-5`). +- `schedule_policy` 필드에 `workday` 정책이 설정되어 있으면 공휴일/주말을 건너뛴다 (`evaluate_schedule_policy()`). +- `channel_id`는 `config.channel_id` DB 필드에서 전달받는다. -## 엔드포인트 -- 아웃바운드: `POST http://192.168.219.52:8505/api/news/naver/startup-headlines` -- 아웃바운드: `POST http://192.168.219.52:8505/api/news/sea/headlines` -- 아웃바운드: Slack `chat.postMessage` (채널: C09C98KK2TT) +## 주요 노드 (LangGraph) + +| 노드 | 함수 | 설명 | +|------|------|------| +| fetch_naver | `fetch_naver_node()` | `SkillCommands.fetch_naver_headlines(fmt="slack")` 호출 | +| fetch_sea | `fetch_sea_node()` | `SkillCommands.fetch_sea_headlines(fmt="json")` 호출 | +| format | `format_node()` | 네이버 헤드라인 텍스트 끝에 동남아 섹션 삽입 | +| send | `send_node()` | Slack `chat.postMessage` 전송 | + +## Slack 봇 토큰 해소 +- `_get_slack_bot_token_for_channel(channel_id)` → DB에서 채널별 workspace bot_token 조회. +- 실패 시 `settings.COMPANYX_SLACK_BOT_TOKEN` → `settings.SLACK_BOT_TOKEN` 순서로 폴백. + +## 실패 분기 +- 네이버 또는 동남아 API 호출 실패 시 `errors` 리스트에 기록하고 빈 텍스트로 진행. +- 보낼 텍스트가 비어 있으면 Slack 전송을 스킵하고 `message_ts=None`으로 종료. +- Slack 전송 실패 시 `errors`에 기록. ## 관련 문서 - [skill_news_briefing_request](../02_skills/skill_news_briefing_request.md) diff --git a/workflow/04_scheduler/scheduled_healthcheck_alert.md b/workflow/04_scheduler/scheduled_healthcheck_alert.md index ce3b11e..735cebe 100644 --- a/workflow/04_scheduler/scheduled_healthcheck_alert.md +++ b/workflow/04_scheduler/scheduled_healthcheck_alert.md @@ -1,25 +1,70 @@ +--- +tags: [workflow, scheduler, healthcheck, monitoring] +type: workflow +last_updated: 2026-04-06 +--- + # scheduled_healthcheck_alert 워크플로우 ## 목적 -10분마다 rb8001의 health 엔드포인트를 확인하고, 실패 시 Slack 알림을 보낸다. +rb8001의 `/health` 엔드포인트를 통해 모든 외부 스킬 서비스와 인프라의 상태를 확인한다. + +## 아키텍처 +- **엔드포인트**: `GET /health` (`app/router/system_endpoint.py`) +- **라우터 인스턴스**: `RobeingRouter.get_service_status()` (`app/router/router.py`) +- **n8n 미사용**. rb8001이 FastAPI 엔드포인트로 직접 제공. + +## 코드 SSOT +- `rb8001/app/router/system_endpoint.py` — `/health` 엔드포인트 정의 +- `rb8001/app/router/router.py` — `get_service_status()` 구현 + +## 헬스체크 흐름 -## 흐름 ``` -Schedule Trigger (*/10 * * * *) → Check rb8001 Health → Health Failed? → (true) Send Alert - → (false) 종료 +GET /health + → system_endpoint.health_check() + → router_instance.get_service_status() + → registry.load_all() [SKILL.md 기반 스킬 목록] + → 각 external_http 스킬: GET {skill_url}/health (timeout 5초) + → GET {STATE_SERVICE_URL}/healthz (state 서비스) + → brain 상태 확인 (decision_engine 초기화 여부) + → 응답 조립 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Schedule Trigger | cron `*/10 * * * *` (매 10분) | -| Check rb8001 Health | `GET :8001/health` | -| Health Failed? | statusCode !== 200 분기 | -| Send Alert | Slack C_ALERTS 채널에 장애 알림 | +## 응답 형식 -## 엔드포인트 -- 아웃바운드: `GET http://192.168.219.52:8001/health` -- 아웃바운드: Slack `chat.postMessage` (채널: C_ALERTS) +```json +{ + "status": "healthy", + "robeing_id": "{settings.ROBEING_ID}", + "services": { + "skill-news": true, + "skill-rag-file": true, + "skill-calendar": false, + "state": true, + "brain": true, + "brain_stats": { "..." : "..." } + }, + "brain": "integrated", + "memory_store": "postgresql", + "memory_limit": "{settings.MAX_MEMORY_SIZE}MB" +} +``` + +## 스킬 상태 확인 방식 +- `registry.load_all()`이 SKILL.md frontmatter에서 `runtime_kind=external_http`인 스킬 목록을 로드한다. +- 각 스킬의 `resolve_url()`로 URL을 해소한 뒤 `GET {url}/health`를 호출한다 (timeout 5초). +- HTTP 200이면 `true`, 그 외 또는 예외 시 `false`. + +## 실패 분기 +- `router_instance`가 `None`이면 HTTP 503 반환. +- 개별 스킬 헬스체크 실패는 해당 스킬만 `false`로 표시하고, 전체 응답은 정상 반환. +- 전체 예외 발생 시 HTTP 503 + 상세 메시지 반환. + +## 환경변수 +- `STATE_SERVICE_URL` — state 서비스 URL (settings 경유). +- `ROBEING_ID` — 로빙 식별자 (settings 경유). +- `MAX_MEMORY_SIZE` — 메모리 한도 (settings 경유). ## 관련 문서 -- [service_health_check](../05_admin/service_health_check.md) +- [service_health_check (레거시, _archive 이동됨)](../_archive/service_health_check.md) diff --git a/workflow/05_admin/diary_reflection_pipeline.md b/workflow/05_admin/diary_reflection_pipeline.md index 81d3ff8..de1a87c 100644 --- a/workflow/05_admin/diary_reflection_pipeline.md +++ b/workflow/05_admin/diary_reflection_pipeline.md @@ -1,37 +1,70 @@ +--- +tags: [workflow, admin, diary, scheduler] +type: workflow +last_updated: 2026-04-06 +--- + # diary_reflection_pipeline 워크플로우 ## 목적 -매일 새벽 2시에 전날의 로빙 일기를 자동 생성하고 Slack에 요약을 게시한다. 수동 트리거도 지원한다. +매일 오전 2시에 전날의 로빙 일기를 자동 생성하고, DB에 저장하며, DOCS 레포에 마크다운 파일로 커밋·푸시한다. + +## 아키텍처 +- **스케줄러**: APScheduler (DB 기반). `db_loader.py`가 `scheduler_jobs` 테이블에서 `job_type=diary_generator` 잡을 로드하여 등록. +- **기본 스케줄**: `settings.DIARY_GENERATOR_SCHEDULE` = `0 2 * * *` (매일 02:00). +- **활성화 플래그**: `settings.DIARY_GENERATOR_ENABLED` (기본 `True`). +- **n8n 미사용**. systemd로 rb8001이 직접 실행. + +## 코드 SSOT +- `rb8001/app/scheduler/jobs/diary_generator.py` — 잡 래퍼 + 일기 생성 로직 +- `rb8001/app/services/diary/aggregator.py` — `DiaryAggregator` (데이터 집계) +- `rb8001/app/services/diary/generator.py` — `DiaryGenerator` (LLM 일기 생성) +- `rb8001/app/state/diary_repository.py` — `save_diary()` (DB 저장) +- `rb8001/app/router/diary_endpoint.py` — 일기 조회 API +- `rb8001/app/scheduler/db_loader.py` — DB 잡 로더 (`JOB_TYPE_MAP["diary_generator"]`) ## 흐름 + ``` -Daily at 2AM ─┐ -Manual Trigger ─┤→ Set Yesterday Date → Generate Diary (rb8001) → Get Diary Content → Post to Slack +APScheduler cron trigger (0 2 * * *) + → db_loader가 등록한 guarded_job (schedule_policy 평가) + → _run_diary_generator_with_logging() + → _generate_diary() + 1. draft_watcher 선행 실행 (non-blocking) + 2. DiaryAggregator.aggregate(target_date, robeing_id) — 전날 활동 데이터 집계 + 3. DiaryGenerator.generate(target_date, robeing_id, aggregated_data) — LLM 일기 생성 + 4. save_diary() — DB 저장 (full_content, summary, dominant_emotion, stats) + 5. _save_diary_to_git() — DOCS 레포에 마크다운 저장 + git add/commit/push + 6. ActivityLogger.log_scheduler_job() — 스케줄러 작업 로그 기록 ``` -## 주요 노드 -| 노드 | 설명 | -|---|---| -| Daily at 2AM | cron `0 2 * * *` | -| Manual Trigger | `POST /admin/diary/trigger` (수동 실행용) | -| Set Yesterday Date | 전날 날짜(YYYY-MM-DD) 계산 | -| Generate Diary (rb8001) | `POST :8001/api/diary/generate` (timeout 300초) | -| Get Diary Content | `GET :8001/api/diary/{date}?robeing_id=rb8001` | -| Post to Slack | 일기 요약 + 감정 + 대시보드 링크를 Slack에 게시 | +## 일기 조회 API (현행) -## 엔드포인트 -- 인바운드: `POST /admin/diary/trigger` (n8n webhook, 수동) -- 아웃바운드: `POST http://192.168.219.52:8001/api/diary/generate` -- 아웃바운드: `GET http://192.168.219.52:8001/api/diary/{date}` -- 아웃바운드: Slack `chat.postMessage` +| 메서드 | 경로 | 설명 | +|--------|------|------| +| `GET` | `/api/diary/{target_date}?robeing_id=rb8001` | 특정 날짜 일기 조회 | +| `GET` | `/api/diary/list?robeing_id=rb8001&limit=100` | 일기 목록 조회 | -## 260319 변경 영향 +**주의**: `/api/diary/generate` 엔드포인트는 코드에 존재하지 않는다. 일기 생성은 APScheduler 잡이 내부적으로 `DiaryGenerator.generate()`를 직접 호출한다. -이 워크플로우는 rb8001의 `/api/diary/generate`를 호출한다. 내부적으로 `llm_service.process_request()`가 `task_type=chat`, `context={}`로 실행되므로: +## 일기 저장 경로 +- **DB**: `diary` 테이블 (`date`, `robeing_id`, `full_content`, `summary`, `dominant_emotion`, `stats`) +- **Git**: `$DOCS_REPO_PATH/book/700_for_robeing/diary/{YYYY-MM-DD}.md` + - `DOCS_REPO_PATH` 환경변수, 기본값: `$WORKSPACE_ROOT/robeing/DOCS` -- **프롬프트 DB v3 주입: 적용됨** -- DB `prompt_versions` 활성 프롬프트가 일기 생성 시 system prompt로 주입된다. -- **neutral 감정 constraints 생략: 적용됨** -- 일기 생성 시 감정이 neutral이면 감정 constraints가 생략된다. -- 이전(~260318)에는 하드코딩 프롬프트 + 모든 감정에 constraints 주입이었으므로, 일기 톤에 변화가 있을 수 있다. +## 선행 작업 +- 일기 생성 전 `draft_watcher`를 실행하여 NAS 드래프트 변경사항을 activity_log에 기록한다 (실패 시 non-blocking). + +## 실패 분기 +- `DiaryGenerator.generate()`가 `None`을 반환하면 `RuntimeError` 발생. +- DB 저장 실패 시 에러 로그 + ActivityLogger에 `status=error` 기록. +- Git push 실패 시 에러 로그만 남기고 계속 진행 (non-blocking). + +## 환경변수 +- `DIARY_GENERATOR_ENABLED` — 일기 생성 활성화 여부 (settings 경유, 기본 `True`). +- `DIARY_GENERATOR_SCHEDULE` — cron 표현식 (settings 경유, 기본 `0 2 * * *`). +- `DOCS_REPO_PATH` — DOCS 레포 경로 (환경변수, 기본 `$WORKSPACE_ROOT/robeing/DOCS`). +- `ROBEING_ID` — 로빙 식별자 (settings 경유). ## 관련 문서 -- [service_health_check](./service_health_check.md) +- [service_health_check (레거시, _archive 이동됨)](../_archive/service_health_check.md) diff --git a/workflow/04_scheduler/scheduled_rag_reindex_retry.md b/workflow/_archive/scheduled_rag_reindex_retry.md similarity index 100% rename from workflow/04_scheduler/scheduled_rag_reindex_retry.md rename to workflow/_archive/scheduled_rag_reindex_retry.md