Continue Generating (#2286)

* add stop reason

* add initial propagation

* add continue generating full functionality

* proper continue across chat session

* add new look

* propagate proper types

* fix typing

* cleaner continue generating functionality

* update types

* remove unused imports

* proper infodump

* temp

* add standardized stream handling

* validateing chosen tool args

* properly handle tools

* proper ports

* remove logs + build

* minor typing fix

* fix more minor typing issues

* add stashed reversion for tool call chunks

* ignore model dump types

* remove stop stream

* fix typing
This commit is contained in:
pablodanswer
2024-09-02 15:49:56 -07:00
committed by GitHub
parent 812ca69949
commit 6afcaafe54
12 changed files with 214 additions and 46 deletions

View File

@@ -1,5 +1,6 @@
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Any from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
@@ -44,6 +45,20 @@ class QADocsResponse(RetrievalDocs):
return initial_dict return initial_dict
class StreamStopReason(Enum):
CONTEXT_LENGTH = "context_length"
CANCELLED = "cancelled"
class StreamStopInfo(BaseModel):
stop_reason: StreamStopReason
def model_dump(self, *args: list, **kwargs: dict[str, Any]) -> dict[str, Any]: # type: ignore
data = super().model_dump(mode="json", *args, **kwargs) # type: ignore
data["stop_reason"] = self.stop_reason.name
return data
class LLMRelevanceFilterResponse(BaseModel): class LLMRelevanceFilterResponse(BaseModel):
relevant_chunk_indices: list[int] relevant_chunk_indices: list[int]
@@ -144,6 +159,7 @@ AnswerQuestionPossibleReturn = (
| ImageGenerationDisplay | ImageGenerationDisplay
| CustomToolResponse | CustomToolResponse
| StreamingError | StreamingError
| StreamStopInfo
) )

View File

@@ -1,5 +1,6 @@
from collections.abc import Callable from collections.abc import Callable
from collections.abc import Iterator from collections.abc import Iterator
from typing import Any
from typing import cast from typing import cast
from uuid import uuid4 from uuid import uuid4
@@ -12,6 +13,8 @@ from danswer.chat.models import AnswerQuestionPossibleReturn
from danswer.chat.models import CitationInfo from danswer.chat.models import CitationInfo
from danswer.chat.models import DanswerAnswerPiece from danswer.chat.models import DanswerAnswerPiece
from danswer.chat.models import LlmDoc from danswer.chat.models import LlmDoc
from danswer.chat.models import StreamStopInfo
from danswer.chat.models import StreamStopReason
from danswer.configs.chat_configs import QA_PROMPT_OVERRIDE from danswer.configs.chat_configs import QA_PROMPT_OVERRIDE
from danswer.file_store.utils import InMemoryChatFile from danswer.file_store.utils import InMemoryChatFile
from danswer.llm.answering.models import AnswerStyleConfig from danswer.llm.answering.models import AnswerStyleConfig
@@ -35,7 +38,7 @@ from danswer.llm.answering.stream_processing.quotes_processing import (
from danswer.llm.answering.stream_processing.utils import DocumentIdOrderMapping from danswer.llm.answering.stream_processing.utils import DocumentIdOrderMapping
from danswer.llm.answering.stream_processing.utils import map_document_id_order from danswer.llm.answering.stream_processing.utils import map_document_id_order
from danswer.llm.interfaces import LLM from danswer.llm.interfaces import LLM
from danswer.llm.utils import message_generator_to_string_generator from danswer.llm.interfaces import ToolChoiceOptions
from danswer.natural_language_processing.utils import get_tokenizer from danswer.natural_language_processing.utils import get_tokenizer
from danswer.tools.custom.custom_tool_prompt_builder import ( from danswer.tools.custom.custom_tool_prompt_builder import (
build_user_message_for_custom_tool_for_non_tool_calling_llm, build_user_message_for_custom_tool_for_non_tool_calling_llm,
@@ -190,7 +193,9 @@ class Answer:
def _raw_output_for_explicit_tool_calling_llms( def _raw_output_for_explicit_tool_calling_llms(
self, self,
) -> Iterator[str | ToolCallKickoff | ToolResponse | ToolCallFinalResult]: ) -> Iterator[
str | StreamStopInfo | ToolCallKickoff | ToolResponse | ToolCallFinalResult
]:
prompt_builder = AnswerPromptBuilder(self.message_history, self.llm.config) prompt_builder = AnswerPromptBuilder(self.message_history, self.llm.config)
tool_call_chunk: AIMessageChunk | None = None tool_call_chunk: AIMessageChunk | None = None
@@ -225,6 +230,7 @@ class Answer:
self.tools, self.force_use_tool self.tools, self.force_use_tool
) )
] ]
for message in self.llm.stream( for message in self.llm.stream(
prompt=prompt, prompt=prompt,
tools=final_tool_definitions if final_tool_definitions else None, tools=final_tool_definitions if final_tool_definitions else None,
@@ -298,21 +304,41 @@ class Answer:
yield tool_runner.tool_final_result() yield tool_runner.tool_final_result()
prompt = prompt_builder.build(tool_call_summary=tool_call_summary) prompt = prompt_builder.build(tool_call_summary=tool_call_summary)
for token in message_generator_to_string_generator(
self.llm.stream( yield from self._process_llm_stream(
prompt=prompt, prompt=prompt,
tools=[tool.tool_definition() for tool in self.tools], tools=[tool.tool_definition() for tool in self.tools],
) )
):
if self.is_cancelled:
return
yield token
return return
# This method processes the LLM stream and yields the content or stop information
def _process_llm_stream(
self,
prompt: Any,
tools: list[dict] | None = None,
tool_choice: ToolChoiceOptions | None = None,
) -> Iterator[str | StreamStopInfo]:
for message in self.llm.stream(
prompt=prompt, tools=tools, tool_choice=tool_choice
):
if isinstance(message, AIMessageChunk):
if message.content:
if self.is_cancelled:
return StreamStopInfo(stop_reason=StreamStopReason.CANCELLED)
yield cast(str, message.content)
if (
message.additional_kwargs.get("usage_metadata", {}).get("stop")
== "length"
):
yield StreamStopInfo(stop_reason=StreamStopReason.CONTEXT_LENGTH)
def _raw_output_for_non_explicit_tool_calling_llms( def _raw_output_for_non_explicit_tool_calling_llms(
self, self,
) -> Iterator[str | ToolCallKickoff | ToolResponse | ToolCallFinalResult]: ) -> Iterator[
str | StreamStopInfo | ToolCallKickoff | ToolResponse | ToolCallFinalResult
]:
prompt_builder = AnswerPromptBuilder(self.message_history, self.llm.config) prompt_builder = AnswerPromptBuilder(self.message_history, self.llm.config)
chosen_tool_and_args: tuple[Tool, dict] | None = None chosen_tool_and_args: tuple[Tool, dict] | None = None
@@ -387,13 +413,10 @@ class Answer:
) )
) )
prompt = prompt_builder.build() prompt = prompt_builder.build()
for token in message_generator_to_string_generator( yield from self._process_llm_stream(
self.llm.stream(prompt=prompt) prompt=prompt,
): tools=None,
if self.is_cancelled: )
return
yield token
return return
tool, tool_args = chosen_tool_and_args tool, tool_args = chosen_tool_and_args
@@ -447,12 +470,8 @@ class Answer:
yield final yield final
prompt = prompt_builder.build() prompt = prompt_builder.build()
for token in message_generator_to_string_generator(
self.llm.stream(prompt=prompt) yield from self._process_llm_stream(prompt=prompt, tools=None)
):
if self.is_cancelled:
return
yield token
@property @property
def processed_streamed_output(self) -> AnswerStream: def processed_streamed_output(self) -> AnswerStream:
@@ -470,7 +489,7 @@ class Answer:
) )
def _process_stream( def _process_stream(
stream: Iterator[ToolCallKickoff | ToolResponse | str], stream: Iterator[ToolCallKickoff | ToolResponse | str | StreamStopInfo],
) -> AnswerStream: ) -> AnswerStream:
message = None message = None
@@ -524,12 +543,15 @@ class Answer:
answer_style_configs=self.answer_style_config, answer_style_configs=self.answer_style_config,
) )
def _stream() -> Iterator[str]: def _stream() -> Iterator[str | StreamStopInfo]:
if message: yield cast(str | StreamStopInfo, message)
yield cast(str, message) yield from (cast(str | StreamStopInfo, item) for item in stream)
yield from cast(Iterator[str], stream)
yield from process_answer_stream_fn(_stream()) for item in _stream():
if isinstance(item, StreamStopInfo):
yield item
else:
yield from process_answer_stream_fn(iter([item]))
processed_stream = [] processed_stream = []
for processed_packet in _process_stream(output_generator): for processed_packet in _process_stream(output_generator):

View File

@@ -11,7 +11,6 @@ from danswer.llm.answering.stream_processing.utils import DocumentIdOrderMapping
from danswer.prompts.constants import TRIPLE_BACKTICK from danswer.prompts.constants import TRIPLE_BACKTICK
from danswer.utils.logger import setup_logger from danswer.utils.logger import setup_logger
logger = setup_logger() logger = setup_logger()
@@ -204,7 +203,9 @@ def extract_citations_from_stream(
def build_citation_processor( def build_citation_processor(
context_docs: list[LlmDoc], doc_id_to_rank_map: DocumentIdOrderMapping context_docs: list[LlmDoc], doc_id_to_rank_map: DocumentIdOrderMapping
) -> StreamProcessor: ) -> StreamProcessor:
def stream_processor(tokens: Iterator[str]) -> AnswerQuestionStreamReturn: def stream_processor(
tokens: Iterator[str],
) -> AnswerQuestionStreamReturn:
yield from extract_citations_from_stream( yield from extract_citations_from_stream(
tokens=tokens, tokens=tokens,
context_docs=context_docs, context_docs=context_docs,

View File

@@ -285,7 +285,9 @@ def process_model_tokens(
def build_quotes_processor( def build_quotes_processor(
context_docs: list[LlmDoc], is_json_prompt: bool context_docs: list[LlmDoc], is_json_prompt: bool
) -> Callable[[Iterator[str]], AnswerQuestionStreamReturn]: ) -> Callable[[Iterator[str]], AnswerQuestionStreamReturn]:
def stream_processor(tokens: Iterator[str]) -> AnswerQuestionStreamReturn: def stream_processor(
tokens: Iterator[str],
) -> AnswerQuestionStreamReturn:
yield from process_model_tokens( yield from process_model_tokens(
tokens=tokens, tokens=tokens,
context_docs=context_docs, context_docs=context_docs,

View File

@@ -138,7 +138,9 @@ def _convert_message_to_dict(message: BaseMessage) -> dict:
def _convert_delta_to_message_chunk( def _convert_delta_to_message_chunk(
_dict: dict[str, Any], curr_msg: BaseMessage | None _dict: dict[str, Any],
curr_msg: BaseMessage | None,
stop_reason: str | None = None,
) -> BaseMessageChunk: ) -> BaseMessageChunk:
"""Adapted from langchain_community.chat_models.litellm._convert_delta_to_message_chunk""" """Adapted from langchain_community.chat_models.litellm._convert_delta_to_message_chunk"""
role = _dict.get("role") or (_base_msg_to_role(curr_msg) if curr_msg else None) role = _dict.get("role") or (_base_msg_to_role(curr_msg) if curr_msg else None)
@@ -163,12 +165,23 @@ def _convert_delta_to_message_chunk(
args=tool_call.function.arguments, args=tool_call.function.arguments,
index=0, # only support a single tool call atm index=0, # only support a single tool call atm
) )
return AIMessageChunk( return AIMessageChunk(
content=content, content=content,
additional_kwargs=additional_kwargs,
tool_call_chunks=[tool_call_chunk], tool_call_chunks=[tool_call_chunk],
additional_kwargs={
"usage_metadata": {"stop": stop_reason},
**additional_kwargs,
},
) )
return AIMessageChunk(content=content, additional_kwargs=additional_kwargs)
return AIMessageChunk(
content=content,
additional_kwargs={
"usage_metadata": {"stop": stop_reason},
**additional_kwargs,
},
)
elif role == "system": elif role == "system":
return SystemMessageChunk(content=content) return SystemMessageChunk(content=content)
elif role == "function": elif role == "function":
@@ -206,7 +219,7 @@ class DefaultMultiLLM(LLM):
self._api_version = api_version self._api_version = api_version
self._custom_llm_provider = custom_llm_provider self._custom_llm_provider = custom_llm_provider
# This can be used to store the maximum output tkoens for this model. # This can be used to store the maximum output tokens for this model.
# self._max_output_tokens = ( # self._max_output_tokens = (
# max_output_tokens # max_output_tokens
# if max_output_tokens is not None # if max_output_tokens is not None
@@ -349,10 +362,16 @@ class DefaultMultiLLM(LLM):
) )
try: try:
for part in response: for part in response:
if len(part["choices"]) == 0: if not part["choices"]:
continue continue
delta = part["choices"][0]["delta"]
message_chunk = _convert_delta_to_message_chunk(delta, output) choice = part["choices"][0]
message_chunk = _convert_delta_to_message_chunk(
choice["delta"],
output,
stop_reason=choice["finish_reason"],
)
if output is None: if output is None:
output = message_chunk output = message_chunk
else: else:

View File

@@ -65,7 +65,12 @@ import { FiArrowDown } from "react-icons/fi";
import { ChatIntro } from "./ChatIntro"; import { ChatIntro } from "./ChatIntro";
import { AIMessage, HumanMessage } from "./message/Messages"; import { AIMessage, HumanMessage } from "./message/Messages";
import { StarterMessage } from "./StarterMessage"; import { StarterMessage } from "./StarterMessage";
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces"; import {
AnswerPiecePacket,
DanswerDocument,
StreamStopInfo,
StreamStopReason,
} from "@/lib/search/interfaces";
import { buildFilters } from "@/lib/search/utils"; import { buildFilters } from "@/lib/search/utils";
import { SettingsContext } from "@/components/settings/SettingsProvider"; import { SettingsContext } from "@/components/settings/SettingsProvider";
import Dropzone from "react-dropzone"; import Dropzone from "react-dropzone";
@@ -94,6 +99,7 @@ import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import { SEARCH_TOOL_NAME } from "./tools/constants"; import { SEARCH_TOOL_NAME } from "./tools/constants";
import { useUser } from "@/components/user/UserProvider"; import { useUser } from "@/components/user/UserProvider";
import { Stop } from "@phosphor-icons/react";
const TEMP_USER_MESSAGE_ID = -1; const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2; const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -338,6 +344,7 @@ export function ChatPage({
} }
return; return;
} }
clearSelectedDocuments(); clearSelectedDocuments();
setIsFetchingChatMessages(true); setIsFetchingChatMessages(true);
const response = await fetch( const response = await fetch(
@@ -624,6 +631,24 @@ export function ChatPage({
const currentRegenerationState = (): RegenerationState | null => { const currentRegenerationState = (): RegenerationState | null => {
return regenerationState.get(currentSessionId()) || null; return regenerationState.get(currentSessionId()) || null;
}; };
const [canContinue, setCanContinue] = useState<Map<number | null, boolean>>(
new Map([[null, false]])
);
const updateCanContinue = (newState: boolean, sessionId?: number | null) => {
setCanContinue((prevState) => {
const newCanContinueState = new Map(prevState);
newCanContinueState.set(
sessionId !== undefined ? sessionId : currentSessionId(),
newState
);
return newCanContinueState;
});
};
const currentCanContinue = (): boolean => {
return canContinue.get(currentSessionId()) || false;
};
const currentSessionChatState = currentChatState(); const currentSessionChatState = currentChatState();
const currentSessionRegenerationState = currentRegenerationState(); const currentSessionRegenerationState = currentRegenerationState();
@@ -864,6 +889,13 @@ export function ChatPage({
} }
}; };
const continueGenerating = () => {
onSubmit({
messageOverride:
"Continue Generating (pick up exactly where you left off)",
});
};
const onSubmit = async ({ const onSubmit = async ({
messageIdToResend, messageIdToResend,
messageOverride, messageOverride,
@@ -884,6 +916,7 @@ export function ChatPage({
regenerationRequest?: RegenerationRequest | null; regenerationRequest?: RegenerationRequest | null;
} = {}) => { } = {}) => {
let frozenSessionId = currentSessionId(); let frozenSessionId = currentSessionId();
updateCanContinue(false, frozenSessionId);
if (currentChatState() != "input") { if (currentChatState() != "input") {
setPopup({ setPopup({
@@ -978,6 +1011,8 @@ export function ChatPage({
let messageUpdates: Message[] | null = null; let messageUpdates: Message[] | null = null;
let answer = ""; let answer = "";
let stopReason: StreamStopReason | null = null;
let query: string | null = null; let query: string | null = null;
let retrievalType: RetrievalType = let retrievalType: RetrievalType =
selectedDocuments.length > 0 selectedDocuments.length > 0
@@ -1174,6 +1209,11 @@ export function ChatPage({
stackTrace = (packet as StreamingError).stack_trace; stackTrace = (packet as StreamingError).stack_trace;
} else if (Object.hasOwn(packet, "message_id")) { } else if (Object.hasOwn(packet, "message_id")) {
finalMessage = packet as BackendMessage; finalMessage = packet as BackendMessage;
} else if (Object.hasOwn(packet, "stop_reason")) {
const stop_reason = (packet as StreamStopInfo).stop_reason;
if (stop_reason === StreamStopReason.CONTEXT_LENGTH) {
updateCanContinue(true, frozenSessionId);
}
} }
// on initial message send, we insert a dummy system message // on initial message send, we insert a dummy system message
@@ -1237,6 +1277,7 @@ export function ChatPage({
alternateAssistantID: alternativeAssistant?.id, alternateAssistantID: alternativeAssistant?.id,
stackTrace: stackTrace, stackTrace: stackTrace,
overridden_model: finalMessage?.overridden_model, overridden_model: finalMessage?.overridden_model,
stopReason: stopReason,
}, },
]); ]);
} }
@@ -1835,6 +1876,12 @@ export function ChatPage({
} }
> >
<AIMessage <AIMessage
continueGenerating={
i == messageHistory.length - 1 &&
currentCanContinue()
? continueGenerating
: undefined
}
overriddenModel={message.overridden_model} overriddenModel={message.overridden_model}
regenerate={createRegenerator({ regenerate={createRegenerator({
messageId: message.messageId, messageId: message.messageId,

View File

@@ -2,6 +2,7 @@ import {
DanswerDocument, DanswerDocument,
Filters, Filters,
SearchDanswerDocument, SearchDanswerDocument,
StreamStopReason,
} from "@/lib/search/interfaces"; } from "@/lib/search/interfaces";
export enum RetrievalType { export enum RetrievalType {
@@ -89,6 +90,7 @@ export interface Message {
alternateAssistantID?: number | null; alternateAssistantID?: number | null;
stackTrace?: string | null; stackTrace?: string | null;
overridden_model?: string; overridden_model?: string;
stopReason?: StreamStopReason | null;
} }
export interface BackendChatSession { export interface BackendChatSession {

View File

@@ -2,6 +2,7 @@ import {
AnswerPiecePacket, AnswerPiecePacket,
DanswerDocument, DanswerDocument,
Filters, Filters,
StreamStopInfo,
} from "@/lib/search/interfaces"; } from "@/lib/search/interfaces";
import { handleSSEStream, handleStream } from "@/lib/search/streamingUtils"; import { handleSSEStream, handleStream } from "@/lib/search/streamingUtils";
import { ChatState, FeedbackType } from "./types"; import { ChatState, FeedbackType } from "./types";
@@ -111,7 +112,8 @@ export type PacketType =
| DocumentsResponse | DocumentsResponse
| ImageGenerationDisplay | ImageGenerationDisplay
| StreamingError | StreamingError
| MessageResponseIDInfo; | MessageResponseIDInfo
| StreamStopInfo;
export async function* sendMessage({ export async function* sendMessage({
regenerate, regenerate,

View File

@@ -0,0 +1,37 @@
import { EmphasizedClickable } from "@/components/BasicClickable";
import { useEffect, useState } from "react";
import { FiBook, FiPlayCircle } from "react-icons/fi";
export function ContinueGenerating({
handleContinueGenerating,
}: {
handleContinueGenerating: () => void;
}) {
const [showExplanation, setShowExplanation] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowExplanation(true);
}, 1000);
return () => clearTimeout(timer);
}, []);
return (
<div className="flex justify-center w-full">
<div className="relative group">
<EmphasizedClickable onClick={handleContinueGenerating}>
<>
<FiPlayCircle className="mr-2" />
Continue Generation
</>
</EmphasizedClickable>
{showExplanation && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1 bg-gray-800 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 whitespace-nowrap">
LLM reached its token limit. Click to continue.
</div>
)}
</div>
</div>
);
}

View File

@@ -65,6 +65,8 @@ import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
import RegenerateOption from "../RegenerateOption"; import RegenerateOption from "../RegenerateOption";
import { LlmOverride } from "@/lib/hooks"; import { LlmOverride } from "@/lib/hooks";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal"; import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import { EmphasizedClickable } from "@/components/BasicClickable";
import { ContinueGenerating } from "./ContinueMessage";
const TOOLS_WITH_CUSTOM_HANDLING = [ const TOOLS_WITH_CUSTOM_HANDLING = [
SEARCH_TOOL_NAME, SEARCH_TOOL_NAME,
@@ -123,6 +125,7 @@ function FileDisplay({
export const AIMessage = ({ export const AIMessage = ({
regenerate, regenerate,
overriddenModel, overriddenModel,
continueGenerating,
shared, shared,
isActive, isActive,
toggleDocumentSelection, toggleDocumentSelection,
@@ -150,6 +153,7 @@ export const AIMessage = ({
}: { }: {
shared?: boolean; shared?: boolean;
isActive?: boolean; isActive?: boolean;
continueGenerating?: () => void;
otherMessagesCanSwitchTo?: number[]; otherMessagesCanSwitchTo?: number[];
onMessageSelection?: (messageId: number) => void; onMessageSelection?: (messageId: number) => void;
selectedDocuments?: DanswerDocument[] | null; selectedDocuments?: DanswerDocument[] | null;
@@ -283,11 +287,12 @@ export const AIMessage = ({
size="small" size="small"
assistant={alternativeAssistant || currentPersona} assistant={alternativeAssistant || currentPersona}
/> />
<div className="w-full"> <div className="w-full">
<div className="max-w-message-max break-words"> <div className="max-w-message-max break-words">
<div className="w-full ml-4"> <div className="w-full ml-4">
<div className="max-w-message-max break-words"> <div className="max-w-message-max break-words">
{(!toolCall || toolCall.tool_name === SEARCH_TOOL_NAME) && ( {!toolCall || toolCall.tool_name === SEARCH_TOOL_NAME ? (
<> <>
{query !== undefined && {query !== undefined &&
handleShowRetrieved !== undefined && handleShowRetrieved !== undefined &&
@@ -315,7 +320,8 @@ export const AIMessage = ({
</div> </div>
)} )}
</> </>
)} ) : null}
{toolCall && {toolCall &&
!TOOLS_WITH_CUSTOM_HANDLING.includes( !TOOLS_WITH_CUSTOM_HANDLING.includes(
toolCall.tool_name toolCall.tool_name
@@ -633,6 +639,11 @@ export const AIMessage = ({
</div> </div>
</div> </div>
</div> </div>
{(!toolCall || toolCall.tool_name === SEARCH_TOOL_NAME) &&
!query &&
continueGenerating && (
<ContinueGenerating handleContinueGenerating={continueGenerating} />
)}
</div> </div>
</div> </div>
); );

View File

@@ -27,7 +27,7 @@ export function SkippedSearch({
handleForceSearch: () => void; handleForceSearch: () => void;
}) { }) {
return ( return (
<div className="flex text-sm !pt-0 p-1"> <div className="flex text-sm !pt-0 p-1">
<div className="flex mb-auto"> <div className="flex mb-auto">
<FiBook className="my-auto flex-none mr-2" size={14} /> <FiBook className="my-auto flex-none mr-2" size={14} />
<div className="my-auto cursor-default"> <div className="my-auto cursor-default">

View File

@@ -19,6 +19,15 @@ export interface AnswerPiecePacket {
answer_piece: string; answer_piece: string;
} }
export enum StreamStopReason {
CONTEXT_LENGTH = "CONTEXT_LENGTH",
CANCELLED = "CANCELLED",
}
export interface StreamStopInfo {
stop_reason: StreamStopReason;
}
export interface ErrorMessagePacket { export interface ErrorMessagePacket {
error: string; error: string;
} }