에이전트 루프 수명주기와 tool_result 복원 품질을 마감한다
목적: - AgentLoopService 시작/종료 책임을 분리해 루프 본체를 더 얇은 orchestration 구조로 정리한다. - 장기 세션과 분기 대화에서 tool_result preview가 다른 tool_use_id로 재등장해도 안정적으로 복원되도록 replacement state를 보강한다. - DOCX/PPTX/XLSX 반환 경로까지 문서 품질 출력 formatter를 통일해 포맷별 quality summary와 repair guide 표현을 일관되게 맞춘다. 핵심 수정: - AgentLoopRunLifecycle.cs를 추가해 BeginRun/BootstrapRunAsync/FinalizeRun/ResetRunTransientState를 분리하고, AgentLoopService는 해당 helper를 사용하도록 정리했다. - run 종료 metric에 빈 run id가 기록되던 흐름을 수정해 실제 run id가 유지되도록 고쳤다. - AgentMessageInvariantHelper에 persisted preview map, fingerprint preview map, tool_use_id 재바인딩 로직을 추가해 저장/재개/분기 이후 preview 복원 품질을 높였다. - ArtifactQualityOutputFormatter를 DocxSkill, PptxSkill, ExcelSkill 멀티시트 출력 경로까지 연결해 quality summary/repair guide 문자열 조립을 공통 helper로 통일했다. - AgentMessageInvariantHelperTests에 fingerprint 재바인딩 회귀를 추가했다. - README.md, docs/DEVELOPMENT.md, docs/NEXT_ROADMAP.md에 2026-04-15 10:05 (KST) 기준 변경 이력을 반영했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_run_finalize\\ -p:IntermediateOutputPath=obj\\verify_run_finalize\\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentCommandQueueTests|AgentQueuedCommandProjectorTests|AgentMessageInvariantHelperTests|AgentQueryContextBuilderTests|ChatStorageServiceTests|PptxSkillGoldenDeckTests|ExcelSkillGoldenWorkbookTests|DocxSkillGoldenDocumentTests|HtmlSkillGoldenReportTests -p:OutputPath=bin\\verify_run_finalize_tests2\\ -p:IntermediateOutputPath=obj\\verify_run_finalize_tests2\\ : 통과 18
This commit is contained in:
@@ -1948,3 +1948,12 @@ MIT License
|
||||
- 테스트는 [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs)와 [WorkspaceContextGeneratorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs)를 확장해 workflow summary와 workspace context 내 language workflow 섹션을 회귀 검증합니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_language_workflow\\ -p:IntermediateOutputPath=obj\\verify_language_workflow\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests" -p:OutputPath=bin\\verify_language_workflow_tests\\ -p:IntermediateOutputPath=obj\\verify_language_workflow_tests\\` 통과 33
|
||||
|
||||
업데이트: 2026-04-15 10:05 (KST)
|
||||
- 에이전틱 루프의 시작/종료 책임을 helper로 더 분리했습니다. 새 [AgentLoopRunLifecycle.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopRunLifecycle.cs)는 run bootstrap, runtime finalization, transient state reset을 담당하고, [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 실제 반복 orchestration에 더 집중하도록 정리했습니다.
|
||||
- 이 과정에서 장기 세션 로그 정확도도 함께 보강했습니다. run 종료 후 `_currentRunId`를 먼저 비워 성능 로그에 빈 run id가 남던 흐름을 고쳐, 종료 metric에도 실제 run id가 안정적으로 기록됩니다.
|
||||
- `tool_result` preview 복원은 `tool_use_id`뿐 아니라 fingerprint 재바인딩까지 지원합니다. [AgentMessageInvariantHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentMessageInvariantHelper.cs)는 같은 tool output이 다른 세션/분기에서 재등장할 때 기존 preview를 현재 `tool_use_id`에 맞춰 다시 묶어 저장/재개 후 query preview 일관성을 더 높입니다.
|
||||
- 문서 포맷 품질 출력도 DOCX/PPTX/Excel 멀티시트 경로까지 공통화했습니다. [ArtifactQualityOutputFormatter.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityOutputFormatter.cs)를 [DocxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DocxSkill.cs), [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs), [ExcelSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ExcelSkill.cs)에도 연결해 quality summary와 repair guide 반환 형식을 한 줄기로 맞췄습니다.
|
||||
- 테스트는 [AgentMessageInvariantHelperTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentMessageInvariantHelperTests.cs)에 fingerprint 재바인딩 회귀를 추가했고, golden 회귀는 [PptxSkillGoldenDeckTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillGoldenDeckTests.cs), [ExcelSkillGoldenWorkbookTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ExcelSkillGoldenWorkbookTests.cs), [DocxSkillGoldenDocumentTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DocxSkillGoldenDocumentTests.cs), [HtmlSkillGoldenReportTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillGoldenReportTests.cs) 기준으로 다시 확인했습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_run_finalize\\ -p:IntermediateOutputPath=obj\\verify_run_finalize\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|AgentQueuedCommandProjectorTests|AgentMessageInvariantHelperTests|AgentQueryContextBuilderTests|ChatStorageServiceTests|PptxSkillGoldenDeckTests|ExcelSkillGoldenWorkbookTests|DocxSkillGoldenDocumentTests|HtmlSkillGoldenReportTests" -p:OutputPath=bin\\verify_run_finalize_tests2\\ -p:IntermediateOutputPath=obj\\verify_run_finalize_tests2\\` 통과 18
|
||||
|
||||
@@ -1022,3 +1022,26 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_language_workflow\\ -p:IntermediateOutputPath=obj\\verify_language_workflow\\`
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests" -p:OutputPath=bin\\verify_language_workflow_tests\\ -p:IntermediateOutputPath=obj\\verify_language_workflow_tests\\`
|
||||
|
||||
업데이트: 2026-04-15 10:05 (KST)
|
||||
- `AgentLoopRunLifecycle.cs` 추가:
|
||||
- `BeginRun()`으로 run stopwatch, user query, iteration budget, retry budget 초기화
|
||||
- `BootstrapRunAsync()`로 intent 분류, exploration/path state, session learnings, task/execution policy 계산 분리
|
||||
- `FinalizeRun()`으로 run summary metric, exploration breadth, stats 기록, transient state reset 일원화
|
||||
- `AgentLoopService.RunAsync()` 정리:
|
||||
- 시작부의 `run id/iteration budget/intent bootstrap` 블록을 helper 호출로 대체
|
||||
- 종료부의 run summary logging을 helper 호출로 대체
|
||||
- 종료 metric에 빈 run id가 남던 흐름을 수정해 실제 run id를 유지
|
||||
- `AgentMessageInvariantHelper` 고도화:
|
||||
- persisted preview map과 fingerprint preview map을 분리
|
||||
- 같은 tool output이 다른 `tool_use_id`로 재등장할 때 preview를 현재 id로 재바인딩
|
||||
- preview 탐색 우선순위를 `저장된 preview → fingerprint 재바인딩 → synthetic` 순서로 고정
|
||||
- 문서 품질 출력 경로 정리:
|
||||
- `ArtifactQualityOutputFormatter`를 `DocxSkill`, `PptxSkill`, `ExcelSkill` 멀티시트 반환 경로까지 연결
|
||||
- 포맷별 quality summary/repair guide 문자열 조립을 공통 helper로 통일
|
||||
- 테스트 보강:
|
||||
- `AgentMessageInvariantHelperTests`: fingerprint 기반 preview 재바인딩 회귀 추가
|
||||
- 재검증 대상: `AgentCommandQueueTests`, `AgentQueuedCommandProjectorTests`, `AgentQueryContextBuilderTests`, `ChatStorageServiceTests`, `PptxSkillGoldenDeckTests`, `ExcelSkillGoldenWorkbookTests`, `DocxSkillGoldenDocumentTests`, `HtmlSkillGoldenReportTests`
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_run_finalize\\ -p:IntermediateOutputPath=obj\\verify_run_finalize\\`
|
||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|AgentQueuedCommandProjectorTests|AgentMessageInvariantHelperTests|AgentQueryContextBuilderTests|ChatStorageServiceTests|PptxSkillGoldenDeckTests|ExcelSkillGoldenWorkbookTests|DocxSkillGoldenDocumentTests|HtmlSkillGoldenReportTests" -p:OutputPath=bin\\verify_run_finalize_tests2\\ -p:IntermediateOutputPath=obj\\verify_run_finalize_tests2\\`
|
||||
|
||||
@@ -150,3 +150,10 @@
|
||||
1. 개발언어 고도화는 `지원 언어 표시`를 넘어서 `Language Workflow`를 실제 컨텍스트에 주입하는 단계로 넘어갔습니다.
|
||||
2. 다음 구현 배치는 `AgentLoopService` 세분화, `tool_result replacement state` 장기 세션 고정, 문서 포맷 공통 quality formatter 확장 순으로 진행합니다.
|
||||
3. 문서 포맷은 PPTX가 가장 앞서 있고, DOCX/XLSX/HTML은 공통 critic/repair와 golden 회귀를 같은 수준으로 끌어올리는 마감 단계에 들어갑니다.
|
||||
|
||||
업데이트: 2026-04-15 10:05 (KST)
|
||||
|
||||
### 추가 진행 메모
|
||||
1. 에이전틱 루프는 이제 `queued command projector + run lifecycle helper` 구조까지 들어와 시작/종료 책임이 분리됐습니다. 다음 마감 단계는 `RunAsync` 본체의 iteration pipeline을 더 잘게 나누는 작업입니다.
|
||||
2. `tool_result replacement state`는 synthetic preview를 넘어 fingerprint 재바인딩까지 들어갔습니다. 남은 방향은 compact/branch 이후의 replacement policy를 세션 단위 상태로 더 오래 유지하는 것입니다.
|
||||
3. 문서 포맷은 `ArtifactQualityOutputFormatter`가 HTML/XLSX뿐 아니라 DOCX/PPTX까지 확장되었습니다. 다음 마감은 포맷별 critic/repair 자체를 더 깊게 하고, golden fixture 샘플을 확대하는 단계입니다.
|
||||
|
||||
@@ -55,4 +55,32 @@ public class AgentMessageInvariantHelperTests
|
||||
|
||||
map["call-explicit"].Should().Be(explicitPreview);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PopulateMissingToolResultPreviews_ShouldRebindPreview_WhenFingerprintMatchesDifferentToolUseId()
|
||||
{
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new()
|
||||
{
|
||||
MsgId = "tool-result-1",
|
||||
Role = "user",
|
||||
Content = """{"type":"tool_result","tool_use_id":"call-original","tool_name":"file_read","content":"same long output that should map across sessions"}""",
|
||||
QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-original","tool_name":"file_read","content":"stable preview"}"""
|
||||
},
|
||||
new()
|
||||
{
|
||||
MsgId = "tool-result-2",
|
||||
Role = "user",
|
||||
Content = """{"type":"tool_result","tool_use_id":"call-replayed","tool_name":"file_read","content":"same long output that should map across sessions"}"""
|
||||
}
|
||||
};
|
||||
|
||||
var changed = AgentMessageInvariantHelper.PopulateMissingToolResultPreviews(messages);
|
||||
|
||||
changed.Should().BeTrue();
|
||||
messages[1].QueryPreviewContent.Should().Contain("call-replayed");
|
||||
messages[1].QueryPreviewContent.Should().Contain("stable preview");
|
||||
messages[1].QueryPreviewContent.Should().NotContain("call-original");
|
||||
}
|
||||
}
|
||||
|
||||
228
src/AxCopilot/Services/Agent/AgentLoopRunLifecycle.cs
Normal file
228
src/AxCopilot/Services/Agent/AgentLoopRunLifecycle.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using System.Diagnostics;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
private sealed record AgentLoopRunBootstrap(
|
||||
Stopwatch RunStopwatch,
|
||||
string UserQuery,
|
||||
int MaxIterations,
|
||||
int MaxRetry,
|
||||
IntentResult IntentResult,
|
||||
ExplorationTrackingState ExplorationState,
|
||||
PathAccessTrackingState PathAccessState,
|
||||
SessionLearningCollector? SessionLearnings,
|
||||
TaskTypePolicy TaskPolicy,
|
||||
ModelExecutionProfileCatalog.ExecutionPolicy ExecutionPolicy);
|
||||
|
||||
private sealed record AgentLoopRunFinalizerState(
|
||||
string RunId,
|
||||
Stopwatch RunStopwatch,
|
||||
DateTime StatsStart,
|
||||
ExplorationTrackingState ExplorationState,
|
||||
TaskTypePolicy TaskPolicy,
|
||||
int Iteration,
|
||||
int TotalToolCalls,
|
||||
int StatsSuccessCount,
|
||||
int StatsFailCount,
|
||||
int StatsInputTokens,
|
||||
int StatsOutputTokens,
|
||||
int StatsRepeatedFailureBlocks,
|
||||
int StatsRecoveredAfterFailure,
|
||||
IReadOnlyList<string> StatsUsedTools,
|
||||
IReadOnlyDictionary<string, int> FailedToolHistogram);
|
||||
|
||||
private (Stopwatch RunStopwatch, string UserQuery, int MaxIterations, int MaxRetry) BeginRun(List<ChatMessage> messages)
|
||||
{
|
||||
var runStopwatch = Stopwatch.StartNew();
|
||||
IsRunning = true;
|
||||
_currentRunId = Guid.NewGuid().ToString("N");
|
||||
_docFallbackAttempted = false;
|
||||
_documentPlanApproved = false;
|
||||
_pendingCommands.Clear();
|
||||
|
||||
var llm = _settings.Settings.Llm;
|
||||
var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25;
|
||||
var maxRetry = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3;
|
||||
var userQuery = messages.LastOrDefault(m => m.Role == "user")?.Content ?? "";
|
||||
return (runStopwatch, userQuery, baseMax, maxRetry);
|
||||
}
|
||||
|
||||
private async Task<AgentLoopRunBootstrap> BootstrapRunAsync(
|
||||
List<ChatMessage> messages,
|
||||
Stopwatch runStopwatch,
|
||||
string userQuery,
|
||||
int maxIterations,
|
||||
int maxRetry,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var intentGate = new IntentGateService(_llm);
|
||||
var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false);
|
||||
var explorationState = new ExplorationTrackingState
|
||||
{
|
||||
Scope = intentResult.SuggestedScope,
|
||||
SelectiveHit = true,
|
||||
};
|
||||
var pathAccessState = new PathAccessTrackingState();
|
||||
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
||||
? new SessionLearningCollector(_settings.Settings.Llm.MaxSessionLearnings)
|
||||
: null;
|
||||
var taskPolicy = TaskTypePolicy.FromTaskType(intentResult.TaskType);
|
||||
var executionPolicy = ExecutionPolicyMerger.Apply(
|
||||
_llm.GetActiveExecutionPolicy(),
|
||||
intentResult.PolicyOverlay);
|
||||
|
||||
maxRetry = ComputeAdaptiveMaxRetry(maxRetry, taskPolicy.TaskType);
|
||||
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
|
||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
||||
|
||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
||||
|
||||
if (intentResult.IsComplexTask && !string.IsNullOrWhiteSpace(intentResult.DecompositionHint))
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = $"[System:DecompositionHint]\n{intentResult.DecompositionHint}\n" +
|
||||
"Consider using spawn_agents to run independent sub-tasks in parallel.",
|
||||
MetaKind = "decomposition_hint",
|
||||
});
|
||||
}
|
||||
|
||||
return new AgentLoopRunBootstrap(
|
||||
runStopwatch,
|
||||
userQuery,
|
||||
maxIterations,
|
||||
maxRetry,
|
||||
intentResult,
|
||||
explorationState,
|
||||
pathAccessState,
|
||||
sessionLearnings,
|
||||
taskPolicy,
|
||||
executionPolicy);
|
||||
}
|
||||
|
||||
private void FinalizeRun(AgentLoopRunFinalizerState state)
|
||||
{
|
||||
var compactNoiseSuppressed = _runPostCompactionSuppressedThinkingCount;
|
||||
var compactedToolResultCount = _runPostCompactionToolResultCompactions;
|
||||
|
||||
WorkflowLogService.LogAgentLifecycle(
|
||||
_conversationId,
|
||||
state.RunId,
|
||||
"end",
|
||||
summary: $"iterations={state.Iteration}, tools={state.TotalToolCalls}, success={state.StatsSuccessCount}, fail={state.StatsFailCount}");
|
||||
|
||||
if (state.TotalToolCalls > 0)
|
||||
{
|
||||
AgentPerformanceLogService.LogExplorationBreadth(
|
||||
_conversationId,
|
||||
ActiveTab,
|
||||
new
|
||||
{
|
||||
scope = state.ExplorationState.Scope.ToString().ToLowerInvariant(),
|
||||
folder_map_calls = state.ExplorationState.FolderMapCalls,
|
||||
total_files_read = state.ExplorationState.TotalFilesRead,
|
||||
multi_read_files = state.ExplorationState.MultiReadFilesRead,
|
||||
broad_scan = state.ExplorationState.BroadScanDetected,
|
||||
selective_hit = state.ExplorationState.SelectiveHit,
|
||||
corrective_hint = state.ExplorationState.CorrectiveHintInjected
|
||||
});
|
||||
|
||||
var durationMs = (long)(DateTime.Now - state.StatsStart).TotalMilliseconds;
|
||||
AgentStatsService.RecordSession(new AgentStatsService.AgentSessionRecord
|
||||
{
|
||||
Timestamp = state.StatsStart,
|
||||
Tab = ActiveTab ?? "",
|
||||
TaskType = state.TaskPolicy.TaskType,
|
||||
Model = _settings.Settings.Llm.Model ?? "",
|
||||
ToolCalls = state.TotalToolCalls,
|
||||
SuccessCount = state.StatsSuccessCount,
|
||||
FailCount = state.StatsFailCount,
|
||||
InputTokens = state.StatsInputTokens,
|
||||
OutputTokens = state.StatsOutputTokens,
|
||||
DurationMs = durationMs,
|
||||
RepeatedFailureBlockedCount = state.StatsRepeatedFailureBlocks,
|
||||
RecoveredAfterFailureCount = state.StatsRecoveredAfterFailure,
|
||||
UsedTools = [.. state.StatsUsedTools],
|
||||
});
|
||||
|
||||
var llm = _settings.Settings.Llm;
|
||||
if (llm.ShowTotalCallStats)
|
||||
{
|
||||
var totalTokens = state.StatsInputTokens + state.StatsOutputTokens;
|
||||
var durationSec = durationMs / 1000.0;
|
||||
var toolList = string.Join(", ", state.StatsUsedTools);
|
||||
var retryTotal = state.StatsRepeatedFailureBlocks + state.StatsRecoveredAfterFailure;
|
||||
var retryQuality = retryTotal > 0
|
||||
? $"{(state.StatsRecoveredAfterFailure * 100.0 / retryTotal):F0}%"
|
||||
: "100%";
|
||||
var compactNoiseSummary = compactNoiseSuppressed > 0
|
||||
? $" | compact 로그 축약 {compactNoiseSuppressed}건"
|
||||
: "";
|
||||
var compactToolResultSummary = compactedToolResultCount > 0
|
||||
? $" | compact 결과 축약 {compactedToolResultCount}건"
|
||||
: "";
|
||||
var topFailed = BuildTopFailureSummary(new Dictionary<string, int>(state.FailedToolHistogram, StringComparer.OrdinalIgnoreCase));
|
||||
var summary = $"📊 전체 통계: LLM {state.Iteration}회 호출 | 도구 {state.TotalToolCalls}회 (성공 {state.StatsSuccessCount}, 실패 {state.StatsFailCount}) | " +
|
||||
$"토큰 {state.StatsInputTokens:N0}→{state.StatsOutputTokens:N0} (합계 {totalTokens:N0}) | " +
|
||||
$"소요 {durationSec:F1}초 | 재시도 품질 {retryQuality} (복구 {state.StatsRecoveredAfterFailure}, 차단 {state.StatsRepeatedFailureBlocks}){compactNoiseSummary}{compactToolResultSummary} | " +
|
||||
$"실패 상위: {topFailed} | 사용 도구: {toolList}";
|
||||
EmitEvent(AgentEventType.StepDone, "total_stats", summary);
|
||||
}
|
||||
}
|
||||
|
||||
state.RunStopwatch.Stop();
|
||||
AgentPerformanceLogService.LogMetric(
|
||||
"agent_loop",
|
||||
"run_summary",
|
||||
_conversationId,
|
||||
ActiveTab ?? "",
|
||||
state.RunStopwatch.ElapsedMilliseconds,
|
||||
new
|
||||
{
|
||||
runId = state.RunId,
|
||||
iterations = state.Iteration,
|
||||
toolCalls = state.TotalToolCalls,
|
||||
toolSuccess = state.StatsSuccessCount,
|
||||
toolFail = state.StatsFailCount,
|
||||
inputTokens = state.StatsInputTokens,
|
||||
outputTokens = state.StatsOutputTokens,
|
||||
repeatedFailureBlocks = state.StatsRepeatedFailureBlocks,
|
||||
recoveredAfterFailure = state.StatsRecoveredAfterFailure,
|
||||
postCompactionNoiseSuppressed = compactNoiseSuppressed,
|
||||
postCompactionToolResultCompactions = compactedToolResultCount,
|
||||
taskType = state.TaskPolicy.TaskType,
|
||||
model = _settings.Settings.Llm.Model ?? "",
|
||||
});
|
||||
|
||||
ResetRunTransientState();
|
||||
}
|
||||
|
||||
private void ResetRunTransientState()
|
||||
{
|
||||
IsRunning = false;
|
||||
_currentRunId = "";
|
||||
_runPendingPostCompactionTurn = false;
|
||||
_runPostCompactionTurnCounter = 0;
|
||||
_runPostCompactionSuppressedThinkingCount = 0;
|
||||
_runLastCompactionStageSummary = "";
|
||||
_runLastCompactionSavedTokens = 0;
|
||||
_runPostCompactionToolResultCompactions = 0;
|
||||
|
||||
if (IsPaused)
|
||||
{
|
||||
IsPaused = false;
|
||||
try
|
||||
{
|
||||
_pauseSemaphore.Release();
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,34 +233,21 @@ public partial class AgentLoopService
|
||||
{
|
||||
if (IsRunning) throw new InvalidOperationException("에이전트가 이미 실행 중입니다.");
|
||||
|
||||
var runStopwatch = Stopwatch.StartNew();
|
||||
IsRunning = true;
|
||||
_currentRunId = Guid.NewGuid().ToString("N");
|
||||
_docFallbackAttempted = false;
|
||||
_documentPlanApproved = false;
|
||||
_pendingCommands.Clear(); // 이전 실행의 잔여 큐 제거
|
||||
var llm = _settings.Settings.Llm;
|
||||
var baseMax = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25;
|
||||
var maxIterations = baseMax; // 동적 조정 가능
|
||||
var maxRetry = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3;
|
||||
var iteration = 0;
|
||||
|
||||
// 사용자 원본 요청 캡처 (문서 생성 폴백 판단용)
|
||||
var userQuery = messages.LastOrDefault(m => m.Role == "user")?.Content ?? "";
|
||||
// IntentGate: 통합 의도 분류
|
||||
var intentGate = new IntentGateService(_llm);
|
||||
var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false);
|
||||
|
||||
var explorationState = new ExplorationTrackingState
|
||||
{
|
||||
Scope = intentResult.SuggestedScope,
|
||||
SelectiveHit = true,
|
||||
};
|
||||
var pathAccessState = new PathAccessTrackingState();
|
||||
// P3: 누적 학습 — 도구 결과에서 자동 학습 포인트 수집
|
||||
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
||||
? new SessionLearningCollector(_settings.Settings.Llm.MaxSessionLearnings)
|
||||
: null;
|
||||
var bootstrap = BeginRun(messages);
|
||||
var runStopwatch = bootstrap.RunStopwatch;
|
||||
var userQuery = bootstrap.UserQuery;
|
||||
var maxIterations = bootstrap.MaxIterations;
|
||||
var baseMax = bootstrap.MaxIterations;
|
||||
var maxRetry = bootstrap.MaxRetry;
|
||||
var runBootstrap = await BootstrapRunAsync(messages, runStopwatch, userQuery, maxIterations, maxRetry, ct).ConfigureAwait(false);
|
||||
maxIterations = runBootstrap.MaxIterations;
|
||||
maxRetry = runBootstrap.MaxRetry;
|
||||
var intentResult = runBootstrap.IntentResult;
|
||||
var explorationState = runBootstrap.ExplorationState;
|
||||
var pathAccessState = runBootstrap.PathAccessState;
|
||||
var sessionLearnings = runBootstrap.SessionLearnings;
|
||||
DateTime? lastToolResultAtUtc = null;
|
||||
string? lastToolResultToolName = null;
|
||||
|
||||
@@ -304,9 +291,8 @@ public partial class AgentLoopService
|
||||
string? lastArtifactFilePath = null; // 생성/수정 산출물 파일 추적
|
||||
var statsModifiedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // 수정/생성된 파일 추적
|
||||
var taskType = intentResult.TaskType;
|
||||
var taskPolicy = TaskTypePolicy.FromTaskType(taskType);
|
||||
var executionPolicy = ExecutionPolicyMerger.Apply(
|
||||
_llm.GetActiveExecutionPolicy(), intentResult.PolicyOverlay);
|
||||
var taskPolicy = runBootstrap.TaskPolicy;
|
||||
var executionPolicy = runBootstrap.ExecutionPolicy;
|
||||
var consecutiveNoToolResponses = 0;
|
||||
var noToolResponseThreshold = AgentLoopRuntimeThresholds.GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold);
|
||||
var noToolRecoveryMaxRetries = AgentLoopRuntimeThresholds.GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries);
|
||||
@@ -317,25 +303,8 @@ public partial class AgentLoopService
|
||||
var runState = new RunState();
|
||||
var requireHighImpactCodeVerification = false;
|
||||
string? lastModifiedCodeFilePath = null;
|
||||
maxRetry = ComputeAdaptiveMaxRetry(maxRetry, taskPolicy.TaskType);
|
||||
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
|
||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
||||
|
||||
var context = BuildContext();
|
||||
InjectTaskTypeGuidance(messages, taskPolicy);
|
||||
InjectExplorationScopeGuidance(messages, explorationState.Scope);
|
||||
|
||||
// P5: 복합 요청 감지 시 DecompositionHint 주입
|
||||
if (intentResult.IsComplexTask && !string.IsNullOrWhiteSpace(intentResult.DecompositionHint))
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = $"[System:DecompositionHint]\n{intentResult.DecompositionHint}\n" +
|
||||
"Consider using spawn_agents to run independent sub-tasks in parallel.",
|
||||
MetaKind = "decomposition_hint",
|
||||
});
|
||||
}
|
||||
|
||||
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||
explorationState,
|
||||
@@ -1869,112 +1838,23 @@ public partial class AgentLoopService
|
||||
{
|
||||
if (runtimeOverrideApplied)
|
||||
_llm.PopInferenceOverride();
|
||||
|
||||
// 워크플로우 상세 로그: 에이전트 루프 종료
|
||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "end",
|
||||
summary: $"iterations={iteration}, tools={totalToolCalls}, success={statsSuccessCount}, fail={statsFailCount}");
|
||||
|
||||
IsRunning = false;
|
||||
_currentRunId = "";
|
||||
_runPendingPostCompactionTurn = false;
|
||||
_runPostCompactionTurnCounter = 0;
|
||||
_runPostCompactionSuppressedThinkingCount = 0;
|
||||
_runLastCompactionStageSummary = "";
|
||||
_runLastCompactionSavedTokens = 0;
|
||||
_runPostCompactionToolResultCompactions = 0;
|
||||
|
||||
// 일시정지 상태 리셋
|
||||
if (IsPaused)
|
||||
{
|
||||
IsPaused = false;
|
||||
try { _pauseSemaphore.Release(); }
|
||||
catch (SemaphoreFullException) { }
|
||||
}
|
||||
|
||||
// 통계 기록 (도구 호출이 1회 이상인 세션만)
|
||||
if (totalToolCalls > 0)
|
||||
{
|
||||
AgentPerformanceLogService.LogExplorationBreadth(
|
||||
_conversationId,
|
||||
ActiveTab,
|
||||
new
|
||||
{
|
||||
scope = explorationState.Scope.ToString().ToLowerInvariant(),
|
||||
folder_map_calls = explorationState.FolderMapCalls,
|
||||
total_files_read = explorationState.TotalFilesRead,
|
||||
multi_read_files = explorationState.MultiReadFilesRead,
|
||||
broad_scan = explorationState.BroadScanDetected,
|
||||
selective_hit = explorationState.SelectiveHit,
|
||||
corrective_hint = explorationState.CorrectiveHintInjected
|
||||
});
|
||||
|
||||
var durationMs = (long)(DateTime.Now - statsStart).TotalMilliseconds;
|
||||
|
||||
AgentStatsService.RecordSession(new AgentStatsService.AgentSessionRecord
|
||||
{
|
||||
Timestamp = statsStart,
|
||||
Tab = ActiveTab ?? "",
|
||||
TaskType = taskPolicy.TaskType,
|
||||
Model = _settings.Settings.Llm.Model ?? "",
|
||||
ToolCalls = totalToolCalls,
|
||||
SuccessCount = statsSuccessCount,
|
||||
FailCount = statsFailCount,
|
||||
InputTokens = statsInputTokens,
|
||||
OutputTokens = statsOutputTokens,
|
||||
DurationMs = durationMs,
|
||||
RepeatedFailureBlockedCount = statsRepeatedFailureBlocks,
|
||||
RecoveredAfterFailureCount = statsRecoveredAfterFailure,
|
||||
UsedTools = statsUsedTools,
|
||||
});
|
||||
|
||||
// 전체 호출·토큰 합계 표시 (개발자 모드 설정)
|
||||
if (llm.ShowTotalCallStats)
|
||||
{
|
||||
var totalTokens = statsInputTokens + statsOutputTokens;
|
||||
var durationSec = durationMs / 1000.0;
|
||||
var toolList = string.Join(", ", statsUsedTools);
|
||||
var retryTotal = statsRepeatedFailureBlocks + statsRecoveredAfterFailure;
|
||||
var retryQuality = retryTotal > 0
|
||||
? $"{(statsRecoveredAfterFailure * 100.0 / retryTotal):F0}%"
|
||||
: "100%";
|
||||
var compactNoiseSummary = _runPostCompactionSuppressedThinkingCount > 0
|
||||
? $" | compact 로그 축약 {_runPostCompactionSuppressedThinkingCount}건"
|
||||
: "";
|
||||
var compactToolResultSummary = _runPostCompactionToolResultCompactions > 0
|
||||
? $" | compact 결과 축약 {_runPostCompactionToolResultCompactions}건"
|
||||
: "";
|
||||
var topFailed = BuildTopFailureSummary(failedToolHistogram);
|
||||
var summary = $"📊 전체 통계: LLM {iteration}회 호출 | 도구 {totalToolCalls}회 (성공 {statsSuccessCount}, 실패 {statsFailCount}) | " +
|
||||
$"토큰 {statsInputTokens:N0}→{statsOutputTokens:N0} (합계 {totalTokens:N0}) | " +
|
||||
$"소요 {durationSec:F1}초 | 재시도 품질 {retryQuality} (복구 {statsRecoveredAfterFailure}, 차단 {statsRepeatedFailureBlocks}){compactNoiseSummary}{compactToolResultSummary} | " +
|
||||
$"실패 상위: {topFailed} | 사용 도구: {toolList}";
|
||||
EmitEvent(AgentEventType.StepDone, "total_stats", summary);
|
||||
}
|
||||
}
|
||||
|
||||
runStopwatch.Stop();
|
||||
AgentPerformanceLogService.LogMetric(
|
||||
"agent_loop",
|
||||
"run_summary",
|
||||
_conversationId,
|
||||
ActiveTab ?? "",
|
||||
runStopwatch.ElapsedMilliseconds,
|
||||
new
|
||||
{
|
||||
runId = _currentRunId,
|
||||
iterations = iteration,
|
||||
toolCalls = totalToolCalls,
|
||||
toolSuccess = statsSuccessCount,
|
||||
toolFail = statsFailCount,
|
||||
inputTokens = statsInputTokens,
|
||||
outputTokens = statsOutputTokens,
|
||||
repeatedFailureBlocks = statsRepeatedFailureBlocks,
|
||||
recoveredAfterFailure = statsRecoveredAfterFailure,
|
||||
postCompactionNoiseSuppressed = _runPostCompactionSuppressedThinkingCount,
|
||||
postCompactionToolResultCompactions = _runPostCompactionToolResultCompactions,
|
||||
taskType = taskPolicy.TaskType,
|
||||
model = _settings.Settings.Llm.Model ?? "",
|
||||
});
|
||||
var completedRunId = _currentRunId;
|
||||
FinalizeRun(new AgentLoopRunFinalizerState(
|
||||
completedRunId,
|
||||
runStopwatch,
|
||||
statsStart,
|
||||
explorationState,
|
||||
taskPolicy,
|
||||
iteration,
|
||||
totalToolCalls,
|
||||
statsSuccessCount,
|
||||
statsFailCount,
|
||||
statsInputTokens,
|
||||
statsOutputTokens,
|
||||
statsRepeatedFailureBlocks,
|
||||
statsRecoveredAfterFailure,
|
||||
statsUsedTools,
|
||||
failedToolHistogram));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace AxCopilot.Services.Agent;
|
||||
internal static class AgentMessageInvariantHelper
|
||||
{
|
||||
private const int SyntheticPreviewTextLimit = 280;
|
||||
private const int PreviewFingerprintTextLimit = 480;
|
||||
|
||||
public static Dictionary<string, string> BuildToolResultPreviewMap(IEnumerable<ChatMessage>? messages)
|
||||
{
|
||||
@@ -46,8 +47,9 @@ internal static class AgentMessageInvariantHelper
|
||||
if (messages == null || messages.Count == 0)
|
||||
return false;
|
||||
|
||||
var previews = BuildToolResultPreviewMap(messages);
|
||||
if (previews.Count == 0)
|
||||
var previews = BuildPersistedToolResultPreviewMap(messages);
|
||||
var previewsByFingerprint = BuildToolResultPreviewFingerprintMap(messages);
|
||||
if (previews.Count == 0 && previewsByFingerprint.Count == 0)
|
||||
return false;
|
||||
|
||||
var changed = false;
|
||||
@@ -57,8 +59,23 @@ internal static class AgentMessageInvariantHelper
|
||||
continue;
|
||||
if (!TryGetToolResultId(message, out var toolResultId))
|
||||
continue;
|
||||
|
||||
if (!previews.TryGetValue(toolResultId, out var preview))
|
||||
{
|
||||
if (!TryGetToolResultFingerprint(message, out var fingerprint)
|
||||
|| !previewsByFingerprint.TryGetValue(fingerprint, out var fingerprintPreview))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
preview = TryRebindToolResultPreview(fingerprintPreview, toolResultId, message, out var reboundPreview)
|
||||
? reboundPreview
|
||||
: fingerprintPreview;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(preview) && !TryBuildSyntheticToolResultPreview(message, out preview))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
message.QueryPreviewContent = preview;
|
||||
changed = true;
|
||||
@@ -217,6 +234,142 @@ internal static class AgentMessageInvariantHelper
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildToolResultPreviewFingerprintMap(IEnumerable<ChatMessage> messages)
|
||||
{
|
||||
var previews = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (!TryGetToolResultFingerprint(message, out var fingerprint))
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(message.QueryPreviewContent))
|
||||
{
|
||||
previews[fingerprint] = message.QueryPreviewContent!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previews.ContainsKey(fingerprint))
|
||||
continue;
|
||||
|
||||
if (TryBuildSyntheticToolResultPreview(message, out var preview))
|
||||
previews[fingerprint] = preview;
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildPersistedToolResultPreviewMap(IEnumerable<ChatMessage> messages)
|
||||
{
|
||||
var previews = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message.QueryPreviewContent))
|
||||
continue;
|
||||
if (!TryGetToolResultId(message, out var toolResultId))
|
||||
continue;
|
||||
|
||||
previews[toolResultId] = message.QueryPreviewContent!;
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
private static bool TryGetToolResultFingerprint(ChatMessage message, out string fingerprint)
|
||||
{
|
||||
fingerprint = "";
|
||||
if (!string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var content = message.Content ?? "";
|
||||
if (!content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
var root = doc.RootElement;
|
||||
var toolName = root.TryGetProperty("tool_name", out var toolNameEl)
|
||||
? toolNameEl.GetString() ?? ""
|
||||
: "";
|
||||
var previewText = ExtractPreviewText(root);
|
||||
if (string.IsNullOrWhiteSpace(previewText))
|
||||
return false;
|
||||
|
||||
if (previewText.Length > PreviewFingerprintTextLimit)
|
||||
previewText = previewText[..PreviewFingerprintTextLimit];
|
||||
|
||||
fingerprint = $"{toolName.Trim().ToLowerInvariant()}::{previewText.Trim()}";
|
||||
return !string.IsNullOrWhiteSpace(fingerprint);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryRebindToolResultPreview(string preview, string toolResultId, ChatMessage message, out string reboundPreview)
|
||||
{
|
||||
reboundPreview = preview;
|
||||
if (string.IsNullOrWhiteSpace(preview) || string.IsNullOrWhiteSpace(toolResultId))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var previewDoc = JsonDocument.Parse(preview);
|
||||
if (previewDoc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
var toolName = TryGetToolResultToolName(message, out var extractedToolName)
|
||||
? extractedToolName
|
||||
: (previewDoc.RootElement.TryGetProperty("tool_name", out var previewToolNameEl)
|
||||
? previewToolNameEl.GetString() ?? ""
|
||||
: "");
|
||||
var content = previewDoc.RootElement.TryGetProperty("content", out var contentEl)
|
||||
? contentEl.GetString() ?? ""
|
||||
: "";
|
||||
|
||||
reboundPreview = JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = toolResultId,
|
||||
tool_name = toolName,
|
||||
content
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetToolResultToolName(ChatMessage message, out string toolName)
|
||||
{
|
||||
toolName = "";
|
||||
if (!string.Equals(message.Role, "user", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var content = message.Content ?? "";
|
||||
if (!content.StartsWith("{\"type\":\"tool_result\"", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (!doc.RootElement.TryGetProperty("tool_name", out var toolNameEl))
|
||||
return false;
|
||||
|
||||
toolName = toolNameEl.GetString() ?? "";
|
||||
return !string.IsNullOrWhiteSpace(toolName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractPreviewText(JsonElement root)
|
||||
{
|
||||
if (root.TryGetProperty("content", out var contentEl))
|
||||
|
||||
@@ -3,8 +3,18 @@ namespace AxCopilot.Services.Agent;
|
||||
public static class ArtifactQualityOutputFormatter
|
||||
{
|
||||
public static IReadOnlyList<string> BuildLines(ArtifactQualityReport review)
|
||||
=> [review.ToToolSummary(), ArtifactRepairGuideService.BuildGuide(review)];
|
||||
=> BuildLines(review.ToToolSummary(), ArtifactRepairGuideService.BuildGuide(review));
|
||||
|
||||
public static IReadOnlyList<string> BuildLines(DeckQualityReport review)
|
||||
=> [review.ToToolSummary(), DeckRepairGuideService.BuildGuide(review)];
|
||||
=> BuildLines(review.ToToolSummary(), DeckRepairGuideService.BuildGuide(review));
|
||||
|
||||
private static IReadOnlyList<string> BuildLines(string summary, string repairGuide)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
lines.Add(summary);
|
||||
if (!string.IsNullOrWhiteSpace(repairGuide))
|
||||
lines.Add(repairGuide);
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,8 +338,7 @@ public class DocxSkill : IAgentTool
|
||||
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "reference", "supplement", "부록", "참고"))
|
||||
));
|
||||
|
||||
parts.Add(review.ToToolSummary());
|
||||
parts.Add(ArtifactRepairGuideService.BuildGuide(review));
|
||||
parts.AddRange(ArtifactQualityOutputFormatter.BuildLines(review));
|
||||
return ToolResult.Ok(
|
||||
$"Word 문서 생성 완료: {fullPath}\n{string.Join(", ", parts)}",
|
||||
fullPath);
|
||||
|
||||
@@ -478,9 +478,13 @@ public class ExcelSkill : IAgentTool
|
||||
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series") || HasSummaryItems(summarySheet, "dashboard_tiles") || HasSummaryItems(summarySheet, "variance_series"),
|
||||
HasStructuredSummaryContent(summarySheet, "decision_summary"),
|
||||
HasSummaryItems(summarySheet, "sheet_summaries")));
|
||||
return ToolResult.Ok(
|
||||
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}\n{review.ToToolSummary()}\n{ArtifactRepairGuideService.BuildGuide(review)}",
|
||||
fullPath);
|
||||
var outputLines = new List<string>
|
||||
{
|
||||
$"Excel 파일 생성 완료: {fullPath}",
|
||||
$"시트: {totalSheets}개, 총 데이터 행: {totalRows}"
|
||||
};
|
||||
outputLines.AddRange(ArtifactQualityOutputFormatter.BuildLines(review));
|
||||
return ToolResult.Ok(string.Join("\n", outputLines), fullPath);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
@@ -1029,10 +1029,7 @@ public class PptxSkill : IAgentTool
|
||||
if (!string.IsNullOrWhiteSpace(templatePackName))
|
||||
outputParts.Add($"Template pack: {templatePackName}");
|
||||
if (deckReview != null)
|
||||
{
|
||||
outputParts.Add(deckReview.ToToolSummary());
|
||||
outputParts.Add(DeckRepairGuideService.BuildGuide(deckReview));
|
||||
}
|
||||
outputParts.AddRange(ArtifactQualityOutputFormatter.BuildLines(deckReview));
|
||||
return ToolResult.Ok(string.Join("\n", outputParts), fullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user