Spaces:
Sleeping
Sleeping
ChatBIA ์๋๋ก์ด๋ ์คํธ๋ฆฌ๋ฐ ์ฐ๋ ๊ฐ์ด๋
๐ก API ์๋ํฌ์ธํธ
1. ์ผ๋ฐ ์ฑํ (๋น์คํธ๋ฆฌ๋ฐ)
POST /chat
- ํ์์์ ์ํ: ๊ธด ์๋ต ์ ํ์์์ ๋ฐ์ ๊ฐ๋ฅ
- ์๋๋ก์ด๋์์ ๊ถ์ฅํ์ง ์์
2. ์คํธ๋ฆฌ๋ฐ ์ฑํ โ ๊ถ์ฅ
POST /chat/stream
- ํ์์์ ๋ฐฉ์ง: ํ ํฐ ๋จ์๋ก ์ค์๊ฐ ์์
- ์๋๋ก์ด๋์ ์ต์ ํ
- SSE (Server-Sent Events) ๋ฐฉ์
๐ง ์๋๋ก์ด๋ ๊ตฌํ (Kotlin)
1. build.gradle ์์กด์ฑ ์ถ๊ฐ
dependencies {
// OkHttp for SSE streaming
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-sse:4.12.0")
// JSON ํ์ฑ
implementation("com.google.code.gson:gson:2.10.1")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
2. ๋ฐ์ดํฐ ๋ชจ๋ธ
// ChatRequest.kt
data class ChatRequest(
val message: String,
val mode: String = "bsl", // "bsl" or "general"
val max_tokens: Int = 1024,
val temperature: Float = 0.7f
)
// StreamingResponse.kt
data class StreamingResponse(
val token: String = "",
val done: Boolean = false,
val token_count: Int = 0,
val mode: String = "",
val error: String? = null
)
3. ChatBIA API ํด๋ผ์ด์ธํธ
// ChatBiaApiClient.kt
import com.google.gson.Gson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class ChatBiaApiClient(private val baseUrl: String) {
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) // ์คํธ๋ฆฌ๋ฐ์ ๊ธด ํ์์์
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val gson = Gson()
/**
* ์คํธ๋ฆฌ๋ฐ ์ฑํ
(๊ถ์ฅ)
* Flow๋ฅผ ํตํด ํ ํฐ ๋จ์๋ก ์ค์๊ฐ ์์
*/
fun chatStream(request: ChatRequest): Flow<StreamingResponse> = flow {
suspendCoroutine<Unit> { continuation ->
val url = "$baseUrl/chat/stream"
// JSON ์์ฒญ body
val jsonBody = gson.toJson(request)
val requestBody = jsonBody.toRequestBody("application/json".toMediaType())
val httpRequest = Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Accept", "text/event-stream")
.build()
// SSE EventSource ์์ฑ
val eventSource = EventSources.createFactory(client)
.newEventSource(httpRequest, object : EventSourceListener() {
override fun onOpen(eventSource: EventSource, response: Response) {
// ์ฐ๊ฒฐ ์ฑ๊ณต
}
override fun onEvent(
eventSource: EventSource,
id: String?,
type: String?,
data: String
) {
try {
val response = gson.fromJson(data, StreamingResponse::class.java)
// Flow๋ก emit
trySend(response)
// ์๋ฃ ์ ์ฐ๊ฒฐ ์ข
๋ฃ
if (response.done) {
eventSource.cancel()
continuation.resume(Unit)
}
} catch (e: Exception) {
eventSource.cancel()
continuation.resumeWithException(e)
}
}
override fun onFailure(
eventSource: EventSource,
t: Throwable?,
response: Response?
) {
continuation.resumeWithException(
t ?: Exception("SSE ์ฐ๊ฒฐ ์คํจ: ${response?.code}")
)
}
override fun onClosed(eventSource: EventSource) {
if (!continuation.isCompleted) {
continuation.resume(Unit)
}
}
})
}
}
/**
* ์ผ๋ฐ ์ฑํ
(๋น์คํธ๋ฆฌ๋ฐ)
* ๊ธด ์๋ต ์ ํ์์์ ์ํ ์์
*/
suspend fun chat(request: ChatRequest): ChatResponse = suspendCoroutine { continuation ->
val url = "$baseUrl/chat"
val jsonBody = gson.toJson(request)
val requestBody = jsonBody.toRequestBody("application/json".toMediaType())
val httpRequest = Request.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(httpRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val body = response.body?.string()
val chatResponse = gson.fromJson(body, ChatResponse::class.java)
continuation.resume(chatResponse)
} else {
continuation.resumeWithException(
Exception("HTTP ${response.code}: ${response.message}")
)
}
}
})
}
data class ChatResponse(
val response: String,
val mode: String,
val tokens: Int
)
}
4. ViewModel ์ฌ์ฉ ์์
// ChatViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch
class ChatViewModel : ViewModel() {
private val apiClient = ChatBiaApiClient("https://your-hf-space.hf.space")
private val _chatState = MutableStateFlow("")
val chatState: StateFlow<String> = _chatState
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
/**
* ์คํธ๋ฆฌ๋ฐ ์ฑํ
์ ์ก
*/
fun sendStreamingMessage(message: String, mode: String = "bsl") {
viewModelScope.launch {
_isLoading.value = true
_chatState.value = "" // ์ด๊ธฐํ
val request = ChatRequest(
message = message,
mode = mode,
max_tokens = 1024,
temperature = 0.7f
)
apiClient.chatStream(request)
.catch { e ->
_chatState.value = "์ค๋ฅ: ${e.message}"
_isLoading.value = false
}
.collect { response ->
if (response.error != null) {
_chatState.value = "์๋ฒ ์ค๋ฅ: ${response.error}"
_isLoading.value = false
} else if (response.done) {
// ์๋ฃ
_isLoading.value = false
} else {
// ํ ํฐ ์ถ๊ฐ
_chatState.value += response.token
}
}
}
}
}
5. Compose UI ์์
// ChatScreen.kt
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ChatScreen(viewModel: ChatViewModel = viewModel()) {
val chatState by viewModel.chatState.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
var inputText by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// ์ฑํ
์ถ๋ ฅ
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Text(
text = chatState,
modifier = Modifier.padding(16.dp)
)
}
Spacer(modifier = Modifier.height(16.dp))
// ์
๋ ฅ ํ๋
Row(
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("๋ฉ์์ง ์
๋ ฅ...") },
enabled = !isLoading
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (inputText.isNotBlank()) {
viewModel.sendStreamingMessage(inputText)
inputText = ""
}
},
enabled = !isLoading
) {
Text(if (isLoading) "์ ์ก ์ค..." else "์ ์ก")
}
}
}
}
๐งช ํ ์คํธ ๋ฐฉ๋ฒ
1. ๋ก์ปฌ ์๋ฒ ์คํ
cd ChatBIA-Server
uvicorn main:app --host 0.0.0.0 --port 8000
2. Python ํ ์คํธ
python test_streaming.py
3. ์๋๋ก์ด๋ ์ฑ์์ ์ฐ๊ฒฐ
// ๋ก์ปฌ ํ
์คํธ (์๋ฎฌ๋ ์ดํฐ)
val apiClient = ChatBiaApiClient("http://10.0.2.2:8000")
// ์ค์ ๋๋ฐ์ด์ค (๊ฐ์ ๋คํธ์ํฌ)
val apiClient = ChatBiaApiClient("http://YOUR_IP:8000")
// Hugging Face Spaces (๋ฐฐํฌ ํ)
val apiClient = ChatBiaApiClient("https://your-space.hf.space")
๐ ์๋ต ํ์
์คํธ๋ฆฌ๋ฐ ์๋ต (SSE)
data: {"token":"์๋
","done":false,"token_count":1}
data: {"token":"ํ์ธ์","done":false,"token_count":2}
data: {"token":"!","done":false,"token_count":3}
data: {"token":"","done":true,"token_count":3,"mode":"bsl"}
์ต์ข ์๋ต
{
"token": "",
"done": true,
"token_count": 150,
"mode": "bsl"
}
์ค๋ฅ ์๋ต
{
"error": "์ค๋ฅ ๋ฉ์์ง",
"done": true
}
โก ์ฑ๋ฅ ์ต์ ํ ํ
ํ์์์ ์ค์
- Connect: 30์ด
- Read: 60์ด (์คํธ๋ฆฌ๋ฐ)
- Write: 30์ด
์ฌ์ฐ๊ฒฐ ๋ก์ง
fun retryOnFailure(maxRetries: Int = 3) { var attempts = 0 while (attempts < maxRetries) { try { chatStream(request).collect { } break } catch (e: Exception) { attempts++ delay(2000 * attempts) // ์ง์ ๋ฐฑ์คํ } } }๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
- Flow๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌ
- UI ์ ๋ฐ์ดํธ๋ StateFlow๋ก ์ต์ ํ
๐ ๋ฐฐํฌ ํ ์ฌ์ฉ
Hugging Face Spaces์ ๋ฐฐํฌ ํ:
val BASE_URL = "https://your-username-chatbia-server.hf.space"
val apiClient = ChatBiaApiClient(BASE_URL)
์ฃผ์: Hugging Face Spaces ๋ฌด๋ฃ ํ๋์ CPU๋ง ์ ๊ณต๋๋ฏ๋ก ์๋ต ์๋๊ฐ ๋๋ฆด ์ ์์ต๋๋ค. ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์์ด ๋์ฑ ์ค์ํฉ๋๋ค!