@dssp/dcsp 1.0.2 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +13 -10
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
- package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
- package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
- package/dist-client/components/kpi-boxplot-chart.js +306 -0
- package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
- package/dist-client/components/kpi-lookup-chart.js +464 -0
- package/dist-client/components/kpi-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
- package/dist-client/components/kpi-mini-trend-chart.js +180 -0
- package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
- package/dist-client/components/kpi-radar-chart.d.ts +17 -0
- package/dist-client/components/kpi-radar-chart.js +259 -0
- package/dist-client/components/kpi-radar-chart.js.map +1 -0
- package/dist-client/components/kpi-single-boxplot-chart.d.ts +36 -0
- package/dist-client/components/kpi-single-boxplot-chart.js +417 -0
- package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -0
- package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
- package/dist-client/components/kpi-step-lookup-chart.js +181 -0
- package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
- package/dist-client/components/kpi-trend-chart.d.ts +25 -0
- package/dist-client/components/kpi-trend-chart.js +241 -0
- package/dist-client/components/kpi-trend-chart.js.map +1 -0
- package/dist-client/google-map/common-google-map.d.ts +35 -0
- package/dist-client/google-map/common-google-map.js +349 -0
- package/dist-client/google-map/common-google-map.js.map +1 -0
- package/dist-client/google-map/google-map-loader.d.ts +6 -0
- package/dist-client/google-map/google-map-loader.js +23 -0
- package/dist-client/google-map/google-map-loader.js.map +1 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
- package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
- package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +69 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +790 -0
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +22 -0
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +344 -0
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +21 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +335 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
- package/dist-client/pages/sv-project-complete.d.ts +31 -0
- package/dist-client/pages/sv-project-complete.js +364 -0
- package/dist-client/pages/sv-project-complete.js.map +1 -0
- package/dist-client/pages/sv-project-detail.d.ts +73 -0
- package/dist-client/pages/sv-project-detail.js +1387 -0
- package/dist-client/pages/sv-project-detail.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +15 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/shared/complete-api.d.ts +45 -0
- package/dist-client/shared/complete-api.js +274 -0
- package/dist-client/shared/complete-api.js.map +1 -0
- package/dist-client/shared/func.d.ts +2 -0
- package/dist-client/shared/func.js +22 -0
- package/dist-client/shared/func.js.map +1 -0
- package/dist-client/shared/integration-fetch.d.ts +35 -0
- package/dist-client/shared/integration-fetch.js +53 -0
- package/dist-client/shared/integration-fetch.js.map +1 -0
- package/dist-client/shared/kpi-project-sync.d.ts +16 -0
- package/dist-client/shared/kpi-project-sync.js +129 -0
- package/dist-client/shared/kpi-project-sync.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/menu-tools.d.ts +7 -0
- package/dist-client/viewparts/menu-tools.js +168 -21
- package/dist-client/viewparts/menu-tools.js.map +1 -1
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +5 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/index.d.ts +7 -0
- package/dist-server/middlewares/index.js +100 -0
- package/dist-server/middlewares/index.js.map +1 -0
- package/dist-server/service/index.d.ts +4 -0
- package/dist-server/service/index.js +19 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/kpi-metric-value/index.d.ts +4 -0
- package/dist-server/service/kpi-metric-value/index.js +8 -0
- package/dist-server/service/kpi-metric-value/index.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +198 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +1067 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +15 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +72 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
- package/dist-server/service/kpi-stat/index.d.ts +4 -0
- package/dist-server/service/kpi-stat/index.js +8 -0
- package/dist-server/service/kpi-stat/index.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
- package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js +126 -0
- package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
- package/dist-server/service/kpi-value/index.d.ts +3 -0
- package/dist-server/service/kpi-value/index.js +7 -0
- package/dist-server/service/kpi-value/index.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +8 -0
- package/dist-server/service/kpi-value/kpi-value-query.js +90 -0
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/python/RUNNER_CONVERSION_GUIDE.md +81 -0
- package/python/doc_summary_engine_handoff/.env.example +21 -0
- package/python/doc_summary_engine_handoff/HANDOFF.md +266 -0
- package/python/doc_summary_engine_handoff/README.md +71 -0
- package/python/doc_summary_engine_handoff/__main__.py +159 -0
- package/python/doc_summary_engine_handoff/api.py +165 -0
- package/python/doc_summary_engine_handoff/extract.py +290 -0
- package/python/doc_summary_engine_handoff/requirements.txt +15 -0
- package/python/doc_summary_engine_handoff/summarize.py +247 -0
- package/python/inspection_ai_engine_handoff/.env.example +22 -0
- package/python/inspection_ai_engine_handoff/HANDOFF.md +541 -0
- package/python/inspection_ai_engine_handoff/README.md +83 -0
- package/python/inspection_ai_engine_handoff/__main__.py +169 -0
- package/python/inspection_ai_engine_handoff/activities.py +147 -0
- package/python/inspection_ai_engine_handoff/api.py +273 -0
- package/python/inspection_ai_engine_handoff/checklist.py +53 -0
- package/python/inspection_ai_engine_handoff/cv_handlers.py +46 -0
- package/python/inspection_ai_engine_handoff/cv_model_registry.py +252 -0
- package/python/inspection_ai_engine_handoff/cv_models/README.md +58 -0
- package/python/inspection_ai_engine_handoff/cv_models/_example_frame_alignment/handler.py +39 -0
- package/python/inspection_ai_engine_handoff/cv_models/_example_frame_alignment/model.json +22 -0
- package/python/inspection_ai_engine_handoff/engine.py +333 -0
- package/python/inspection_ai_engine_handoff/requirements.txt +12 -0
- package/python/inspection_ai_engine_handoff/router.py +200 -0
- package/python/inspection_ai_engine_handoff/shot_catalog.py +79 -0
- package/python/inspection_ai_engine_handoff/shot_planner.py +278 -0
- package/python/inspection_ai_engine_handoff/vlm.py +335 -0
- package/python/spec-matching-0.5.5/README.md +113 -0
- package/python/spec-matching-0.5.5/__main__.py +331 -0
- package/python/spec-matching-0.5.5/kcs.csv +6028 -0
- package/python/spec-matching-0.5.5/kcs_embeddings.meta.json +1 -0
- package/python/spec-matching-0.5.5/kcs_embeddings.npy +0 -0
- package/python/spec-matching-0.5.5/requirements.txt +3 -0
- package/python/spec-matching-0.5.5//342/230/2057 SF-TD4 /341/204/221/341/205/263/341/204/205/341/205/251/341/204/200/341/205/263/341/204/205/341/205/242/341/206/267/341/204/211/341/205/245/341/206/257/341/204/200/341/205/250/341/204/211/341/205/245 /341/204/213/341/205/243/341/206/274/341/204/211/341/205/265/341/206/250_V1.0.hwp +0 -0
- package/schema.graphql +1274 -22
- package/things-factory.config.js +7 -1
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dssp/dcsp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"docker:push": "docker image push hatiolab/dcsp:latest && docker image push hatiolab/dcsp:$npm_package_version"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@dssp/building-complex": "^1.0.
|
|
43
|
-
"@dssp/integration-allbaro": "^1.0.
|
|
44
|
-
"@dssp/integration-kiscon": "^1.0.
|
|
45
|
-
"@dssp/project": "^1.0.
|
|
46
|
-
"@dssp/supervision": "^1.0.
|
|
42
|
+
"@dssp/building-complex": "^1.0.6",
|
|
43
|
+
"@dssp/integration-allbaro": "^1.0.5",
|
|
44
|
+
"@dssp/integration-kiscon": "^1.0.5",
|
|
45
|
+
"@dssp/project": "^1.0.6",
|
|
46
|
+
"@dssp/supervision": "^1.0.6",
|
|
47
47
|
"@material/web": "^2.4.0",
|
|
48
48
|
"@operato/chart": "^10.0.0",
|
|
49
49
|
"@operato/gantt": "^10.0.0",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"@things-factory/i18n-base": "^10.0.0",
|
|
82
82
|
"@things-factory/integration-base": "^10.0.0",
|
|
83
83
|
"@things-factory/integration-ui": "^10.0.0",
|
|
84
|
+
"@things-factory/kpi": "^10.0.0",
|
|
84
85
|
"@things-factory/lite-menu": "^10.0.0",
|
|
85
86
|
"@things-factory/more-base": "^10.0.0",
|
|
86
87
|
"@things-factory/more-ui": "^10.0.0",
|
|
@@ -95,5 +96,5 @@
|
|
|
95
96
|
"devDependencies": {
|
|
96
97
|
"@things-factory/builder": "^10.0.0"
|
|
97
98
|
},
|
|
98
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "f6b181b9e137f92aaf9e516f7c9f0b706fbd6035"
|
|
99
100
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# operato-runner 파이썬 모듈 변환 지침
|
|
2
|
+
|
|
3
|
+
외부에서 받은 파이썬 소스(FastAPI 서버, CLI 스크립트 등)를 **operato-runner 모듈**로 변환할 때 따르는 규칙.
|
|
4
|
+
이 폴더의 세 모듈이 레퍼런스다.
|
|
5
|
+
|
|
6
|
+
| 폴더 | 기능 | 원본 형태 | 비고 |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| `doc_summary_engine_handoff` | 문서 요약 | FastAPI (`api.py`) | 가장 단순한 레퍼런스 |
|
|
9
|
+
| `spec-matching-0.5.5` | KCS 시방서 매칭 | 단일 스크립트 | SBERT 임베딩 캐시 파일 동봉 |
|
|
10
|
+
| `inspection_ai_engine_handoff` | 시공감리 AI 검측 | FastAPI (`api.py`) | action 분기(멀티 기능) 레퍼런스 |
|
|
11
|
+
|
|
12
|
+
## 핵심 원칙
|
|
13
|
+
|
|
14
|
+
1. **엔진 로직은 건드리지 않는다.** 원본의 비즈니스 로직 파일(`engine.py`, `activities.py` 등)은 그대로 두고,
|
|
15
|
+
`__main__.py` 진입점 파일 **하나만 새로 만든다**. 원본 `api.py`도 삭제하지 않고 참고용으로 남긴다.
|
|
16
|
+
2. **보안**: OpenAI 등 API 키는 코드/클라이언트에 절대 넣지 않는다. runner 모듈의 **env-vars 설정**으로만 주입한다
|
|
17
|
+
(`os.environ.get(...)`, 로컬 개발용으로만 `.env` + dotenv 허용).
|
|
18
|
+
3. 함수/어려운 로직에는 한국어 주석.
|
|
19
|
+
|
|
20
|
+
## `__main__.py` 필수 구조
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
def main(input):
|
|
24
|
+
"""operato-runner가 호출하는 진입점. input은 dict."""
|
|
25
|
+
try:
|
|
26
|
+
# 1) input 언랩: 'input' 키로 몇 겹 감싸져 와도 필수 키가 나올 때까지 벗긴다.
|
|
27
|
+
# 단, runner가 최상위에 주입하는 input_files는 벗긴 뒤에도 유지(setdefault).
|
|
28
|
+
# 2) 파라미터 파싱/검증 (JSON 문자열로 온 배열도 json.loads 허용)
|
|
29
|
+
# 3) 기존 엔진 함수 호출
|
|
30
|
+
return {"status": "success", "result": {...}}
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return {"status": "error", "error": f"처리 중 오류가 발생했습니다: {str(e)}"}
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
# 로컬 테스트: python __main__.py '{"...": ...}' 또는 기본 테스트 입력
|
|
36
|
+
import sys, json
|
|
37
|
+
input_data = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {기본 테스트 입력}
|
|
38
|
+
print(json.dumps(main(input_data), ensure_ascii=False, indent=2))
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 반환 형식 (고정)
|
|
42
|
+
|
|
43
|
+
- 성공: `{"status": "success", ..., "result": {엔진 결과}}`
|
|
44
|
+
- 실패: `{"status": "error", "error": "메시지"}` — 예외를 밖으로 던지지 않는다.
|
|
45
|
+
|
|
46
|
+
### 파일 입력 (사진/문서)
|
|
47
|
+
|
|
48
|
+
runner가 multipart `files`로 업로드된 파일을 `input["input_files"]`로 주입한다:
|
|
49
|
+
`[{file_id, path, filename, content_type, size}]` — `path`를 열어서 읽는다.
|
|
50
|
+
URL 입력도 지원하려면 `image_urls`/`file_urls` + 선택적 `headers`(다운로드용 HTTP 헤더)를 함께 받는다.
|
|
51
|
+
개수/크기 제한은 env로 조절 가능하게 (`..._MAX_IMAGES`, `..._MAX_BYTES` 패턴).
|
|
52
|
+
|
|
53
|
+
### 멀티 기능(엔드포인트가 여러 개였던 경우)
|
|
54
|
+
|
|
55
|
+
원본 FastAPI에 엔드포인트가 여러 개면 `input["action"]`으로 분기한다.
|
|
56
|
+
예: inspection — `action: "plan-shots" | "inspect"`, 생략 시 사진 유무로 기본값 결정.
|
|
57
|
+
|
|
58
|
+
### 기타 관례
|
|
59
|
+
|
|
60
|
+
- `script_dir = os.path.dirname(os.path.abspath(__file__))` 기준으로 동봉 데이터 파일(csv, 캐시 등) 접근.
|
|
61
|
+
- HEIC 등 특수 포맷 지원(`pillow_heif`)이나 dotenv는 `try/except ImportError`로 감싸서 선택 의존성으로.
|
|
62
|
+
- 배포 전 `__pycache__` 삭제 후 폴더 전체를 zip.
|
|
63
|
+
|
|
64
|
+
## 호출 방법 (WAS 클라이언트 쪽)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
POST {runner-host}/api/run/{module-name}
|
|
68
|
+
Content-Type: multipart/form-data
|
|
69
|
+
input: JSON 문자열 (파라미터)
|
|
70
|
+
files: 업로드 파일들 (선택)
|
|
71
|
+
Authorization: Basic (runner 계정)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
응답의 `result` 키(중첩돼 있으면 `raw?.result`)를 언랩해서 사용한다.
|
|
75
|
+
|
|
76
|
+
## 배포 체크리스트
|
|
77
|
+
|
|
78
|
+
1. `__pycache__` 삭제 → 폴더 zip
|
|
79
|
+
2. runner에 모듈 업로드/활성화 (모듈명 예: `inspection-ai`, `doc-summary`)
|
|
80
|
+
3. 모듈 env-vars에 `OPENAI_API_KEY` 등 비밀키 등록
|
|
81
|
+
4. `python __main__.py`(기본 테스트 입력)로 로컬 스모크 테스트 먼저
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 필수: OpenAI API 키
|
|
2
|
+
OPENAI_API_KEY=sk-...
|
|
3
|
+
|
|
4
|
+
# 사용할 모델
|
|
5
|
+
OPENAI_MODEL=gpt-5.5
|
|
6
|
+
|
|
7
|
+
# 브리핑 reasoning 강도 (none|low|medium|high|xhigh)
|
|
8
|
+
# - 한 줄 브리핑은 충실한 압축이 목적 → low 권장 (속도·비용↓, 환각 여지↓).
|
|
9
|
+
# - reasoning 미지원 모델이면 자동 무시.
|
|
10
|
+
DOC_BRIEF_REASONING_EFFORT=low
|
|
11
|
+
|
|
12
|
+
# ── HTTP API (api.py) ─────────────────────────────────────────────
|
|
13
|
+
DOC_API_PORT=8100
|
|
14
|
+
DOC_API_MAX_BYTES=31457280 # 파일 1개 최대 크기 (30MB)
|
|
15
|
+
DOC_API_ALLOWED_ORIGINS=* # 운영에선 플랫폼 도메인 명시 권장
|
|
16
|
+
|
|
17
|
+
# ── 추출/요약 튜닝 (보통 기본값으로 충분) ─────────────────────────
|
|
18
|
+
# DOC_MIN_TEXT_CHARS=20 # 이보다 본문이 짧으면 브리핑 빈칸
|
|
19
|
+
# DOC_MAX_XLSX_ROWS=200 # 엑셀 시트당 읽을 최대 행
|
|
20
|
+
# DOC_SINGLE_PASS_CHARS=12000 # 이보다 길면 map-reduce
|
|
21
|
+
# DOC_CHUNK_CHARS=8000 # map 단계 청크 크기
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# 문서 AI 브리핑 엔진 — 인계 문서
|
|
2
|
+
|
|
3
|
+
> 받는 분: 시공감리 플랫폼을 개발하시는 분께.
|
|
4
|
+
> 「현장 문서 확인」 의 문서 상세에 **한 줄 "✨ AI 브리핑"** 을 채우기 위한 엔진입니다.
|
|
5
|
+
> 이 문서 하나로 통합에 필요한 정보 다 들어 있습니다.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 이 엔진이 무엇을 하는가
|
|
10
|
+
|
|
11
|
+
플랫폼에 등록된 문서(HWP/PDF/Excel 등) 파일 하나를 받아, 본문을 읽고
|
|
12
|
+
**감리자·시공자가 클릭 한 번에 내용을 파악할 수 있는 한 줄 브리핑**을 돌려주는 HTTP API.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
[현장 문서 확인] [이 엔진]
|
|
16
|
+
문서 업로드
|
|
17
|
+
│
|
|
18
|
+
└─ POST /brief-doc ───────────────▶ ① 파일 종류 판별 (hwp/hwpx/pdf/xlsx/docx/pptx)
|
|
19
|
+
(multipart) ② 본문 텍스트 추출
|
|
20
|
+
file: <문서파일> ③ LLM 으로 한 줄 브리핑 생성
|
|
21
|
+
category: "사업관리 · …" ④ { briefing: "...", ... } 반환
|
|
22
|
+
◀ { briefing, doc_type, ... }
|
|
23
|
+
→ 플랫폼이 briefing 을 DB 에 저장
|
|
24
|
+
→ 문서 클릭 시 "✨ AI 브리핑" 칸에 표시
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
핵심 특성:
|
|
28
|
+
- **문서종류 무관** — 공문·보고서·일지·계획표·대장 등 분류 전체를 한 파이프라인으로.
|
|
29
|
+
- **Stateless** — DB·세션 없음. 브리핑 저장은 플랫폼 측.
|
|
30
|
+
- **환각 최소화가 1순위** — 본문에 있는 내용만 사용. 텍스트를 못 읽는 문서(스캔 PDF·도면·사진·DRM hwp)는 **억지로 지어내지 않고 `briefing: null`** 로 반환.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. 빠른 시작 (10분)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd doc_summary_engine
|
|
38
|
+
pip install -r requirements.txt
|
|
39
|
+
|
|
40
|
+
# .env 만들기 (.env.example 복사 후 OPENAI_API_KEY 채우기)
|
|
41
|
+
cp .env.example .env
|
|
42
|
+
|
|
43
|
+
python api.py
|
|
44
|
+
# → http://localhost:8100
|
|
45
|
+
# → Swagger UI: http://localhost:8100/docs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
검증:
|
|
49
|
+
```bash
|
|
50
|
+
curl http://localhost:8100/health
|
|
51
|
+
|
|
52
|
+
curl -X POST http://localhost:8100/brief-doc \
|
|
53
|
+
-F "file=@문서명 04.hwp" \
|
|
54
|
+
-F "category=사업관리 · 공사작업일보 · 민원처리보고"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> `pyhwp` 설치가 환경에 따라 까다로울 수 있습니다(§7 참고). PDF/XLSX 먼저 검증하셔도 됩니다.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. 통합 지점 — 「현장 문서 확인」
|
|
62
|
+
|
|
63
|
+
문서 상세 영역의 **"✨ AI 브리핑"** 칸(현재 빈칸)을 채우는 한 곳만 연결하면 됩니다.
|
|
64
|
+
|
|
65
|
+
| 시점 | 동작 | API |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| **문서 업로드 시 (권장)** | 파일을 엔진에 1회 보내 `briefing` 을 받아 **문서 레코드에 저장** | `POST /brief-doc` |
|
|
68
|
+
| 문서 클릭(상세 열기) | 저장된 `briefing` 을 "✨ AI 브리핑" 칸에 표시 (재호출 X) | — |
|
|
69
|
+
| `briefing` 이 `null` 인 경우 | 칸을 비우거나 "본문을 읽을 수 없는 문서" 안내 (`note` 참고) | — |
|
|
70
|
+
|
|
71
|
+
> **왜 업로드 시 1회인가:** 엔진은 stateless 라 클릭마다 호출하면 매번 LLM 비용·지연이
|
|
72
|
+
> 발생합니다. 업로드 때 한 번 생성해 저장하면 클릭은 즉시·무료. 재생성이 필요하면
|
|
73
|
+
> (문서 교체 등) 그때만 다시 호출.
|
|
74
|
+
|
|
75
|
+
### 권장 배치 구조 (보안)
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
[브라우저/앱] ── HTTPS ──▶ [플랫폼 백엔드] ── HTTP(내부망) ──▶ [이 엔진] ── OpenAI
|
|
79
|
+
│ │
|
|
80
|
+
│ 인증/로깅/브리핑 DB 저장 │ OpenAI API 호출
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**OpenAI 키를 프론트엔드에 넣지 마세요.** 반드시 플랫폼 백엔드에서 이 엔진으로 프록시.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. HTTP API 상세
|
|
88
|
+
|
|
89
|
+
서버 기본 `http://localhost:8100` (Swagger: `/docs`).
|
|
90
|
+
|
|
91
|
+
### GET /health
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"ok": true,
|
|
96
|
+
"server_time": 1716700000.1,
|
|
97
|
+
"supported_formats": ["hwp", "hwpx", "pdf", "xlsx", "docx", "pptx"],
|
|
98
|
+
"model": "gpt-5.5"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### POST /brief-doc
|
|
103
|
+
|
|
104
|
+
문서 1개 → 한 줄 브리핑. **multipart/form-data**.
|
|
105
|
+
|
|
106
|
+
| 필드 | 타입 | 필수 | 설명 |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `file` | file | ✅ | 문서 파일 1개. HWP(5.x)/HWPX/PDF/XLSX/DOCX/PPTX. |
|
|
109
|
+
| `category` | text | ❌ | 분류 문자열(대/중/소분류). 근거 힌트. 예: `"사업관리 · 공사작업일보 · 민원처리보고"`. |
|
|
110
|
+
| `filename` | text | ❌ | 파일명 override. 없으면 업로드 파일명 사용. |
|
|
111
|
+
|
|
112
|
+
**응답 (200):**
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"ok": true,
|
|
116
|
+
"filename": "문서명 04.hwp",
|
|
117
|
+
"kind": "hwp",
|
|
118
|
+
"char_count": 812,
|
|
119
|
+
"briefing": "OO 시행사의 PF 조달 및 분양가 갈등에 따른 공사 중지 지시(20.03.18)와 (주)OO건설의 현장 관리 계획 회신·사업 협의에 대한 공문입니다.",
|
|
120
|
+
"doc_type": "공문",
|
|
121
|
+
"has_content": true,
|
|
122
|
+
"note": "",
|
|
123
|
+
"model": "gpt-5.5",
|
|
124
|
+
"elapsed_sec": 2.1
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**필드 의미:**
|
|
129
|
+
- `briefing`: 화면 "✨ AI 브리핑" 칸에 그대로 표시. **`null` 이면 본문을 읽지 못한 것** → 칸 비움.
|
|
130
|
+
- `doc_type`: 본문 근거로 추정한 문서 종류(공문/보고서/회의록/계획서/기타). 표시는 선택.
|
|
131
|
+
- `has_content`: 브리핑을 실제로 생성했는지 (`briefing != null` 과 동일 취지).
|
|
132
|
+
- `note`: `briefing` 이 `null` 일 때 사유(사람용). 예: `"이미지 문서 (사진) — 텍스트 본문 없음"`, `"본문 텍스트가 거의 없음 (스캔 이미지/빈 문서 가능성)"`, `"지원하지 않는 형식(.hwp 3.x …)"`.
|
|
133
|
+
- `kind`: 판별된 파일 종류. `char_count`: 추출된 본문 글자 수(공백 제외).
|
|
134
|
+
|
|
135
|
+
> **항상 200 입니다.** 읽을 수 없는 문서도 에러가 아니라 `briefing: null` + `note` 로 정상 반환합니다 (UI 분기 단순화).
|
|
136
|
+
|
|
137
|
+
**에러 응답:**
|
|
138
|
+
- 400 — 빈 파일.
|
|
139
|
+
- 413 — 파일 크기 초과(`DOC_API_MAX_BYTES`, 기본 30MB).
|
|
140
|
+
- 500 — OpenAI 클라이언트 초기화 실패(`OPENAI_API_KEY` 누락 등).
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 5. 호출 예제
|
|
145
|
+
|
|
146
|
+
### JavaScript (업로드 직후 브리핑 받아 저장)
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
async function makeBriefing(fileBlob, fileName, categoryLabel) {
|
|
150
|
+
const fd = new FormData();
|
|
151
|
+
fd.append("file", fileBlob, fileName);
|
|
152
|
+
fd.append("category", categoryLabel ?? "");
|
|
153
|
+
|
|
154
|
+
const res = await fetch("http://engine:8100/brief-doc", { method: "POST", body: fd });
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
|
|
157
|
+
// data.briefing 을 문서 레코드에 저장.
|
|
158
|
+
// null 이면 빈칸 (data.note 에 사유).
|
|
159
|
+
return data.briefing; // 문서 클릭 시 "✨ AI 브리핑" 칸에 이 값 표시
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Python (서버측 프록시)
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import requests
|
|
167
|
+
|
|
168
|
+
with open(path, "rb") as f:
|
|
169
|
+
resp = requests.post(
|
|
170
|
+
"http://engine:8100/brief-doc",
|
|
171
|
+
files={"file": (file_name, f)},
|
|
172
|
+
data={"category": "사업관리 · 공사작업일보 · 민원처리보고"},
|
|
173
|
+
timeout=120,
|
|
174
|
+
)
|
|
175
|
+
briefing = resp.json()["briefing"] # None 이면 빈칸
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### curl
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
curl -X POST http://localhost:8100/brief-doc \
|
|
182
|
+
-F "file=@report.pdf" \
|
|
183
|
+
-F "category=공정관리 · 공정관리계획 · 주간공정현황보고"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. 환각(없는 내용 지어내기) 방지 설계
|
|
189
|
+
|
|
190
|
+
플랫폼 화면에 잘못된 한 줄이 뜨면 신뢰를 크게 해치므로, 다음 3중으로 막습니다:
|
|
191
|
+
|
|
192
|
+
1. **추출 본문에만 근거** — 프롬프트가 "본문에 없는 사실·배경·추측은 절대 쓰지 마라" 를 명시. 분류명(category)을 근거로 내용을 지어내는 것도 금지(힌트로만).
|
|
193
|
+
2. **못 읽으면 차라리 빈칸** — 텍스트가 빈약하거나(스캔·이미지) 형식을 못 읽으면(DRM hwp 등) **LLM 을 아예 호출하지 않고** `briefing: null` 반환. 비용도 0.
|
|
194
|
+
3. **모델 자체 거부 허용** — 본문이 있어도 요지를 확정 못 하면 모델이 `briefing: null` 을 내도록 허용(억지 요약 금지).
|
|
195
|
+
|
|
196
|
+
> 그래도 LLM 요약은 100% 정확을 보장하지 않습니다. 화면에 "AI 자동 생성 — 참고용"
|
|
197
|
+
> 정도의 라벨을 같이 두는 것을 권장합니다.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 7. 파일 구성 & HWP 관련 주의
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
doc_summary_engine/
|
|
205
|
+
├── api.py # HTTP API (FastAPI) — 진입점. POST /brief-doc
|
|
206
|
+
├── extract.py # 파일 → 본문 텍스트 (hwp/hwpx/pdf/xlsx/docx/pptx)
|
|
207
|
+
├── summarize.py # 본문 → 한 줄 브리핑 (OpenAI, 긴 문서 map-reduce)
|
|
208
|
+
├── requirements.txt
|
|
209
|
+
├── .env.example # (실제 .env 는 키 포함 — 별도 전달, 커밋 금지)
|
|
210
|
+
├── .gitignore
|
|
211
|
+
├── HANDOFF.md # 이 문서
|
|
212
|
+
└── README.md
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**`.hwp` (한글 5.x) 텍스트 추출 = `pyhwp`:**
|
|
216
|
+
- 한컴 오피스 설치 없이 순수 파이썬으로 본문 추출. 공문·보고서처럼 글 위주 문서에 충분.
|
|
217
|
+
- `pip install pyhwp` 가 환경에 따라 빌드 이슈가 있을 수 있음 → 설치 안 되면
|
|
218
|
+
엔진은 `hwp5txt` CLI 폴백도 시도. 둘 다 안 되면 그 문서만 `briefing: null`(다른 포맷은 정상).
|
|
219
|
+
- **못 읽는 경우 (정상적으로 빈칸 처리):** HWPX 가 아닌 신형 일부, HWP 3.x, DRM/암호 hwp, 스캔 PDF, 사진·도면(jpg/png/dwg). → `note` 에 사유.
|
|
220
|
+
- 표·그림 레이아웃은 손실되지만 한 줄 브리핑에는 영향 적음.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 8. 환경변수
|
|
225
|
+
|
|
226
|
+
| 변수 | 기본값 | 의미 |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `OPENAI_API_KEY` | (필수) | OpenAI 키. |
|
|
229
|
+
| `OPENAI_MODEL` | `gpt-5.5` | 요약 모델. |
|
|
230
|
+
| `DOC_BRIEF_REASONING_EFFORT` | `low` | `none\|low\|medium\|high\|xhigh`. 브리핑은 low 권장. |
|
|
231
|
+
| `DOC_API_PORT` | `8100` | HTTP 서버 포트. (검측 엔진 8000 과 분리) |
|
|
232
|
+
| `DOC_API_MAX_BYTES` | `31457280` | 파일 1개 최대 크기(30MB). |
|
|
233
|
+
| `DOC_API_ALLOWED_ORIGINS` | `*` | CORS. 운영에선 도메인 명시 권장. |
|
|
234
|
+
| `DOC_MIN_TEXT_CHARS` | `20` | 본문이 이보다 짧으면 빈칸. |
|
|
235
|
+
| `DOC_SINGLE_PASS_CHARS` | `12000` | 이보다 길면 map-reduce 요약. |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 9. 성능/비용 메모
|
|
240
|
+
|
|
241
|
+
- 한 문서 = LLM 호출 **1회**(짧은 문서) ~ **여러 회**(긴 문서 map-reduce). 보통 1~5초.
|
|
242
|
+
- **업로드 시 1회 생성·저장** 구조면 클릭 시 추가 호출 0 → 사용자 체감 즉시.
|
|
243
|
+
- 못 읽는 문서는 LLM 호출 0(비용 0).
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 10. 자주 받는 질문
|
|
248
|
+
|
|
249
|
+
**Q: 여러 문서를 한 번에 보낼 수 있나요?**
|
|
250
|
+
A: 현재는 1개씩(`/brief-doc`). 배치(`/brief-docs`)가 필요하면 추가 가능 — 요청 주세요.
|
|
251
|
+
|
|
252
|
+
**Q: 브리핑을 더 길게/짧게, 또는 정해진 양식으로?**
|
|
253
|
+
A: 문구·길이·항목은 `summarize.py` 의 프롬프트 한 곳에서 조정. 원하는 형태 알려주시면 맞춥니다.
|
|
254
|
+
|
|
255
|
+
**Q: 이미지(사진)·도면도 브리핑 되나요?**
|
|
256
|
+
A: 현재는 텍스트 문서만. 사진·도면은 `briefing: null`. 이미지 브리핑은 Vision 모델이 필요한 별개 작업 — 필요 시 검측 엔진(VLM)과 연계해 추가 가능.
|
|
257
|
+
|
|
258
|
+
**Q: 파일을 base64 JSON 으로 보낼 수 있나요?**
|
|
259
|
+
A: 현재 multipart 만. base64 JSON 도 받게 추가 가능 — 요청 주세요.
|
|
260
|
+
|
|
261
|
+
**Q: 인증/권한은?**
|
|
262
|
+
A: 엔진 자체엔 없음. 플랫폼 백엔드/게이트웨이에서 처리. CORS 만 주의.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
문의: (보내실 분 정보)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 문서 AI 브리핑 엔진
|
|
2
|
+
|
|
3
|
+
시공감리 플랫폼 「현장 문서 확인」 의 문서를 받아, 문서 상세의 **한 줄 "✨ AI 브리핑"** 을
|
|
4
|
+
채워주는 HTTP API.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
[현장 문서 확인] [엔진]
|
|
8
|
+
문서 업로드
|
|
9
|
+
│
|
|
10
|
+
└─ POST /brief-doc ──▶ hwp/hwpx/pdf/xlsx/docx/pptx 본문 추출
|
|
11
|
+
→ LLM 한 줄 브리핑
|
|
12
|
+
→ { briefing: "...", ... }
|
|
13
|
+
→ 플랫폼이 저장, 클릭 시 "✨ AI 브리핑" 칸에 표시
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
특징:
|
|
17
|
+
- **문서종류 무관** — 공문·보고서·일지·계획표·대장 등 분류 전체를 한 파이프라인으로
|
|
18
|
+
- **Stateless** — 저장은 플랫폼 측 (업로드 시 1회 생성·저장 권장)
|
|
19
|
+
- **환각 최소화** — 본문에 있는 내용만. 못 읽는 문서(스캔·도면·사진·DRM)는 `briefing: null`
|
|
20
|
+
|
|
21
|
+
상세 통합 가이드는 **[HANDOFF.md](HANDOFF.md)** 참고.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 빠른 시작
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -r requirements.txt
|
|
29
|
+
cp .env.example .env # OPENAI_API_KEY 채우기
|
|
30
|
+
python api.py
|
|
31
|
+
# → http://localhost:8100 (Swagger: /docs)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -X POST http://localhost:8100/brief-doc \
|
|
36
|
+
-F "file=@report.pdf" \
|
|
37
|
+
-F "category=공정관리 · 공정관리계획 · 주간공정현황보고"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
기본 모델 `gpt-5.5`. `.env` 의 `OPENAI_MODEL`/`DOC_BRIEF_REASONING_EFFORT` 로 변경.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 파일 구성
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
doc_summary_engine/
|
|
48
|
+
├── api.py # HTTP API (FastAPI) — POST /brief-doc
|
|
49
|
+
├── extract.py # 파일 → 본문 텍스트 (hwp/hwpx/pdf/xlsx/docx/pptx)
|
|
50
|
+
├── summarize.py # 본문 → 한 줄 브리핑 (OpenAI, 긴 문서 map-reduce)
|
|
51
|
+
├── requirements.txt
|
|
52
|
+
├── .env / .env.example
|
|
53
|
+
├── HANDOFF.md # 통합 가이드 (이거 보세요)
|
|
54
|
+
└── README.md # 이 파일
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 지원 포맷
|
|
60
|
+
|
|
61
|
+
| 포맷 | 추출 방법 |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `.hwp` (한글 5.x) | pyhwp (한컴 설치 불필요) |
|
|
64
|
+
| `.hwpx` | zip + XML (표준 라이브러리) |
|
|
65
|
+
| `.pdf` | PyMuPDF |
|
|
66
|
+
| `.xlsx` | openpyxl (시트 → 표) |
|
|
67
|
+
| `.docx` | python-docx |
|
|
68
|
+
| `.pptx` | python-pptx (슬라이드 텍스트 + 노트) |
|
|
69
|
+
|
|
70
|
+
스캔 PDF·사진(jpg/png)·도면(dwg/dxf)·DRM hwp 등 텍스트를 읽을 수 없는 문서는
|
|
71
|
+
`briefing: null` + `note`(사유)로 정상 반환합니다.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""문서 AI 브리핑 — operato-runner 진입점.
|
|
2
|
+
|
|
3
|
+
문서 파일 1개(URL 또는 로컬 경로)를 받아 본문을 추출하고 한 줄 AI 브리핑을 반환한다.
|
|
4
|
+
텍스트를 못 읽는 문서(스캔·도면·사진·DRM·미지원 형식)는 briefing=null 로 정상 응답 (환각 방지).
|
|
5
|
+
|
|
6
|
+
지원 포맷: HWP(5.x) / HWPX / PDF / XLSX / DOCX / PPTX
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import urllib.request
|
|
11
|
+
from urllib.parse import urlparse, unquote
|
|
12
|
+
|
|
13
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
load_dotenv(os.path.join(script_dir, ".env"))
|
|
18
|
+
except ImportError:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
from extract import extract_text
|
|
22
|
+
from summarize import brief_document
|
|
23
|
+
|
|
24
|
+
MAX_BYTES = int(os.environ.get("DOC_API_MAX_BYTES", str(30 * 1024 * 1024))) # 30MB
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _download(url: str, headers: dict = None) -> bytes:
|
|
28
|
+
# 문서 파일을 URL에서 다운로드하여 바이트로 반환
|
|
29
|
+
req = urllib.request.Request(url, headers=headers or {"User-Agent": "doc-summary/1.0"})
|
|
30
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
31
|
+
return resp.read()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _filename_from_url(url: str) -> str:
|
|
35
|
+
# URL 경로의 마지막 세그먼트를 파일명으로 사용 (확장자 판별용)
|
|
36
|
+
return unquote(os.path.basename(urlparse(url).path))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(input):
|
|
40
|
+
"""
|
|
41
|
+
operato-runner에서 호출되는 메인 함수.
|
|
42
|
+
|
|
43
|
+
문서 1건을 받아 본문 텍스트를 추출하고 OpenAI로 한 줄 브리핑을 생성한다.
|
|
44
|
+
본문을 읽을 수 없는 문서는 briefing=null 로 정상 응답한다 (내용을 지어내지 않음).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
input (dict):
|
|
48
|
+
- 'file_url' (str): 문서 파일 URL (file_path 없으면 필수)
|
|
49
|
+
- 'file_path' (str, 선택): 로컬 파일 경로 (테스트/공유 볼륨용, file_url보다 우선)
|
|
50
|
+
- 'filename' (str, 선택): 파일명 — 확장자 판별용. 없으면 URL/경로에서 유추
|
|
51
|
+
- 'category' (str, 선택): 문서 분류(대/중/소분류) 문자열. LLM 근거 힌트
|
|
52
|
+
- 'headers' (dict, 선택): 파일 다운로드 시 HTTP 헤더(인증 등)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict: {"status": "success", "result": {filename, kind, char_count,
|
|
56
|
+
briefing, doc_type, has_content, note, model, elapsed_sec}}
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
raw_input = input # 원본(감싸진 형태 포함) 보존
|
|
60
|
+
# input 키로 몇 겹 감싸져 들어와도 파일 정보가 나올 때까지 벗김
|
|
61
|
+
while (
|
|
62
|
+
isinstance(input, dict)
|
|
63
|
+
and not any(k in input for k in ("file_url", "file_path", "documentPath", "input_files"))
|
|
64
|
+
and isinstance(input.get("input"), dict)
|
|
65
|
+
):
|
|
66
|
+
input = input["input"]
|
|
67
|
+
|
|
68
|
+
file_path = (input.get("file_path") or "").strip()
|
|
69
|
+
file_url = (input.get("file_url") or input.get("documentPath") or "").strip()
|
|
70
|
+
filename = (input.get("filename") or input.get("documentName") or "").strip()
|
|
71
|
+
category = (input.get("category") or "").strip()
|
|
72
|
+
headers = input.get("headers") or None
|
|
73
|
+
|
|
74
|
+
# operato-runner 파일 업로드 실행 지원: 업로드 파일은 input_files로 주입됨
|
|
75
|
+
# [{"file_id", "path", "filename", "content_type", "size"}, ...] — 첫 번째 파일 사용
|
|
76
|
+
input_files = input.get("input_files") or []
|
|
77
|
+
if not file_path and input_files:
|
|
78
|
+
file_path = (input_files[0].get("path") or "").strip()
|
|
79
|
+
filename = filename or (input_files[0].get("filename") or "").strip()
|
|
80
|
+
|
|
81
|
+
# 1) 파일 바이트 확보 (로컬 경로 우선, 없으면 URL 다운로드)
|
|
82
|
+
if file_path:
|
|
83
|
+
filename = filename or os.path.basename(file_path)
|
|
84
|
+
with open(file_path, "rb") as f:
|
|
85
|
+
data = f.read()
|
|
86
|
+
elif file_url.lower().startswith(("http://", "https://")):
|
|
87
|
+
filename = filename or _filename_from_url(file_url)
|
|
88
|
+
data = _download(file_url, headers=headers)
|
|
89
|
+
else:
|
|
90
|
+
return {
|
|
91
|
+
"status": "error",
|
|
92
|
+
"error": "file_url(또는 file_path)이 제공되지 않았습니다.",
|
|
93
|
+
"received_type": type(raw_input).__name__,
|
|
94
|
+
"received_keys": list(raw_input.keys()) if isinstance(raw_input, dict) else None,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if not data:
|
|
98
|
+
return {"status": "error", "error": "빈 파일입니다."}
|
|
99
|
+
if len(data) > MAX_BYTES:
|
|
100
|
+
return {"status": "error", "error": f"파일 크기 초과 ({len(data)} > {MAX_BYTES} bytes)."}
|
|
101
|
+
|
|
102
|
+
model = os.environ.get("OPENAI_MODEL", "gpt-5.5")
|
|
103
|
+
|
|
104
|
+
# 2) 본문 추출 — 못 읽으면 LLM 호출 없이 브리핑 빈칸으로 정상 응답 (환각 방지 + 비용 0)
|
|
105
|
+
ex = extract_text(data, filename)
|
|
106
|
+
if not ex.ok:
|
|
107
|
+
return {
|
|
108
|
+
"status": "success",
|
|
109
|
+
"result": {
|
|
110
|
+
"filename": filename,
|
|
111
|
+
"kind": ex.kind,
|
|
112
|
+
"char_count": ex.char_count,
|
|
113
|
+
"briefing": None,
|
|
114
|
+
"doc_type": "기타",
|
|
115
|
+
"has_content": False,
|
|
116
|
+
"note": ex.reason,
|
|
117
|
+
"model": model,
|
|
118
|
+
"elapsed_sec": 0.0,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# 3) 한 줄 브리핑 (OpenAI)
|
|
123
|
+
r = brief_document(ex.text, filename=filename, category=category)
|
|
124
|
+
return {
|
|
125
|
+
"status": "success",
|
|
126
|
+
"result": {
|
|
127
|
+
"filename": filename,
|
|
128
|
+
"kind": ex.kind,
|
|
129
|
+
"char_count": ex.char_count,
|
|
130
|
+
"briefing": r.briefing,
|
|
131
|
+
"doc_type": r.doc_type,
|
|
132
|
+
"has_content": r.has_content,
|
|
133
|
+
"note": r.note,
|
|
134
|
+
"model": r.model,
|
|
135
|
+
"elapsed_sec": r.elapsed_sec,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return {
|
|
141
|
+
"status": "error",
|
|
142
|
+
"error": f"처리 중 오류가 발생했습니다: {str(e)}",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
import sys
|
|
148
|
+
if len(sys.argv) > 1:
|
|
149
|
+
# 명령행 인자로 JSON 입력 받기
|
|
150
|
+
input_data = json.loads(sys.argv[1])
|
|
151
|
+
else:
|
|
152
|
+
# 기본 테스트 입력 (file_url을 지정해야 동작)
|
|
153
|
+
input_data = {
|
|
154
|
+
"file_url": "",
|
|
155
|
+
"category": "사업관리 · 공사작업일보 · 민원처리보고",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
result = main(input_data)
|
|
159
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|