@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
293 lines (253 loc) • 7.06 kB
text/typescript
export interface ChatRequest {
message: string;
sessionId?: string;
context?: Record<string, any>;
}
export interface ChatResponse {
id: string;
content: string;
timestamp: Date;
sessionId: string;
sources?: Array<{
title: string;
content: string;
url?: string;
score: number;
}>;
metadata?: Record<string, any>;
}
export interface StreamingChatResponse {
id: string;
content: string;
isComplete: boolean;
timestamp: Date;
sessionId: string;
sources?: Array<{
title: string;
content: string;
url?: string;
score: number;
}>;
error?: string;
}
export class ChatbotAPIError extends Error {
constructor(message: string, public status?: number, public code?: string) {
super(message);
this.name = "ChatbotAPIError";
}
}
export class ChatbotAPI {
private baseUrl: string;
private apiKey?: string;
private sessionId: string;
constructor(baseUrl: string = "/api/chatbot", apiKey?: string) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.sessionId = this.generateSessionId();
}
private generateSessionId(): string {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private async makeRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
};
if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}
try {
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ChatbotAPIError(
errorData.message ||
`HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData.code
);
}
return await response.json();
} catch (error) {
if (error instanceof ChatbotAPIError) {
throw error;
}
if (error instanceof TypeError && error.message.includes("fetch")) {
throw new ChatbotAPIError(
"네트워크 연결을 확인해주세요",
0,
"NETWORK_ERROR"
);
}
throw new ChatbotAPIError(
"API 요청 중 오류가 발생했습니다",
0,
"UNKNOWN_ERROR"
);
}
}
async sendMessage(request: ChatRequest): Promise<ChatResponse> {
const response = await this.makeRequest<ChatResponse>("/chat", {
method: "POST",
body: JSON.stringify({
...request,
sessionId: request.sessionId || this.sessionId,
}),
});
return {
...response,
timestamp: new Date(response.timestamp),
};
}
async *streamMessage(
request: ChatRequest
): AsyncGenerator<StreamingChatResponse, void, unknown> {
const url = `${this.baseUrl}/chat/stream`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "text/event-stream",
};
if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
...request,
sessionId: request.sessionId || this.sessionId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ChatbotAPIError(
errorData.message ||
`HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData.code
);
}
const reader = response.body?.getReader();
if (!reader) {
throw new ChatbotAPIError(
"스트림을 읽을 수 없습니다",
0,
"STREAM_ERROR"
);
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
return;
}
try {
const parsed: StreamingChatResponse = JSON.parse(data);
yield {
...parsed,
timestamp: new Date(parsed.timestamp),
};
} catch (parseError) {
console.warn("Failed to parse streaming response:", parseError);
}
}
}
}
} finally {
reader.releaseLock();
}
} catch (error) {
if (error instanceof ChatbotAPIError) {
throw error;
}
throw new ChatbotAPIError(
"스트리밍 중 오류가 발생했습니다",
0,
"STREAM_ERROR"
);
}
}
async getSessionHistory(sessionId?: string): Promise<ChatResponse[]> {
const id = sessionId || this.sessionId;
const response = await this.makeRequest<{ messages: ChatResponse[] }>(
`/sessions/${id}/history`
);
return response.messages.map((msg) => ({
...msg,
timestamp: new Date(msg.timestamp),
}));
}
async clearSession(sessionId?: string): Promise<void> {
const id = sessionId || this.sessionId;
await this.makeRequest(`/sessions/${id}`, {
method: "DELETE",
});
}
getSessionId(): string {
return this.sessionId;
}
setSessionId(sessionId: string): void {
this.sessionId = sessionId;
}
// Health check for the API
async healthCheck(): Promise<{ status: string; timestamp: Date }> {
const response = await this.makeRequest<{
status: string;
timestamp: string;
}>("/health");
return {
...response,
timestamp: new Date(response.timestamp),
};
}
}
// Hook for using the chatbot API
export function useChatbotAPI(baseUrl?: string, apiKey?: string) {
const api = new ChatbotAPI(baseUrl, apiKey);
return api;
}
// Retry mechanism for failed requests
export async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
// Don't retry on client errors (4xx)
if (
error instanceof ChatbotAPIError &&
error.status &&
error.status >= 400 &&
error.status < 500
) {
throw error;
}
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, delay * attempt));
}
}
}
throw lastError!;
}