채팅 추가
This commit is contained in:
155
Assets/02_Scripts/Chat/ChatSession.cs
Normal file
155
Assets/02_Scripts/Chat/ChatSession.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 대화 기록을 들고 백엔드에 요청을 넘긴다. UI 와 백엔드 사이의 유일한 연결점.
|
||||
///
|
||||
/// UI 는 이 클래스의 이벤트만 구독하고 백엔드를 전혀 모른다. 반대로 백엔드는
|
||||
/// UI 를 모른다. 나중에 대화 흐름을 사이드카로 옮기거나 음성 입력을 붙일 때
|
||||
/// 이 자리가 그대로 접합부가 된다.
|
||||
///
|
||||
/// MonoBehaviour 가 아닌 이유: 씬에 붙일 이유가 없고, 테스트에서 그냥 new 할 수 있다.
|
||||
/// </summary>
|
||||
public class ChatSession
|
||||
{
|
||||
readonly List<ChatMessage> history = new List<ChatMessage>();
|
||||
IChatBackend backend;
|
||||
int maxHistoryMessages;
|
||||
|
||||
/// <summary>사용자 발화가 기록에 들어갔을 때.</summary>
|
||||
public event Action<string> UserMessageAdded;
|
||||
|
||||
/// <summary>응답이 시작될 때. UI 는 여기서 빈 말풍선을 만든다.</summary>
|
||||
public event Action ResponseStarted;
|
||||
|
||||
/// <summary>응답 조각이 도착할 때마다.</summary>
|
||||
public event Action<string> ResponseDelta;
|
||||
|
||||
/// <summary>응답이 끝났을 때. 인자는 전체 응답.</summary>
|
||||
public event Action<string> ResponseCompleted;
|
||||
|
||||
/// <summary>실패했을 때. 인자는 사용자에게 보여줄 문장.</summary>
|
||||
public event Action<string> ResponseFailed;
|
||||
|
||||
public ChatSession(IChatBackend backend, int maxHistoryMessages)
|
||||
{
|
||||
this.backend = backend ?? throw new ArgumentNullException(nameof(backend));
|
||||
this.maxHistoryMessages = Mathf.Max(2, maxHistoryMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지금 쓰는 백엔드. 설정에서 제공자(Claude / Gemini)를 바꾸면 여기가 교체된다.
|
||||
///
|
||||
/// 세션을 새로 만들지 않고 갈아끼우는 이유는 대화를 잇기 위해서다. 기록은
|
||||
/// 역할과 글자뿐이라 제공자가 달라져도 그대로 넘어간다.
|
||||
/// 교체 순간 진행 중이던 응답은 버린다 — 다른 곳으로 보낸 요청의 답을
|
||||
/// 새 제공자의 대화에 이어붙일 수는 없다.
|
||||
/// </summary>
|
||||
public IChatBackend Backend
|
||||
{
|
||||
get => backend;
|
||||
set
|
||||
{
|
||||
if (value == null || ReferenceEquals(value, backend)) return;
|
||||
Cancel();
|
||||
backend = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 들고 갈 대화 길이. 설정에서 바꾸면 여기로 들어온다.
|
||||
/// 세션을 새로 만들지 않고 갈아끼우는 이유는 지금까지의 대화를 잃지 않기 위해서다.
|
||||
/// </summary>
|
||||
public int MaxHistoryMessages
|
||||
{
|
||||
get => maxHistoryMessages;
|
||||
set
|
||||
{
|
||||
maxHistoryMessages = Mathf.Max(2, value);
|
||||
TrimHistory();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>응답을 기다리는 중인지.</summary>
|
||||
public bool IsBusy => backend.IsBusy;
|
||||
|
||||
public IReadOnlyList<ChatMessage> History => history;
|
||||
|
||||
/// <summary>사용자 발화를 기록에 넣고 응답을 요청한다.</summary>
|
||||
public void Send(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
|
||||
if (backend.IsBusy)
|
||||
{
|
||||
ResponseFailed?.Invoke("아직 대답하는 중이야. 잠깐만.");
|
||||
return;
|
||||
}
|
||||
|
||||
text = text.Trim();
|
||||
history.Add(new ChatMessage(ChatRole.User, text));
|
||||
TrimHistory();
|
||||
UserMessageAdded?.Invoke(text);
|
||||
|
||||
ResponseStarted?.Invoke();
|
||||
|
||||
backend.Send(
|
||||
history,
|
||||
delta => ResponseDelta?.Invoke(delta),
|
||||
full =>
|
||||
{
|
||||
history.Add(new ChatMessage(ChatRole.Assistant, full));
|
||||
TrimHistory();
|
||||
ResponseCompleted?.Invoke(full);
|
||||
},
|
||||
reason =>
|
||||
{
|
||||
// 실패한 턴의 사용자 발화는 기록에서 뺀다. 남겨두면 다음 요청에서
|
||||
// user 가 연달아 두 번 나오는 모양이 되고, 재시도할 때 중복된다.
|
||||
if (history.Count > 0 && history[history.Count - 1].Role == ChatRole.User)
|
||||
{
|
||||
history.RemoveAt(history.Count - 1);
|
||||
}
|
||||
ResponseFailed?.Invoke(reason);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>진행 중인 응답을 중단한다.</summary>
|
||||
public void Cancel()
|
||||
{
|
||||
if (!backend.IsBusy) return;
|
||||
|
||||
backend.Cancel();
|
||||
if (history.Count > 0 && history[history.Count - 1].Role == ChatRole.User)
|
||||
{
|
||||
history.RemoveAt(history.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>대화를 처음부터 다시 시작한다.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Cancel();
|
||||
history.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 오래된 대화를 버린다.
|
||||
///
|
||||
/// 앞에서부터 버리되 assistant 로 시작하지 않도록 맞춘다. API 는 user 로 시작하는
|
||||
/// 기록을 기대하고, assistant 로 시작하면 프리필로 해석돼 최신 모델에서는 거부된다.
|
||||
/// </summary>
|
||||
void TrimHistory()
|
||||
{
|
||||
while (history.Count > maxHistoryMessages)
|
||||
{
|
||||
history.RemoveAt(0);
|
||||
}
|
||||
|
||||
while (history.Count > 0 && history[0].Role == ChatRole.Assistant)
|
||||
{
|
||||
history.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user