에이전트 루프 수명주기와 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 섹션을 회귀 검증합니다.
|
- 테스트는 [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 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
|
- 검증: `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 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\\`
|
- `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`를 실제 컨텍스트에 주입하는 단계로 넘어갔습니다.
|
1. 개발언어 고도화는 `지원 언어 표시`를 넘어서 `Language Workflow`를 실제 컨텍스트에 주입하는 단계로 넘어갔습니다.
|
||||||
2. 다음 구현 배치는 `AgentLoopService` 세분화, `tool_result replacement state` 장기 세션 고정, 문서 포맷 공통 quality formatter 확장 순으로 진행합니다.
|
2. 다음 구현 배치는 `AgentLoopService` 세분화, `tool_result replacement state` 장기 세션 고정, 문서 포맷 공통 quality formatter 확장 순으로 진행합니다.
|
||||||
3. 문서 포맷은 PPTX가 가장 앞서 있고, DOCX/XLSX/HTML은 공통 critic/repair와 golden 회귀를 같은 수준으로 끌어올리는 마감 단계에 들어갑니다.
|
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);
|
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("에이전트가 이미 실행 중입니다.");
|
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 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 iteration = 0;
|
||||||
|
var bootstrap = BeginRun(messages);
|
||||||
// 사용자 원본 요청 캡처 (문서 생성 폴백 판단용)
|
var runStopwatch = bootstrap.RunStopwatch;
|
||||||
var userQuery = messages.LastOrDefault(m => m.Role == "user")?.Content ?? "";
|
var userQuery = bootstrap.UserQuery;
|
||||||
// IntentGate: 통합 의도 분류
|
var maxIterations = bootstrap.MaxIterations;
|
||||||
var intentGate = new IntentGateService(_llm);
|
var baseMax = bootstrap.MaxIterations;
|
||||||
var intentResult = await intentGate.ClassifyAsync(userQuery, ActiveTab, ct).ConfigureAwait(false);
|
var maxRetry = bootstrap.MaxRetry;
|
||||||
|
var runBootstrap = await BootstrapRunAsync(messages, runStopwatch, userQuery, maxIterations, maxRetry, ct).ConfigureAwait(false);
|
||||||
var explorationState = new ExplorationTrackingState
|
maxIterations = runBootstrap.MaxIterations;
|
||||||
{
|
maxRetry = runBootstrap.MaxRetry;
|
||||||
Scope = intentResult.SuggestedScope,
|
var intentResult = runBootstrap.IntentResult;
|
||||||
SelectiveHit = true,
|
var explorationState = runBootstrap.ExplorationState;
|
||||||
};
|
var pathAccessState = runBootstrap.PathAccessState;
|
||||||
var pathAccessState = new PathAccessTrackingState();
|
var sessionLearnings = runBootstrap.SessionLearnings;
|
||||||
// P3: 누적 학습 — 도구 결과에서 자동 학습 포인트 수집
|
|
||||||
var sessionLearnings = (_settings.Settings.Llm.EnableSessionLearnings)
|
|
||||||
? new SessionLearningCollector(_settings.Settings.Llm.MaxSessionLearnings)
|
|
||||||
: null;
|
|
||||||
DateTime? lastToolResultAtUtc = null;
|
DateTime? lastToolResultAtUtc = null;
|
||||||
string? lastToolResultToolName = null;
|
string? lastToolResultToolName = null;
|
||||||
|
|
||||||
@@ -304,9 +291,8 @@ public partial class AgentLoopService
|
|||||||
string? lastArtifactFilePath = null; // 생성/수정 산출물 파일 추적
|
string? lastArtifactFilePath = null; // 생성/수정 산출물 파일 추적
|
||||||
var statsModifiedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // 수정/생성된 파일 추적
|
var statsModifiedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // 수정/생성된 파일 추적
|
||||||
var taskType = intentResult.TaskType;
|
var taskType = intentResult.TaskType;
|
||||||
var taskPolicy = TaskTypePolicy.FromTaskType(taskType);
|
var taskPolicy = runBootstrap.TaskPolicy;
|
||||||
var executionPolicy = ExecutionPolicyMerger.Apply(
|
var executionPolicy = runBootstrap.ExecutionPolicy;
|
||||||
_llm.GetActiveExecutionPolicy(), intentResult.PolicyOverlay);
|
|
||||||
var consecutiveNoToolResponses = 0;
|
var consecutiveNoToolResponses = 0;
|
||||||
var noToolResponseThreshold = AgentLoopRuntimeThresholds.GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold);
|
var noToolResponseThreshold = AgentLoopRuntimeThresholds.GetNoToolCallResponseThreshold(executionPolicy.NoToolResponseThreshold);
|
||||||
var noToolRecoveryMaxRetries = AgentLoopRuntimeThresholds.GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries);
|
var noToolRecoveryMaxRetries = AgentLoopRuntimeThresholds.GetNoToolCallRecoveryMaxRetries(executionPolicy.NoToolRecoveryMaxRetries);
|
||||||
@@ -317,25 +303,8 @@ public partial class AgentLoopService
|
|||||||
var runState = new RunState();
|
var runState = new RunState();
|
||||||
var requireHighImpactCodeVerification = false;
|
var requireHighImpactCodeVerification = false;
|
||||||
string? lastModifiedCodeFilePath = null;
|
string? lastModifiedCodeFilePath = null;
|
||||||
maxRetry = ComputeAdaptiveMaxRetry(maxRetry, taskPolicy.TaskType);
|
|
||||||
var recentTaskRetryQuality = TryGetRecentTaskRetryQuality(taskPolicy.TaskType);
|
|
||||||
maxRetry = ComputeQualityAwareMaxRetry(maxRetry, recentTaskRetryQuality, taskPolicy.TaskType);
|
|
||||||
|
|
||||||
var context = BuildContext();
|
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(
|
var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
|
||||||
explorationState,
|
explorationState,
|
||||||
@@ -1869,112 +1838,23 @@ public partial class AgentLoopService
|
|||||||
{
|
{
|
||||||
if (runtimeOverrideApplied)
|
if (runtimeOverrideApplied)
|
||||||
_llm.PopInferenceOverride();
|
_llm.PopInferenceOverride();
|
||||||
|
var completedRunId = _currentRunId;
|
||||||
// 워크플로우 상세 로그: 에이전트 루프 종료
|
FinalizeRun(new AgentLoopRunFinalizerState(
|
||||||
WorkflowLogService.LogAgentLifecycle(_conversationId, _currentRunId, "end",
|
completedRunId,
|
||||||
summary: $"iterations={iteration}, tools={totalToolCalls}, success={statsSuccessCount}, fail={statsFailCount}");
|
runStopwatch,
|
||||||
|
statsStart,
|
||||||
IsRunning = false;
|
explorationState,
|
||||||
_currentRunId = "";
|
taskPolicy,
|
||||||
_runPendingPostCompactionTurn = false;
|
iteration,
|
||||||
_runPostCompactionTurnCounter = 0;
|
totalToolCalls,
|
||||||
_runPostCompactionSuppressedThinkingCount = 0;
|
statsSuccessCount,
|
||||||
_runLastCompactionStageSummary = "";
|
statsFailCount,
|
||||||
_runLastCompactionSavedTokens = 0;
|
statsInputTokens,
|
||||||
_runPostCompactionToolResultCompactions = 0;
|
statsOutputTokens,
|
||||||
|
statsRepeatedFailureBlocks,
|
||||||
// 일시정지 상태 리셋
|
statsRecoveredAfterFailure,
|
||||||
if (IsPaused)
|
statsUsedTools,
|
||||||
{
|
failedToolHistogram));
|
||||||
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 ?? "",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace AxCopilot.Services.Agent;
|
|||||||
internal static class AgentMessageInvariantHelper
|
internal static class AgentMessageInvariantHelper
|
||||||
{
|
{
|
||||||
private const int SyntheticPreviewTextLimit = 280;
|
private const int SyntheticPreviewTextLimit = 280;
|
||||||
|
private const int PreviewFingerprintTextLimit = 480;
|
||||||
|
|
||||||
public static Dictionary<string, string> BuildToolResultPreviewMap(IEnumerable<ChatMessage>? messages)
|
public static Dictionary<string, string> BuildToolResultPreviewMap(IEnumerable<ChatMessage>? messages)
|
||||||
{
|
{
|
||||||
@@ -46,8 +47,9 @@ internal static class AgentMessageInvariantHelper
|
|||||||
if (messages == null || messages.Count == 0)
|
if (messages == null || messages.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var previews = BuildToolResultPreviewMap(messages);
|
var previews = BuildPersistedToolResultPreviewMap(messages);
|
||||||
if (previews.Count == 0)
|
var previewsByFingerprint = BuildToolResultPreviewFingerprintMap(messages);
|
||||||
|
if (previews.Count == 0 && previewsByFingerprint.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var changed = false;
|
var changed = false;
|
||||||
@@ -57,8 +59,23 @@ internal static class AgentMessageInvariantHelper
|
|||||||
continue;
|
continue;
|
||||||
if (!TryGetToolResultId(message, out var toolResultId))
|
if (!TryGetToolResultId(message, out var toolResultId))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!previews.TryGetValue(toolResultId, out var preview))
|
if (!previews.TryGetValue(toolResultId, out var preview))
|
||||||
|
{
|
||||||
|
if (!TryGetToolResultFingerprint(message, out var fingerprint)
|
||||||
|
|| !previewsByFingerprint.TryGetValue(fingerprint, out var fingerprintPreview))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
preview = TryRebindToolResultPreview(fingerprintPreview, toolResultId, message, out var reboundPreview)
|
||||||
|
? reboundPreview
|
||||||
|
: fingerprintPreview;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(preview) && !TryBuildSyntheticToolResultPreview(message, out preview))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
message.QueryPreviewContent = preview;
|
message.QueryPreviewContent = preview;
|
||||||
changed = true;
|
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)
|
private static string ExtractPreviewText(JsonElement root)
|
||||||
{
|
{
|
||||||
if (root.TryGetProperty("content", out var contentEl))
|
if (root.TryGetProperty("content", out var contentEl))
|
||||||
|
|||||||
@@ -3,8 +3,18 @@ namespace AxCopilot.Services.Agent;
|
|||||||
public static class ArtifactQualityOutputFormatter
|
public static class ArtifactQualityOutputFormatter
|
||||||
{
|
{
|
||||||
public static IReadOnlyList<string> BuildLines(ArtifactQualityReport review)
|
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)
|
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", "부록", "참고"))
|
headings.Any(h => ArtifactQualityReviewService.ContainsBusinessKeyword(h, "appendix", "reference", "supplement", "부록", "참고"))
|
||||||
));
|
));
|
||||||
|
|
||||||
parts.Add(review.ToToolSummary());
|
parts.AddRange(ArtifactQualityOutputFormatter.BuildLines(review));
|
||||||
parts.Add(ArtifactRepairGuideService.BuildGuide(review));
|
|
||||||
return ToolResult.Ok(
|
return ToolResult.Ok(
|
||||||
$"Word 문서 생성 완료: {fullPath}\n{string.Join(", ", parts)}",
|
$"Word 문서 생성 완료: {fullPath}\n{string.Join(", ", parts)}",
|
||||||
fullPath);
|
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"),
|
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"),
|
HasStructuredSummaryContent(summarySheet, "decision_summary"),
|
||||||
HasSummaryItems(summarySheet, "sheet_summaries")));
|
HasSummaryItems(summarySheet, "sheet_summaries")));
|
||||||
return ToolResult.Ok(
|
var outputLines = new List<string>
|
||||||
$"Excel 파일 생성 완료: {fullPath}\n시트: {totalSheets}개, 총 데이터 행: {totalRows}\n{review.ToToolSummary()}\n{ArtifactRepairGuideService.BuildGuide(review)}",
|
{
|
||||||
fullPath);
|
$"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))
|
if (!string.IsNullOrWhiteSpace(templatePackName))
|
||||||
outputParts.Add($"Template pack: {templatePackName}");
|
outputParts.Add($"Template pack: {templatePackName}");
|
||||||
if (deckReview != null)
|
if (deckReview != null)
|
||||||
{
|
outputParts.AddRange(ArtifactQualityOutputFormatter.BuildLines(deckReview));
|
||||||
outputParts.Add(deckReview.ToToolSummary());
|
|
||||||
outputParts.Add(DeckRepairGuideService.BuildGuide(deckReview));
|
|
||||||
}
|
|
||||||
return ToolResult.Ok(string.Join("\n", outputParts), fullPath);
|
return ToolResult.Ok(string.Join("\n", outputParts), fullPath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user