microsoft/CrewMeal
Python
Captured source
source ↗microsoft/CrewMeal
Language: Python
License: MIT
Stars: 0
Forks: 0
Open issues: 1
Created: 2026-07-19T00:58:59Z
Pushed: 2026-07-19T04:16:19Z
Default branch: main
Fork: no
Archived: no
README:
CrewMeal — 멀티포맷 콘텐츠 강화 플랫폼
> CrewMeal = Copilot에게 제공하는 양질의 콘텐츠 → 부조종사(co-pilot)가 기내에서 먹는 > 기내식 Crew Meal. Copilot이 잘 일하도록 먹여주는 잘 차려진 한 끼라는 뜻입니다.
복잡한 사내 문서(.pptx·.pdf·.hwp)를 구조화된 검색 메타데이터로 강화해 Microsoft Graph 검색 커넥터(Copilot Connector)에 externalItem으로 색인하는 로컬 워커입니다. 각 페이지를 이미지로 렌더링하고, 원문 텍스트·도형 근거를 함께 비전 LLM에 전달해 strict JSON Schema로 분석한 뒤 허용 태그 HTML로 렌더링합니다. 원본 문서는 수정하지 않습니다. 분석 모델은 관리 포털에서 교체할 수 있으며(비용 절감), 포맷별 지원과 암호화 복호화는 관리자 토글로 켜고 끕니다.
> 📄 소개 사이트: docs/의 정적 페이지(실제 구동 화면 캡처 포함)는 GitHub Pages로 > 배포됩니다 — . 프로젝트 저장소는 > 입니다.
CrewMeal 확장 기능
| 기능 | 상태 | 설명 | | --- | --- | --- | | PPTX | ✅ 구현됨 | LibreOffice → PDF → 렌더 · 간트 geometry 근거 | | PDF | ✅ 구현됨 | PyMuPDF 직접 렌더 · 페이지 텍스트 근거 · 암호화 PDF 라우팅 | | HWP/HWPX | ✅ 구현됨 | LibreOffice Writer 필터 → PDF → 렌더 | | DOCX | 🧩 구조만 | 핸들러 등록·감지, 추출 로직은 명확한 NotImplemented | | XLSX | 🧩 구조만 | 핸들러 등록·감지, 셀/표 추출 예정 | | 모델 교체 | ✅ 구조 | 관리 포털에서 provider·배포·엔드포인트·reasoning 교체(env fallback) | | MIP 복호화 | 🧩 구조·토글 | 관리자 on/off 훅. 활성화 시 실제 복호화는 배포 환경에 맞춰 확장 | | 기타 복호화 | 🧩 구조·토글 | 배포별 암호화 솔루션용 복호화 파이프라인 훅 |
- 모든 포맷은 SharePoint 인제스트 경로로 흐릅니다. 포맷을 켜면 인제스트·재조정·시연
업로드에 자동 편입됩니다. 포맷별 활성화는 format..enabled 설정으로 관리하며 구현되지 않은(스켈레톤) 포맷은 켤 수 없습니다.
- 분석 모델 교체는
/admin/settings의 「이미지 분석 모델」 카드에서 설정하며, 빈 값은
환경 변수 기본값을 사용합니다. 변경은 워커 재시작 후 적용됩니다.
- 복호화 파이프라인은 기본 꺼짐이며, 아직 구현되지 않은 제공자를 켜면 해당 문서 처리
시 명확한 오류로 실패합니다(활성화 사실이 로그·상태에 남습니다).
처리 흐름
src/crewmeal/search_enhancement의 워커가 SharePoint 라이브러리를 폴링하고 등록된 문서마다 다음을 수행합니다.
1. 문서를 임시 폴더에만 내려받고 포맷을 감지(확장자·매직·크기)한 뒤 검증합니다. 2. (활성화 시) 복호화 훅이 암호화된 문서를 복호화합니다. 3. 페이지의 보이는 원문 텍스트와 요소 수를 source manifest로 추출합니다. 4. 포맷 핸들러가 각 페이지를 이미지(PPTX/HWP는 LibreOffice→PDF, PDF는 PyMuPDF)로 렌더링합니다. 5. (PPTX) Open XML에서 간트 막대·연결선 같은 결정적 geometry 근거를 계산합니다. 6. 각 페이지 이미지를 비전 LLM에 근거와 함께 전달해 병렬 분석하고 strict JSON Schema 응답을 받습니다. 노트·alt text·숨김 요소는 근거에 포함하지 않습니다. 7. 응답을 허용 태그 HTML로 렌더링하고 externalItem으로 게시합니다.
원본 문서·PDF·PNG·HTML 본문과 비밀 값은 SQLite 상태에 저장하지 않습니다.
> 프로덕션 서버 모드에서는 상태 저장소가 PostgreSQL이고, 단계별 진행은 job_events > 타임라인으로 기록되며, 추출 HTML·구조화 JSON 등 산출물은 산출물 저장소에 저장되어 > 상태 페이지에서 열람합니다. 산출물 저장소는 CREWMEAL_ARTIFACT_BACKEND로 고르며 > blob(Azure Blob), database(PostgreSQL artifact_blobs 테이블), 로컬 파일 중 > 하나입니다. 배포된 PoC는 Storage·Key Vault 공개 접근이 구독 정책으로 강제 차단되어 > database 저장소와 Container App 인라인 시크릿을 사용합니다. 로컬 워커와 단위 > 테스트는 계속 SQLite와 로컬 파일을 사용합니다.
준비 사항
- Python 3.11 이상
- LibreOffice (
soffice.exe) - Azure CLI 및 Azure Developer CLI 1.28.0 이상
- 대상 구독의
Owner또는 리소스 생성 권한과
Microsoft.Authorization/roleAssignments/write
- SharePoint 테스트 사이트와 Microsoft 365 앱 권한
- 비민감 테스트 문서
현재 Bicep 기본값은 구독 1004da59-37b1-4a22-80e1-019cc29ce8f1의 eastus2 배포를 전제로 하며, PostgreSQL만 구독 정책상 offer가 제한되지 않는 centralus에 둡니다(postgresLocation 파라미터). 공개 endpoint를 사용하므로 민감 문서를 입력하지 마세요.
로컬 설치
python -m venv .venv .\.venv\Scripts\python.exe -m pip install -e ".[dev]"
LibreOffice 자동 탐색 경로에 없다면 설정합니다.
$env:SOFFICE_PATH = "C:\Program Files\LibreOffice\program\soffice.exe"
Azure 배포
프로덕션은 단일 컨테이너 이미지를 웹 앱(Ingest API + 상태 페이지 + 관리자 포탈)과 워커(LibreOffice 변환 파이프라인)로 각각 실행합니다. 요청은 SharePoint 열이 아니라 서버 DB 큐(PostgreSQL)에 적재되고 워커가 claim/lease로 자동 처리합니다. 이미지는 Dockerfile 하나이며 APP_ROLE=web|worker 환경변수로 역할을 분기합니다.
infra/main.bicep 배포 항목:
- Microsoft Foundry
AIServicesS0 계정 +gpt-5.2/gpt-5-mini/text-embedding-3-large배포 - Container Apps 환경(+ Log Analytics), 웹 Container App(외부 ingress:8000), 워커 Container App(ingress 없음, min 1)
- Azure Container Registry(Standard), PostgreSQL Flexible Server 16(+
crewmealDB,centralus), Storage(Blobartifacts컨테이너) - Key Vault(M365 secret·admin key·session secret·DB URL), user-assigned Managed Identity
- MI RBAC: AcrPull, Storage Blob Data Contributor, Key Vault Secrets User, Cognitive Services OpenAI User
> 배포된 PoC에서는 구독 정책이 Storage·Key Vault의 공개 접근을 강제로 Disabled로 > 되돌립니다. 그래서 실제 실행 구성은 Blob·Key Vault 대신 PostgreSQL 산출물 저장소 > (CREWMEAL_ARTIFACT_BACKEND=database)와 Container App 인라인 시크릿을 사용합니다. > Storage·Key Vault 리소스는 여전히 배포되지만 이 경로에서는 사용하지 않습니다.
배포 전 시크릿·환경변수
azd env set으로 다음을 설정합니다. Microsoft 365 값은 로컬 개발 환경변수와 동일합니다.
azd auth login azd env new poc azd env set AZURE_SUBSCRIPTION_ID 1004da59-37b1-4a22-80e1-019cc29ce8f1 azd env set AZURE_LOCATION eastus2 # 생성 시크릿 — DATABASE_URL에 들어가므로 암호는 URL 안전 문자만 사용(@ : / ? 회피) azd env set POSTGRES_ADMIN_PASSWORD "" azd env set CREWMEAL_ADMIN_KEY "" azd env set CREWMEAL_WEB_SESSION_SECRET "" # Microsoft 365 앱 (워커 처리 및 ingest driveItem 해석에 필요) azd env set CREWMEAL_M365_TENANT_ID "" azd env set CREWMEAL_M365_CLIENT_ID "" azd env set CREWMEAL_M365_CLIENT_SECRET "" azd env set CREWMEAL_M365_SITE_ID "" azd env set CREWMEAL_M365_DRIVE_ID "" azd env set CREWMEAL_M365_LIST_ID "" azd env set CREWMEAL_M365_SITE_URL "" azd env set CREWMEAL_M365_CONNECTION_ID "" # Ingest 인증: 초기 PoC는 무인증으로 시작할 수 있습니다. azd env set CREWMEAL_INGEST_REQUIRE_AUTH false azd env set CREWMEAL_INGEST_AUDIENCE "api://crewmeal-ingest" # 무인증이면 자리표시자
인증을 켜려면 API용 Entra 앱 등록을 만들고, 그 App ID URI를 CREWMEAL_INGEST_AUDIENCE, 허용 앱 ID를 CREWMEAL_INGEST_ALLOWED_APP_IDS(CSV)에 설정한 뒤 CREWMEAL_INGEST_REQUIRE_AUTH=true로 둡니다. SPFx 확장에도 같은 audience와 웹 앱 URL을 구성합니다(아래 SPFx 섹션).
배포
azd up
azd up은 인프라를 프로비저닝하고 Dockerfile을 빌드해 ACR에 푸시한 뒤 웹·워커 Container App을 갱신합니다. 완료 후 상태 페이지·관리자 포탈 URL은 SERVICE_WEB_URI 출력으로 확인합니다(관리자 포탈은 SERVICE_WEB_URI/admin, 키는 CREWMEAL_ADMIN_KEY).
Foundry만 프로비저닝하려면 azd provision, 코드만 재배포하려면 azd deploy web 또는 azd deploy worker를 사용합니다.
로컬 워커를 Foundry 출력으로 실행하려면 배포 후 환경변수를 현재 PowerShell에 로드합니다.
$values = azd env get-values --output json | ConvertFrom-Json
$values.PSObject.Properties | ForEach-Object {
Set-Item -Path "Env:$($_.Name)" -Value $_.Value
}API key는 사용하거나 저장하지 않습니다. Azure OpenAI 및 Blob 저장소(사용 시) 인증은 워커에 연결된 user-assigned Managed Identity(AZURE_CLIENT_ID)를 통한 DefaultAzureCredential을 사용하고, 로컬 실행 시에는 Azure CLI/azd 로그인을 사용합니다. 워커가 사용하는 endpoint 환경변수는 CONTENTUNDERSTANDING_ENDPOINT이며 같은 Foundry 리소스를 가리킵니다.
기본 모델을 GPT-5.2로 전환하려면 실행 전에 다음 환경변수를 설정합니다.
$env:SLIDE_IMAGE_MODEL = "gpt-5.2"
웹 엔드포인트
배포된 웹 앱(SERVICE_WEB_URI)이 제공하는 경로입니다.
GET /healthz,GET /readyz— liveness / readiness(= DB...
Excerpt shown — open the source for the full document.