feat(ds-app): freeze S1 contract + S2 AIProvider interface baseline
S1 = contract/CONTRACT.md + 14 fixtures + README + AI-ROUTING.
S2 = Sources/AI/{AIProvider,AIRouter,MockAIProvider} + Providers skeletons.
Baseline before S3 (device app) scaffold work begins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Swift / SwiftPM
|
||||
.build/
|
||||
.swiftpm/
|
||||
Package.resolved
|
||||
|
||||
# Xcode
|
||||
DerivedData/
|
||||
build/
|
||||
*.xcodeproj/project.xcworkspace/xcuserdata/
|
||||
*.xcodeproj/xcuserdata/
|
||||
*.xcworkspace/xcuserdata/
|
||||
*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
xcuserdata/
|
||||
*.moved-aside
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
# Misc
|
||||
*.swp
|
||||
@@ -0,0 +1,143 @@
|
||||
// AIProvider.swift — S2 인터페이스 동결 (멀티디바이스 LLM 라우팅 패브릭)
|
||||
//
|
||||
// 이 파일이 앱(S3)이 코딩하는 "AI 호출 모양"을 동결한다.
|
||||
// S3 는 이 프로토콜 + MockAIProvider 로 빌드하고, S2 가 실제 provider
|
||||
// (OnDevice / LocalMLX / RemoteDS / Specialized)를 그 뒤에서 채운다.
|
||||
//
|
||||
// Foundation 외 의존 0 → 표준 출발선. 실제 구현(FoundationModels / MLX / URLSession)은
|
||||
// Providers/ 의 각 스켈레톤에서 S2 가 결선.
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Provider 식별 (디바이스 역할 = 인프라 패브릭과 1:1)
|
||||
|
||||
public enum AIProviderID: String, Codable, Sendable, CaseIterable {
|
||||
/// 맥북·아이폰 온디바이스 (Apple Foundation Models). 즉답·오프라인·프라이버시.
|
||||
case onDevice
|
||||
/// 맥미니 메인 로컬 LLM 허브 (Gemma 4 26B, MLX :8801 / llm-router :8890). 무거운 생성.
|
||||
case localMLX
|
||||
/// 원격 DS 코퍼스 RAG (`GET /search/ask`). 전체 지식베이스 질의 — 온디바이스 불가 영역.
|
||||
case remoteDS
|
||||
/// GPU 특화 통로 (rerank / embed / vision / OCR). 온디맨드.
|
||||
case specialized
|
||||
}
|
||||
|
||||
// MARK: - 태스크 분류 (앱이 AI 에 요청하는 일의 종류 → 라우팅 기준)
|
||||
|
||||
public enum AITask: String, Codable, Sendable {
|
||||
/// 선택 텍스트/문서 빠른 요약.
|
||||
case quickSummarize
|
||||
/// 메모 정리·제목 생성·태그 제안.
|
||||
case memoAssist
|
||||
/// 현재 문서/선택에 대한 질문 (로컬 컨텍스트, 코퍼스 불필요).
|
||||
case askSelection
|
||||
/// 전체 지식베이스 RAG 질의 (코퍼스 필요 → 원격 DS).
|
||||
case corpusAsk
|
||||
/// 문서 분류 (도메인/카테고리 제안).
|
||||
case classify
|
||||
/// 이미지/스캔 PDF 이해 (비전).
|
||||
case vision
|
||||
}
|
||||
|
||||
public enum AIConfidence: String, Codable, Sendable {
|
||||
case high, medium, low
|
||||
}
|
||||
|
||||
public enum AIFinishReason: String, Codable, Sendable {
|
||||
case completed // 정상 완료
|
||||
case refused // 모델이 답변 거부 (근거 부족 등)
|
||||
case timeout // 생성 지연으로 생략
|
||||
case unavailable // provider 사용 불가 (503 계열)
|
||||
case noEvidence // (corpusAsk) 관련 근거 없음
|
||||
}
|
||||
|
||||
// MARK: - 요청 / 응답
|
||||
|
||||
public struct AICompletionRequest: Sendable {
|
||||
public var task: AITask
|
||||
public var prompt: String
|
||||
/// 선택 텍스트·문서 본문 등 로컬 컨텍스트 (corpusAsk 는 보통 nil — 코퍼스가 컨텍스트).
|
||||
public var context: String?
|
||||
public var systemPrompt: String?
|
||||
public var maxTokens: Int?
|
||||
/// 사용자가 명시적으로 고른 provider. 지정 시 **자동 fallback 금지**(명시 opt-in).
|
||||
public var explicitProvider: AIProviderID?
|
||||
|
||||
public init(
|
||||
task: AITask,
|
||||
prompt: String,
|
||||
context: String? = nil,
|
||||
systemPrompt: String? = nil,
|
||||
maxTokens: Int? = nil,
|
||||
explicitProvider: AIProviderID? = nil
|
||||
) {
|
||||
self.task = task
|
||||
self.prompt = prompt
|
||||
self.context = context
|
||||
self.systemPrompt = systemPrompt
|
||||
self.maxTokens = maxTokens
|
||||
self.explicitProvider = explicitProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// corpusAsk 응답의 근거 — DS `Citation`(CONTRACT.md §4)에서 매핑.
|
||||
public struct AICitation: Codable, Sendable, Identifiable {
|
||||
public var id: Int { n }
|
||||
public var n: Int
|
||||
public var docId: Int
|
||||
public var title: String?
|
||||
public var sectionTitle: String?
|
||||
public var spanText: String
|
||||
|
||||
public init(n: Int, docId: Int, title: String?, sectionTitle: String?, spanText: String) {
|
||||
self.n = n
|
||||
self.docId = docId
|
||||
self.title = title
|
||||
self.sectionTitle = sectionTitle
|
||||
self.spanText = spanText
|
||||
}
|
||||
}
|
||||
|
||||
public struct AICompletionResponse: Sendable {
|
||||
public var text: String
|
||||
public var providerUsed: AIProviderID
|
||||
public var finishReason: AIFinishReason
|
||||
public var citations: [AICitation]
|
||||
public var confidence: AIConfidence?
|
||||
public var latencyMs: Double?
|
||||
/// 라우팅 가시성 — rule-based fallback 으로 1차 선택이 빗나갔을 때 채워짐(silent 금지).
|
||||
public var routingNote: String?
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
providerUsed: AIProviderID,
|
||||
finishReason: AIFinishReason = .completed,
|
||||
citations: [AICitation] = [],
|
||||
confidence: AIConfidence? = nil,
|
||||
latencyMs: Double? = nil,
|
||||
routingNote: String? = nil
|
||||
) {
|
||||
self.text = text
|
||||
self.providerUsed = providerUsed
|
||||
self.finishReason = finishReason
|
||||
self.citations = citations
|
||||
self.confidence = confidence
|
||||
self.latencyMs = latencyMs
|
||||
self.routingNote = routingNote
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 프로토콜 (S2 가 구현, S3 가 소비)
|
||||
|
||||
public protocol AIProvider: Sendable {
|
||||
var id: AIProviderID { get }
|
||||
/// 능력 프로브 (온디바이스 Apple Intelligence 가용? 맥미니 도달 가능? 등).
|
||||
var isAvailable: Bool { get async }
|
||||
func complete(_ request: AICompletionRequest) async throws -> AICompletionResponse
|
||||
}
|
||||
|
||||
public enum AIProviderError: Error, Sendable {
|
||||
case notImplemented(AIProviderID)
|
||||
case unavailable(AIProviderID)
|
||||
case backendError(AIProviderID, status: Int, reason: String?)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// AIRouter.swift — 태스크 → provider 라우팅 정책 (3단 fallback: explicit > rule > error)
|
||||
//
|
||||
// 정책 = feedback_task_routing_hybrid + feedback_no_silent_fallback_explicit_opt_in:
|
||||
// 1) 명시 provider 지정 → 그것만, 불가 시 **에러**(자동 fallback 금지).
|
||||
// 2) 미지정 → 태스크별 선호 체인(rule), 정의된 fallback 은 routingNote 로 가시화.
|
||||
// 3) 전부 불가 → noProviderAvailable.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct AIRoutingPolicy: Sendable {
|
||||
/// 태스크별 provider 선호 순서. 앞쪽 우선, 불가/실패 시 다음으로(가시 fallback).
|
||||
public var preference: [AITask: [AIProviderID]]
|
||||
|
||||
public init(preference: [AITask: [AIProviderID]]) {
|
||||
self.preference = preference
|
||||
}
|
||||
|
||||
public func chain(for task: AITask) -> [AIProviderID] {
|
||||
preference[task] ?? [.onDevice, .localMLX, .remoteDS]
|
||||
}
|
||||
|
||||
/// 기본 정책 — 디바이스 역할 표와 일치.
|
||||
/// 빠른/사적 = 온디바이스 우선, 무거운 생성 = 맥미니, 코퍼스 = 원격 DS, 비전 = GPU 특화.
|
||||
public static let `default` = AIRoutingPolicy(preference: [
|
||||
.quickSummarize: [.onDevice, .localMLX],
|
||||
.memoAssist: [.onDevice, .localMLX],
|
||||
.askSelection: [.onDevice, .localMLX, .remoteDS],
|
||||
.corpusAsk: [.remoteDS], // 코퍼스는 원격만 — 온디바이스로 폴백 불가
|
||||
.classify: [.localMLX, .remoteDS, .onDevice],
|
||||
.vision: [.specialized, .onDevice],
|
||||
])
|
||||
}
|
||||
|
||||
public enum AIRoutingError: Error, Sendable {
|
||||
case providerNotConfigured(AIProviderID)
|
||||
case explicitProviderUnavailable(AIProviderID) // 명시 지정인데 불가 → 자동 fallback 안 함
|
||||
case noProviderAvailable(AITask)
|
||||
}
|
||||
|
||||
public struct AIRouter: Sendable {
|
||||
public let providers: [AIProviderID: any AIProvider]
|
||||
public let policy: AIRoutingPolicy
|
||||
/// 라우팅 로그 훅 (silent skip 방지 — 가시 fallback). 기본은 무시.
|
||||
public let log: @Sendable (String) -> Void
|
||||
|
||||
public init(
|
||||
providers: [AIProviderID: any AIProvider],
|
||||
policy: AIRoutingPolicy = .default,
|
||||
log: @escaping @Sendable (String) -> Void = { _ in }
|
||||
) {
|
||||
self.providers = providers
|
||||
self.policy = policy
|
||||
self.log = log
|
||||
}
|
||||
|
||||
public func route(_ request: AICompletionRequest) async throws -> AICompletionResponse {
|
||||
// 1) 명시 opt-in — 자동 fallback 금지.
|
||||
if let explicit = request.explicitProvider {
|
||||
guard let provider = providers[explicit] else {
|
||||
throw AIRoutingError.providerNotConfigured(explicit)
|
||||
}
|
||||
guard await provider.isAvailable else {
|
||||
log("explicit provider \(explicit.rawValue) unavailable — no fallback (opt-in)")
|
||||
throw AIRoutingError.explicitProviderUnavailable(explicit)
|
||||
}
|
||||
return try await provider.complete(request)
|
||||
}
|
||||
|
||||
// 2) rule-based 선호 체인 — 정의된 fallback 은 routingNote 로 가시화.
|
||||
let chain = policy.chain(for: request.task)
|
||||
var attempted: [AIProviderID] = []
|
||||
var lastError: Error?
|
||||
|
||||
for id in chain {
|
||||
guard let provider = providers[id] else { continue }
|
||||
guard await provider.isAvailable else {
|
||||
log("provider \(id.rawValue) unavailable for \(request.task.rawValue) — trying next")
|
||||
attempted.append(id)
|
||||
continue
|
||||
}
|
||||
do {
|
||||
var response = try await provider.complete(request)
|
||||
if !attempted.isEmpty {
|
||||
response.routingNote = "fallback from \(attempted.map(\.rawValue).joined(separator: ",")) → \(id.rawValue)"
|
||||
}
|
||||
return response
|
||||
} catch {
|
||||
log("provider \(id.rawValue) failed for \(request.task.rawValue): \(error) — trying next")
|
||||
attempted.append(id)
|
||||
lastError = error
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 전부 불가.
|
||||
if let lastError { throw lastError }
|
||||
throw AIRoutingError.noProviderAvailable(request.task)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// MockAIProvider.swift — S3 가 실 LLM 없이 빌드/프리뷰하기 위한 mock.
|
||||
//
|
||||
// 어떤 AIProviderID 로도 가장해 태스크별 canned 응답을 반환한다.
|
||||
// S3 SwiftUI 프리뷰 / UI 테스트가 이걸 주입해 AI 흐름을 끝까지 그릴 수 있다.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct MockAIProvider: AIProvider {
|
||||
public let id: AIProviderID
|
||||
public let available: Bool
|
||||
/// 인위적 지연(ms) — 로딩 UI 확인용.
|
||||
public let simulatedLatencyMs: Double
|
||||
|
||||
public init(id: AIProviderID = .onDevice, available: Bool = true, simulatedLatencyMs: Double = 120) {
|
||||
self.id = id
|
||||
self.available = available
|
||||
self.simulatedLatencyMs = simulatedLatencyMs
|
||||
}
|
||||
|
||||
public var isAvailable: Bool {
|
||||
get async { available }
|
||||
}
|
||||
|
||||
public func complete(_ request: AICompletionRequest) async throws -> AICompletionResponse {
|
||||
guard available else { throw AIProviderError.unavailable(id) }
|
||||
|
||||
switch request.task {
|
||||
case .quickSummarize:
|
||||
return AICompletionResponse(
|
||||
text: "요약(mock): 엘보 내경 가공 시 공차 +0.1/-0.0 관리와 최종 치수 검사 기록이 핵심.",
|
||||
providerUsed: id, confidence: .high, latencyMs: simulatedLatencyMs
|
||||
)
|
||||
case .memoAssist:
|
||||
return AICompletionResponse(
|
||||
text: "제목 제안(mock): 엘보 발주 확인 / 태그: 발주, 엘보, 업무",
|
||||
providerUsed: id, confidence: .medium, latencyMs: simulatedLatencyMs
|
||||
)
|
||||
case .askSelection:
|
||||
return AICompletionResponse(
|
||||
text: "선택 구간 답변(mock): UCS-66 면제 곡선은 재료군과 두께로 MDMT 면제를 판정합니다.",
|
||||
providerUsed: id, confidence: .high, latencyMs: simulatedLatencyMs
|
||||
)
|
||||
case .corpusAsk:
|
||||
return AICompletionResponse(
|
||||
text: "충격시험 면제는 UCS-66 면제 곡선으로 판정합니다 [1]. 설계 응력비가 낮으면 UCS-66.1로 MDMT를 더 낮출 수 있습니다 [1].",
|
||||
providerUsed: .remoteDS,
|
||||
citations: [
|
||||
AICitation(n: 1, docId: 4912,
|
||||
title: "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
sectionTitle: "2. UCS-66 면제 곡선",
|
||||
spanText: "재료군과 두께에 따라 MDMT에서의 충격시험 면제 여부를 결정한다.")
|
||||
],
|
||||
confidence: .high, latencyMs: 2840
|
||||
)
|
||||
case .classify:
|
||||
return AICompletionResponse(
|
||||
text: "분류 제안(mock): Engineering / 압력용기",
|
||||
providerUsed: id, confidence: .medium, latencyMs: simulatedLatencyMs
|
||||
)
|
||||
case .vision:
|
||||
return AICompletionResponse(
|
||||
text: "비전(mock): 스캔 도면에서 표제란·치수선·용접기호가 식별됨.",
|
||||
providerUsed: .specialized, confidence: .medium, latencyMs: 900
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// LocalMLXProvider.swift — S2 구현 스켈레톤 (맥미니 메인 로컬 LLM 허브).
|
||||
//
|
||||
// 실제 구현: 맥미니 Gemma 4 26B (MLX) OpenAI 호환 엔드포인트 호출.
|
||||
// - 엔드포인트: llm-router :8890 (권장) 또는 MLX :8801 (Tailscale 100.76.254.116)
|
||||
// - isAvailable = 짧은 health 핑 (도달 + 모델 로드)
|
||||
// - complete = POST /v1/chat/completions (messages: system/user 분리, call-shape 고정)
|
||||
// 인터페이스 동결 단계에서는 스텁.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct LocalMLXProvider: AIProvider {
|
||||
public let id: AIProviderID = .localMLX
|
||||
|
||||
/// 맥미니 허브 베이스 URL (S2 가 설정/Keychain 에서 주입).
|
||||
public let baseURL: URL
|
||||
|
||||
public init(baseURL: URL) {
|
||||
self.baseURL = baseURL
|
||||
}
|
||||
|
||||
public var isAvailable: Bool {
|
||||
get async {
|
||||
// S2: GET /v1/models 또는 경량 health 핑으로 교체.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
public func complete(_ request: AICompletionRequest) async throws -> AICompletionResponse {
|
||||
// S2: OpenAI 호환 chat/completions 호출 → AICompletionResponse(providerUsed: .localMLX).
|
||||
// messages 구조(system/user 분리)는 production 호출과 단일 source-of-truth.
|
||||
throw AIProviderError.notImplemented(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// OnDeviceProvider.swift — S2 구현 스켈레톤 (맥북·아이폰 온디바이스).
|
||||
//
|
||||
// 실제 구현: `import FoundationModels` 후 SystemLanguageModel / LanguageModelSession 사용.
|
||||
// - isAvailable = SystemLanguageModel.default.availability == .available
|
||||
// - complete = LanguageModelSession 으로 prompt 응답 (citations 없음 — 로컬 생성)
|
||||
// 인터페이스 동결 단계에서는 Foundation-only 스텁(notImplemented).
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct OnDeviceProvider: AIProvider {
|
||||
public let id: AIProviderID = .onDevice
|
||||
|
||||
public init() {}
|
||||
|
||||
public var isAvailable: Bool {
|
||||
get async {
|
||||
// S2: FoundationModels 가용성 프로브로 교체.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
public func complete(_ request: AICompletionRequest) async throws -> AICompletionResponse {
|
||||
// S2: LanguageModelSession(.default) 호출 → AICompletionResponse(providerUsed: .onDevice).
|
||||
throw AIProviderError.notImplemented(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// RemoteDSProvider.swift — S2 구현 스켈레톤 (원격 DS 코퍼스 RAG).
|
||||
//
|
||||
// 이 provider 가 S1 계약과 만나는 다리:
|
||||
// complete(corpusAsk) → GET /search/ask?q=&backend= (CONTRACT.md §4, AskResponse)
|
||||
// AskResponse.citations → [AICitation] 매핑
|
||||
// AskResponse.synthesis_status → AIFinishReason
|
||||
// AskResponse.backend_used → routingNote (어느 LLM 이 응답했는지)
|
||||
// backend 인자: nil(=mac-mini-default) 또는 explicitProvider 매핑(localMLX→gemma-macmini 등).
|
||||
// 인터페이스 동결 단계에서는 스텁(S3 의 DS API client 주입 후 S2 가 결선).
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct RemoteDSProvider: AIProvider {
|
||||
public let id: AIProviderID = .remoteDS
|
||||
|
||||
public init() {}
|
||||
|
||||
public var isAvailable: Bool {
|
||||
get async { true } // 원격 코퍼스는 항상 후보 (실제 도달 실패는 complete 에서 backendError).
|
||||
}
|
||||
|
||||
public func complete(_ request: AICompletionRequest) async throws -> AICompletionResponse {
|
||||
// corpusAsk 외 태스크는 이 provider 의 책임 아님(라우터가 거름).
|
||||
guard request.task == .corpusAsk else {
|
||||
throw AIProviderError.notImplemented(id)
|
||||
}
|
||||
// S2: DS API client.ask(q:) 호출 → AskResponse 디코딩 → 아래 매핑.
|
||||
// let r = try await dsClient.ask(q: request.prompt, backend: mappedBackend(request.explicitProvider))
|
||||
// return Self.map(r)
|
||||
throw AIProviderError.notImplemented(id)
|
||||
}
|
||||
|
||||
/// AskResponse(JSON) → AICompletionResponse 매핑 규칙(고정). S2 가 이 형태로 결선.
|
||||
/// 시그니처만 동결 — 실제 호출은 S3 DS client 와 결합.
|
||||
static func finishReason(fromSynthesisStatus status: String) -> AIFinishReason {
|
||||
switch status {
|
||||
case "completed": return .completed
|
||||
case "timeout": return .timeout
|
||||
case "no_evidence", "skipped": return .noEvidence
|
||||
case "backend_unavailable": return .unavailable
|
||||
default: return .refused
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# DS App AI 라우팅 계약 (S2 인터페이스 동결) — v0.1
|
||||
|
||||
S1(데이터 fixture)이 앱의 **데이터 모양**을 동결했듯, 이 문서 + `Sources/AI/`는 앱의 **AI 호출 모양**을 동결한다.
|
||||
S3 는 `AIProvider` 프로토콜 + `MockAIProvider` 로 AI 흐름을 끝까지 그리고, S2 가 실 provider 를 뒤에서 채운다.
|
||||
|
||||
- **Frozen**: 2026-06-04 · **typecheck PASS** (swift 6 strict concurrency) · **router 스모크 PASS**
|
||||
- **위치**: `Sources/AI/{AIProvider,AIRouter,MockAIProvider}.swift` + `Sources/AI/Providers/*.swift`
|
||||
- **파일 경계**: 이 `AI/` 디렉토리 = **S2 소유**(앱 나머지 = S3). 같은 Xcode repo 디렉토리 단위 분담 → 충돌 0.
|
||||
|
||||
## 1. Provider 티어 (= 디바이스 역할 표와 1:1)
|
||||
|
||||
| `AIProviderID` | 노드 | 용도 | 구현 경로 |
|
||||
|---|---|---|---|
|
||||
| `.onDevice` | 맥북·아이폰 | 즉답·오프라인·프라이버시 | Apple **FoundationModels** (`SystemLanguageModel`/`LanguageModelSession`) |
|
||||
| `.localMLX` | 맥미니 허브 | 무거운 로컬 생성 | Gemma 4 26B — llm-router `:8890` / MLX `:8801` (OpenAI 호환) |
|
||||
| `.remoteDS` | GPU(원격) | **코퍼스 RAG** | `GET /search/ask?backend=` (CONTRACT.md §4) |
|
||||
| `.specialized` | GPU | rerank·embed·**vision**·OCR | 특화 모델 통로(온디맨드) |
|
||||
|
||||
## 2. 태스크 → 티어 정책 (`AIRoutingPolicy.default`)
|
||||
|
||||
| `AITask` | 선호 체인 | 근거 |
|
||||
|---|---|---|
|
||||
| `quickSummarize` | onDevice → localMLX | 빠르고 사적 |
|
||||
| `memoAssist` | onDevice → localMLX | 짧은 보조 |
|
||||
| `askSelection` | onDevice → localMLX → remoteDS | 로컬 컨텍스트, 부족 시 승급 |
|
||||
| `corpusAsk` | **remoteDS only** | 코퍼스 필요 — 온디바이스로 폴백 불가 |
|
||||
| `classify` | localMLX → remoteDS → onDevice | 분류 품질 우선 |
|
||||
| `vision` | specialized → onDevice | GPU VLM 우선 |
|
||||
|
||||
## 3. 3단 fallback 규칙 (feedback_task_routing_hybrid + no_silent_fallback)
|
||||
|
||||
1. **explicit > rule > error.**
|
||||
2. **명시 opt-in**(`request.explicitProvider`) → 그 provider 만. 불가 시 **에러**(`explicitProviderUnavailable`) — 자동 다른 티어 호출 **금지**.
|
||||
3. **미지정** → 태스크 선호 체인 순회. 불가/실패는 다음으로 넘기되 **`routingNote`로 가시화**(silent skip 금지) + `log` 훅.
|
||||
4. 전부 불가 → `noProviderAvailable`.
|
||||
|
||||
스모크 검증:
|
||||
```
|
||||
quickSummarize → onDevice corpusAsk → remoteDS (citations=1)
|
||||
vision → specialized classify → localMLX
|
||||
explicit onDevice 불가 → 에러(자동 fallback X)
|
||||
rule fallback: onDevice 불가 → localMLX note="fallback from onDevice → localMLX"
|
||||
```
|
||||
|
||||
## 4. S1 계약과의 다리 (`RemoteDSProvider`)
|
||||
|
||||
`corpusAsk` 만 `.remoteDS` 책임. 매핑(고정):
|
||||
```
|
||||
GET /search/ask?q=<prompt>&backend=<map(explicitProvider)> → AskResponse (CONTRACT.md §4)
|
||||
AskResponse.ai_answer → AICompletionResponse.text
|
||||
AskResponse.citations[] → AICitation[] (n, doc_id, title, section_title, span_text)
|
||||
AskResponse.synthesis_status → AIFinishReason (completed/timeout/no_evidence/backend_unavailable/…)
|
||||
AskResponse.confidence → AIConfidence
|
||||
AskResponse.backend_used → routingNote (어느 LLM 이 응답했는지)
|
||||
```
|
||||
`backend` 매핑: `nil`→`mac-mini-default` · `.localMLX`→`gemma-macmini` · (M5 Max Qwen 경로)→`qwen-macbook` · cloud→`claude-cloud`(503, 별 PR).
|
||||
|
||||
## 5. S2 가 다음에 채울 것 (인터페이스는 고정)
|
||||
- `OnDeviceProvider`: `import FoundationModels` 가용성 프로브 + `LanguageModelSession` 호출.
|
||||
- `LocalMLXProvider`: 맥미니 OpenAI 호환 호출(messages system/user 분리 call-shape 고정).
|
||||
- `RemoteDSProvider`: S3 의 DS API client 주입 → 위 §4 매핑 결선.
|
||||
- `SpecializedProvider`: GPU 비전/특화 통로(필요 태스크만).
|
||||
|
||||
## 동시 출발선 (S1 + S2 둘 다 동결 완료)
|
||||
- **S3** = `MockAIProvider` 주입 + S1 `fixtures/` 디코딩 → 실 백엔드·실 LLM 대기 0 으로 앱 전체 빌드.
|
||||
- **S1** = `[S1-ADD]` 구현 + 응답 shape 유지.
|
||||
- **S2** = 위 §5 provider 결선. 인터페이스(`AIProvider`/`AIRouter`) 변경 시만 합의.
|
||||
@@ -0,0 +1,224 @@
|
||||
# DS App ↔ Backend API 계약 (S1 인터페이스 동결)
|
||||
|
||||
> **목적**: 멀티디바이스 DS 앱(S3)이 빌드/프리뷰 시 의존하는 **응답 shape를 동결**한다.
|
||||
> 백엔드 구현(S1)·LLM 라우팅(S2)과 무관하게 이 계약 + `fixtures/*.json`만 보고 앱을 만든다.
|
||||
> **이 계약이 곧 S1·S2·S3 동시 출발선이다.** 변경은 버전 bump + 합의로만.
|
||||
|
||||
- **Contract version**: `v0.1` (frozen 2026-06-04)
|
||||
- **Base URL**: `https://document.hyungi.net/api` (TLS, 공인) · 대안 Tailscale `http://100.110.63.63:8000/api`
|
||||
- **출처**: 실제 GPU 백엔드 Pydantic 응답 모델에서 추출(지어내지 않음). 파일 = `app/api/{documents,search,memos,digest,auth}.py`.
|
||||
- **표기**: `[EXISTING]` = 현재 백엔드가 이미 반환. `[S1-ADD]` = 신규 요구(MD-first 전포맷·중복검사·다운로드 편의)로 **S1이 추가**할 필드/엔드포인트 — 앱은 옵셔널로 디코딩(`?`), 없으면 폴백.
|
||||
|
||||
---
|
||||
|
||||
## 0. 공통 규약
|
||||
|
||||
- **datetime**: ISO-8601 문자열 (`"2026-06-03T08:12:44.120Z"`). Swift `Date` 디코딩 시 ISO8601 + fractional seconds.
|
||||
- **date**: `"2026-06-03"` (date-only).
|
||||
- **null**: 필드 부재 가능 → 앱 모델 전부 옵셔널(`String?`). 위 모델의 `| None` = 옵셔널.
|
||||
- **페이지네이션**: `{ items, total, page, page_size }`. 요청 `?page=1&page_size=20`.
|
||||
- **에러 shape**: `{ "detail": "<메시지>" }` 또는 `{ "detail": { "error_code": "...", "message": "..." } }`. HTTP status로 분기(401/404/422/503).
|
||||
- **인증**: 모든 `/api/*`(auth 제외)는 `Authorization: Bearer <access_token>` 헤더.
|
||||
|
||||
### 인증 흐름 (네이티브)
|
||||
- `POST /api/auth/login {username, password, totp_code?}` → `AccessTokenResponse {access_token, token_type}`.
|
||||
- refresh는 **HttpOnly 쿠키**(`path=/api/auth`)로 내려옴 → `URLSession`의 `HTTPCookieStorage`가 자동 보관, `POST /api/auth/refresh`로 access 재발급 가능(네이티브에서도 동작).
|
||||
- **권장**: access_token은 **Keychain** 보관. 만료 시 refresh → 실패하면 재로그인. (장수명 365d 토큰 옵션도 가능하나 v1은 정식 로그인.)
|
||||
- 로그아웃 `POST /api/auth/logout`, 현재 사용자 `GET /api/auth/me` → `UserResponse`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Auth
|
||||
|
||||
| Method | Path | 요청 | 응답 | fixture |
|
||||
|---|---|---|---|---|
|
||||
| POST | `/auth/login` | `{username, password, totp_code?}` | `AccessTokenResponse` | `auth_login.json` |
|
||||
| POST | `/auth/refresh` | (쿠키) | `AccessTokenResponse` | `auth_login.json` |
|
||||
| GET | `/auth/me` | — | `UserResponse` | `auth_me.json` |
|
||||
| POST | `/auth/logout` | — | `{}` | — |
|
||||
|
||||
```
|
||||
AccessTokenResponse { access_token: String, token_type: String("bearer") } [EXISTING]
|
||||
UserResponse { id: Int, username: String, is_active: Bool, totp_enabled: Bool, last_login_at: Date? } [EXISTING]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Documents (MD-first 뷰 핵심)
|
||||
|
||||
| Method | Path | 요청 | 응답 | fixture |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/documents/` | `page, page_size, domain?, sub_group?, source?, format?, review_status?, category?, has_suggestion?, proposed_category?` | `DocumentListResponse` | `documents_list.json` |
|
||||
| GET | `/documents/{id}` | — | `DocumentDetailResponse` (**md_content 동봉**) | `document_detail.json` |
|
||||
| GET | `/documents/{id}/file` | `?token=<access>&download=true` | **바이너리 원본** (PDF/이미지/오디오/원본) | — |
|
||||
| GET | `/documents/{id}/content` | — | 경량 텍스트(`content` 15k cap) | `document_content.json` |
|
||||
| GET | `/documents/tree` | — | 도메인 트리(사이드바) | `documents_tree.json` |
|
||||
| GET | `/documents/stats/category-counts` | — | 카테고리 카운트 | `documents_stats.json` |
|
||||
| POST | `/documents/` (multipart) | 파일 업로드 | `DocumentResponse` (201) | `document_detail.json` |
|
||||
| PATCH | `/documents/{id}` | `DocumentUpdate` | `DocumentResponse` | — |
|
||||
| PUT | `/documents/{id}/content` | `{content}` (md 편집 저장) | `{}` | — |
|
||||
| POST | `/documents/{id}/accept-suggestion` | `{expected_source_updated_at}` | `DocumentResponse` | — |
|
||||
| DELETE | `/documents/{id}/suggestion` | — | 204 | — |
|
||||
| DELETE | `/documents/{id}` | — | 204 | — |
|
||||
|
||||
### DocumentResponse (리스트 행 — 경량, md 본문 없음) `[EXISTING]`
|
||||
```
|
||||
id: Int
|
||||
file_path: String? file_format: String file_size: Int? file_type: String
|
||||
title: String?
|
||||
ai_domain: String? ai_sub_group: String? ai_tags: [String]? ai_summary: String?
|
||||
document_type: String? importance: String? ai_confidence: Double?
|
||||
user_note: String? user_tags: [String]? pinned: Bool? ask_includable: Bool?
|
||||
derived_path: String? original_format: String? conversion_status: String?
|
||||
is_read: Bool? review_status: String? edit_url: String? preview_status: String?
|
||||
source_channel: String? data_origin: String? doc_purpose: String?
|
||||
facet_company: String? facet_topic: String? facet_year: Int? facet_doctype: String?
|
||||
category: String? ai_suggestion: [String:Any]?
|
||||
ai_tldr: String? ai_bullets: [String]? ai_detail_summary: String?
|
||||
ai_inconsistencies: [String]? ai_analysis_tier: String? // 'triage' | 'deep' | null
|
||||
extracted_at: Date? ai_processed_at: Date? embedded_at: Date?
|
||||
created_at: Date updated_at: Date
|
||||
read_count: Int(=0) last_read_at: Date?
|
||||
```
|
||||
|
||||
### DocumentDetailResponse (단건 — 위 전부 + 본문/canonical markdown) `[EXISTING]`
|
||||
```
|
||||
…DocumentResponse 전 필드…
|
||||
extracted_text: String?
|
||||
md_content: String? // ← MD-first 뷰의 1차 렌더 소스 (canonical markdown)
|
||||
md_frontmatter: [String:Any]?
|
||||
md_status: String? // pending|processing|completed|partial|failed|skipped (enum은 S1 동결)
|
||||
md_extraction_quality: [String:Any]?
|
||||
md_extraction_error: String?
|
||||
md_extraction_engine: String? md_extraction_engine_version: String?
|
||||
md_generated_at: Date?
|
||||
```
|
||||
|
||||
### `[S1-ADD]` (신규 요구 반영 — 앱은 옵셔널 디코딩, 없으면 폴백)
|
||||
```
|
||||
DocumentResponse / Detail 에 추가 예정:
|
||||
original_filename: String? // 다운로드 버튼 라벨용 (없으면 file_path basename)
|
||||
duplicate_of: Int? // 중복검사 개선 — canonical doc id (자기 자신이 canonical이면 null)
|
||||
duplicate_count: Int(=0) // 이 문서와 동일 판정된 사본 수
|
||||
신규 엔드포인트:
|
||||
GET /documents/duplicates // 중복 그룹 목록 { groups: [{ canonical_id, members:[id], reason }] }
|
||||
```
|
||||
|
||||
### MD-first 렌더 규칙 (앱 측 계약)
|
||||
1. 본문 뷰 = **`md_content` 우선** (md_status ∈ {completed, partial}일 때). 일관성 = 모든 포맷을 markdown으로 본다.
|
||||
2. md 없음(md_status ∈ {pending, processing, failed, skipped, null}) → `extracted_text` 폴백 + "원본 다운로드" 강조 + "MD 변환 대기" 배지.
|
||||
3. **원본 접근 = 항상 다운로드 버튼** → `GET /documents/{id}/file?token=<access>&download=true`.
|
||||
- 주의: 이 엔드포인트는 **Authorization 헤더가 아니라 `?token=` 쿼리 파라미터**로 인증(iframe/다운로드 호환). 앱은 access_token을 쿼리로 붙인다.
|
||||
- `note`(메모)는 물리 파일 없음 → 404. 다운로드 버튼 숨김.
|
||||
4. 앱은 **절대 SMB를 보지 않는다.** 원본/스토리지 계층(맥미니 4TB ↔ NAS Docker)은 이 URL 뒤에서 S1이 추상화. 앱엔 단일 다운로드 URL만 노출.
|
||||
|
||||
---
|
||||
|
||||
## 3. Search
|
||||
|
||||
| Method | Path | 요청 | 응답 | fixture |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/search/` | `q, mode?(text|vector|hybrid), page?, debug?` | `SearchResponse` | `search.json` |
|
||||
|
||||
```
|
||||
SearchResponse { results: [SearchResult], total: Int, query: String, mode: String, debug: SearchDebug? } [EXISTING]
|
||||
SearchResult {
|
||||
id: Int // doc_id
|
||||
title: String? ai_domain: String? ai_summary: String? file_format: String
|
||||
score: Double snippet: String? match_reason: String?
|
||||
chunk_id: Int? chunk_index: Int? section_title: String?
|
||||
rerank_score: Double? freshness_debug: [String:Any]?
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Ask (RAG — 원격 DS, S2 LLM 라우팅과 연결)
|
||||
|
||||
| Method | Path | 요청 | 응답 | fixture |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/search/ask` | `q, limit?, backend?, debug?` | `AskResponse` | `ask.json` |
|
||||
| POST | `/search/ask/react` | `{...}` | `AskReactResponse` | — |
|
||||
|
||||
```
|
||||
AskResponse { [EXISTING]
|
||||
results: [SearchResult]
|
||||
ai_answer: String?
|
||||
citations: [Citation]
|
||||
synthesis_status: "completed"|"timeout"|"skipped"|"no_evidence"|"parse_failed"|"llm_error"|"backend_unavailable"
|
||||
synthesis_ms: Double
|
||||
confidence: "high"|"medium"|"low" | null
|
||||
refused: Bool no_results_reason: String? query: String total: Int
|
||||
completeness: "full"|"partial"|"insufficient" // 기본 "full"
|
||||
covered_aspects: [String]? missing_aspects: [String]?
|
||||
confirmed_items: [ConfirmedItem]?
|
||||
backend_requested: String? backend_used: String? // S2 라우팅 메타
|
||||
debug: AskDebug?
|
||||
}
|
||||
Citation { n: Int, chunk_id: Int?, doc_id: Int, title: String?, section_title: String?,
|
||||
span_text: String, full_snippet: String, relevance: Double, rerank_score: Double }
|
||||
ConfirmedItem { aspect: String, text: String, citations: [Int] }
|
||||
```
|
||||
|
||||
- **`backend` 쿼리** = S2 인터페이스 접점: `qwen-macbook | gemma-macmini | mac-mini-default | claude-cloud | auto`. 미지정 = `mac-mini-default`(맥미니 26B).
|
||||
- **앱 라우팅 규칙(S2 계약)**: 빠른 요약/선택문 ask/메모 보조 = **온디바이스(Apple FM)** 로컬 처리(이 엔드포인트 미호출). **전체 코퍼스 RAG** = 이 `/search/ask` 호출(backend로 맥미니/특화 선택). `[S1-ADD]` 없음 — backend 인자는 이미 존재.
|
||||
|
||||
---
|
||||
|
||||
## 5. Memos (캡처/쓰기)
|
||||
|
||||
| Method | Path | 요청 | 응답 | fixture |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/memos/` | `page, page_size, pinned?, archived?` | `MemoListResponse` | `memos_list.json` |
|
||||
| GET | `/memos/{id}` | — | `MemoResponse` | `memo_detail.json` |
|
||||
| POST | `/memos/` | `MemoCreate {content, title?, ask_includable?, source_channel?, source_metadata?}` | `MemoResponse` (201) | `memo_detail.json` |
|
||||
| PATCH | `/memos/{id}` | `MemoUpdate {content, title?}` | `MemoResponse` | — |
|
||||
| PATCH | `/memos/{id}/pin` · `/archive` · `/ask-includable` | `{...}` | `MemoResponse` | — |
|
||||
| PATCH | `/memos/{id}/tasks/{task_index}` | `{checked}` | `MemoResponse` | — |
|
||||
| POST | `/memos/{id}/promote-to-event` | — | 201 | — |
|
||||
| DELETE | `/memos/{id}` | — | 204 | — |
|
||||
|
||||
```
|
||||
MemoResponse { [EXISTING]
|
||||
id: Int title: String? content: String? // = extracted_text
|
||||
file_format: String file_type: String? // "audio"(음성) | "note"(텍스트)
|
||||
file_path: String? // 음성 메모 오디오 경로(있으면 재생 가능)
|
||||
user_tags: [String]? ai_tags: [String]?
|
||||
ai_domain: String? ai_sub_group: String? ai_summary: String?
|
||||
pinned: Bool archived: Bool ask_includable: Bool
|
||||
memo_task_state: [String:Any] // {"0": {"checked_at": "ISO"}}
|
||||
ai_event_kind: String? ai_event_confidence: Double?
|
||||
source_channel: String? source_metadata: [String:Any]
|
||||
created_at: Date updated_at: Date
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Digest (뉴스)
|
||||
|
||||
| Method | Path | 요청 | 응답 | fixture |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/digest` | `?date=YYYY-MM-DD&country?` | `DigestResponse` | `digest.json` |
|
||||
| GET | `/digest/dates` | — | `[DigestDateSummary]` | — |
|
||||
|
||||
```
|
||||
DigestResponse { [EXISTING]
|
||||
digest_date: Date(date-only) window_start: Date window_end: Date
|
||||
decay_lambda: Double total_articles: Int total_countries: Int total_topics: Int
|
||||
generation_ms: Int? llm_calls: Int llm_failures: Int status: String
|
||||
countries: [CountryGroup]
|
||||
}
|
||||
CountryGroup { country: String, topics: [TopicResponse] }
|
||||
TopicResponse { topic_rank: Int, topic_label: String, summary: String,
|
||||
article_ids: [Int], articles: [ArticleRef], article_count: Int,
|
||||
importance_score: Double, raw_weight_sum: Double, llm_fallback_used: Bool }
|
||||
ArticleRef { id: Int, title: String? }
|
||||
DigestDateSummary { digest_date: Date, total_topics: Int, total_countries: Int, total_articles: Int, status: String }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 관리
|
||||
- 이 계약을 깨는 변경(필드 제거/타입 변경) = `version` bump + S1/S2/S3 합의. `[S1-ADD]`는 옵셔널이라 non-breaking.
|
||||
- 통합 결선 시 fixture와 실 응답 **call-shape regression test**로 대조([[feedback_fixture_first_call_shape]]).
|
||||
@@ -0,0 +1,38 @@
|
||||
# DS App Contract (S1 인터페이스 동결) — v0.1
|
||||
|
||||
S1·S2·S3 **동시 출발선**. 앱(S3)은 실 백엔드/실 LLM 없이 **이 디렉토리만 보고** 빌드한다.
|
||||
|
||||
```
|
||||
contract/
|
||||
CONTRACT.md ← 엔드포인트 + 요청/응답 shape 동결 스펙 (읽기 시작점)
|
||||
fixtures/ ← 응답 JSON 박제 (앱 프리뷰/디코딩 테스트가 로드)
|
||||
auth_login.json POST /auth/login
|
||||
auth_me.json GET /auth/me
|
||||
documents_list.json GET /documents/ (DocumentListResponse)
|
||||
document_detail.json GET /documents/{id} (md_status=completed — MD-first 렌더)
|
||||
document_detail_pending_md.json GET /documents/{id} (md_status=pending — extracted_text 폴백 케이스)
|
||||
document_content.json GET /documents/{id}/content
|
||||
documents_tree.json GET /documents/tree
|
||||
documents_stats.json GET /documents/stats/category-counts
|
||||
documents_duplicates.json GET /documents/duplicates [S1-ADD] 중복검사
|
||||
search.json GET /search/
|
||||
ask.json GET /search/ask
|
||||
memos_list.json GET /memos/
|
||||
memo_detail.json GET /memos/{id}
|
||||
digest.json GET /digest
|
||||
```
|
||||
|
||||
## 동결 원칙
|
||||
- 모든 shape는 **실제 GPU 백엔드 Pydantic 모델에서 추출**(지어내지 않음). `[S1-ADD]` 필드만 신규.
|
||||
- 앱 모델(Swift Codable)은 위 fixtures를 그대로 디코딩할 수 있어야 한다 = 1차 수용 테스트.
|
||||
- 통합 시 fixture ↔ 실 응답 **call-shape regression**으로 대조.
|
||||
|
||||
## `[S1-ADD]` (S1이 추가할 신규 — 앱은 옵셔널 디코딩)
|
||||
- `original_filename`, `duplicate_of`, `duplicate_count` (DocumentResponse/Detail)
|
||||
- `GET /documents/duplicates` (중복검사 개선 트랙)
|
||||
- Word/Excel/이미지/오디오 → `md_content` 채우기 (MD-first 전포맷 — marker는 현재 PDF 전용)
|
||||
|
||||
## 다음
|
||||
- S3: 이 fixtures를 디코딩하는 Swift `Codable` 모델 + API client(프로토콜) → macOS 앱.
|
||||
- S1: `[S1-ADD]` 구현 + 위 shape 유지(깨면 version bump).
|
||||
- S2: `/search/ask?backend=` + 온디바이스 provider 추상화(`AIProvider`).
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": 4912,
|
||||
"title": "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
"ai_domain": "Engineering",
|
||||
"ai_summary": "압력용기 재료의 충격시험 면제/요구 조건(UCS-66 등)을 정리.",
|
||||
"file_format": "pdf",
|
||||
"score": 0.8714,
|
||||
"snippet": "...UCS-66 면제 곡선과 MDMT 적용...",
|
||||
"match_reason": "vector+rerank",
|
||||
"chunk_id": 88213,
|
||||
"chunk_index": 3,
|
||||
"section_title": "2. UCS-66 면제 곡선",
|
||||
"rerank_score": 0.913,
|
||||
"freshness_debug": null
|
||||
}
|
||||
],
|
||||
"ai_answer": "충격시험 면제는 UCS-66 면제 곡선으로 판정합니다 [1]. 재료군(Curve A~D)과 거버닝 두께에 따라 최소설계금속온도(MDMT)에서 면제 여부가 정해지며, 설계 응력비가 낮으면 UCS-66.1에 따라 MDMT를 추가로 낮출 수 있습니다 [1].",
|
||||
"citations": [
|
||||
{
|
||||
"n": 1,
|
||||
"chunk_id": 88213,
|
||||
"doc_id": 4912,
|
||||
"title": "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
"section_title": "2. UCS-66 면제 곡선",
|
||||
"span_text": "재료군(Curve A~D)과 거버닝 두께에 따라 최소설계금속온도(MDMT)에서의 충격시험 면제 여부를 결정한다.",
|
||||
"full_snippet": "재료군(Curve A~D)과 거버닝 두께에 따라 최소설계금속온도(MDMT)에서의 충격시험 면제 여부를 결정한다. 설계 응력비가 낮으면 UCS-66.1에 따라 MDMT를 추가로 낮출 수 있다. 면제되지 않는 경우 UG-84에 따라 Charpy V-notch 시험을 수행한다.",
|
||||
"relevance": 0.91,
|
||||
"rerank_score": 0.913
|
||||
}
|
||||
],
|
||||
"synthesis_status": "completed",
|
||||
"synthesis_ms": 2841.5,
|
||||
"confidence": "high",
|
||||
"refused": false,
|
||||
"no_results_reason": null,
|
||||
"query": "충격시험은 언제 면제되나",
|
||||
"total": 1,
|
||||
"completeness": "full",
|
||||
"covered_aspects": ["면제 곡선", "MDMT 적용"],
|
||||
"missing_aspects": null,
|
||||
"confirmed_items": null,
|
||||
"backend_requested": "mac-mini-default",
|
||||
"backend_used": "gemma-macmini",
|
||||
"debug": null
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJoeXVuZ2kiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzQ5MDAwMDAwfQ.FIXTURE_SIGNATURE_NOT_REAL",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": 1,
|
||||
"username": "hyungi",
|
||||
"is_active": true,
|
||||
"totp_enabled": false,
|
||||
"last_login_at": "2026-06-04T07:55:12.330Z"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"digest_date": "2026-06-03",
|
||||
"window_start": "2026-05-27T00:00:00.000Z",
|
||||
"window_end": "2026-06-03T00:00:00.000Z",
|
||||
"decay_lambda": 0.18,
|
||||
"total_articles": 312,
|
||||
"total_countries": 4,
|
||||
"total_topics": 9,
|
||||
"generation_ms": 18420,
|
||||
"llm_calls": 11,
|
||||
"llm_failures": 0,
|
||||
"status": "completed",
|
||||
"countries": [
|
||||
{
|
||||
"country": "KR",
|
||||
"topics": [
|
||||
{
|
||||
"topic_rank": 1,
|
||||
"topic_label": "산업안전 규제 개정",
|
||||
"summary": "중대재해처벌법 후속 시행령 개정 논의가 이어지며 제조업 현장 점검이 강화되는 흐름.",
|
||||
"article_ids": [880123, 880140, 880155],
|
||||
"articles": [
|
||||
{ "id": 880123, "title": "고용부, 중대재해 시행령 개정안 입법예고" },
|
||||
{ "id": 880140, "title": "제조 현장 안전점검 확대" },
|
||||
{ "id": 880155, "title": "압력설비 검사 주기 단축 검토" }
|
||||
],
|
||||
"article_count": 3,
|
||||
"importance_score": 0.91,
|
||||
"raw_weight_sum": 2.74,
|
||||
"llm_fallback_used": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"country": "US",
|
||||
"topics": [
|
||||
{
|
||||
"topic_rank": 1,
|
||||
"topic_label": "ASME 코드 업데이트",
|
||||
"summary": "ASME BPVC 2025 에디션 관련 산업계 적용 사례와 해설 자료가 늘어남.",
|
||||
"article_ids": [880301, 880322],
|
||||
"articles": [
|
||||
{ "id": 880301, "title": "ASME BPVC 2025 adoption notes" },
|
||||
{ "id": 880322, "title": "Impact test exemption clarifications" }
|
||||
],
|
||||
"article_count": 2,
|
||||
"importance_score": 0.77,
|
||||
"raw_weight_sum": 1.62,
|
||||
"llm_fallback_used": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": 4912,
|
||||
"title": "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
"domain": "Engineering",
|
||||
"sub_group": "압력용기",
|
||||
"document_type": "standard",
|
||||
"ai_summary": "압력용기 재료의 충격시험 면제/요구 조건(UCS-66 등)을 정리.",
|
||||
"ai_tags": ["ASME", "Section VIII", "충격시험", "UCS-66"],
|
||||
"content": "ASME Section VIII Division 1 Impact Test Requirements\nUCS-66 면제 곡선과 MDMT 적용 ... (최대 15000자) ...",
|
||||
"content_length": 8421,
|
||||
"truncated": false
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": 4912,
|
||||
"file_path": "Engineering/ASME/ASME_SecVIII_Div1_Impact_Test.pdf",
|
||||
"file_format": "pdf",
|
||||
"file_size": 1338920,
|
||||
"file_type": "document",
|
||||
"title": "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
"ai_domain": "Engineering",
|
||||
"ai_sub_group": "압력용기",
|
||||
"ai_tags": ["ASME", "Section VIII", "충격시험", "UCS-66"],
|
||||
"ai_summary": "압력용기 재료의 충격시험 면제/요구 조건(UCS-66 등)을 정리.",
|
||||
"document_type": "standard",
|
||||
"importance": "high",
|
||||
"ai_confidence": 0.93,
|
||||
"user_note": "MDMT 판정 시 자주 참조",
|
||||
"user_tags": ["자주봄"],
|
||||
"pinned": true,
|
||||
"ask_includable": true,
|
||||
"derived_path": "Engineering/ASME/ASME_SecVIII_Div1_Impact_Test.md",
|
||||
"original_format": "pdf",
|
||||
"conversion_status": "completed",
|
||||
"is_read": true,
|
||||
"review_status": "approved",
|
||||
"edit_url": null,
|
||||
"preview_status": "ready",
|
||||
"source_channel": "upload",
|
||||
"data_origin": "external",
|
||||
"doc_purpose": "reference",
|
||||
"facet_company": "ASME",
|
||||
"facet_topic": "압력용기",
|
||||
"facet_year": 2023,
|
||||
"facet_doctype": "standard",
|
||||
"category": "library",
|
||||
"ai_suggestion": null,
|
||||
"ai_tldr": "충격시험 면제 곡선(UCS-66)과 MDMT 적용.",
|
||||
"ai_bullets": ["UCS-66 면제 곡선", "UCS-66.1 감액", "UG-84 시험 요건"],
|
||||
"ai_detail_summary": "본 표준 절은 탄소강·저합금강 압력용기 재료의 노치 인성(충격시험) 요구를 다룬다. UCS-66 면제 곡선은 재료군(A~D)과 두께에 따라 최소설계금속온도(MDMT)에서의 시험 면제 여부를 정한다.",
|
||||
"ai_inconsistencies": [],
|
||||
"ai_analysis_tier": "deep",
|
||||
"extracted_at": "2026-05-22T05:00:11.000Z",
|
||||
"ai_processed_at": "2026-05-22T05:04:40.000Z",
|
||||
"embedded_at": "2026-05-22T05:06:02.000Z",
|
||||
"created_at": "2026-05-22T04:59:50.000Z",
|
||||
"updated_at": "2026-06-01T09:21:33.000Z",
|
||||
"read_count": 11,
|
||||
"last_read_at": "2026-06-03T18:02:10.000Z",
|
||||
"original_filename": "ASME_SecVIII_Div1_Impact_Test.pdf",
|
||||
"duplicate_of": null,
|
||||
"duplicate_count": 1,
|
||||
"extracted_text": "ASME Section VIII Division 1 Impact Test Requirements\nUCS-66 ... (원문 추출 텍스트, 폴백용) ...",
|
||||
"md_content": "# ASME Section VIII Div 1 — 충격시험 요건\n\n## 1. 범위\n탄소강 및 저합금강 압력용기 재료의 노치 인성(충격시험) 요구를 규정한다.\n\n## 2. UCS-66 면제 곡선\n재료군(Curve A~D)과 거버닝 두께에 따라 **최소설계금속온도(MDMT)** 에서의 충격시험 면제 여부를 결정한다.\n\n| 곡선 | 대표 재료 | 비고 |\n|---|---|---|\n| A | SA-516 (비정규화) | 가장 보수적 |\n| B | SA-516 정규화 | |\n| C | SA-537 | |\n| D | 인성 우수 재료 | 가장 관대 |\n\n## 3. UCS-66.1 감액\n설계 응력비(stress ratio)가 낮으면 MDMT 를 추가로 낮출 수 있다.\n\n## 4. UG-84 시험 요건\n면제되지 않는 경우 Charpy V-notch 시험으로 흡수에너지/측면팽창 기준을 만족해야 한다.\n",
|
||||
"md_frontmatter": {
|
||||
"title": "ASME Section VIII Div 1 — 충격시험 요건",
|
||||
"domain": "Engineering",
|
||||
"source": "ASME_SecVIII_Div1_Impact_Test.pdf"
|
||||
},
|
||||
"md_status": "completed",
|
||||
"md_extraction_quality": { "page_count": 14, "table_count": 3, "ocr_used": false },
|
||||
"md_extraction_error": null,
|
||||
"md_extraction_engine": "marker",
|
||||
"md_extraction_engine_version": "1.10.2",
|
||||
"md_generated_at": "2026-05-22T05:03:30.000Z"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"id": 5301,
|
||||
"file_path": "General/매뉴얼/02_왕복압축기_운전매뉴얼.docx",
|
||||
"file_format": "docx",
|
||||
"file_size": 73402,
|
||||
"file_type": "document",
|
||||
"title": "02 왕복압축기 운전 매뉴얼",
|
||||
"ai_domain": "General",
|
||||
"ai_sub_group": "설비매뉴얼",
|
||||
"ai_tags": ["왕복압축기", "운전", "매뉴얼"],
|
||||
"ai_summary": "왕복동식 압축기 기동/정지/점검 절차 매뉴얼.",
|
||||
"document_type": "manual",
|
||||
"importance": "normal",
|
||||
"ai_confidence": 0.81,
|
||||
"user_note": null,
|
||||
"user_tags": null,
|
||||
"pinned": false,
|
||||
"ask_includable": true,
|
||||
"derived_path": null,
|
||||
"original_format": "docx",
|
||||
"conversion_status": "pending",
|
||||
"is_read": false,
|
||||
"review_status": "pending",
|
||||
"edit_url": null,
|
||||
"preview_status": "pending",
|
||||
"source_channel": "upload",
|
||||
"data_origin": "internal",
|
||||
"doc_purpose": "reference",
|
||||
"facet_company": null,
|
||||
"facet_topic": "설비매뉴얼",
|
||||
"facet_year": 2024,
|
||||
"facet_doctype": "manual",
|
||||
"category": "library",
|
||||
"ai_suggestion": null,
|
||||
"ai_tldr": null,
|
||||
"ai_bullets": null,
|
||||
"ai_detail_summary": null,
|
||||
"ai_inconsistencies": null,
|
||||
"ai_analysis_tier": "triage",
|
||||
"extracted_at": "2026-06-03T01:20:00.000Z",
|
||||
"ai_processed_at": "2026-06-03T01:22:14.000Z",
|
||||
"embedded_at": null,
|
||||
"created_at": "2026-06-03T01:19:55.000Z",
|
||||
"updated_at": "2026-06-03T01:22:14.000Z",
|
||||
"read_count": 0,
|
||||
"last_read_at": null,
|
||||
"original_filename": "02_왕복압축기_운전매뉴얼.docx",
|
||||
"duplicate_of": null,
|
||||
"duplicate_count": 0,
|
||||
"extracted_text": "왕복압축기 운전 매뉴얼\n1. 기동 전 점검\n - 윤활유 레벨 확인\n - 흡입/토출 밸브 상태 확인\n2. 기동 절차 ...",
|
||||
"md_content": null,
|
||||
"md_frontmatter": null,
|
||||
"md_status": "pending",
|
||||
"md_extraction_quality": null,
|
||||
"md_extraction_error": null,
|
||||
"md_extraction_engine": null,
|
||||
"md_extraction_engine_version": null,
|
||||
"md_generated_at": null
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"canonical_id": 4912,
|
||||
"members": [4912, 4977],
|
||||
"reason": "content_hash",
|
||||
"detail": "동일 본문 해시 (md_content normalized SHA-256 일치)"
|
||||
},
|
||||
{
|
||||
"canonical_id": 5120,
|
||||
"members": [5120, 5121, 5260],
|
||||
"reason": "near_duplicate",
|
||||
"detail": "제목/본문 유사도 0.97 (cross-format: pdf + docx 동일 문서)"
|
||||
}
|
||||
],
|
||||
"total_groups": 2,
|
||||
"total_duplicate_docs": 3
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 5187,
|
||||
"file_path": "Engineering/기계가공/엘보_내경가공_절차서.pdf",
|
||||
"file_format": "pdf",
|
||||
"file_size": 482113,
|
||||
"file_type": "document",
|
||||
"title": "엘보 내경가공 절차서",
|
||||
"ai_domain": "Engineering",
|
||||
"ai_sub_group": "기계가공",
|
||||
"ai_tags": ["엘보", "내경가공", "절차서", "가공공차"],
|
||||
"ai_summary": "엘보 내경 가공 시 공차 관리와 가공 순서를 정리한 사내 절차서.",
|
||||
"document_type": "procedure",
|
||||
"importance": "normal",
|
||||
"ai_confidence": 0.86,
|
||||
"user_note": null,
|
||||
"user_tags": null,
|
||||
"pinned": false,
|
||||
"ask_includable": true,
|
||||
"derived_path": "Engineering/기계가공/엘보_내경가공_절차서.md",
|
||||
"original_format": "pdf",
|
||||
"conversion_status": "completed",
|
||||
"is_read": true,
|
||||
"review_status": "approved",
|
||||
"edit_url": null,
|
||||
"preview_status": "ready",
|
||||
"source_channel": "upload",
|
||||
"data_origin": "internal",
|
||||
"doc_purpose": "reference",
|
||||
"facet_company": null,
|
||||
"facet_topic": "기계가공",
|
||||
"facet_year": 2025,
|
||||
"facet_doctype": "procedure",
|
||||
"category": "library",
|
||||
"ai_suggestion": null,
|
||||
"ai_tldr": "엘보 내경 가공 공차·순서 절차.",
|
||||
"ai_bullets": ["가공 전 소재 검사", "내경 공차 +0.1/-0.0", "최종 치수 검사 기록"],
|
||||
"ai_detail_summary": null,
|
||||
"ai_inconsistencies": null,
|
||||
"ai_analysis_tier": "triage",
|
||||
"extracted_at": "2026-05-30T02:11:04.000Z",
|
||||
"ai_processed_at": "2026-05-30T02:13:51.000Z",
|
||||
"embedded_at": "2026-05-30T02:15:09.000Z",
|
||||
"created_at": "2026-05-30T02:10:58.000Z",
|
||||
"updated_at": "2026-05-30T02:15:09.000Z",
|
||||
"read_count": 3,
|
||||
"last_read_at": "2026-06-02T13:40:22.000Z",
|
||||
"original_filename": "엘보_내경가공_절차서.pdf",
|
||||
"duplicate_of": null,
|
||||
"duplicate_count": 0
|
||||
},
|
||||
{
|
||||
"id": 4912,
|
||||
"file_path": "Engineering/ASME/ASME_SecVIII_Div1_Impact_Test.pdf",
|
||||
"file_format": "pdf",
|
||||
"file_size": 1338920,
|
||||
"file_type": "document",
|
||||
"title": "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
"ai_domain": "Engineering",
|
||||
"ai_sub_group": "압력용기",
|
||||
"ai_tags": ["ASME", "Section VIII", "충격시험", "UCS-66"],
|
||||
"ai_summary": "압력용기 재료의 충격시험 면제/요구 조건(UCS-66 등)을 정리.",
|
||||
"document_type": "standard",
|
||||
"importance": "high",
|
||||
"ai_confidence": 0.93,
|
||||
"user_note": "MDMT 판정 시 자주 참조",
|
||||
"user_tags": ["자주봄"],
|
||||
"pinned": true,
|
||||
"ask_includable": true,
|
||||
"derived_path": "Engineering/ASME/ASME_SecVIII_Div1_Impact_Test.md",
|
||||
"original_format": "pdf",
|
||||
"conversion_status": "completed",
|
||||
"is_read": true,
|
||||
"review_status": "approved",
|
||||
"edit_url": null,
|
||||
"preview_status": "ready",
|
||||
"source_channel": "upload",
|
||||
"data_origin": "external",
|
||||
"doc_purpose": "reference",
|
||||
"facet_company": "ASME",
|
||||
"facet_topic": "압력용기",
|
||||
"facet_year": 2023,
|
||||
"facet_doctype": "standard",
|
||||
"category": "library",
|
||||
"ai_suggestion": null,
|
||||
"ai_tldr": "충격시험 면제 곡선(UCS-66)과 MDMT 적용.",
|
||||
"ai_bullets": ["UCS-66 면제 곡선", "UCS-66.1 감액", "UG-84 시험 요건"],
|
||||
"ai_detail_summary": null,
|
||||
"ai_inconsistencies": null,
|
||||
"ai_analysis_tier": "deep",
|
||||
"extracted_at": "2026-05-22T05:00:11.000Z",
|
||||
"ai_processed_at": "2026-05-22T05:04:40.000Z",
|
||||
"embedded_at": "2026-05-22T05:06:02.000Z",
|
||||
"created_at": "2026-05-22T04:59:50.000Z",
|
||||
"updated_at": "2026-06-01T09:21:33.000Z",
|
||||
"read_count": 11,
|
||||
"last_read_at": "2026-06-03T18:02:10.000Z",
|
||||
"original_filename": "ASME_SecVIII_Div1_Impact_Test.pdf",
|
||||
"duplicate_of": null,
|
||||
"duplicate_count": 1
|
||||
},
|
||||
{
|
||||
"id": 5301,
|
||||
"file_path": "General/매뉴얼/02_왕복압축기_운전매뉴얼.docx",
|
||||
"file_format": "docx",
|
||||
"file_size": 73402,
|
||||
"file_type": "document",
|
||||
"title": "02 왕복압축기 운전 매뉴얼",
|
||||
"ai_domain": "General",
|
||||
"ai_sub_group": "설비매뉴얼",
|
||||
"ai_tags": ["왕복압축기", "운전", "매뉴얼"],
|
||||
"ai_summary": "왕복동식 압축기 기동/정지/점검 절차 매뉴얼.",
|
||||
"document_type": "manual",
|
||||
"importance": "normal",
|
||||
"ai_confidence": 0.81,
|
||||
"user_note": null,
|
||||
"user_tags": null,
|
||||
"pinned": false,
|
||||
"ask_includable": true,
|
||||
"derived_path": null,
|
||||
"original_format": "docx",
|
||||
"conversion_status": "pending",
|
||||
"is_read": false,
|
||||
"review_status": "pending",
|
||||
"edit_url": null,
|
||||
"preview_status": "pending",
|
||||
"source_channel": "upload",
|
||||
"data_origin": "internal",
|
||||
"doc_purpose": "reference",
|
||||
"facet_company": null,
|
||||
"facet_topic": "설비매뉴얼",
|
||||
"facet_year": 2024,
|
||||
"facet_doctype": "manual",
|
||||
"category": "library",
|
||||
"ai_suggestion": null,
|
||||
"ai_tldr": null,
|
||||
"ai_bullets": null,
|
||||
"ai_detail_summary": null,
|
||||
"ai_inconsistencies": null,
|
||||
"ai_analysis_tier": "triage",
|
||||
"extracted_at": "2026-06-03T01:20:00.000Z",
|
||||
"ai_processed_at": "2026-06-03T01:22:14.000Z",
|
||||
"embedded_at": null,
|
||||
"created_at": "2026-06-03T01:19:55.000Z",
|
||||
"updated_at": "2026-06-03T01:22:14.000Z",
|
||||
"read_count": 0,
|
||||
"last_read_at": null,
|
||||
"original_filename": "02_왕복압축기_운전매뉴얼.docx",
|
||||
"duplicate_of": null,
|
||||
"duplicate_count": 0
|
||||
}
|
||||
],
|
||||
"total": 783,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"total": 1163,
|
||||
"documents": 783,
|
||||
"by_domain": {
|
||||
"Industrial_Safety": 426,
|
||||
"Engineering": 351,
|
||||
"General": 189,
|
||||
"Programming": 60,
|
||||
"법령": 23,
|
||||
"Philosophy": 12
|
||||
},
|
||||
"review_pending": 725,
|
||||
"pipeline_failed": 19
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{ "name": "Industrial_Safety", "path": "Industrial_Safety", "count": 426,
|
||||
"children": [
|
||||
{ "name": "위험성평가", "path": "Industrial_Safety/위험성평가", "count": 118, "children": [] },
|
||||
{ "name": "KGS", "path": "Industrial_Safety/KGS", "count": 73, "children": [] }
|
||||
] },
|
||||
{ "name": "Engineering", "path": "Engineering", "count": 351,
|
||||
"children": [
|
||||
{ "name": "압력용기", "path": "Engineering/압력용기", "count": 96, "children": [] },
|
||||
{ "name": "기계가공", "path": "Engineering/기계가공", "count": 54, "children": [] }
|
||||
] },
|
||||
{ "name": "General", "path": "General", "count": 189, "children": [] },
|
||||
{ "name": "Programming", "path": "Programming", "count": 60, "children": [] },
|
||||
{ "name": "법령", "path": "법령", "count": 23, "children": [] },
|
||||
{ "name": "Philosophy", "path": "Philosophy", "count": 12, "children": [] }
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": 20238,
|
||||
"title": "엘보 발주 확인 건",
|
||||
"content": "엘보 내경가공 발주서 금요일까지 확인.\n- [ ] 도면 rev C 기준 공차 재확인\n- [ ] 발주처에 납기 회신",
|
||||
"file_format": "txt",
|
||||
"user_tags": ["업무"],
|
||||
"ai_tags": ["발주", "엘보"],
|
||||
"ai_domain": "General",
|
||||
"ai_sub_group": "업무메모",
|
||||
"ai_summary": "엘보 발주서 금요일까지 확인, 도면 rev C 공차 재확인.",
|
||||
"pinned": true,
|
||||
"archived": false,
|
||||
"ask_includable": true,
|
||||
"memo_task_state": { "0": { "checked_at": "2026-06-03T10:02:00.000Z" } },
|
||||
"ai_event_kind": "task",
|
||||
"ai_event_confidence": 0.78,
|
||||
"source_channel": "memo",
|
||||
"source_metadata": {},
|
||||
"file_type": "note",
|
||||
"file_path": null,
|
||||
"created_at": "2026-06-03T09:40:00.000Z",
|
||||
"updated_at": "2026-06-03T10:02:00.000Z"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 20238,
|
||||
"title": "엘보 발주 확인 건",
|
||||
"content": "엘보 내경가공 발주서 금요일까지 확인. 도면 rev C 기준으로 공차 재확인 필요.",
|
||||
"file_format": "txt",
|
||||
"user_tags": ["업무"],
|
||||
"ai_tags": ["발주", "엘보"],
|
||||
"ai_domain": "General",
|
||||
"ai_sub_group": "업무메모",
|
||||
"ai_summary": "엘보 발주서 금요일까지 확인, 도면 rev C 공차 재확인.",
|
||||
"pinned": true,
|
||||
"archived": false,
|
||||
"ask_includable": true,
|
||||
"memo_task_state": { "0": { "checked_at": "2026-06-03T10:02:00.000Z" } },
|
||||
"ai_event_kind": "task",
|
||||
"ai_event_confidence": 0.78,
|
||||
"source_channel": "memo",
|
||||
"source_metadata": {},
|
||||
"file_type": "note",
|
||||
"file_path": null,
|
||||
"created_at": "2026-06-03T09:40:00.000Z",
|
||||
"updated_at": "2026-06-03T10:02:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": 20251,
|
||||
"title": "음성 메모 — 현장 점검",
|
||||
"content": "3공장 압축기 베어링 소음. 다음 점검 때 진동 측정 추가하기로.",
|
||||
"file_format": "m4a",
|
||||
"user_tags": null,
|
||||
"ai_tags": ["현장", "압축기", "점검"],
|
||||
"ai_domain": "Industrial_Safety",
|
||||
"ai_sub_group": "현장메모",
|
||||
"ai_summary": "3공장 압축기 베어링 소음, 진동 측정 추가 예정.",
|
||||
"pinned": false,
|
||||
"archived": false,
|
||||
"ask_includable": true,
|
||||
"memo_task_state": {},
|
||||
"ai_event_kind": "note",
|
||||
"ai_event_confidence": 0.64,
|
||||
"source_channel": "voice",
|
||||
"source_metadata": { "duration_s": 23, "device": "iPhone" },
|
||||
"file_type": "audio",
|
||||
"file_path": "memos/voice/2026/06/test-voice-memo.m4a",
|
||||
"created_at": "2026-06-02T17:11:00.000Z",
|
||||
"updated_at": "2026-06-02T17:11:40.000Z"
|
||||
}
|
||||
],
|
||||
"total": 4807,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": 4912,
|
||||
"title": "ASME Section VIII Div 1 — Impact Test 요건",
|
||||
"ai_domain": "Engineering",
|
||||
"ai_summary": "압력용기 재료의 충격시험 면제/요구 조건(UCS-66 등)을 정리.",
|
||||
"file_format": "pdf",
|
||||
"score": 0.8714,
|
||||
"snippet": "...UCS-66 면제 곡선과 MDMT 적용. 충격시험 면제 여부는 재료군과 두께로 결정...",
|
||||
"match_reason": "vector+rerank",
|
||||
"chunk_id": 88213,
|
||||
"chunk_index": 3,
|
||||
"section_title": "2. UCS-66 면제 곡선",
|
||||
"rerank_score": 0.913,
|
||||
"freshness_debug": null
|
||||
},
|
||||
{
|
||||
"id": 5044,
|
||||
"title": "KGS FU211 §2.5 — 가스설비 충격 관련 요건",
|
||||
"ai_domain": "Industrial_Safety",
|
||||
"ai_summary": "KGS FU211 가스 사용시설 기준 중 충격/내압 관련 조항.",
|
||||
"file_format": "pdf",
|
||||
"score": 0.7321,
|
||||
"snippet": "...§2.5 충격에 의한 손상 방지... §2.8 내압 시험...",
|
||||
"match_reason": "vector",
|
||||
"chunk_id": 90122,
|
||||
"chunk_index": 1,
|
||||
"section_title": "2.5",
|
||||
"rerank_score": 0.742,
|
||||
"freshness_debug": null
|
||||
}
|
||||
],
|
||||
"total": 2,
|
||||
"query": "충격시험 면제",
|
||||
"mode": "hybrid",
|
||||
"debug": null
|
||||
}
|
||||
Reference in New Issue
Block a user