From 8a9c65486f11820b38cf4a472593f8f94b789743 Mon Sep 17 00:00:00 2001 From: Simon Farshid Date: Tue, 8 Oct 2024 16:00:24 -0700 Subject: [PATCH] feat: assistant-stream (#952) --- packages/assistant-stream/package.json | 62 ++- packages/assistant-stream/scripts/build.mts | 10 + packages/assistant-stream/src/ai-sdk.ts | 1 + packages/assistant-stream/src/ai-sdk/index.ts | 135 ++++++ .../src/core/AssistantStream.ts | 69 +++ .../core/accumulators/assistant-message.ts | 191 ++++++++ .../accumulators/partial-json/fix-json.ts | 417 ++++++++++++++++++ .../partial-json/parse-partial-json.ts | 14 + .../src/core/accumulators/types.ts | 84 ++++ packages/assistant-stream/src/core/index.ts | 19 + .../assistant-stream/src/core/modules/runs.ts | 185 ++++++++ .../assistant-stream/src/core/modules/text.ts | 47 ++ .../src/core/modules/tool-call.ts | 100 +++++ .../src/core/serialization/DataStream.ts | 194 ++++++++ .../src/core/serialization/PlainText.ts | 81 ++++ .../serialization/streamPart/StreamPart.ts | 3 + packages/assistant-stream/src/core/test.ts | 31 ++ .../src/core/utils/PipeableTransformStream.ts | 10 + .../src/core/utils/UnderlyingReadable.ts | 5 + .../src/core/utils/generateId.tsx | 6 + .../assistant-stream/src/core/utils/types.ts | 84 ++++ packages/assistant-stream/src/index.ts | 1 + packages/assistant-stream/tsconfig.json | 10 + pnpm-lock.yaml | 113 ++++- python/assistant-stream/LICENSE | 21 + python/assistant-stream/README.md | 1 + python/assistant-stream/poetry.lock | 254 +++++++++++ python/assistant-stream/pyproject.toml | 31 ++ .../src/assistant_stream/__init__.py | 0 .../src/assistant_stream/assistant_stream.py | 6 + .../assistant_stream_chunk.py | 35 ++ .../assistant_stream_response.py | 16 + .../assistant_stream/create_assistant_run.py | 44 ++ .../src/assistant_stream/modules/text.py | 0 .../src/assistant_stream/modules/tool_call.py | 0 .../serialization/data_stream.py | 18 + 36 files changed, 2285 insertions(+), 13 deletions(-) create mode 100644 packages/assistant-stream/scripts/build.mts create mode 100644 packages/assistant-stream/src/ai-sdk.ts create mode 100644 packages/assistant-stream/src/ai-sdk/index.ts create mode 100644 packages/assistant-stream/src/core/AssistantStream.ts create mode 100644 packages/assistant-stream/src/core/accumulators/assistant-message.ts create mode 100644 packages/assistant-stream/src/core/accumulators/partial-json/fix-json.ts create mode 100644 packages/assistant-stream/src/core/accumulators/partial-json/parse-partial-json.ts create mode 100644 packages/assistant-stream/src/core/accumulators/types.ts create mode 100644 packages/assistant-stream/src/core/index.ts create mode 100644 packages/assistant-stream/src/core/modules/runs.ts create mode 100644 packages/assistant-stream/src/core/modules/text.ts create mode 100644 packages/assistant-stream/src/core/modules/tool-call.ts create mode 100644 packages/assistant-stream/src/core/serialization/DataStream.ts create mode 100644 packages/assistant-stream/src/core/serialization/PlainText.ts create mode 100644 packages/assistant-stream/src/core/serialization/streamPart/StreamPart.ts create mode 100644 packages/assistant-stream/src/core/test.ts create mode 100644 packages/assistant-stream/src/core/utils/PipeableTransformStream.ts create mode 100644 packages/assistant-stream/src/core/utils/UnderlyingReadable.ts create mode 100644 packages/assistant-stream/src/core/utils/generateId.tsx create mode 100644 packages/assistant-stream/src/core/utils/types.ts create mode 100644 packages/assistant-stream/src/index.ts create mode 100644 packages/assistant-stream/tsconfig.json create mode 100644 python/assistant-stream/LICENSE create mode 100644 python/assistant-stream/README.md create mode 100644 python/assistant-stream/poetry.lock create mode 100644 python/assistant-stream/pyproject.toml create mode 100644 python/assistant-stream/src/assistant_stream/__init__.py create mode 100644 python/assistant-stream/src/assistant_stream/assistant_stream.py create mode 100644 python/assistant-stream/src/assistant_stream/assistant_stream_chunk.py create mode 100644 python/assistant-stream/src/assistant_stream/assistant_stream_response.py create mode 100644 python/assistant-stream/src/assistant_stream/create_assistant_run.py create mode 100644 python/assistant-stream/src/assistant_stream/modules/text.py create mode 100644 python/assistant-stream/src/assistant_stream/modules/tool_call.py create mode 100644 python/assistant-stream/src/assistant_stream/serialization/data_stream.py diff --git a/packages/assistant-stream/package.json b/packages/assistant-stream/package.json index 753d2bc2c..4ee7ec6de 100644 --- a/packages/assistant-stream/package.json +++ b/packages/assistant-stream/package.json @@ -1,6 +1,62 @@ { "name": "assistant-stream", - "version": "0.0.0", - "description": "", - "author": "Simon Farshid" + "version": "0.0.0-rc.2", + "license": "MIT", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./ai-sdk": { + "import": { + "types": "./dist/ai-sdk.d.mts", + "default": "./dist/ai-sdk.mjs" + }, + "require": { + "types": "./dist/ai-sdk.d.ts", + "default": "./dist/ai-sdk.js" + } + } + }, + "source": "./src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "sideEffects": false, + "scripts": { + "build": "tsx scripts/build.mts" + }, + "devDependencies": { + "@assistant-ui/tsconfig": "workspace:*", + "ai": "^3.4.9", + "eslint": "^8", + "eslint-config-next": "14.2.14", + "tsup": "8.3.0", + "tsx": "^4.19.1" + }, + "publishConfig": { + "access": "public" + }, + "homepage": "https://assistant-ui.com/", + "repository": { + "type": "git", + "url": "git+https://github.com/Yonom/assistant-ui.git" + }, + "bugs": { + "url": "https://github.com/Yonom/assistant-ui/issues" + }, + "dependencies": { + "nanoid": "^5.0.7", + "secure-json-parse": "^2.7.0" + } } diff --git a/packages/assistant-stream/scripts/build.mts b/packages/assistant-stream/scripts/build.mts new file mode 100644 index 000000000..86d4a3ef5 --- /dev/null +++ b/packages/assistant-stream/scripts/build.mts @@ -0,0 +1,10 @@ +import { build } from "tsup"; + +// JS +await build({ + entry: ["src/index.ts", "src/ai-sdk.ts"], + format: ["cjs", "esm"], + dts: true, + sourcemap: true, + clean: true, +}); diff --git a/packages/assistant-stream/src/ai-sdk.ts b/packages/assistant-stream/src/ai-sdk.ts new file mode 100644 index 000000000..458ab76de --- /dev/null +++ b/packages/assistant-stream/src/ai-sdk.ts @@ -0,0 +1 @@ +export * from "./ai-sdk/index"; diff --git a/packages/assistant-stream/src/ai-sdk/index.ts b/packages/assistant-stream/src/ai-sdk/index.ts new file mode 100644 index 000000000..e2aeff26a --- /dev/null +++ b/packages/assistant-stream/src/ai-sdk/index.ts @@ -0,0 +1,135 @@ +import type { TextStreamPart, CoreTool, ObjectStreamPart } from "ai"; +import { AssistantStream, AssistantStreamChunk } from "../core"; +import { generateId } from "../core/utils/generateId"; + +export const fromAISDKStreamText = ( + stream: ReadableStream>>, +): AssistantStream => { + const transformer = new TransformStream< + TextStreamPart>, + AssistantStreamChunk + >({ + transform(chunk, controller) { + const { type } = chunk; + switch (type) { + case "text-delta": { + const { textDelta } = chunk; + controller.enqueue({ + type: "text-delta", + textDelta, + }); + break; + } + case "tool-call-streaming-start": { + const { toolCallId, toolName } = chunk; + controller.enqueue({ + type: "tool-call-begin", + toolCallId, + toolName, + }); + break; + } + case "tool-call-delta": { + const { toolCallId, argsTextDelta } = chunk; + controller.enqueue({ + type: "tool-call-delta", + toolCallId, + argsTextDelta, + }); + break; + } + case "tool-result" as string: { + const { toolCallId, result } = chunk as unknown as { + toolCallId: string; + result: unknown; + }; + controller.enqueue({ + type: "tool-result", + toolCallId, + result, + }); + break; + } + case "tool-call": { + const { toolCallId, toolName, args } = chunk; + controller.enqueue({ + type: "tool-call-begin", + toolCallId, + toolName, + }); + controller.enqueue({ + type: "tool-call-delta", + toolCallId, + argsTextDelta: JSON.stringify(args), + }); + break; + } + case "step-finish": + case "error": + case "finish": { + break; + } + + default: { + const unhandledType: never = type; + throw new Error(`Unhandled chunk type: ${unhandledType}`); + } + } + }, + }); + + return new AssistantStream(stream.pipeThrough(transformer)); +}; + +export const fromAISDKStreamObject = ( + stream: ReadableStream>, + toolName: string, +): AssistantStream => { + const toolCallId = generateId(); + const transformer = new TransformStream< + ObjectStreamPart, + AssistantStreamChunk + >({ + start(controller) { + controller.enqueue({ + type: "tool-call-begin", + toolName, + toolCallId, + }); + }, + transform(chunk, controller) { + const { type } = chunk; + switch (type) { + case "text-delta": { + const { textDelta } = chunk; + controller.enqueue({ + type: "tool-call-delta", + toolCallId, + argsTextDelta: textDelta, + }); + break; + } + case "finish": { + controller.enqueue({ + type: "tool-result", + toolCallId, + result: "", + }); + break; + } + + case "object": + case "error": { + break; + } + + default: { + const unhandledType: never = type; + throw new Error(`Unhandled chunk type: ${unhandledType}`); + } + } + }, + }); + + return new AssistantStream(stream.pipeThrough(transformer)); +}; diff --git a/packages/assistant-stream/src/core/AssistantStream.ts b/packages/assistant-stream/src/core/AssistantStream.ts new file mode 100644 index 000000000..f5da9ef52 --- /dev/null +++ b/packages/assistant-stream/src/core/AssistantStream.ts @@ -0,0 +1,69 @@ +export type AssistantStream2 = { + // kind: AssistantStreamKind; + readable: ReadableStream; + toResponse( + format: ReadableWritablePair, + ): Response; + toByteStream( + format: ReadableWritablePair, + ): ReadableStream; + tee(): [AssistantStream, AssistantStream]; +}; + +export type AssistantStreamChunk = + | { + type: "text-delta"; + textDelta: string; + } + | { + type: "tool-call-begin"; + toolCallId: string; + toolName: string; + } + | { + type: "tool-call-delta"; + toolCallId: string; + argsTextDelta: string; + } + | { + type: "tool-result"; + toolCallId: string; + result: any; + }; + +export class AssistantStream { + constructor(public readonly readable: ReadableStream) { + this.readable = readable; + } + + toResponse( + transformer: ReadableWritablePair, + ) { + return new Response(this.toByteStream(transformer)); + } + + static fromResponse( + response: Response, + transformer: ReadableWritablePair, + ) { + return AssistantStream.fromByteStream(response.body!, transformer); + } + + toByteStream( + transformer: ReadableWritablePair, + ) { + return this.readable.pipeThrough(transformer); + } + + static fromByteStream( + readable: ReadableStream, + transformer: ReadableWritablePair, + ) { + return new AssistantStream(readable.pipeThrough(transformer)); + } + + tee(): [AssistantStream, AssistantStream] { + const [readable1, readable2] = this.readable.tee(); + return [new AssistantStream(readable1), new AssistantStream(readable2)]; + } +} diff --git a/packages/assistant-stream/src/core/accumulators/assistant-message.ts b/packages/assistant-stream/src/core/accumulators/assistant-message.ts new file mode 100644 index 000000000..7d8b2803b --- /dev/null +++ b/packages/assistant-stream/src/core/accumulators/assistant-message.ts @@ -0,0 +1,191 @@ +import { AssistantStream, AssistantStreamChunk } from "../AssistantStream"; +import { parsePartialJson } from "./partial-json/parse-partial-json"; +import { AssistantMessage, ToolCallContentPart } from "./types"; + +export type AsyncIterableStream = AsyncIterable & + ReadableStream; + +export function createAsyncIterableStream( + source: ReadableStream, + transform: TransformStream, +): AsyncIterableStream { + const transformedStream: any = source.pipeThrough(transform); + + transformedStream[Symbol.asyncIterator] = () => { + const reader = transformedStream.getReader(); + return { + async next(): Promise> { + const { done, value } = await reader.read(); + return done ? { done: true, value: undefined } : { done: false, value }; + }, + }; + }; + + return transformedStream; +} + +export const toAssistantMessageStream = (stream: AssistantStream) => { + let message: AssistantMessage = { + role: "assistant", + content: [], + status: { type: "running" }, + metadata: { + steps: [], + custom: {}, + }, + }; + const transformer = new TransformStream< + AssistantStreamChunk, + AssistantMessage + >({ + transform(chunk, controller) { + const { type } = chunk; + switch (type) { + case "text-delta": { + message = appendOrUpdateText(message, chunk.textDelta); + controller.enqueue(message); + break; + } + + case "tool-call-begin": { + const { toolCallId, toolName } = chunk; + message = appendToolCall(message, toolCallId, toolName); + controller.enqueue(message); + break; + } + case "tool-call-delta": { + const { toolCallId, argsTextDelta } = chunk; + message = appendToolArgsTextDelta(message, toolCallId, argsTextDelta); + controller.enqueue(message); + break; + } + case "tool-result" as string: { + const { toolCallId, result } = chunk as unknown as { + toolCallId: string; + result: unknown; + }; + message = setToolResult(message, toolCallId, result); + controller.enqueue(message); + + break; + } + } + }, + flush(controller) { + message = appendOrUpdateFinish(message); + controller.enqueue(message); + }, + }); + + return createAsyncIterableStream(stream.readable, transformer); +}; + +const appendOrUpdateText = (message: AssistantMessage, textDelta: string) => { + let contentParts = message.content ?? []; + let contentPart = message.content?.at(-1); + if (contentPart?.type !== "text") { + contentPart = { + type: "text", + text: textDelta, + status: { type: "running" }, + }; + } else { + contentParts = contentParts.slice(0, -1); + contentPart = { + type: "text", + text: contentPart.text + textDelta, + status: { type: "running" }, + }; + } + return { + ...message, + content: contentParts.concat([contentPart]), + }; +}; + +const appendToolCall = ( + message: AssistantMessage, + toolCallId: string, + toolName: string, +): AssistantMessage => { + return { + ...message, + content: [ + ...message.content, + { + type: "tool-call", + toolCallId, + toolName, + argsText: "", + args: {}, + status: { type: "running", isArgsComplete: false }, + }, + ], + }; +}; + +const appendToolArgsTextDelta = ( + message: AssistantMessage, + toolCallId: string, + argsTextDelta: string, +): AssistantMessage => { + const contentPartIdx = message.content.findIndex( + (part) => part.type === "tool-call" && part.toolCallId === toolCallId, + ); + if (contentPartIdx === -1) + throw new Error( + `Received tool call delta for unknown tool call "${toolCallId}".`, + ); + const contentPart = message.content[contentPartIdx]! as ToolCallContentPart; + const newArgsText = contentPart.argsText + argsTextDelta; + + return { + ...message, + content: [ + ...message.content.slice(0, contentPartIdx), + { + ...contentPart, + argsText: newArgsText, + args: parsePartialJson(newArgsText), + }, + ...message.content.slice(contentPartIdx + 1), + ], + }; +}; + +const setToolResult = ( + message: AssistantMessage, + toolCallId: string, + result: any, +) => { + let found = false; + const newContentParts = message.content?.map((part) => { + if (part.type !== "tool-call" || part.toolCallId !== toolCallId) + return part; + found = true; + + return { + ...part, + result, + }; + }); + if (!found) + throw new Error( + `Received tool result for unknown tool call "${toolCallId}". This is likely an internal bug in assistant-ui.`, + ); + + return { + ...message, + content: newContentParts!, + }; +}; + +const appendOrUpdateFinish = (message: AssistantMessage): AssistantMessage => { + return { + ...message, + status: { + type: "complete", + reason: "unknown", + }, + }; +}; diff --git a/packages/assistant-stream/src/core/accumulators/partial-json/fix-json.ts b/packages/assistant-stream/src/core/accumulators/partial-json/fix-json.ts new file mode 100644 index 000000000..d26602dab --- /dev/null +++ b/packages/assistant-stream/src/core/accumulators/partial-json/fix-json.ts @@ -0,0 +1,417 @@ +// LICENSE for this file only + +// Copyright 2023 Vercel, Inc. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +type State = + | "ROOT" + | "FINISH" + | "INSIDE_STRING" + | "INSIDE_STRING_ESCAPE" + | "INSIDE_LITERAL" + | "INSIDE_NUMBER" + | "INSIDE_OBJECT_START" + | "INSIDE_OBJECT_KEY" + | "INSIDE_OBJECT_AFTER_KEY" + | "INSIDE_OBJECT_BEFORE_VALUE" + | "INSIDE_OBJECT_AFTER_VALUE" + | "INSIDE_OBJECT_AFTER_COMMA" + | "INSIDE_ARRAY_START" + | "INSIDE_ARRAY_AFTER_VALUE" + | "INSIDE_ARRAY_AFTER_COMMA"; + +// Implemented as a scanner with additional fixing +// that performs a single linear time scan pass over the partial JSON. +// +// The states should ideally match relevant states from the JSON spec: +// https://www.json.org/json-en.html +// +// Please note that invalid JSON is not considered/covered, because it +// is assumed that the resulting JSON will be processed by a standard +// JSON parser that will detect any invalid JSON. +export function fixJson(input: string): string { + const stack: State[] = ["ROOT"]; + let lastValidIndex = -1; + let literalStart: number | null = null; + + function processValueStart(char: string, i: number, swapState: State) { + { + switch (char) { + case '"': { + lastValidIndex = i; + stack.pop(); + stack.push(swapState); + stack.push("INSIDE_STRING"); + break; + } + + case "f": + case "t": + case "n": { + lastValidIndex = i; + literalStart = i; + stack.pop(); + stack.push(swapState); + stack.push("INSIDE_LITERAL"); + break; + } + + case "-": { + stack.pop(); + stack.push(swapState); + stack.push("INSIDE_NUMBER"); + break; + } + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": { + lastValidIndex = i; + stack.pop(); + stack.push(swapState); + stack.push("INSIDE_NUMBER"); + break; + } + + case "{": { + lastValidIndex = i; + stack.pop(); + stack.push(swapState); + stack.push("INSIDE_OBJECT_START"); + break; + } + + case "[": { + lastValidIndex = i; + stack.pop(); + stack.push(swapState); + stack.push("INSIDE_ARRAY_START"); + break; + } + } + } + } + + function processAfterObjectValue(char: string, i: number) { + switch (char) { + case ",": { + stack.pop(); + stack.push("INSIDE_OBJECT_AFTER_COMMA"); + break; + } + case "}": { + lastValidIndex = i; + stack.pop(); + break; + } + } + } + + function processAfterArrayValue(char: string, i: number) { + switch (char) { + case ",": { + stack.pop(); + stack.push("INSIDE_ARRAY_AFTER_COMMA"); + break; + } + case "]": { + lastValidIndex = i; + stack.pop(); + break; + } + } + } + + for (let i = 0; i < input.length; i++) { + const char = input[i]!; + const currentState = stack[stack.length - 1]; + + switch (currentState) { + case "ROOT": + processValueStart(char, i, "FINISH"); + break; + + case "INSIDE_OBJECT_START": { + switch (char) { + case '"': { + stack.pop(); + stack.push("INSIDE_OBJECT_KEY"); + break; + } + case "}": { + lastValidIndex = i; + stack.pop(); + break; + } + } + break; + } + + case "INSIDE_OBJECT_AFTER_COMMA": { + switch (char) { + case '"': { + stack.pop(); + stack.push("INSIDE_OBJECT_KEY"); + break; + } + } + break; + } + + case "INSIDE_OBJECT_KEY": { + switch (char) { + case '"': { + stack.pop(); + stack.push("INSIDE_OBJECT_AFTER_KEY"); + break; + } + } + break; + } + + case "INSIDE_OBJECT_AFTER_KEY": { + switch (char) { + case ":": { + stack.pop(); + stack.push("INSIDE_OBJECT_BEFORE_VALUE"); + + break; + } + } + break; + } + + case "INSIDE_OBJECT_BEFORE_VALUE": { + processValueStart(char, i, "INSIDE_OBJECT_AFTER_VALUE"); + break; + } + + case "INSIDE_OBJECT_AFTER_VALUE": { + processAfterObjectValue(char, i); + break; + } + + case "INSIDE_STRING": { + switch (char) { + case '"': { + stack.pop(); + lastValidIndex = i; + break; + } + + case "\\": { + stack.push("INSIDE_STRING_ESCAPE"); + break; + } + + default: { + lastValidIndex = i; + } + } + + break; + } + + case "INSIDE_ARRAY_START": { + switch (char) { + case "]": { + lastValidIndex = i; + stack.pop(); + break; + } + + default: { + lastValidIndex = i; + processValueStart(char, i, "INSIDE_ARRAY_AFTER_VALUE"); + break; + } + } + break; + } + + case "INSIDE_ARRAY_AFTER_VALUE": { + switch (char) { + case ",": { + stack.pop(); + stack.push("INSIDE_ARRAY_AFTER_COMMA"); + break; + } + + case "]": { + lastValidIndex = i; + stack.pop(); + break; + } + + default: { + lastValidIndex = i; + break; + } + } + + break; + } + + case "INSIDE_ARRAY_AFTER_COMMA": { + processValueStart(char, i, "INSIDE_ARRAY_AFTER_VALUE"); + break; + } + + case "INSIDE_STRING_ESCAPE": { + stack.pop(); + lastValidIndex = i; + + break; + } + + case "INSIDE_NUMBER": { + switch (char) { + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": { + lastValidIndex = i; + break; + } + + case "e": + case "E": + case "-": + case ".": { + break; + } + + case ",": { + stack.pop(); + + if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") { + processAfterArrayValue(char, i); + } + + if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") { + processAfterObjectValue(char, i); + } + + break; + } + + case "}": { + stack.pop(); + + if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") { + processAfterObjectValue(char, i); + } + + break; + } + + case "]": { + stack.pop(); + + if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") { + processAfterArrayValue(char, i); + } + + break; + } + + default: { + stack.pop(); + break; + } + } + + break; + } + + case "INSIDE_LITERAL": { + const partialLiteral = input.substring(literalStart!, i + 1); + + if ( + !"false".startsWith(partialLiteral) && + !"true".startsWith(partialLiteral) && + !"null".startsWith(partialLiteral) + ) { + stack.pop(); + + if (stack[stack.length - 1] === "INSIDE_OBJECT_AFTER_VALUE") { + processAfterObjectValue(char, i); + } else if (stack[stack.length - 1] === "INSIDE_ARRAY_AFTER_VALUE") { + processAfterArrayValue(char, i); + } + } else { + lastValidIndex = i; + } + + break; + } + } + } + + let result = input.slice(0, lastValidIndex + 1); + + for (let i = stack.length - 1; i >= 0; i--) { + const state = stack[i]; + + switch (state) { + case "INSIDE_STRING": { + result += '"'; + break; + } + + case "INSIDE_OBJECT_KEY": + case "INSIDE_OBJECT_AFTER_KEY": + case "INSIDE_OBJECT_AFTER_COMMA": + case "INSIDE_OBJECT_START": + case "INSIDE_OBJECT_BEFORE_VALUE": + case "INSIDE_OBJECT_AFTER_VALUE": { + result += "}"; + break; + } + + case "INSIDE_ARRAY_START": + case "INSIDE_ARRAY_AFTER_COMMA": + case "INSIDE_ARRAY_AFTER_VALUE": { + result += "]"; + break; + } + + case "INSIDE_LITERAL": { + const partialLiteral = input.substring(literalStart!, input.length); + + if ("true".startsWith(partialLiteral)) { + result += "true".slice(partialLiteral.length); + } else if ("false".startsWith(partialLiteral)) { + result += "false".slice(partialLiteral.length); + } else if ("null".startsWith(partialLiteral)) { + result += "null".slice(partialLiteral.length); + } + } + } + } + + return result; +} diff --git a/packages/assistant-stream/src/core/accumulators/partial-json/parse-partial-json.ts b/packages/assistant-stream/src/core/accumulators/partial-json/parse-partial-json.ts new file mode 100644 index 000000000..8681ae3f4 --- /dev/null +++ b/packages/assistant-stream/src/core/accumulators/partial-json/parse-partial-json.ts @@ -0,0 +1,14 @@ +import sjson from "secure-json-parse"; +import { fixJson } from "./fix-json"; + +export const parsePartialJson = (json: string) => { + try { + return sjson.parse(json); + } catch { + try { + return sjson.parse(fixJson(json)); + } catch { + return undefined; + } + } +}; diff --git a/packages/assistant-stream/src/core/accumulators/types.ts b/packages/assistant-stream/src/core/accumulators/types.ts new file mode 100644 index 000000000..b04622521 --- /dev/null +++ b/packages/assistant-stream/src/core/accumulators/types.ts @@ -0,0 +1,84 @@ +type TextStatus = + | { + type: "running"; + } + | { + type: "complete"; + reason: "stop" | "unknown"; + } + | { + type: "incomplete"; + reason: "cancelled" | "length" | "content-filter" | "other"; + }; + +type TextContentPart = { + type: "text"; + text: string; + status: TextStatus; +}; + +type ToolCallStatus = + | { + type: "running"; + isArgsComplete: boolean; + } + | { + type: "requires-action"; + reason: "tool-call-result"; + } + | { + type: "complete"; + reason: "stop" | "unknown"; + } + | { + type: "incomplete"; + reason: "cancelled" | "length" | "content-filter" | "other"; + }; + +export type ToolCallContentPart = { + type: "tool-call"; + status: ToolCallStatus; + toolCallId: string; + toolName: string; + argsText: string; + args: Record; + result?: unknown; +}; + +type AssistantMessageContentPart = TextContentPart | ToolCallContentPart; + +type AssistantMessageStepMetadata = {}; + +export type AssitantMessageStatus = + | { + type: "running"; + } + | { + type: "requires-action"; + reason: "tool-calls"; + } + | { + type: "complete"; + reason: "stop" | "unknown"; + } + | { + type: "incomplete"; + reason: + | "cancelled" + | "tool-calls" + | "length" + | "content-filter" + | "other" + | "error"; + error?: unknown; + }; + +export type AssistantMessage = { + role: "assistant"; + status: AssitantMessageStatus; + content: AssistantMessageContentPart[]; + metadata: { + steps: AssistantMessageStepMetadata[]; + custom: Record; + }; +}; diff --git a/packages/assistant-stream/src/core/index.ts b/packages/assistant-stream/src/core/index.ts new file mode 100644 index 000000000..8c177f265 --- /dev/null +++ b/packages/assistant-stream/src/core/index.ts @@ -0,0 +1,19 @@ +// tools type error + +export { createAssistantRun } from "./modules/runs"; +export { AssistantStream, type AssistantStreamChunk } from "./AssistantStream"; +export { + DataStreamDecoder, + DataStreamEncoder, +} from "./serialization/DataStream"; +export { PlainTextDecoder, PlainTextEncoder } from "./serialization/PlainText"; +export { + toAssistantMessageStream, + type AsyncIterableStream, +} from "./accumulators/assistant-message"; + + +/** + * @deprecated Use `createAssistantRun` instead. This will be removed in 0.0.1. + */ +export { createAssistantRun as createRun } from "./modules/runs"; \ No newline at end of file diff --git a/packages/assistant-stream/src/core/modules/runs.ts b/packages/assistant-stream/src/core/modules/runs.ts new file mode 100644 index 000000000..9b8af40c6 --- /dev/null +++ b/packages/assistant-stream/src/core/modules/runs.ts @@ -0,0 +1,185 @@ +import { AssistantStream, AssistantStreamChunk } from "../AssistantStream"; +import { generateId } from "../utils/generateId"; +import { createTextStream, TextStreamController } from "./text"; +import { createToolCallStream, ToolCallStreamController } from "./tool-call"; + +type MergeStreamItem = { + reader: ReadableStreamDefaultReader; + promise?: Promise | undefined; +}; + +const promiseWithResolvers = () => { + let resolve: () => void; + let reject: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve!, reject: reject! }; +}; + +const createMergeStream = () => { + const list: MergeStreamItem[] = []; + let sealed = false; + let controller: ReadableStreamDefaultController; + let currentPull: ReturnType | undefined; + + const handlePull = (item: MergeStreamItem) => { + if (!item.promise) { + item.promise = item.reader + .read() + .then(({ done, value }) => { + item.promise = undefined; + if (done) { + list.splice(list.indexOf(item), 1); + if (sealed && list.length === 0) { + controller.close(); + } + } else { + controller.enqueue(value); + } + + currentPull?.resolve(); + currentPull = undefined; + }) + .catch((e) => { + list.forEach((item) => { + item.reader.cancel(); + }); + list.length = 0; + + controller.error(e); + + currentPull?.reject(e); + currentPull = undefined; + }); + } + }; + + const readable = new ReadableStream({ + start(c) { + controller = c; + }, + async pull() { + list.map((item) => { + handlePull(item); + return item.promise!; + }); + + currentPull = promiseWithResolvers(); + return currentPull.promise; + }, + cancel() { + list.forEach((item) => { + item.reader.cancel(); + }); + list.length = 0; + }, + }); + + return { + stream: readable, + seal() { + sealed = true; + if (list.length === 0) controller.close(); + }, + addStream(stream: AssistantStream) { + if (sealed) + throw new Error( + "Cannot add streams after the run callback has settled.", + ); + + const item = { reader: stream.readable.getReader() }; + list.push(item); + if (list.length === 1) { + handlePull(item); + } + }, + }; +}; + +export type RunController = { + appendText(textDelta: string): void; + // addTextPart(): TextStreamController; + // addToolCallPart(toolName: string): ToolCallStreamController; + // addToolCallPart(options: { + // toolCallId: string; + // toolName: string; + // }): ToolCallStreamController; + appendStep(stream: AssistantStream): void; +}; + +class RunControllerImpl implements RunController { + private _merge = createMergeStream(); + private _textPartController: TextStreamController | undefined; + + getReadable() { + return this._merge.stream; + } + + close() { + this._merge.seal(); + this._textPartController?.close(); + } + + appendStep(stream: AssistantStream) { + this._merge.addStream(stream); + } + + appendText(textDelta: string) { + if (!this._textPartController) { + this._textPartController = this.addTextPart(); + } + this._textPartController.append(textDelta); + } + + addTextPart() { + let controller: TextStreamController; + const textStream = createTextStream({ + start(c) { + controller = c; + }, + }); + this.appendStep(textStream); + return controller!; + } + + addToolCallPart( + options: + | string + | { + toolCallId: string; + toolName: string; + }, + ): ToolCallStreamController { + const opt = + typeof options === "string" + ? { toolName: options, toolCallId: generateId() } + : options; + + let controller: ToolCallStreamController; + const toolCallStream = createToolCallStream({ + toolCallId: opt.toolCallId, + toolName: opt.toolName, + start(c) { + controller = c; + }, + }); + this.appendStep(toolCallStream); + return controller!; + } +} + +export function createAssistantRun( + callback: (controller: RunController) => Promise | void, +): AssistantStream { + const controller = new RunControllerImpl(); + const promiseOrVoid = callback(controller); + if (promiseOrVoid instanceof Promise) { + promiseOrVoid.finally(() => controller.close()); + } else { + controller.close(); + } + + return new AssistantStream(controller.getReadable()); +} diff --git a/packages/assistant-stream/src/core/modules/text.ts b/packages/assistant-stream/src/core/modules/text.ts new file mode 100644 index 000000000..bffc75fdc --- /dev/null +++ b/packages/assistant-stream/src/core/modules/text.ts @@ -0,0 +1,47 @@ +import { AssistantStream, AssistantStreamChunk } from "../AssistantStream"; +import { UnderlyingReadable } from "../utils/UnderlyingReadable"; + +export type TextStreamController = { + append(textDelta: string): void; + close(): void; // TODO reason? error? +}; + +class TextStreamControllerImpl implements TextStreamController { + private _controller: ReadableStreamDefaultController; + + constructor( + controller: ReadableStreamDefaultController, + ) { + this._controller = controller; + } + + append(textDelta: string) { + this._controller.enqueue({ + type: "text-delta", + textDelta, + }); + return this; + } + + close() { + this._controller.close(); + } +} + +export const createTextStream = ( + readable: UnderlyingReadable, +): AssistantStream => { + return new AssistantStream( + new ReadableStream({ + start(c) { + return readable.start?.(new TextStreamControllerImpl(c)); + }, + pull(c) { + return readable.pull?.(new TextStreamControllerImpl(c)); + }, + cancel(c) { + return readable.cancel?.(c); + }, + }), + ); +}; diff --git a/packages/assistant-stream/src/core/modules/tool-call.ts b/packages/assistant-stream/src/core/modules/tool-call.ts new file mode 100644 index 000000000..924aa218b --- /dev/null +++ b/packages/assistant-stream/src/core/modules/tool-call.ts @@ -0,0 +1,100 @@ +import { AssistantStream, AssistantStreamChunk } from "../AssistantStream"; +import { UnderlyingReadable } from "../utils/UnderlyingReadable"; +import { createTextStream, TextStreamController } from "./text"; + +export type ToolCallStreamController = { + readonly toolCallId: string; + readonly toolName: string; + + argsText: TextStreamController; + + setResult(result: unknown): void; + close(): void; +}; + +class ToolCallStreamControllerImpl implements ToolCallStreamController { + public get toolCallId() { + return this._options.toolCallId; + } + + public get toolName() { + return this._options.toolName; + } + + constructor( + private _controller: ReadableStreamDefaultController, + private _options: { toolCallId: string; toolName: string }, + ) { + this._controller.enqueue({ + type: "tool-call-begin", + toolCallId: this._options.toolCallId, + toolName: this._options.toolName, + }); + + const stream = createTextStream({ + start: (c) => { + this._argsTextController = c; + }, + }); + stream.readable.pipeTo( + new WritableStream({ + write: (chunk) => { + if (chunk.type !== "text-delta") + throw new Error("Unexpected chunk type"); + + this._controller.enqueue({ + type: "tool-call-delta", + toolCallId: this._options.toolCallId, + argsTextDelta: chunk.textDelta, + }); + }, + }), + ); + } + + get argsText() { + return this._argsTextController; + } + + private _argsTextController!: TextStreamController; + + setResult(result: unknown) { + this._controller.enqueue({ + type: "tool-result", + toolCallId: this._options.toolCallId, + result, + }); + } + + close() { + this._controller.close(); + } +} + +type UnderlyingToolCallStreamReadable = + UnderlyingReadable & { + toolCallId: string; + toolName: string; + }; + +export const createToolCallStream = ( + readable: UnderlyingToolCallStreamReadable, +): AssistantStream => { + const options = { + toolCallId: readable.toolCallId, + toolName: readable.toolName, + }; + return new AssistantStream( + new ReadableStream({ + start(c) { + return readable.start?.(new ToolCallStreamControllerImpl(c, options)); + }, + pull(c) { + return readable.pull?.(new ToolCallStreamControllerImpl(c, options)); + }, + cancel(c) { + return readable.cancel?.(c); + }, + }), + ); +}; diff --git a/packages/assistant-stream/src/core/serialization/DataStream.ts b/packages/assistant-stream/src/core/serialization/DataStream.ts new file mode 100644 index 000000000..e279b713e --- /dev/null +++ b/packages/assistant-stream/src/core/serialization/DataStream.ts @@ -0,0 +1,194 @@ +import { AssistantStreamChunk } from "../AssistantStream"; +import { PipeableTransformStream } from "../utils/PipeableTransformStream"; +import { StreamPart } from "./streamPart/StreamPart"; + +export class DataStreamEncoder { + private _transformStream; + + public get writable() { + return this._transformStream.writable; + } + + public get readable() { + return this._transformStream.readable; + } + + constructor() { + this._transformStream = new PipeableTransformStream< + AssistantStreamChunk, + Uint8Array + >((readable) => { + const transform = new TransformStream({ + transform(chunk, controller) { + const type = chunk.type; + switch (type) { + case "text-delta": + controller.enqueue("0:" + JSON.stringify(chunk.textDelta) + "\n"); + break; + + case "tool-call-begin": + controller.enqueue( + "b:" + + JSON.stringify({ + toolCallId: chunk.toolCallId, + toolName: chunk.toolName, + }) + + "\n", + ); + break; + + case "tool-call-delta": + controller.enqueue( + "c:" + + JSON.stringify({ + toolCallId: chunk.toolCallId, + argsTextDelta: chunk.argsTextDelta, + }) + + "\n", + ); + break; + + case "tool-result": + controller.enqueue( + "a:" + + JSON.stringify({ + toolCallId: chunk.toolCallId, + result: chunk.result, + }) + + "\n", + ); + break; + + default: + const exhaustiveCheck: never = type; + throw new Error(`unsupported chunk type: ${exhaustiveCheck}`); + } + }, + }); + + return readable + .pipeThrough(transform) + .pipeThrough(new TextEncoderStream()); + }); + } +} + +const decodeStreamPart = >( + part: string, +): StreamPart => { + const index = part.indexOf(":"); + if (index === -1) throw new Error("Invalid stream part"); + return { + type: part.slice(0, index), + value: JSON.parse(part.slice(index + 1)), + }; +}; + +export class DataStreamDecoder { + private _transformStream; + + public get writable() { + return this._transformStream.writable; + } + + public get readable() { + return this._transformStream.readable; + } + + constructor() { + this._transformStream = new PipeableTransformStream< + Uint8Array, + AssistantStreamChunk + >((readable) => { + const transform = new TransformStream({ + transform(chunk, controller) { + const { type, value } = decodeStreamPart(chunk); + switch (type) { + case "0": + controller.enqueue({ + type: "text-delta", + textDelta: value as string, + }); + break; + + case "b": { + const { toolCallId, toolName } = value as { + toolCallId: string; + toolName: string; + }; + controller.enqueue({ + type: "tool-call-begin", + toolCallId, + toolName, + }); + break; + } + + case "c": { + const { toolCallId, argsTextDelta } = value as { + toolCallId: string; + argsTextDelta: string; + }; + controller.enqueue({ + type: "tool-call-delta", + toolCallId, + argsTextDelta, + }); + break; + } + + case "a": { + const { toolCallId, result } = value as { + toolCallId: string; + result: any; + }; + + controller.enqueue({ + type: "tool-result", + toolCallId, + result, + }); + break; + } + + default: + const exhaustiveCheck: string = type; + throw new Error(`unsupported chunk type: ${exhaustiveCheck}`); + } + }, + }); + + return readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new ChunkByLineStream()) + .pipeThrough(transform); + }); + } +} + +export class ChunkByLineStream extends TransformStream { + private buffer = ""; + + constructor() { + super({ + transform: (chunk, controller) => { + this.buffer += chunk; + const lines = this.buffer.split("\n"); + + // Process all complete lines + for (let i = 0; i < lines.length - 1; i++) { + controller.enqueue(lines[i]); + } + + // Keep the last incomplete line in the buffer + this.buffer = lines[lines.length - 1]!; + }, + flush: (controller) => { + // flush any remaining content in the buffer + if (this.buffer) { + controller.enqueue(this.buffer); + } + }, + }); + } +} diff --git a/packages/assistant-stream/src/core/serialization/PlainText.ts b/packages/assistant-stream/src/core/serialization/PlainText.ts new file mode 100644 index 000000000..af6f72d15 --- /dev/null +++ b/packages/assistant-stream/src/core/serialization/PlainText.ts @@ -0,0 +1,81 @@ +import { AssistantStreamChunk } from "../AssistantStream"; +import { PipeableTransformStream } from "../utils/PipeableTransformStream"; + +export class PlainTextEncoder + implements ReadableWritablePair +{ + private _transformStream; + + public get writable() { + return this._transformStream.writable; + } + + public get readable() { + return this._transformStream.readable; + } + + constructor() { + this._transformStream = new PipeableTransformStream< + AssistantStreamChunk, + Uint8Array + >((readable) => { + const transform = new TransformStream({ + transform(chunk, controller) { + const type = chunk.type; + switch (type) { + case "text-delta": + controller.enqueue(chunk.textDelta); + break; + + case "tool-call-begin": + case "tool-call-delta": + case "tool-result": + throw new Error( + `tool-call-begin and tool-call-delta are not supported in plain text serialization`, + ); + + default: + const exhaustiveCheck: never = type; + throw new Error(`unsupported chunk type: ${exhaustiveCheck}`); + } + }, + }); + + return readable + .pipeThrough(transform) + .pipeThrough(new TextEncoderStream()); + }); + } +} + +export class PlainTextDecoder { + private _transformStream; + + public get writable() { + return this._transformStream.writable; + } + + public get readable() { + return this._transformStream.readable; + } + + constructor() { + this._transformStream = new PipeableTransformStream< + Uint8Array, + AssistantStreamChunk + >((readable) => { + const transform = new TransformStream({ + transform(chunk, controller) { + controller.enqueue({ + type: "text-delta", + textDelta: chunk, + }); + }, + }); + + return readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(transform); + }); + } +} diff --git a/packages/assistant-stream/src/core/serialization/streamPart/StreamPart.ts b/packages/assistant-stream/src/core/serialization/streamPart/StreamPart.ts new file mode 100644 index 000000000..2c7f73a08 --- /dev/null +++ b/packages/assistant-stream/src/core/serialization/streamPart/StreamPart.ts @@ -0,0 +1,3 @@ +export type StreamPart> = { + [K in keyof T]: { type: K; value: T[K] }; +}[keyof T]; diff --git a/packages/assistant-stream/src/core/test.ts b/packages/assistant-stream/src/core/test.ts new file mode 100644 index 000000000..391621078 --- /dev/null +++ b/packages/assistant-stream/src/core/test.ts @@ -0,0 +1,31 @@ +// span = assistant-stream +// you can nest assistant-streams (inside spans that accept nesting) +// 1 content part = 1 assistant-stream / span + +// root container span +// spans have controllers to emit events +// assistant-stream === span (span: start, events, finish, errors?) + +import { createAssistantRun } from "./modules/runs"; +import { AssistantStream } from "./AssistantStream"; +import { + DataStreamDecoder, + DataStreamEncoder, +} from "./serialization/DataStream"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const POST = () => { + const stream = createAssistantRun(async (controller) => { + controller.appendText("hello "); + await sleep(1000); + controller.appendText("world!"); + }); + return stream.toResponse(new DataStreamEncoder()); +}; + +const stream = AssistantStream.fromResponse(POST(), new DataStreamDecoder()); +const reader = stream.readable.getReader(); +console.log(await reader.read()); +console.log(await reader.read()); +console.log(await reader.read()); diff --git a/packages/assistant-stream/src/core/utils/PipeableTransformStream.ts b/packages/assistant-stream/src/core/utils/PipeableTransformStream.ts new file mode 100644 index 000000000..e97e9fccd --- /dev/null +++ b/packages/assistant-stream/src/core/utils/PipeableTransformStream.ts @@ -0,0 +1,10 @@ +export class PipeableTransformStream extends TransformStream { + constructor(transform: (readable: ReadableStream) => ReadableStream) { + super(); + const readable = transform(super.readable as any); + Object.defineProperty(this, "readable", { + value: readable, + writable: false, + }); + } +} diff --git a/packages/assistant-stream/src/core/utils/UnderlyingReadable.ts b/packages/assistant-stream/src/core/utils/UnderlyingReadable.ts new file mode 100644 index 000000000..1723b1fb1 --- /dev/null +++ b/packages/assistant-stream/src/core/utils/UnderlyingReadable.ts @@ -0,0 +1,5 @@ +export type UnderlyingReadable = { + start?: (controller: TController) => void; + pull?: (controller: TController) => void | PromiseLike; + cancel?: UnderlyingSourceCancelCallback; +}; diff --git a/packages/assistant-stream/src/core/utils/generateId.tsx b/packages/assistant-stream/src/core/utils/generateId.tsx new file mode 100644 index 000000000..0452f47cd --- /dev/null +++ b/packages/assistant-stream/src/core/utils/generateId.tsx @@ -0,0 +1,6 @@ +import { customAlphabet } from "nanoid/non-secure"; + +export const generateId = customAlphabet( + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + 7, +); diff --git a/packages/assistant-stream/src/core/utils/types.ts b/packages/assistant-stream/src/core/utils/types.ts new file mode 100644 index 000000000..8d5bb5c90 --- /dev/null +++ b/packages/assistant-stream/src/core/utils/types.ts @@ -0,0 +1,84 @@ +type TextStatus = + | { + type: "running"; + } + | { + type: "complete"; + reason: "stop" | "unknown"; + } + | { + type: "incomplete"; + reason: "cancelled" | "length" | "content-filter" | "other"; + }; + +type TextContentPart = { + type: "text"; + text: string; + status: TextStatus; +}; + +type ToolCallStatus = + | { + type: "running"; + isArgsComplete: boolean; + } + | { + type: "requires-action"; + reason: "tool-call-result"; + } + | { + type: "complete"; + reason: "stop" | "unknown"; + } + | { + type: "incomplete"; + reason: "cancelled" | "length" | "content-filter" | "other"; + }; + +type ToolCallContentPart = { + type: "tool-call"; + status: ToolCallStatus; + toolCallId: string; + toolName: string; + argsText: string; + args: Record; + result?: unknown; +}; + +type AssistantMessageContentPart = TextContentPart | ToolCallContentPart; + +type AssistantMessageStepMetadata = {}; + +export type AssitantMessageStatus = + | { + type: "running"; + } + | { + type: "requires-action"; + reason: "tool-calls"; + } + | { + type: "complete"; + reason: "stop" | "unknown"; + } + | { + type: "incomplete"; + reason: + | "cancelled" + | "tool-calls" + | "length" + | "content-filter" + | "other" + | "error"; + error?: unknown; + }; + +export type AssistantMessage = { + role: "assistant"; + status: AssitantMessageStatus; + content: AssistantMessageContentPart[]; + metadata: { + steps: AssistantMessageStepMetadata[]; + custom: Record; + }; +}; diff --git a/packages/assistant-stream/src/index.ts b/packages/assistant-stream/src/index.ts new file mode 100644 index 000000000..8d119dee8 --- /dev/null +++ b/packages/assistant-stream/src/index.ts @@ -0,0 +1 @@ +export * from "./core"; diff --git a/packages/assistant-stream/tsconfig.json b/packages/assistant-stream/tsconfig.json new file mode 100644 index 000000000..d1150302c --- /dev/null +++ b/packages/assistant-stream/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@assistant-ui/tsconfig/base.json", + "compilerOptions": { + "paths": { + "@assistant-stream/*": ["../*/src"] + } + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3b829791..19f2942a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -935,7 +935,33 @@ importers: specifier: ^5.6.2 version: 5.6.2 - packages/assistant-stream: {} + packages/assistant-stream: + dependencies: + nanoid: + specifier: ^5.0.7 + version: 5.0.7 + secure-json-parse: + specifier: ^2.7.0 + version: 2.7.0 + devDependencies: + '@assistant-ui/tsconfig': + specifier: workspace:* + version: link:../tsconfig + ai: + specifier: ^3.4.9 + version: 3.4.9(openai@4.67.1(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.6.2))(zod@3.23.8) + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 14.2.14 + version: 14.2.14(eslint@8.57.1)(typescript@5.6.2) + tsup: + specifier: 8.3.0 + version: 8.3.0(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.2)(yaml@2.5.1) + tsx: + specifier: ^4.19.1 + version: 4.19.1 packages/cli: dependencies: @@ -1584,6 +1610,15 @@ packages: vue: optional: true + '@ai-sdk/vue@0.0.54': + resolution: {integrity: sha512-Ltu6gbuii8Qlp3gg7zdwdnHdS4M8nqKDij2VVO1223VOtIFwORFJzKqpfx44U11FW8z2TPVBYN+FjkyVIcN2hg==} + engines: {node: '>=18'} + peerDependencies: + vue: ^3.3.4 + peerDependenciesMeta: + vue: + optional: true + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -3427,6 +3462,27 @@ packages: zod: optional: true + ai@3.4.9: + resolution: {integrity: sha512-wmVzpIHNGjCEjIJ/3945a/DIkz+gwObjC767ZRgO8AmtIZMO5KqvqNr7n2KF+gQrCPCMC8fM1ICQFXSvBZnBlA==} + engines: {node: '>=18'} + peerDependencies: + openai: ^4.42.0 + react: ^18 || ^19 + sswr: ^2.1.0 + svelte: ^3.0.0 || ^4.0.0 + zod: ^3.0.0 + peerDependenciesMeta: + openai: + optional: true + react: + optional: true + sswr: + optional: true + svelte: + optional: true + zod: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -4105,6 +4161,7 @@ packages: eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -6653,6 +6710,16 @@ snapshots: transitivePeerDependencies: - zod + '@ai-sdk/vue@0.0.54(vue@3.5.10(typescript@5.6.2))(zod@3.23.8)': + dependencies: + '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) + swrv: 1.0.4(vue@3.5.10(typescript@5.6.2)) + optionalDependencies: + vue: 3.5.10(typescript@5.6.2) + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -8756,6 +8823,32 @@ snapshots: - solid-js - vue + ai@3.4.9(openai@4.67.1(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.10(typescript@5.6.2))(zod@3.23.8): + dependencies: + '@ai-sdk/provider': 0.0.24 + '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) + '@ai-sdk/react': 0.0.62(react@18.3.1)(zod@3.23.8) + '@ai-sdk/solid': 0.0.49(zod@3.23.8) + '@ai-sdk/svelte': 0.0.51(svelte@4.2.19)(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) + '@ai-sdk/vue': 0.0.54(vue@3.5.10(typescript@5.6.2))(zod@3.23.8) + '@opentelemetry/api': 1.9.0 + eventsource-parser: 1.1.2 + json-schema: 0.4.0 + jsondiffpatch: 0.6.0 + nanoid: 3.3.6 + secure-json-parse: 2.7.0 + zod-to-json-schema: 3.23.2(zod@3.23.8) + optionalDependencies: + openai: 4.67.1(zod@3.23.8) + react: 18.3.1 + sswr: 2.1.0(svelte@4.2.19) + svelte: 4.2.19 + zod: 3.23.8 + transitivePeerDependencies: + - solid-js + - vue + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -9433,8 +9526,8 @@ snapshots: '@typescript-eslint/parser': 8.8.0(eslint@8.57.1)(typescript@5.6.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) eslint-plugin-react: 7.37.1(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -9453,37 +9546,37 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.8.0(eslint@8.57.1)(typescript@5.6.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -9494,7 +9587,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 diff --git a/python/assistant-stream/LICENSE b/python/assistant-stream/LICENSE new file mode 100644 index 000000000..bc8bb2882 --- /dev/null +++ b/python/assistant-stream/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Simon Farshid + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python/assistant-stream/README.md b/python/assistant-stream/README.md new file mode 100644 index 000000000..f1adb2e77 --- /dev/null +++ b/python/assistant-stream/README.md @@ -0,0 +1 @@ +## assistant-stream diff --git a/python/assistant-stream/poetry.lock b/python/assistant-stream/poetry.lock new file mode 100644 index 000000000..5fb45f192 --- /dev/null +++ b/python/assistant-stream/poetry.lock @@ -0,0 +1,254 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "anyio" +version = "4.5.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78"}, + {file = "anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.115.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"}, + {file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.39.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.38.6" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"}, + {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8.0" +content-hash = "c963b47bcfc1f1664e7ac65e14836e10b3bd8d3d07e7d7f72327d1c4b6f97517" diff --git a/python/assistant-stream/pyproject.toml b/python/assistant-stream/pyproject.toml new file mode 100644 index 000000000..169d31eb6 --- /dev/null +++ b/python/assistant-stream/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[project] +name = "assistant-stream" +version = "0.0.1" +authors = [ + { name="Simon Farshid", email="simon.farshid@outlook.com" }, +] +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/Yonom/assistant-ui" +Issues = "https://github.com/Yonom/assistant-ui/issues" + +[tool.poetry] +name = "assistant-stream" +version = "0.0.1" +description = "" +authors = ["Simon Farshid "] + +[tool.poetry.dependencies] +python = "^3.8.0" +fastapi = "^0.115.0" diff --git a/python/assistant-stream/src/assistant_stream/__init__.py b/python/assistant-stream/src/assistant_stream/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/assistant-stream/src/assistant_stream/assistant_stream.py b/python/assistant-stream/src/assistant_stream/assistant_stream.py new file mode 100644 index 000000000..1e260b1be --- /dev/null +++ b/python/assistant-stream/src/assistant_stream/assistant_stream.py @@ -0,0 +1,6 @@ +import asyncio + + +class AssistantStream: + def __init__(self, queue: asyncio.Queue): + self.queue = queue diff --git a/python/assistant-stream/src/assistant_stream/assistant_stream_chunk.py b/python/assistant-stream/src/assistant_stream/assistant_stream_chunk.py new file mode 100644 index 000000000..6e8b1f571 --- /dev/null +++ b/python/assistant-stream/src/assistant_stream/assistant_stream_chunk.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from typing import Any, Union + + +# Define the data classes for different chunk types +@dataclass +class TextDeltaChunk: + type: str # "text-delta" + textDelta: str + +@dataclass +class ToolCallBeginChunk: + type: str # "tool-call-begin" + toolCallId: str + toolName: str + +@dataclass +class ToolCallDeltaChunk: + type: str # "tool-call-delta" + toolCallId: str + argsTextDelta: str + +@dataclass +class ToolResultChunk: + type: str # "tool-result" + toolCallId: str + result: Any + +# Define the union type for AssistantStreamChunk +AssistantStreamChunk = Union[ + TextDeltaChunk, + ToolCallBeginChunk, + ToolCallDeltaChunk, + ToolResultChunk, +] \ No newline at end of file diff --git a/python/assistant-stream/src/assistant_stream/assistant_stream_response.py b/python/assistant-stream/src/assistant_stream/assistant_stream_response.py new file mode 100644 index 000000000..59544fe53 --- /dev/null +++ b/python/assistant-stream/src/assistant_stream/assistant_stream_response.py @@ -0,0 +1,16 @@ +from assistant_stream import AssistantStream, DataStreamEncoder +from fastapi.responses import StreamingResponse +from assistant_stream.serialization.data_stream import DataStreamEncoder + + +class AssistantStreamResponse(StreamingResponse): + def __init__(self, stream: AssistantStream, stream_format: DataStreamEncoder): + async def stream_generator(): + while True: + chunk = await stream.queue.get() + if chunk is None: + break + yield stream_format.encode(chunk) + stream.queue.task_done() + + super().__init__(stream_generator(), media_type="text/plain") diff --git a/python/assistant-stream/src/assistant_stream/create_assistant_run.py b/python/assistant-stream/src/assistant_stream/create_assistant_run.py new file mode 100644 index 000000000..d2b78eb82 --- /dev/null +++ b/python/assistant-stream/src/assistant_stream/create_assistant_run.py @@ -0,0 +1,44 @@ +import asyncio +from typing import Any, AsyncGenerator, Callable, Coroutine +from assistant_stream.assistant_stream_chunk import AssistantStreamChunk, TextDeltaChunk + + +class RunController: + def __init__(self, queue): + self.queue = queue + self.loop = asyncio.get_event_loop() + + def append_text(self, text_delta: str): + """Append a text delta to the stream.""" + chunk = TextDeltaChunk(type="text-delta", textDelta=text_delta) + self.loop.call_soon_threadsafe(self.queue.put_nowait, chunk) + + def append_step(self, stream: AsyncGenerator[AssistantStreamChunk, None]): + """Append a substream to the main stream.""" + + async def reader(): + async for chunk in stream: + await self.queue.put(chunk) + + return asyncio.create_task(reader()) + + +async def create_assistant_run( + callback: Callable[[RunController], Coroutine[Any, Any, None]] +) -> AsyncGenerator[AssistantStreamChunk, None]: + queue = asyncio.Queue() + controller = RunController(queue) + + async def background_task(): + await callback(controller) + queue.put_nowait(None) + + task = asyncio.create_task(background_task()) + + while True: + chunk = await controller.queue.get() + if chunk is None: + break + yield chunk + + await task diff --git a/python/assistant-stream/src/assistant_stream/modules/text.py b/python/assistant-stream/src/assistant_stream/modules/text.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/assistant-stream/src/assistant_stream/modules/tool_call.py b/python/assistant-stream/src/assistant_stream/modules/tool_call.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/assistant-stream/src/assistant_stream/serialization/data_stream.py b/python/assistant-stream/src/assistant_stream/serialization/data_stream.py new file mode 100644 index 000000000..9e71408b1 --- /dev/null +++ b/python/assistant-stream/src/assistant_stream/serialization/data_stream.py @@ -0,0 +1,18 @@ +from assistant_stream import AssistantStreamChunk +import json + + +class DataStreamEncoder: + def __init__(self): + pass + + def encode(self, chunk: AssistantStreamChunk) -> str: + if chunk.type == "text-delta": + return f"0:{json.dumps(chunk.textDelta)}\n" + elif chunk.type == "tool-call-begin": + return f"b:{json.dumps({ "toolCallId": chunk.toolCallId, "toolName": chunk.toolName })}\n" + elif chunk.type == "tool-call-delta": + return f"c:{json.dumps({ "toolCallId": chunk.toolCallId, "argsTextDelta": chunk.argsTextDelta })}\n" + elif chunk.type == "tool-result": + return f"a:{json.dumps({ "toolCallId": chunk.toolCallId, "result": chunk.result })}\n" + pass