קבלת מידע ב Streaming היא דווקא די פשוטה
ממשקי Chat מאוד אוהבים להשתמש ב Streaming כי ל AI לוקח המון זמן לייצר את התשובה. בצד הלקוח משיכת מידע משרת HTTP ב Streaming התבררה כמשימה די פשוטה אני מדביק פה את הקוד ואחריו ההסבר:
export default async function* sendQuestion(questionText: string) {
const res = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'llama3.2',
prompt: questionText,
stream: true,
}),
})
if (!res.ok) throw new Error('Failed to connect to Ollama server')
const reader = res.body?.getReader()
if (!reader) throw new Error('Failed to initialize stream reader')
while (true) {
const { done, value } = await reader.read()
if (done) break
// Decode the stream data
const chunk = new TextDecoder().decode(value)
const json = JSON.parse(chunk)
yield json
}
}
הקוד פונה לשרת של Ollama שרץ אצלי מקומית על המחשב ושולח לו שאלה. הפרמטר stream מקבל ערך אמת אבל זה לא הכרחי כי זו ברירת המחדל של פרמטר זה. אם התשובה היתה מגיעה בתור אוביקט אחד הייתי מפעיל את res.json()
או res.text()
כדי לקבל אותה, אבל בגלל שהתשובה מגיעה בתור זרם של אוביקטים אני קורא ל getReader
על ה body, כדי לקבל ממשק קריאה. ה body
הוא בעצם ReadableStream ואפשר לקרוא עליו בהרחבה בדף התיעוד:
https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader
אחרי שלקחתי את ה Reader מתחיל החלק המעייף - עלינו לקרוא מה Reader את המידע חלק אחרי חלק ולפענח כל פעם את החלק הבא בתור אוביקט JSON. בקוד הזה אני מניח שכל Chunk יכיל אוביקט JSON שלם, למרות שבעולם האמיתי ייתכן ויידרש טיפול יותר פרטני כשפיענוח של Chunk נכשל כי אולי זה חלק שמחזיק רק חלק מאוביקט ה JSON וההמשך שלו הוא ב Chunk הבא. שימו לב לשימוש ב TextDecoder שמונע בעיה דומה עבור פיענוח הטקסט, כלומר ה decode של TextDecoderיודע לחתוך את הביטים העודפים אם יש ולהדביק אותם להתחלה של ה Chunk הבא כדי שיוכל להפוך ביטים לטקסט.
הפונקציה כולה היא Generator ואפשר להשתמש בה למשל מתוך קומפוננטת vue באופן הבא:
<script setup lang="ts">
import { ref } from 'vue'
import streamChatResponse from '../streamChatResponse';
const question = ref('')
const response = ref('')
const isLoading = ref(false)
const error = ref<string | null>(null)
async function sendQuestion() {
for await (const chunk of streamChatResponse(question.value)) {
response.value += chunk.response;
}
question.value = '';
}
</script>
<template>
<div class="chat-container">
<form @submit.prevent="sendQuestion" class="question-form">
<textarea
v-model="question"
placeholder="Ask a question..."
:disabled="isLoading"
class="question-input"
/>
<button
type="submit"
:disabled="isLoading || !question.trim()"
class="submit-button"
>
{{ isLoading ? 'Thinking...' : 'Send' }}
</button>
</form>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="response" class="response-container">
<div class="response-text">{{ response }}</div>
</div>
</div>
</template>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.question-form {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.question-input {
flex: 1;
min-height: 80px;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
}
.submit-button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.submit-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.error-message {
color: #dc3545;
margin-bottom: 1rem;
}
.response-container {
padding: 1rem;
background-color: #f8f9fa;
border-radius: 4px;
}
.response-text {
white-space: pre-wrap;
}
</style>