Zenn
Closed5

Structured Output + ストリーミングで常にvalidなレスポンスを返す

kun432kun432

Structured Outputをストリーミングでやる場合、例えば以下のような出力フォーマットの場合

from typing import List
from pydantic import BaseModel

class EntitiesModel(BaseModel):
    colors: List[str]
    animals: List[str]

messages=[
    {
        "role": "system",
        "content": "入力テキストからエンティティを抽出して。"},
    {
        "role": "user",
        "content": "茶色いキツネは青い瞳の怠け者の犬を飛び越える。",
    },
]

ストリーミング中のレスポンスも"valid"であってほしい。こんな感じで。

出力
 {}
 {'colors': []}
 {'colors': ['茶色']}
 {'colors': ['茶色', '青']}
 {'colors': ['茶色', '青'], 'animals': []}
 {'colors': ['茶色', '青'], 'animals': ['キツネ']}
 {'colors': ['茶色', '青'], 'animals': ['キツネ', '犬']}

こうであってほしくない。これだとストリーミングの完了を待たないとパースできないので、ストリーミングである必要がない。

出力
{
'
colors
'
:

[
'
茶
色
'
,
(snip)

でOpenAIの場合はこれが可能。

https://zenn.dev/link/comments/78e3d6b20c6bf6

再掲。以下記事でやった例。

from typing import List
from pydantic import BaseModel
from openai import OpenAI
from enum import Enum


class Sentiment(str, Enum):
    happy = "happy"
    angry = "angry"
    sad = "sad"
    fun = "fun"


class ResponseWithSentiment(BaseModel):
    message: List[str]
    sentiment: Sentiment


client = OpenAI()

with client.beta.chat.completions.stream(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "system",
            "content": "あなたは親切な日本語のアシスタントです。"},
        {
            "role": "user",
            "content": "おはよう。。。昨日は競馬ボロ負けしちゃったよ。。。。",
        },
    ],
    response_format=ResponseWithSentiment,
) as stream:
    for event in stream:
        if event.type == "content.delta":
            if event.parsed is not None:
                # パースされたデータをJSONで出力
                print("パースされた差分コンテンツ:", event.parsed)
        elif event.type == "content.done":
            print("コンテンツ終了")
        elif event.type == "error":
            print("ストリームでエラーが発生しました:", event.error)

final_completion = stream.get_final_completion()
print("最終結果:")
pprint(final_completion.choices[0].message.parsed.model_dump())
出力
パースされた差分コンテンツ: {}
パースされた差分コンテンツ: {}
パースされた差分コンテンツ: {'message': []}
パースされた差分コンテンツ: {'message': []}
パースされた差分コンテンツ: {'message': []}
パースされた差分コンテンツ: {'message': []}
パースされた差分コンテンツ: {'message': []}
パースされた差分コンテンツ: {'message': []}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。']}
パースされた差分コンテンツ: {'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。'], 'sentiment': 'sad'}
コンテンツ終了
最終結果:
{'message': ['おはようございます!', '競馬で負けてしまったのは残念ですね。'],
 'sentiment': <Sentiment.sad: 'sad'>}

さすがに値の中身までストリーミングしてくれるわけではない(なのでListを使っている)が、このあたりはOpenAI SDKがよしなにやってくれる。

なお、Function Callingもレスポンスのストリーミングに対応している。

https://platform.openai.com/docs/guides/function-calling?api-mode=chat#streaming

のだが、こちらは値の中身がストリーミングされるので、結合してやる必要があるみたい。ドキュメントのサンプルコードを元に書いてみた。

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_reply_with_sentiment",
        "description": "ユーザのクエリに対し、回答を返すと共に、ユーザのクエリから読み取れる感情を付与する。",
        "parameters": {
            "type": "object",
            "properties": {
                "reply": {
                    "type": "string",
                    "description": "あなたの返答",
                },
                "sentiment": {
                    "type": "string",
                    "enum": ["happy", "angry", "sad", "fun", "neutral"],
                    "description": "ユーザのクエリから読み取れる感情",
                },
            },
            "required": ["reply", "sentiment"],
            "additionalProperties": False
        },
        "strict": True
    }
}]

stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "先週末の競馬、馬券を買い間違えて、当ててたはずの万馬券を逃しちゃったよ・・・・"
        }
    ],
    tools=tools,
    stream=True
)

final_tool_calls = {}

for chunk in stream:
    for tool_call in chunk.choices[0].delta.tool_calls or []:
        index = tool_call.index

        if index not in final_tool_calls:
            final_tool_calls[index] = tool_call

        final_tool_calls[index].function.arguments += tool_call.function.arguments
        print(final_tool_calls[0].function.arguments)

結果

出力

{"
{"reply
{"reply":"
{"reply":"それ
{"reply":"それは
{"reply":"それは残
{"reply":"それは残念
{"reply":"それは残念でした
{"reply":"それは残念でしたね
{"reply":"それは残念でしたね。
{"reply":"それは残念でしたね。き
{"reply":"それは残念でしたね。きっと
{"reply":"それは残念でしたね。きっと悔
{"reply":"それは残念でしたね。きっと悔しい
{"reply":"それは残念でしたね。きっと悔しい思
{"reply":"それは残念でしたね。きっと悔しい思い
{"reply":"それは残念でしたね。きっと悔しい思いを
{"reply":"それは残念でしたね。きっと悔しい思いをされた
{"reply":"それは残念でしたね。きっと悔しい思いをされたので
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回は
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はき
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出る
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ること
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!また
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャ
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレン
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジ
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジして
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみ
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみて
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてください
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。","
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。","sent
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。","sentiment
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。","sentiment":"
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。","sentiment":"sad
{"reply":"それは残念でしたね。きっと悔しい思いをされたのでしょう。でも、次回はきっと良い結果が出ることを願っています!またチャレンジしてみてくださいね。","sentiment":"sad"}

だめじゃん。

ということで、ネイティブSDKしかでき無いといろいろ辛いし、これは他のモデルプロバイダでも同じ。

ちょっと代替案を探してみる。

kun432kun432

Instructorを使う。ちなみに。EnumやLiteralを使う場合はPartialLiteralMixinを使うのがポイント。

https://python.useinstructor.com/concepts/partial/?h=partialliteralmixin

import openai
import instructor
from typing import List, Literal, Optional
from pydantic import BaseModel, Field
from enum import Enum
from instructor.dsl.partial import PartialLiteralMixin
import time

class Sentiment(Enum):
    happy = "happy"
    angry = "angry"
    sad = "sad"
    fun = "fun"
    neutral = "neutral"


class ResponseWithSentiment(BaseModel, PartialLiteralMixin):
    reply: List[str] = Field(description="あなたの返答。できるだけ長く。")
    sentiment: Sentiment= Field(description="ユーザのクエリから読み取れる感情")


text = "先週末の競馬、馬券を買い間違えて、当ててたはずの万馬券を逃しちゃったよ・・・・"

client = instructor.from_openai(openai.OpenAI())

PartialResponseWithSentiment = instructor.Partial[ResponseWithSentiment]

stream = client.chat.completions.create_partial(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": text},
    ],
    response_model=PartialResponseWithSentiment,
)
for event in stream:
    print(event.model_dump_json())
出力
{"reply":null,"sentiment":null}
{"reply":null,"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":[],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!","もう一度、競馬を楽しんで、是非リベンジしてみてください。応援しています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!","もう一度、競馬を楽しんで、是非リベンジしてみてください。応援しています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!","もう一度、競馬を楽しんで、是非リベンジしてみてください。応援しています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!","もう一度、競馬を楽しんで、是非リベンジしてみてください。応援しています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!","もう一度、競馬を楽しんで、是非リベンジしてみてください。応援しています!"],"sentiment":null}
{"reply":["それは本当に残念でしたね。競馬は運やタイミングが影響しますから、間違えてしまうこともありますよね。","次回はぜひ、買い間違えないように気を付けてください。万馬券を逃したのは悔しいですが、次のチャンスが待っています!","もう一度、競馬を楽しんで、是非リベンジしてみてください。応援しています!"],"sentiment":"sad"}
kun432kun432

json-repairを使う

https://zenn.dev/kun432/scraps/99bc65d6586afc

さっきのFunction Callingのストリーミングのサンプルをjson-repairを使ってみる。

from openai import OpenAI
import json_repair

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_reply_with_sentiment",
        "description": "ユーザのクエリに対し、回答を返すと共に、ユーザのクエリから読み取れる感情を付与する。",
        "parameters": {
            "type": "object",
            "properties": {
                "reply": {
                    "type": "string",
                    "description": "あなたの返答",
                },
                "sentiment": {
                    "type": "string",
                    "enum": ["happy", "angry", "sad", "fun", "neutral"],
                    "description": "ユーザのクエリから読み取れる感情",
                },
            },
            "required": ["reply", "sentiment"],
            "additionalProperties": False
        },
        "strict": True
    }
}]

stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "先週末の競馬、馬券を買い間違えて、当ててたはずの万馬券を逃しちゃったよ・・・・"
        }
    ],
    tools=tools,
    stream=True
)

final_tool_calls = {}

for chunk in stream:
    for tool_call in chunk.choices[0].delta.tool_calls or []:
        index = tool_call.index

        if index not in final_tool_calls:
            final_tool_calls[index] = tool_call

        final_tool_calls[index].function.arguments += tool_call.function.arguments
        print(json_repair.repair_json(final_tool_calls[index].function.arguments, return_objects=True))

結果

出力
{}
{}
{'reply': ''}
{'reply': 'それ'}
{'reply': 'それは'}
{'reply': 'それは本'}
{'reply': 'それは本当に'}
{'reply': 'それは本当に悔'}
{'reply': 'それは本当に悔しい'}
{'reply': 'それは本当に悔しいですよ'}
{'reply': 'それは本当に悔しいですよね'}
{'reply': 'それは本当に悔しいですよね。'}
{'reply': 'それは本当に悔しいですよね。慌'}
{'reply': 'それは本当に悔しいですよね。慌ただ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中で'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中での'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミス'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避け'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けら'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられない'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこと'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないことも'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともあります'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、や'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ち'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになります'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回は'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はし'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっか'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかり'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴ら'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せ'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せる'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せると'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといい'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。', 'sentiment': ''}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。', 'sentiment': 'sad'}
{'reply': 'それは本当に悔しいですよね。慌ただしい中でのミスは避けられないこともありますが、やっぱり残念な気持ちになりますよね。次回はしっかりと確認して、素晴らしい結果を出せるといいですね。', 'sentiment': 'sad'}
kun432kun432

Pydanticで Partial JSON Parsing

https://docs.pydantic.dev/latest/concepts/json/#partial-json-parsing

from openai import OpenAI
from pydantic_core import from_json

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_reply_with_sentiment",
        "description": "ユーザのクエリに対し、回答を返すと共に、ユーザのクエリから読み取れる感情を付与する。",
        "parameters": {
            "type": "object",
            "properties": {
                "reply": {
                    "type": "string",
                    "description": "あなたの返答",
                },
                "sentiment": {
                    "type": "string",
                    "enum": ["happy", "angry", "sad", "fun", "neutral"],
                    "description": "ユーザのクエリから読み取れる感情",
                },
            },
            "required": ["reply", "sentiment"],
            "additionalProperties": False
        },
        "strict": True
    }
}]

stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "user",
            "content": "先週末の競馬、馬券を買い間違えて、当ててたはずの万馬券を逃しちゃったよ・・・・"
        }
    ],
    tools=tools,
    stream=True
)

final_tool_calls = {}

for chunk in stream:
    for tool_call in chunk.choices[0].delta.tool_calls or []:
        index = tool_call.index

        if index not in final_tool_calls:
            final_tool_calls[index] = tool_call

        final_tool_calls[index].function.arguments += tool_call.function.arguments
        if final_tool_calls[index].function.arguments:
            print(from_json(final_tool_calls[index].function.arguments, allow_partial=True))
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{}
{'reply': 'それは大変でしたね。馬券の買い間違いはショックですよね。次回こそは良い結果が出るといいですね。'}
{'reply': 'それは大変でしたね。馬券の買い間違いはショックですよね。次回こそは良い結果が出るといいですね。'}
{'reply': 'それは大変でしたね。馬券の買い間違いはショックですよね。次回こそは良い結果が出るといいですね。'}
{'reply': 'それは大変でしたね。馬券の買い間違いはショックですよね。次回こそは良い結果が出るといいですね。'}
{'reply': 'それは大変でしたね。馬券の買い間違いはショックですよね。次回こそは良い結果が出るといいですね。'}
{'reply': 'それは大変でしたね。馬券の買い間違いはショックですよね。次回こそは良い結果が出るといいですね。', 'sentiment': 'sad'}
kun432kun432

汎用的なやり方であれば、どのモデルプロバイダのものでもある程度なんとかなりそう。

このスクラップは27日前にクローズされました
ログインするとコメントできます