Files
hyungi_document_server/Sources/AppFeature/AI/LiveDSAskClient.swift
T
Hyungi b9b5188265 feat(s3): DSAskClient HTTP bridge + realRouter seam (FU-B)
- LiveDSAskClient: S3-owned concrete DSAskClient (GET /search/ask -> decode AIFabric.AskResponse),
  the piece S2's plan assigned to S3 for the real RemoteDSProvider
- AppAIComposition.realRouter(): makeDefaultRouter(client: LiveDSAskClient) — the one-call swap from
  mock to the real S2 fabric; app default stays mockRouter (offline scaffold)
- DSError.from made public (used cross-module by the bridge)

swift build + swift test green (71). Sources/AI untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:44:18 +09:00

51 lines
2.0 KiB
Swift

import Foundation
import AIFabric
import DSKit
/// S3-owned concrete `DSAskClient` (the HTTP seam S2's `RemoteDSProvider` calls). Hits DS
/// `GET /search/ask?q=&backend=` and decodes **AIFabric.AskResponse** (a decode-only type that
/// RemoteDSProvider maps to AICompletionResponse). This is the piece S2's plan assigns to S3
/// (" DSAskClient(HTTP) = S3 "). FU-A: exercised only when the real router is wired.
public struct LiveDSAskClient: DSAskClient {
private let base: DSBaseURL
private let session: URLSession
private let decoder: JSONDecoder
private let token: @Sendable () async -> String?
public init(
base: DSBaseURL = .publicTLS,
session: URLSession = .shared,
token: @escaping @Sendable () async -> String? = { nil }
) {
self.base = base
self.session = session
self.decoder = DSDecoder.make()
self.token = token
}
public func ask(query: String, backend: String) async throws -> AIFabric.AskResponse {
let raw = base.url.absoluteString + "/search/ask"
guard var comps = URLComponents(string: raw) else {
throw DSError.transport(underlying: "bad ask URL")
}
comps.queryItems = [
URLQueryItem(name: "q", value: query),
URLQueryItem(name: "backend", value: backend),
]
guard let url = comps.url else { throw DSError.transport(underlying: "bad ask URL components") }
var request = URLRequest(url: url)
if let token = await token() {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw DSError.transport(underlying: "no HTTP response")
}
guard (200..<300).contains(http.statusCode) else {
throw DSError.from(status: http.statusCode, data: data)
}
return try decoder.decode(AIFabric.AskResponse.self, from: data)
}
}