Class: Chocomint::LLM::ChatClient

Inherits:
Object
  • Object
show all
Defined in:
lib/chocomint/llm/chat_client.rb

Overview

/edit チャットの「普通の質問」応答を担う軽量クライアント。

役割は 2 つ:

1. classify — ユーザー指示が「ファイル操作/コマンド実行を要するタスク」か、
 「ツール不要の普通の質問・会話」かを 1 語で判定する。
2. answer   — 会話と判定された指示に、ツールを使わず自然文で直接答える。

タスクと判定された指示は呼び出し側 (EditHandlers) が Planner に渡す。

Constant Summary collapse

CLASSIFY_SYSTEM_PROMPT =

分類プロンプト。ローカル小型モデルでも安定するよう、出力を 1 語に強制する。

<<~PROMPT
  あなたはユーザーの指示を 2 種類に分類する分類器です。
  - "task": 実際の操作が必要なもの。ファイルの作成・編集・削除、
    コマンド・プログラムの実行、外部ツールでの調査 (ping・実行・ビルド・
    テスト・検索・一覧取得など) を伴うもの。「〜して」「〜を実行して」
    「〜した結果をまとめて/教えて」のように、動かして結果を得る依頼はすべて task。
    具体的な URL が示されていて、その内容を参照・要約する依頼も task。
  - "chat": 操作を一切伴わず、言葉の説明・質問・相談・雑談だけで済むもの。
    知識の説明や意見を求めるものはこちら。
    また、今日の天気・最新ニュース・現在の株価のように「その場では調べる対象
    (場所・URL など) が特定できない、リアルタイム情報を尋ねる質問」も chat。
    これらは会話として聞き返し、必要な情報を確認するのが適切なため。
  迷ったら task を選ぶこと (操作が必要なのに chat と誤ると何も実行されないため)。
  出力は task または chat のいずれか 1 語のみ。他の文字は一切出力しないこと。
PROMPT
ANSWER_SYSTEM_PROMPT =

会話応答プロンプト。エディタ上のアシスタントとして簡潔に日本語で答えさせる。 現在日時・OS・シェルの情報は answer 実行時に environment_prompt を付加する。

<<~PROMPT
  あなたはコードエディタに組み込まれた親切なアシスタントです。
  ユーザーの質問や相談に、簡潔で正確な日本語で答えてください。
  実際のファイル操作やコマンド実行は行いません (この応答では行えません)。
  操作が必要な場合は、その旨と具体的な手順を説明してください。
  今日の天気・最新ニュースなどリアルタイム情報は、あなた自身は把握していません。
  推測で答えず、参照したいウェブページの URL を教えてもらえれば内容を調べられる旨を
  案内し、必要なら場所や対象を聞き返してください。
  コマンド例を示すときは PowerShell (pwsh) の構文を優先してください。
  絵文字は一切使わないでください。
PROMPT
SUMMARIZE_SYSTEM_PROMPT =

実行結果の要約プロンプト。実行済みのツール列とその結果をもとに、 ユーザー要求に対して何を行い・どうなったかを自然文でまとめさせる。

<<~PROMPT
  あなたはコードエディタのアシスタントです。ユーザー要求を達成するために
  いくつかのツール (ファイル編集やコマンド実行) を実行しました。
  実行したツールとその結果をふまえ、「何をして・結果どうなったか」を
  ユーザー向けに簡潔な日本語でまとめてください。
  コマンドの生出力をそのまま貼るのではなく、要点を短く説明してください。
  絵文字は一切使わないでください。
PROMPT
EXTRACT_SYSTEM_PROMPT =

ウェブページ内容の抽出プロンプト。fetch_url ツールが取得した本文テキストから、 ユーザーの目的 (query) に沿って必要な情報を抜き出し・要約させる。

<<~PROMPT
  あなたはウェブページの内容を読み取るアシスタントです。あるページの本文テキストと、
  ユーザーが知りたいこと (目的) が渡されます。本文から目的に関係する情報だけを
  抜き出し、簡潔で正確な日本語にまとめてください。
  本文に答えが書かれていない場合は、その旨を正直に述べてください。
  推測で補わず、本文に書かれている事実だけを根拠にしてください。
  絵文字は一切使わないでください。
PROMPT
COMPACT_SYSTEM_PROMPT =

会話履歴の圧縮プロンプト。これまでのやり取りを、後から見返して文脈を 思い出せる短い要約に畳み込ませる (/compact 用)。

<<~PROMPT
  あなたはコードエディタのアシスタントです。これまでのユーザーとの
  やり取りの記録を渡します。後から読み返して文脈を思い出せるよう、
  「何を相談し・何を行い・どう決着したか」の要点を、簡潔な日本語の
  箇条書きでまとめてください。未解決の課題や次の一手があれば残してください。
  細部の生出力は省き、重要な事実と結論だけを残します。
  絵文字は一切使わないでください。
PROMPT

Instance Method Summary collapse

Constructor Details

#initialize(base_client:) ⇒ ChatClient

Returns a new instance of ChatClient.



81
82
83
# File 'lib/chocomint/llm/chat_client.rb', line 81

def initialize(base_client:)
  @client = base_client
end

Instance Method Details

#answer(instruction, context: nil, trace_id: nil) ⇒ Object

会話としての自然文回答を返す。現在日時・OS・シェルの情報を毎回付加する (ローカルモデルが日付や実行環境を誤解しないようにするため)。

Raises:



99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/chocomint/llm/chat_client.rb', line 99

def answer(instruction, context: nil, trace_id: nil)
  content = context.to_s.empty? ? instruction.to_s : "#{context}\n\n#{instruction}"
  system_prompt = "#{ANSWER_SYSTEM_PROMPT}\n#{PrimaryClient.environment_prompt}"
  response = @client.create_message(
    system: system_prompt,
    messages: [{ "role" => "user", "content" => content }],
    trace_id: trace_id
  )
  text = strip_emoji(extract_text(response))
  raise Chocomint::Error, "chat response is empty" if text.nil? || text.empty?

  text
end

#classify(instruction, trace_id: nil) ⇒ Object

指示を "task" / "chat" に分類する。判定不能時は安全側の "task" を返す (タスクを会話扱いして取りこぼすより、会話をタスク扱いして Planner に流す方が無難)。



87
88
89
90
91
92
93
94
95
# File 'lib/chocomint/llm/chat_client.rb', line 87

def classify(instruction, trace_id: nil)
  response = @client.create_message(
    system: CLASSIFY_SYSTEM_PROMPT,
    messages: [{ "role" => "user", "content" => instruction.to_s }],
    trace_id: trace_id
  )
  text = extract_text(response).to_s.downcase
  text.include?("chat") ? "chat" : "task"
end

#compact(transcript, trace_id: nil) ⇒ Object

これまでの会話ログ (プレーンテキスト) を短い要約に畳み込む (/compact 用)。 要約できないときは Chocomint::Error を投げる (呼び出し側で 500 にする)。

Raises:



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/chocomint/llm/chat_client.rb', line 158

def compact(transcript, trace_id: nil)
  response = @client.create_message(
    system: COMPACT_SYSTEM_PROMPT,
    messages: [{ "role" => "user", "content" => transcript.to_s }],
    trace_id: trace_id
  )
  text = strip_emoji(extract_text(response))
  raise Chocomint::Error, "compact response is empty" if text.nil? || text.empty?

  text
end

#extract(query, text, url: nil, trace_id: nil) ⇒ Object

ウェブページ本文 (text) から、目的 (query) に沿った情報を抽出・要約する。 fetch_url ツールから呼ばれる。抽出できないときは Chocomint::Error を投げる。

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/chocomint/llm/chat_client.rb', line 137

def extract(query, text, url: nil, trace_id: nil)
  content = <<~MSG
    対象URL: #{url}
    知りたいこと: #{query.to_s.empty? ? '(指定なし。ページの要点をまとめる)' : query}

    ページ本文:
    #{text}
  MSG
  response = @client.create_message(
    system: EXTRACT_SYSTEM_PROMPT,
    messages: [{ "role" => "user", "content" => content }],
    trace_id: trace_id
  )
  result = strip_emoji(extract_text(response))
  raise Chocomint::Error, "extract response is empty" if result.nil? || result.empty?

  result
end

#summarize(request, steps, trace_id: nil) ⇒ Object

実行済みステップ列を自然文でまとめる。失敗しても要約なしで続行できるよう 例外時は nil を返す (呼び出し側は生の結果表示にフォールバックする)。 steps: [{ tool:, arguments:, status:, output: }, ...] 形式。



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/chocomint/llm/chat_client.rb', line 116

def summarize(request, steps, trace_id: nil)
  content = <<~MSG
    ユーザー要求:
    #{request}

    実行したステップ:
    #{format_steps(steps)}
  MSG
  response = @client.create_message(
    system: SUMMARIZE_SYSTEM_PROMPT,
    messages: [{ "role" => "user", "content" => content }],
    trace_id: trace_id
  )
  text = strip_emoji(extract_text(response))
  text.nil? || text.empty? ? nil : text
rescue Chocomint::Error
  nil
end