Skip to content

Feature: chat result hanlder #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion ProjectVG.Api/Models/Chat/Request/ChatRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class ChatRequest
[JsonPropertyName("instruction")]
public string? Instruction { get; set; }

[JsonPropertyName("use_tts")]
public bool UseTTS { get; set; } = true;

public ProcessChatCommand ToProcessChatCommand()
{
return new ProcessChatCommand
Expand All @@ -36,7 +39,8 @@ public ProcessChatCommand ToProcessChatCommand()
RequestedAt = this.RequestedAt,
Action = this.Action,
Instruction = this.Instruction,
UserId = this.UserId
UserId = this.UserId,
UseTTS = this.UseTTS
};
}
}
Expand Down
10 changes: 10 additions & 0 deletions ProjectVG.Application/ApplicationServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using ProjectVG.Application.Services.Character;
using ProjectVG.Application.Services.User;
using ProjectVG.Application.Services.Chat;
using ProjectVG.Application.Services.Chat.CostTracking;
using ProjectVG.Application.Services.Chat.Preprocessors;
using ProjectVG.Application.Services.Chat.Processors;
using ProjectVG.Application.Services.Chat.Validators;
using ProjectVG.Application.Services.Chat.Handlers;
using ProjectVG.Application.Services.WebSocket;
using ProjectVG.Application.Services.Conversation;
using ProjectVG.Application.Services.Session;
Expand All @@ -27,11 +29,19 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddScoped<UserInputAnalysisProcessor>();
services.AddScoped<UserInputActionProcessor>();
services.AddScoped<MemoryContextPreprocessor>();
services.AddScoped<ChatFailureHandler>();

services.AddScoped<IWebSocketManager, WebSocketManager>();
services.AddScoped<IChatService, ChatService>();
services.AddScoped<IConversationService, ConversationService>();
services.AddSingleton<IConnectionRegistry, ConnectionRegistry>();

services.AddScoped<IChatMetricsService, ChatMetricsService>();

Comment on lines +39 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

ChatMetricsService 수명(Scope)으로 인한 백그라운드 작업 파손 위험 — Singleton 권장

현재 IChatMetricsService가 Scoped입니다. 하지만 ChatService에서 Task.Run으로 백그라운드 파이프라인을 구동하고, 해당 메서드 내에서 필드로 보관된 IChatMetricsService 인스턴스를 사용합니다. HTTP 요청이 빠르게 반환되면 원 스코프가 먼저 Dispose되어, 백그라운드에서 Scoped 서비스를 참조하는 순간 예기치 않은 동작/예외가 발생할 수 있습니다. 메트릭 서비스는 AsyncLocal 기반 컨텍스트를 사용하므로 Singleton로 두는 편이 안전하고 일반적입니다.

또한 관련 구현(참조 스니펫의 ChatMetricsService)에는 필드명이 혼재되어 컴파일 실패 가능성도 있습니다: _currentMetrics vs _current_metrics. 이 부분도 함께 정리해야 합니다.

권장 수정:

  • IChatMetricsService를 Singleton으로 전환
  • (별도 PR 또는 동시) ChatService의 백그라운드 처리 전반을 새 IServiceScope 안에서 해결하고, 백그라운드에서 사용할 서비스는 스코프 내부에서 획득하도록 리팩터링
  • ChatMetricsService 내 오탈자 필드명 정리

아래는 등록 수명 변경 diff입니다.

-            services.AddScoped<IChatMetricsService, ChatMetricsService>();
+            services.AddSingleton<IChatMetricsService, ChatMetricsService>();

다음 스크립트로 파손 가능 지점을 빠르게 점검해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Task.Run 사용 지점 확인 (백그라운드 작업) =="
rg -nP --glob '!**/bin/**' --glob '!**/obj/**' '\bTask\.Run\s*\(' -C3

echo
echo "== IChatService 등록 수명 확인 =="
rg -nP --glob '!**/bin/**' --glob '!**/obj/**' 'AddScoped\s*<\s*IChatService\s*,\s*ChatService\s*>\s*\(' -C2

echo
echo "== ChatMetricsService 내 필드명 혼용(_current_metrics) 확인 =="
rg -nP --glob '!**/bin/**' --glob '!**/obj/**' '_current_metrics|_currentMetrics' ProjectVG.Application/Services/Chat/CostTracking -n -C2

Length of output: 7823


ChatService에서 IChatMetricsService 주입 및 Task.Run 내 사용 위치를 확인하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ChatService 클래스 정의 및 생성자 위치 확인 =="
rg -nP 'class ChatService' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5
rg -nP 'public ChatService' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5

echo
echo "== ChatService 내부에서 IChatMetricsService 주입 검색 =="
rg -nP 'IChatMetricsService' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5

echo
echo "== ChatService 내 Task.Run 사용 지점 확인 =="
rg -nP 'Task\.Run' -R ProjectVG.Application/Services/Chat/ChatService.cs -C5

Length of output: 594


다음과 같이 rg 명령의 파일 경로 지정을 수정하여 다시 실행해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ChatService 클래스 정의 및 생성자 위치 확인 =="
rg -nP 'class\s+ChatService' ProjectVG.Application/Services/Chat/ChatService.cs -C5
rg -nP 'public\s+ChatService' ProjectVG.Application/Services/Chat/ChatService.cs -C5

echo
echo "== ChatService 내부에서 IChatMetricsService 주입 검색 =="
rg -nP 'IChatMetricsService' ProjectVG.Application/Services/Chat/ChatService.cs -C5

echo
echo "== ChatService 내 Task.Run 사용 지점 확인 =="
rg -nP 'Task\.Run' ProjectVG.Application/Services/Chat/ChatService.cs -C5

Length of output: 3185


ChatMetricsService를 Singleton으로 전환하여 백그라운드 작업 안전성 확보

Scoped 수명의 IChatMetricsServiceTask.Run으로 실행되는 백그라운드 작업에서 클로저로 사용하면, DI 컨테이너의 스코프가 종료된 이후에도 해당 인스턴스가 참조되어 의도치 않은 경고나 예외가 발생할 수 있습니다.
또한, ChatMetricsService 내부 필드명 _currentMetrics의 혼용(_current_metrics)은 코드 상에서 발견되지 않았으므로 별도 수정이 필요 없습니다.

– 수정 대상

  • ProjectVG.Application/ApplicationServiceCollectionExtensions.cs (라인 39): 수명 변경
  • ProjectVG.Application/Services/Chat/ChatService.cs (라인 71 인근): 백그라운드 작업 시 IServiceScopeFactory.CreateScope()로 새 스코프를 열고, 그 안에서 필요한 서비스들을 Resolve하도록 리팩터링

– 등록 수명 변경 예시

- services.AddScoped<IChatMetricsService, ChatMetricsService>();
+ services.AddSingleton<IChatMetricsService, ChatMetricsService>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
services.AddScoped<IChatMetricsService, ChatMetricsService>();
services.AddSingleton<IChatMetricsService, ChatMetricsService>();

// 비용 추적 데코레이터 등록
services.AddCostTrackingDecorator<ChatLLMProcessor>("LLM_Processing");
services.AddCostTrackingDecorator<ChatTTSProcessor>("TTS_Processing");
services.AddCostTrackingDecorator<UserInputAnalysisProcessor>("User_Input_Analysis");

return services;
}
Expand Down
72 changes: 6 additions & 66 deletions ProjectVG.Application/Models/Chat/ChatMessageSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,6 @@ public class ChatMessageSegment
public bool HasAudio => AudioData != null && AudioData.Length > 0;
public bool IsEmpty => !HasText && !HasAudio;

public override string ToString()
{
var parts = new List<string>();

parts.Add($"Order: {Order}");

if (HasText)
{
parts.Add($"Text: \"{Text}\"");
}

if (HasAudio)
{
parts.Add($"Audio: {AudioData!.Length} bytes, {AudioContentType}, {AudioLength:F2}s");
}

if (!string.IsNullOrEmpty(Emotion))
{
parts.Add($"Emotion: {Emotion}");
}

return $"Segment({string.Join(", ", parts)})";
}

public string ToShortString()
{
var parts = new List<string>();

if (HasText)
{
parts.Add($"\"{Text}\"");
}

if (HasAudio)
{
parts.Add($"[Audio: {AudioLength:F1}s]");
}

return string.Join(" ", parts);
}

public string ToDebugString()
{
return $"Segment[Order={Order}, Text={HasText}, Audio={HasAudio}, Emotion={Emotion ?? "none"}, AudioSize={AudioData?.Length ?? 0} bytes, AudioLength={AudioLength:F2}s]";
}

public static ChatMessageSegment CreateTextOnly(string text, int order = 0)
{
Expand All @@ -67,29 +22,14 @@ public static ChatMessageSegment CreateTextOnly(string text, int order = 0)
Order = order
};
}
public static ChatMessageSegment CreateAudioOnly(byte[] audioData, string contentType, float? audioLength, int order = 0)

public void SetAudioData(byte[]? audioData, string? audioContentType, float? audioLength)
{
return new ChatMessageSegment
{
AudioData = audioData,
AudioContentType = contentType,
AudioLength = audioLength,
Order = order
};
AudioData = audioData;
AudioContentType = audioContentType;
AudioLength = audioLength;
}

public static ChatMessageSegment CreateIntegrated(string text, byte[] audioData, string contentType, float? audioLength, string? emotion = null, int order = 0)
{
return new ChatMessageSegment
{
Text = text,
AudioData = audioData,
AudioContentType = contentType,
AudioLength = audioLength,
Emotion = emotion,
Order = order
};
}

}
}
25 changes: 25 additions & 0 deletions ProjectVG.Application/Models/Chat/ChatMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace ProjectVG.Application.Models.Chat
{
public class ChatMetrics
{
public string SessionId { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public string CharacterId { get; set; } = string.Empty;
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public List<ProcessMetrics> ProcessMetrics { get; set; } = new();
public decimal TotalCost { get; set; }
public TimeSpan TotalDuration { get; set; }
Comment on lines +11 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

코스트 타입을 double로 통일하여 파이프라인과 정합성 확보

파이프라인 전반의 비용 산출이 double(LLM/TTS/Context)인 반면 본 모델은 decimal입니다. 변환 지점이 늘어나고, 인터페이스/서비스 시그니처와 불일치가 생깁니다. 아래처럼 double로 통일하는 것을 권장합니다.

- public decimal TotalCost { get; set; }
+ public double TotalCost { get; set; }
...
- public decimal Cost { get; set; }
+ public double Cost { get; set; }

참고: IChatMetricsService.EndProcessMetrics의 파라미터도 double로 일치시켜 주세요.

Also applies to: 21-21

🤖 Prompt for AI Agents
In ProjectVG.Application/Models/Chat/ChatMetrics.cs around lines 11-12 (and
similarly line 21), the TotalCost property is declared as decimal while the rest
of the pipeline (LLM/TTS/Context) and related interfaces use double; change the
TotalCost property type from decimal to double to ensure type consistency across
the pipeline, and also update the IChatMetricsService.EndProcessMetrics
parameter signature to use double so service/interface signatures match.

}

public class ProcessMetrics
{
public string ProcessName { get; set; } = string.Empty;
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public TimeSpan Duration { get; set; }
public decimal Cost { get; set; }
public string? ErrorMessage { get; set; }
public Dictionary<string, object>? AdditionalData { get; set; }
}
}
9 changes: 0 additions & 9 deletions ProjectVG.Application/Models/Chat/ChatOutputFormatResult.cs

This file was deleted.

86 changes: 0 additions & 86 deletions ProjectVG.Application/Models/Chat/ChatPreprocessContext.cs

This file was deleted.

27 changes: 0 additions & 27 deletions ProjectVG.Application/Models/Chat/ChatPreprocessResult.cs

This file was deleted.

77 changes: 77 additions & 0 deletions ProjectVG.Application/Models/Chat/ChatProcessContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using ProjectVG.Application.Models.Character;
using ProjectVG.Domain.Entities.ConversationHistorys;

namespace ProjectVG.Application.Models.Chat
{
public class ChatProcessContext
{
public string SessionId { get; private set; } = string.Empty;
public Guid UserId { get; private set; }
public Guid CharacterId { get; private set; }
public string UserMessage { get; private set; } = string.Empty;
public string MemoryStore { get; private set; } = string.Empty;
public bool UseTTS { get; private set; } = true;

public CharacterDto? Character { get; private set; }
public IEnumerable<string>? MemoryContext { get; private set; }
public IEnumerable<ConversationHistory>? ConversationHistory { get; private set; }

public string Response { get; private set; } = string.Empty;
public double Cost { get; private set; }
public List<ChatMessageSegment> Segments { get; private set; } = new List<ChatMessageSegment>();

public string FullText => string.Join(" ", Segments.Where(s => s.HasText).Select(s => s.Text));
public bool HasAudio => Segments.Any(s => s.HasAudio);
public bool HasText => Segments.Any(s => s.HasText);


public ChatProcessContext(ProcessChatCommand command)
{
SessionId = command.SessionId;
UserId = command.UserId;
CharacterId = command.CharacterId;
UserMessage = command.Message;
MemoryStore = command.UserId.ToString();
UseTTS = command.UseTTS;
}

public ChatProcessContext(
ProcessChatCommand command,
CharacterDto character,
IEnumerable<ConversationHistory> conversationHistory,
IEnumerable<string> memoryContext)
{
SessionId = command.SessionId;
UserId = command.UserId;
CharacterId = command.CharacterId;
UserMessage = command.Message;
MemoryStore = command.UserId.ToString();
UseTTS = command.UseTTS;

Character = character;
ConversationHistory = conversationHistory;
MemoryContext = memoryContext;
}

public void SetResponse(string response, List<ChatMessageSegment> segments, double cost)
{
Response = response;
Segments = segments;
Cost = cost;
}

public void AddCost(double additionalCost)
{
Cost += additionalCost;
}

public IEnumerable<string> ParseConversationHistory(int count = 5)
{
if (ConversationHistory == null) return Enumerable.Empty<string>();

return ConversationHistory
.Take(count)
.Select(h => $"{h.Role}: {h.Content}");
}
}
}
17 changes: 0 additions & 17 deletions ProjectVG.Application/Models/Chat/ChatProcessResult.cs

This file was deleted.

1 change: 1 addition & 0 deletions ProjectVG.Application/Models/Chat/ProcessChatCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public string RequestId
public DateTime RequestedAt { get; set; }
public string? Action { get; set; }
public string? Instruction { get; set; }
public bool UseTTS { get; set; } = true;

public CharacterDto? Character { get; private set; }

Expand Down
Loading