缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成: ``` <type>(<scope>): <subject> <body> ``` 其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
@@ -135,6 +135,13 @@ struct HealthExportService {
|
||||
throw ServiceError.generationFailed("模型未输出任何内容")
|
||||
}
|
||||
|
||||
// —— 追加确定性趋势段(不经 LLM,零编造) ——
|
||||
let trendBlock = Self.trendSection(snapshot.trends)
|
||||
if !trendBlock.isEmpty {
|
||||
generated += trendBlock
|
||||
continuation.yield(.token(TokenChunk(text: trendBlock, decodeRate: 0)))
|
||||
}
|
||||
|
||||
// —— Phase 4: 持久化 ——
|
||||
let export = HealthExport(
|
||||
prompt: prompt,
|
||||
@@ -170,6 +177,146 @@ struct HealthExportService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 多轮导出页的单轮问答。只回答,不入库。
|
||||
func answer(question: String,
|
||||
conversation: [HealthExportDialogueTurn],
|
||||
in modelContext: ModelContext) -> AsyncThrowingStream<TokenChunk, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let task = Task { @MainActor in
|
||||
do {
|
||||
do {
|
||||
try await AIRuntime.shared.prepare()
|
||||
} catch {
|
||||
throw ServiceError.modelNotReady
|
||||
}
|
||||
|
||||
let snapshot = Self.retrieveDialogueSnapshot(ctx: modelContext)
|
||||
let dataJSON = Self.serializeData(snapshot: snapshot)
|
||||
let transcript = HealthExportDialogueTurn.transcript(from: conversation)
|
||||
let prompt = HealthExportPrompts.dialogueAnswer(
|
||||
latestQuestion: question,
|
||||
transcript: transcript,
|
||||
dataJSON: dataJSON
|
||||
)
|
||||
|
||||
var displayed = ""
|
||||
var rawAccum = ""
|
||||
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 480)
|
||||
for try await chunk in stream {
|
||||
try Task.checkCancellation()
|
||||
rawAccum += chunk.text
|
||||
let clean = Self.stripThinkBlocks(rawAccum)
|
||||
if clean.count > displayed.count, clean.hasPrefix(displayed) {
|
||||
let delta = String(clean.dropFirst(displayed.count))
|
||||
displayed = clean
|
||||
continuation.yield(TokenChunk(text: delta, decodeRate: chunk.decodeRate))
|
||||
} else if clean != displayed {
|
||||
displayed = clean
|
||||
}
|
||||
}
|
||||
|
||||
guard !displayed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
throw ServiceError.generationFailed("模型未输出任何内容")
|
||||
}
|
||||
continuation.finish()
|
||||
} catch is CancellationError {
|
||||
continuation.finish(throwing: ServiceError.cancelled)
|
||||
} catch let e as ServiceError {
|
||||
continuation.finish(throwing: e)
|
||||
} catch {
|
||||
continuation.finish(throwing: ServiceError.generationFailed("\(error)"))
|
||||
}
|
||||
}
|
||||
continuation.onTermination = { _ in task.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
/// 多轮导出页的最终报告生成。保存为现有 HealthExport 历史。
|
||||
func export(conversation: [HealthExportDialogueTurn],
|
||||
in modelContext: ModelContext) -> AsyncThrowingStream<Event, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let task = Task { @MainActor in
|
||||
do {
|
||||
do {
|
||||
try await AIRuntime.shared.prepare()
|
||||
} catch {
|
||||
throw ServiceError.modelNotReady
|
||||
}
|
||||
|
||||
continuation.yield(.phaseChanged(.retrieving))
|
||||
let snapshot = Self.retrieveDialogueSnapshot(ctx: modelContext)
|
||||
let dataJSON = Self.serializeData(snapshot: snapshot)
|
||||
let transcript = HealthExportDialogueTurn.transcript(from: conversation)
|
||||
try Task.checkCancellation()
|
||||
|
||||
continuation.yield(.phaseChanged(.generating))
|
||||
let genPrompt = HealthExportPrompts.dialogueReportGeneration(
|
||||
transcript: transcript,
|
||||
dataJSON: dataJSON
|
||||
)
|
||||
|
||||
var generated = ""
|
||||
var rawAccum = ""
|
||||
var lastRate: Double = 0
|
||||
let stream = await AIRuntime.shared.generate(prompt: genPrompt, maxTokens: 1200)
|
||||
for try await chunk in stream {
|
||||
try Task.checkCancellation()
|
||||
if chunk.decodeRate > 0 { lastRate = chunk.decodeRate }
|
||||
rawAccum += chunk.text
|
||||
let clean = Self.stripThinkBlocks(rawAccum)
|
||||
if clean.count > generated.count, clean.hasPrefix(generated) {
|
||||
let delta = String(clean.dropFirst(generated.count))
|
||||
generated = clean
|
||||
continuation.yield(.token(TokenChunk(text: delta, decodeRate: chunk.decodeRate)))
|
||||
} else if clean != generated {
|
||||
generated = clean
|
||||
}
|
||||
}
|
||||
|
||||
guard !generated.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
throw ServiceError.generationFailed("模型未输出任何内容")
|
||||
}
|
||||
|
||||
// —— 追加确定性趋势段(不经 LLM,零编造) ——
|
||||
let trendBlock = Self.trendSection(snapshot.trends)
|
||||
if !trendBlock.isEmpty {
|
||||
generated += trendBlock
|
||||
continuation.yield(.token(TokenChunk(text: trendBlock, decodeRate: 0)))
|
||||
}
|
||||
|
||||
let export = HealthExport(
|
||||
prompt: transcript,
|
||||
content: generated,
|
||||
referencedIndicatorIDs: snapshot.indicators.map { Self.idString($0.persistentModelID) },
|
||||
referencedReportIDs: [],
|
||||
referencedSymptomIDs: [],
|
||||
referencedDiaryIDs: snapshot.diaries.map { Self.idString($0.persistentModelID) },
|
||||
inferredTimeFromDate: snapshot.fromDate,
|
||||
inferredTimeToDate: snapshot.toDate,
|
||||
inferredIntent: "dialogue_export",
|
||||
inferredLabelCN: "对话整理",
|
||||
modelTag: ModelKind.llm.rawValue,
|
||||
decodeRate: lastRate
|
||||
)
|
||||
modelContext.insert(export)
|
||||
do { try modelContext.save() } catch {
|
||||
print("[HealthExportService] save failed: \(error)")
|
||||
}
|
||||
continuation.yield(.phaseChanged(.completed))
|
||||
continuation.yield(.completed(persistentID: export.persistentModelID))
|
||||
continuation.finish()
|
||||
} catch is CancellationError {
|
||||
continuation.finish(throwing: ServiceError.cancelled)
|
||||
} catch let e as ServiceError {
|
||||
continuation.finish(throwing: e)
|
||||
} catch {
|
||||
continuation.finish(throwing: ServiceError.generationFailed("\(error)"))
|
||||
}
|
||||
}
|
||||
continuation.onTermination = { _ in task.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Phase 1: intent extraction
|
||||
|
||||
struct Intent: Sendable {
|
||||
@@ -251,6 +398,8 @@ struct HealthExportService {
|
||||
var reports: [Report]
|
||||
var diaries: [DiaryEntry]
|
||||
var profile: UserProfile
|
||||
/// 相关指标的趋势行(确定性计算,不进 LLM)。空 → 不渲染「## 指标趋势」段。
|
||||
var trends: [ExportTrend] = []
|
||||
}
|
||||
|
||||
/// 同步 SwiftData 查询。@MainActor。
|
||||
@@ -265,7 +414,8 @@ struct HealthExportService {
|
||||
predicate: #Predicate { $0.capturedAt >= fromDate && $0.capturedAt <= toDate },
|
||||
sortBy: [SortDescriptor(\.capturedAt, order: .reverse)]
|
||||
)
|
||||
var indicators = (try? ctx.fetch(indDesc)) ?? []
|
||||
let allInWindow = (try? ctx.fetch(indDesc)) ?? []
|
||||
var indicators = allInWindow
|
||||
if !intent.keywords.isEmpty {
|
||||
let filtered = indicators.filter { ind in
|
||||
intent.keywords.contains { kw in
|
||||
@@ -328,6 +478,14 @@ struct HealthExportService {
|
||||
// —— Profile(单例) ——
|
||||
let profile = UserProfileStore.loadOrCreate(in: ctx)
|
||||
|
||||
// —— 趋势(确定性,不进 LLM) ——
|
||||
// 用全量 in-window 还原完整序列;裁剪后的 indicators 决定哪些 series 相关。
|
||||
let trends = ExportTrendBuilder.build(
|
||||
allInWindow: allInWindow,
|
||||
relevant: indicators,
|
||||
profile: profile
|
||||
)
|
||||
|
||||
return Snapshot(
|
||||
fromDate: fromDate,
|
||||
toDate: toDate,
|
||||
@@ -335,8 +493,44 @@ struct HealthExportService {
|
||||
symptoms: symptoms,
|
||||
reports: reports,
|
||||
diaries: diaries,
|
||||
profile: profile,
|
||||
trends: trends
|
||||
)
|
||||
}
|
||||
|
||||
/// 多轮导出使用全量指标 + 健康日记作为上下文。为控制 prompt 体积,日记正文在序列化阶段截断。
|
||||
static func retrieveDialogueSnapshot(ctx: ModelContext) -> Snapshot {
|
||||
let indicatorDesc = FetchDescriptor<Indicator>(
|
||||
sortBy: [SortDescriptor(\.capturedAt, order: .reverse)]
|
||||
)
|
||||
let diaryDesc = FetchDescriptor<DiaryEntry>(
|
||||
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
|
||||
)
|
||||
let indicators = (try? ctx.fetch(indicatorDesc)) ?? []
|
||||
let diaries = (try? ctx.fetch(diaryDesc)) ?? []
|
||||
let profile = UserProfileStore.loadOrCreate(in: ctx)
|
||||
|
||||
let dates = indicators.map(\.capturedAt) + diaries.map(\.createdAt)
|
||||
let fromDate = dates.min() ?? Date()
|
||||
let toDate = dates.max() ?? Date()
|
||||
|
||||
// 多轮导出用全量指标,全部视为相关。
|
||||
let trends = ExportTrendBuilder.build(
|
||||
allInWindow: indicators,
|
||||
relevant: indicators,
|
||||
profile: profile
|
||||
)
|
||||
|
||||
return Snapshot(
|
||||
fromDate: fromDate,
|
||||
toDate: toDate,
|
||||
indicators: indicators,
|
||||
symptoms: [],
|
||||
reports: [],
|
||||
diaries: diaries,
|
||||
profile: profile,
|
||||
trends: trends
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Phase 3: serialize data for prompt
|
||||
@@ -480,6 +674,12 @@ struct HealthExportService {
|
||||
"""
|
||||
}
|
||||
|
||||
/// 把趋势行拼成追加到 LLM 输出末尾的「## 指标趋势」段。空 → 返回空串(整段省略)。
|
||||
static func trendSection(_ trends: [ExportTrend]) -> String {
|
||||
guard !trends.isEmpty else { return "" }
|
||||
return "\n\n## 指标趋势\n" + trends.map { $0.line() }.joined(separator: "\n")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// 把 SwiftData persistentModelID 编成稳定字符串。
|
||||
|
||||
Reference in New Issue
Block a user