Skip to content
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
174 changes: 174 additions & 0 deletions scripts/measure-bookie-chat.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
param(
[Parameter(Mandatory = $true)]
[string]$Token,

[Parameter(Mandatory = $false)]
[string]$BaseUrl = "http://localhost:8080",

[Parameter(Mandatory = $false)]
[string]$Message = "힐링되는 소설 추천해줘",

[Parameter(Mandatory = $false)]
[int]$Iterations = 3
)

$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Net.Http

$jsonBody = @{ message = $Message } | ConvertTo-Json -Compress
$headers = @{
Authorization = "Bearer $Token"
Accept = "application/json"
}
$streamHeaders = @{
Authorization = "Bearer $Token"
Accept = "text/event-stream"
}

function Invoke-JsonChat {
param(
[string]$Url,
[hashtable]$Headers,
[string]$Body
)

$client = [System.Net.Http.HttpClient]::new()
try {
foreach ($key in $Headers.Keys) {
if ($key -ieq "Accept") {
$client.DefaultRequestHeaders.Accept.Clear()
$client.DefaultRequestHeaders.Accept.Add(
[System.Net.Http.Headers.MediaTypeWithQualityHeaderValue]::new($Headers[$key])
)
} else {
$client.DefaultRequestHeaders.Remove($key) | Out-Null
$client.DefaultRequestHeaders.Add($key, $Headers[$key])
}
}

$content = [System.Net.Http.StringContent]::new($Body, [System.Text.Encoding]::UTF8, "application/json")
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$response = $client.PostAsync($Url, $content).GetAwaiter().GetResult()
$payload = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$sw.Stop()

[pscustomobject]@{
StatusCode = [int]$response.StatusCode
ElapsedMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 2)
Body = $payload
}
}
finally {
$client.Dispose()
}
}

function Invoke-SseChat {
param(
[string]$Url,
[hashtable]$Headers,
[string]$Body
)

$handler = [System.Net.Http.HttpClientHandler]::new()
$client = [System.Net.Http.HttpClient]::new($handler)

try {
foreach ($key in $Headers.Keys) {
if ($key -ieq "Accept") {
$client.DefaultRequestHeaders.Accept.Clear()
$client.DefaultRequestHeaders.Accept.Add(
[System.Net.Http.Headers.MediaTypeWithQualityHeaderValue]::new($Headers[$key])
)
} else {
$client.DefaultRequestHeaders.Remove($key) | Out-Null
$client.DefaultRequestHeaders.Add($key, $Headers[$key])
}
}

$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, $Url)
$request.Content = [System.Net.Http.StringContent]::new($Body, [System.Text.Encoding]::UTF8, "application/json")

$sw = [System.Diagnostics.Stopwatch]::StartNew()
$response = $client.SendAsync(
$request,
[System.Net.Http.HttpCompletionOption]::ResponseHeadersRead
).GetAwaiter().GetResult()

$stream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
$reader = [System.IO.StreamReader]::new($stream)

$firstEventMs = $null
$lineCount = 0
$events = New-Object System.Collections.Generic.List[string]

try {
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($null -eq $line) {
continue
}

if ($line.Length -gt 0 -and -not $firstEventMs) {
$firstEventMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 2)
}

if ($line.StartsWith("event:")) {
$events.Add($line.Substring(6).Trim())
}

$lineCount++
}
}
finally {
$reader.Dispose()
$stream.Dispose()
$response.Dispose()
}

$sw.Stop()

[pscustomobject]@{
StatusCode = [int]$response.StatusCode
FirstEventMs = $firstEventMs
TotalMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 2)
LineCount = $lineCount
Events = ($events -join ",")
}
}
finally {
$client.Dispose()
$handler.Dispose()
}
}

$jsonResults = @()
$sseResults = @()

for ($i = 1; $i -le $Iterations; $i++) {
$jsonResults += Invoke-JsonChat -Url "$BaseUrl/bookie/chat" -Headers $headers -Body $jsonBody
Start-Sleep -Milliseconds 300
$sseResults += Invoke-SseChat -Url "$BaseUrl/bookie/chat/stream" -Headers $streamHeaders -Body $jsonBody
Start-Sleep -Milliseconds 300
}

$jsonAvg = [math]::Round((($jsonResults | Measure-Object -Property ElapsedMs -Average).Average), 2)
$sseFirstAvg = [math]::Round((($sseResults | Measure-Object -Property FirstEventMs -Average).Average), 2)
$sseTotalAvg = [math]::Round((($sseResults | Measure-Object -Property TotalMs -Average).Average), 2)

Write-Host ""
Write-Host "JSON /bookie/chat"
$jsonResults | Format-Table -AutoSize
Write-Host "Average total: $jsonAvg ms"

Write-Host ""
Write-Host "SSE /bookie/chat/stream"
$sseResults | Format-Table -AutoSize
Write-Host "Average first event: $sseFirstAvg ms"
Write-Host "Average total: $sseTotalAvg ms"

Write-Host ""
Write-Host "Interpretation"
Write-Host "- /bookie/chat compares full response completion."
Write-Host "- /bookie/chat/stream first-event compares perceived responsiveness."
Write-Host "- /bookie/chat/stream total compares full stream completion."
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.capstone.bszip.Bookie.dto.response.MemberChatResponses;
import com.capstone.bszip.Bookie.service.BookieChatService;
import com.capstone.bszip.Member.domain.Member;
import com.capstone.bszip.commonDto.SuccessResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -15,18 +14,23 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
@RequestMapping("/bookie")
@RequiredArgsConstructor
@Tag(name="Bookie", description = "chatbot api")
@Tag(name = "Bookie", description = "chatbot api")
public class BookieChatController {

private final BookieChatService bookieChatService;

@GetMapping("/history")
@Operation(summary = "이전 히스토리 가져오기", description = "[로그인 필수] 해당 회원의 이전 기록 최신순으로 반환",
@Operation(summary = "이전 히스토리 조회", description = "[로그인 필수] 해당 회원의 이전 기록을 최신순으로 반환",
responses = {
@ApiResponse(responseCode = "200", description = "AI 응답 반환 성공",
content = @Content(schema = @Schema(implementation = MemberChatResponses.class))),
Expand All @@ -35,28 +39,40 @@ public class BookieChatController {
}
)
public ResponseEntity<?> getChatHistory(@AuthenticationPrincipal Member member) {
try{
try {
return ResponseEntity.ok(bookieChatService.getChatHistory(member));
} catch (Exception e) {
throw new RuntimeException(e);
}
}


@PostMapping("/chat")
@Operation(summary = "chat 반환", description = "[로그인 필수] ai 모델의 답 가공하지 않고 바로 반환",
@Operation(summary = "chat 반환", description = "[로그인 필수] ai 모델 응답을 한 번에 반환",
responses = {
@ApiResponse(responseCode = "200", description = "AI 응답 반환 성공",
content = @Content(schema = @Schema(implementation = ChatResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "서버 오류")
}
)
public ResponseEntity<?> getChat(@AuthenticationPrincipal Member member,@RequestBody ChatRequest chatRequest) throws JsonProcessingException {
try{
public ResponseEntity<?> getChat(@AuthenticationPrincipal Member member, @RequestBody ChatRequest chatRequest)
throws JsonProcessingException {
try {
return ResponseEntity.ok(bookieChatService.getChat(member, chatRequest));
}catch (Exception e) {
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@PostMapping(value = "/chat/stream", produces = "text/event-stream")
@Operation(summary = "chat stream 반환", description = "[로그인 필수] ai 모델 SSE 응답을 실시간으로 반환",
responses = {
@ApiResponse(responseCode = "200", description = "AI SSE 응답 반환 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "서버 오류")
}
)
public SseEmitter streamChat(@AuthenticationPrincipal Member member, @RequestBody ChatRequest chatRequest) {
return bookieChatService.streamChat(member, chatRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.capstone.bszip.Bookie.service;

import com.capstone.bszip.Bookie.domain.BookieChat;
import com.capstone.bszip.Bookie.dto.request.ChatRequest;
import com.capstone.bszip.Bookie.repository.BookieChatRepository;
import com.capstone.bszip.Member.domain.Member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Slf4j
public class BookieChatAsyncService {

private final BookieChatRepository bookieChatRepository;

@Async
@Transactional
public void saveChatToDB(String chatJson, Member member, ChatRequest chatRequest) {
BookieChat bookieChat = BookieChat.builder()
.question(chatRequest.getMessage())
.answer(chatJson)
.member(member)
.build();
log.info("{}에 대한 응답 저장", bookieChat.getQuestion());
bookieChatRepository.save(bookieChat);
}
}
Loading