Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 184 additions & 7 deletions src/ai/openAiPredictionClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AiPredictionClient,
AiPredictionPrompt
AiPredictionPrompt,
AiPredictionPromptResponseShape
} from "./types.js"

// workflow log가 과도하게 커지지 않도록 OpenAI 실패 응답 본문을 제한
Expand Down Expand Up @@ -96,6 +97,7 @@ function openAiRequestBodyFor(
model: string
): Record<string, unknown> {
const responseShape = prompt.responseShape ?? "prediction"
const responseFormat = responseFormatFor(responseShape)

return {
model,
Expand All @@ -112,18 +114,45 @@ function openAiRequestBodyFor(
text: {
format: {
type: "json_schema",
name: responseShape === "predictionBatch"
? "ai_prediction_batch"
: "ai_prediction",
name: responseFormat.name,
strict: true,
schema: responseShape === "predictionBatch"
? aiPredictionBatchSchema()
: aiPredictionSchema()
schema: responseFormat.schema
}
}
}
}

// prompt가 요청한 응답 형태를 OpenAI schema 이름과 구조에 매핑
function responseFormatFor(
responseShape: AiPredictionPromptResponseShape
): {
name: string
schema: Record<string, unknown>
} {
switch (responseShape) {
case "predictionBatch":
return {
name: "ai_prediction_batch",
schema: aiPredictionBatchSchema()
}
case "predictionPairConfirmedConflict":
return {
name: "ai_prediction_pair_confirmed_conflict",
schema: aiConfirmedConflictSchema()
}
case "predictionPairCleanOverlap":
return {
name: "ai_prediction_pair_clean_overlap",
schema: aiCleanOverlapSchema()
}
default:
return {
name: "ai_prediction",
schema: aiPredictionSchema()
}
}
}

// OpenAI API가 반환한 HTTP 실패 원문을 보존해 workflow log에서 원인을 확인
async function openAiRequestErrorMessageFor(response: Response): Promise<string> {
const detail = await openAiErrorDetailFor(response)
Expand Down Expand Up @@ -222,6 +251,154 @@ function aiPredictionSchema(): Record<string, unknown> {
}
}

// 확정 conflict 해결 응답을 위한 OpenAI structured output schema
function aiConfirmedConflictSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
kind: {
type: "string",
enum: ["confirmed_conflict"]
},
pair: branchPairSchema(),
conflictCause: causeSchema(),
integrationOrder: integrationOrderSchema(),
patches: {
type: "array",
minItems: 1,
items: patchSchema()
}
},
required: [
"kind",
"pair",
"conflictCause",
"integrationOrder",
"patches"
]
}
}

// clean overlap 예방 응답을 위한 OpenAI structured output schema
function aiCleanOverlapSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
kind: {
type: "string",
enum: ["clean_overlap"]
},
pair: branchPairSchema(),
overlapCause: causeSchema(),
integrationOrder: integrationOrderSchema(),
preventiveActions: {
type: "array",
minItems: 1,
items: preventiveActionSchema()
}
},
required: [
"kind",
"pair",
"overlapCause",
"integrationOrder",
"preventiveActions"
]
}
}

// 좌우 순서를 보존하는 branch 조합 식별 schema
function branchPairSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
leftBranchName: { type: "string" },
rightBranchName: { type: "string" }
},
required: ["leftBranchName", "rightBranchName"]
}
}

// conflict 또는 overlap 원인과 관련 파일 schema
function causeSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
summary: { type: "string" },
files: stringArraySchema()
},
required: ["summary", "files"]
}
}

// merge 또는 rebase 작업 순서 schema
function integrationOrderSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
strategy: {
type: "string",
enum: ["merge", "rebase"]
},
firstBranchName: { type: "string" },
secondBranchName: { type: "string" },
reason: { type: "string" },
steps: {
...stringArraySchema(),
minItems: 1
}
},
required: [
"strategy",
"firstBranchName",
"secondBranchName",
"reason",
"steps"
]
}
}

// 확정 conflict의 파일별 patch schema
function patchSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
filePath: { type: "string" },
patch: { type: "string" },
reason: { type: "string" }
},
required: ["filePath", "patch", "reason"]
}
}

// clean overlap의 예방 조치 schema
function preventiveActionSchema(): Record<string, unknown> {
return {
type: "object",
additionalProperties: false,
properties: {
title: { type: "string" },
description: { type: "string" },
files: stringArraySchema()
},
required: ["title", "description", "files"]
}
}

// 문자열 목록 field의 공통 schema
function stringArraySchema(): Record<string, unknown> {
return {
type: "array",
items: { type: "string" }
}
}

// AI recommended action 하나의 JSON 구조를 OpenAI structured output schema로 표현
function recommendedActionSchema(): Record<string, unknown> {
return {
Expand Down
27 changes: 27 additions & 0 deletions src/ai/predictionPairPromptBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
DEFAULT_AI_CLEAN_OVERLAP_SYSTEM_PROMPT,
DEFAULT_AI_CONFIRMED_CONFLICT_SYSTEM_PROMPT
} from "./predictionPairPromptTemplates.js"
import type {
AiPredictionPairEvidencePayload,
AiPredictionPrompt,
AiPredictionPromptBuildOptions
} from "./types.js"

// branch 조합의 deterministic 상태에 맞는 전용 prompt 구성
export function build(
payload: AiPredictionPairEvidencePayload,
options: AiPredictionPromptBuildOptions = {}
): AiPredictionPrompt {
const confirmedConflict = payload.targetStatus === "confirmed_conflict"

return {
systemPrompt: options.systemPrompt ?? (confirmedConflict
? DEFAULT_AI_CONFIRMED_CONFLICT_SYSTEM_PROMPT
: DEFAULT_AI_CLEAN_OVERLAP_SYSTEM_PROMPT),
userPrompt: JSON.stringify(payload, null, 2),
responseShape: confirmedConflict
? "predictionPairConfirmedConflict"
: "predictionPairCleanOverlap"
}
}
76 changes: 76 additions & 0 deletions src/ai/predictionPairPromptTemplates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 확정 conflict의 원인, 반영 순서, 구체적인 patch를 요청하는 system prompt
export const DEFAULT_AI_CONFIRMED_CONFLICT_SYSTEM_PROMPT = [
"You are Watcher's confirmed merge conflict resolution assistant.",
"Use only the provided branch pair evidence and code context.",
"Preserve the exact 'leftBranchName' and 'rightBranchName' from the input in the 'pair' object without swapping them.",
"Do not recalculate or overwrite the deterministic merge status or graph reasons.",
"Explain the confirmed conflict cause from the provided code only.",
"Recommend a merge or rebase order using only the two provided branch names.",
"Provide at least one concrete patch for files present in the evidence.",
"Do not invent APIs, files, behavior, or code outside the provided context.",
"Do not apply any patch or describe a patch as already applied.",
"Write summaries, reasons, steps, and patch reasons in Korean.",
"Return only JSON with this shape:",
Comment thread
opficdev marked this conversation as resolved.
"{",
" \"kind\": \"confirmed_conflict\",",
" \"pair\": {",
" \"leftBranchName\": string,",
" \"rightBranchName\": string",
" },",
" \"conflictCause\": {",
" \"summary\": string,",
" \"files\": string[]",
" },",
" \"integrationOrder\": {",
" \"strategy\": \"merge\" | \"rebase\",",
" \"firstBranchName\": string,",
" \"secondBranchName\": string,",
" \"reason\": string,",
" \"steps\": string[]",
" },",
" \"patches\": [{",
" \"filePath\": string,",
" \"patch\": string,",
" \"reason\": string",
" }]",
"}"
].join("\n")

// clean overlap을 conflict로 단정하지 않고 예방 조치를 요청하는 system prompt
export const DEFAULT_AI_CLEAN_OVERLAP_SYSTEM_PROMPT = [
"You are Watcher's clean merge overlap prevention assistant.",
"Use only the provided branch pair evidence and code context.",
"Preserve the exact 'leftBranchName' and 'rightBranchName' from the input in the 'pair' object without swapping them.",
"The provided merge status is clean and is not a confirmed conflict.",
"Do not recalculate or overwrite the deterministic merge status or graph reasons.",
"Do not claim that a conflict exists or will occur.",
"Explain only the potential interaction visible in the provided code.",
"Recommend a merge or rebase order using only the two provided branch names.",
"Provide at least one preventive action and do not provide patches.",
"Do not invent APIs, files, behavior, or code outside the provided context.",
"Write summaries, reasons, steps, titles, and descriptions in Korean.",
"Return only JSON with this shape:",
Comment thread
opficdev marked this conversation as resolved.
"{",
" \"kind\": \"clean_overlap\",",
" \"pair\": {",
" \"leftBranchName\": string,",
" \"rightBranchName\": string",
" },",
" \"overlapCause\": {",
" \"summary\": string,",
" \"files\": string[]",
" },",
" \"integrationOrder\": {",
" \"strategy\": \"merge\" | \"rebase\",",
" \"firstBranchName\": string,",
" \"secondBranchName\": string,",
" \"reason\": string,",
" \"steps\": string[]",
" },",
" \"preventiveActions\": [{",
" \"title\": string,",
" \"description\": string,",
" \"files\": string[]",
" }]",
"}"
].join("\n")
Loading