Jevが公開され、居ても立っても居られず試してみました。
実務的なシステムに組み込む前に、
そもそも日本語が通るのか、
それと、どこまでへんちくりんな文章を理解するかを試してみました。
環境
- TypeSafe API
- Python 3.11
下記のコードは私の趣味で標準ライブラリのみで書いてます。
たとえば、Lambdaなどの環境でもそのまま手軽に試せるメリットがあります。
なので、お試ししてみたい方はポン付けで利用できるかもしれないです。
※./.envに保存したAPI_KEYに取得したキーを記入してください
(動くと思うけど、そうは言わない小心者)
コード
全体として、
- 関数
-
./.envに保存したAPI_KEYを読み込む - POSTする
- Jevを呼び出す関数※ここが本題
- 文章などを定義した
main()
という構成になっています。
-
'''
標準ライブラリのみでjevを使う
'''
import os
import json
import logging
import urllib.error
import urllib.request
from typing import Any
from pathlib import Path
logger = logging.getLogger(__name__)
def get_api_key(
env_path: str | Path = ".env",
key_name: str = "API_KEY",
encoding: str = "UTF-8",
) -> str | None:
"""環境変数または.envファイルから指定されたキーの値を取得する。
優先順位: 1. プロセス上の環境変数 (os.environ) 2. .envファイル
"""
# 1. Lambdaなどの環境変数を最優先で確認(デファクトスタンダード)
if (value := os.environ.get(key_name)) is not None:
return value
path = Path(env_path)
if not path.is_file():
return None
# 2. .envファイルの走査
try:
for line in path.read_text(encoding=encoding).splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
# キーと値に分割(最初の '=' のみで分割)
if "=" in line:
k, v = line.split("=", 1)
if k.strip() == key_name:
return v.strip().strip("'\"")
except OSError as e:
# ファイル読み込み時の予期せぬI/Oエラー対策
print(f"Failed to read .env file: {e}")
return None
def http_post(
url: str,
api_key: str,
post_data: dict[str, Any] | list[Any],
timeout: float = 10.0,
encoding: str = "UTF-8",
):
"""
指定されたエンドポイントへJSONペイロードをPOST
Args:
url: リクエスト先のurl
api_key: 認証用のBearerトークン
post_data: 送信データ(dictまたはlist)
timeout: タイムアウト秒数
Returns:
str: デコードされたレスポンスボディ
"""
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
json_data = json.dumps(post_data).encode(encoding)
req = urllib.request.Request(
url=url,
data=json_data,
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode(encoding)
except urllib.error.HTTPError as e:
# 4xx, 5xxなどのHTTPステータスエラー
error_body = e.read().decode(encoding, errors="ignore")
logger.error(
"HTTP error occurred: status=%d, reason=%s, body=%s",
e.code,
e.reason,
error_body,
)
raise
except urllib.error.URLError as e:
# ネットワーク接続エラー(DNS名前解決失敗、タイムアウト等)
logger.error("URL error occurred: %s", e.reason)
raise
except Exception as e:
logger.exception("Unexpected error during HTTP POST request: %s", e)
raise
def typesafe_function(
url: str = "https://api.typesafe.ai/v1/systemone",
typesafe_api_key: str | None = None,
model: str = "jev-latest",
state: dict[str, Any] | None = None,
questions: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
jevにリクエストを投げる
"""
post_data: dict[str, Any] = {
"model": model,
}
if state is not None:
post_data["state"] = state
if questions is not None:
post_data["questions"] = questions
response_text = http_post(
url=url,
api_key=typesafe_api_key,
post_data=post_data,
)
try:
result = json.loads(response_text)
if not isinstance(result, dict):
raise TypeError(f"Expected JSON object (dict), got {type(result).__name__}")
return result
except json.JSONDecodeError as e:
raise ValueError(
f"Failed to parse response as JSON. Raw response: {response_text}"
) from e
def main():
"""
choice:選択 リストからオプションを選択
score:スコア 評価基準に基づいて評価
noul:ヌール この記述は正しいか(0-1)
"""
# 日本語での試し
state = "月見そばが大好きだ。熱々の出汁で半熟になったのを崩したり、黄身を分けて箸でつかめるか試したり、白く濁った白身の真ん中にネギを集め箱庭化してそっとネギだけを食べたり。そうやって鑑賞するのが好きなのであって、食べるのはそうではない。"
questions = {
"like": {
"type": "choice",
"instructions": "この人が食べるのが好きなのは?",
"criteria": {
"tamago": "玉子",
"negi": "ネギ",
"soba": "そば"
}
},
"soba_style": {
"type": "score",
"instructions": "月見そばに対する姿勢は?",
"criteria": [
"好き",
"見るだけにしたい",
"嫌い"
]
},
"egg_eat_like": {
"type": "noul",
"instructions": "この人は玉子を食べるのが好き?"
}
}
# 実行
api_key = get_api_key()
if not api_key:
raise ValueError("API key is not set")
response = typesafe_function(
typesafe_api_key = api_key,
state = state,
questions = questions,
)
# 出力
print(json.dumps(response, ensure_ascii=False, indent=2))
return response
if __name__ == "__main__":
main()
お試しの解説
次の内容を判定させました
月見そばが大好きだ。熱々の出汁で半熟になったのを崩したり、黄身を分けて箸でつかめるか試したり、白く濁った白身の真ん中にネギを集め箱庭化してそっとネギだけを食べたり。そうやって鑑賞するのが好きなのであって、食べるのはそうではない。
書きながら「この人どうにかしている」と思っていたのですが、それはそれとして。
ネギ好きの人が月見そばの玉子で遊びながらネギを食べるのが好きというひねくれた文章をもとに、次のような質問をしています。
また、玉子という単語を文面に出さずに玉子について判定させることをやっています。
- この人が食べるのが好きなのは?
- 月見そばに対する姿勢は?
- この人は玉子を食べるのが好き?
簡単なコード解説
今回のスクリプトは、Pythonの標準ライブラリのみで構成されています。
冒頭でも触れていますが、主な役割は次の3つのブロックに分かれています。
-
get_api_key: プロセス上の環境変数(os.environ)、または、カレントディレクトリの.envから、指定したキー(デフォルトでは API_KEY)を取得 -
http_post: 標準のurllib.requestで、指定されたURLへJSONペイロードをPOST送信する。HTTPエラーやネットワークエラーのロギング・例外処理も行っている -
typesafe_function: TypeSafe APIのエンドポイントに対し、モデル名、ステート、質問群をラップしてリクエストを送り、返却されたJSONレスポンスをバリデーションしつつ辞書型で返す
main()では、テキスト(ステート)と、それに対する3つの異なる型(choice, score, noul)の質問を定義し、APIへ渡して結果を出力しています。
出力
生データ
{
"model": "jev-1.13.0",
"answers": {
"like": {
"type": "choice",
"choice": "negi",
"confidence": 0.9,
"probabilities": {
"negi": 0.93,
"soba": 0.04,
"tamago": 0.03
}
},
"soba_style": {
"type": "score",
"score": 0.81,
"confidence": 0.71,
"legend": {
"0": "好き",
"1": "見るだけにしたい",
"2": "嫌い"
},
"probabilities": {
"0": 0.19,
"1": 0.81,
"2": 0.0
}
},
"egg_eat_like": {
"type": "noul",
"noul": 0.34
}
},
"usage": {
"input_tokens": 524,
"output_tokens": 77
}
}
出力の読み取り
戻ってきたJSONを見ると、Jevが文脈やひねくれた表現をどのように解釈したのかが読み取れます。
-
like(
choice型):
選択肢の中からどれが当てはまるかという洗濯をさせた項目です。
「この人が食べるのが好きなのは?」という問いに対し、候補(tamago, negi, soba)からnegiを選び出しています。
確率を見ても negi が 0.93 と圧倒的であり、(月見そばを頼んだのにもかかわらず)「ネギだけを食べるのが好き」という文章のコアな意図をピックアップできています。 -
soba_style(
score型):
文章の中からそれぞれの要素がどれぐらい選ばれているかという項目です。
「月見そばに対する姿勢は?」という問いに対し、「好き」「見るだけにしたい」「嫌い」の3段階(0〜2)で評価するように設計しました。
結果は「見るだけにしたい」がスコア0.81で強く傾倒しているのがわかります。
「鑑賞するのが好きであって、食べるのはそうではない」(念のため、そばは観賞用ではありません)という複雑なニュアンスがスコアとして抽出できたと読み取れます。 -
egg_eat_like(noul型):
Yes/Noのどちらに傾いているかという質問です。
「この人は玉子を食べるのが好き?」という、あえて本文に「玉子」という単語を出さずに判定させました。
結果は 0.34 となっており、これが「好きではない(鑑賞して遊んでいる)」という文脈を好きに対して34%という数値で低く計測し、意図を正しく数値化できているのが読み取れます。
文章の構造はもとより、このようなどうにかしている文章であっても、型付きの質問を通じてここまで定量的に落とし込めるのは非常に興味深い結果と感じました。
今回のお試しを通じて
技術的には、処理速度の早さや既存のLLMに対して圧倒的なコストパフォーマンスであったり、返却される値がjsonに固定されていることでの扱いやすさであったりと、実務屋さんの目をハートにさせる要素が多いと感じています。
合わせて、共通試験(旧センター試験、もしくは旧共通一次)の国語程度であれば、そこら辺の大学に入れちゃうぐらいの結果を出すのではないかと感じました。
何よりも、NLP的な処理(たとえば、文中から日付や場所、名称をピックアップするスロットリングなど)について、Jevである程度肩代わりしてくれるのではないかとも感じました。
期待値として大きく持っているのは、次の2つです。
1つ目は、ユーザーが自由に書いた文章から構造的に必要な項目を抽出し、過不足を判定させるというチェック機構です。
自由文で必要な項目を入力させるというのはテキストベースではあまりないのですが、音声チャットに近いUIでは有用かと感じました。
また、JSONで出力させようとすると、プロンプトに対して少し気を使ったり、出力された値を信用せずにパスーする関数を作ったりという一手間が必要でした。それらが解消されるというのも、ありがたく、使い勝手が良いなと感じました。
本記事では実務的な要素は皆無ですが、まずはお試しのレポートまでに。