chocomint 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/THIRD_PARTY_LICENSES.md +45 -0
- data/bin/chocomint +37 -0
- data/bin/chocomint-console-worker +35 -0
- data/bin/chocomint-server +43 -0
- data/config/config.yml +37 -0
- data/lib/chocomint/config.rb +74 -0
- data/lib/chocomint/console/conpty.rb +296 -0
- data/lib/chocomint/console/server.rb +164 -0
- data/lib/chocomint/console/worker.rb +88 -0
- data/lib/chocomint/console/worker_process.rb +254 -0
- data/lib/chocomint/edit_git_handlers.rb +397 -0
- data/lib/chocomint/edit_handlers.rb +696 -0
- data/lib/chocomint/edit_html.rb +9 -0
- data/lib/chocomint/errors.rb +18 -0
- data/lib/chocomint/factory.rb +145 -0
- data/lib/chocomint/llm/base_client.rb +113 -0
- data/lib/chocomint/llm/chat_client.rb +206 -0
- data/lib/chocomint/llm/primary_client.rb +320 -0
- data/lib/chocomint/llm/verifier_client.rb +127 -0
- data/lib/chocomint/logger/sqlite_logger.rb +245 -0
- data/lib/chocomint/orchestrator.rb +175 -0
- data/lib/chocomint/planner.rb +241 -0
- data/lib/chocomint/security/path_guard.rb +84 -0
- data/lib/chocomint/server.rb +863 -0
- data/lib/chocomint/tools/append_file.rb +50 -0
- data/lib/chocomint/tools/base.rb +88 -0
- data/lib/chocomint/tools/delete_file.rb +42 -0
- data/lib/chocomint/tools/edit.rb +65 -0
- data/lib/chocomint/tools/fetch_url.rb +156 -0
- data/lib/chocomint/tools/file_info.rb +54 -0
- data/lib/chocomint/tools/glob.rb +58 -0
- data/lib/chocomint/tools/grep.rb +78 -0
- data/lib/chocomint/tools/list_dir.rb +41 -0
- data/lib/chocomint/tools/ls.rb +51 -0
- data/lib/chocomint/tools/make_dir.rb +44 -0
- data/lib/chocomint/tools/read_file.rb +45 -0
- data/lib/chocomint/tools/registry.rb +29 -0
- data/lib/chocomint/tools/run_command.rb +96 -0
- data/lib/chocomint/tools/shell.rb +169 -0
- data/lib/chocomint/tools/write_file.rb +54 -0
- data/lib/chocomint/validator/machine_validator.rb +65 -0
- data/lib/chocomint/validator/semantic_validator.rb +52 -0
- data/lib/chocomint.rb +51 -0
- data/public/edit/edit.css +414 -0
- data/public/edit/edit.js +2137 -0
- data/public/edit/index.html +251 -0
- data/public/vendor/monaco/LICENSE.txt +23 -0
- data/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- data/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- data/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- data/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- data/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- data/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- data/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- data/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- data/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- data/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- data/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- data/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- data/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- data/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- data/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- data/public/vendor/monaco/vs/loader.js +11 -0
- data/public/vendor/monaco/vs/nls.messages.de.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.es.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.fr.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.it.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ja.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.ko.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ru.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- data/public/vendor/xterm/LICENSE +25 -0
- data/public/vendor/xterm/addon-fit.js +2 -0
- data/public/vendor/xterm/xterm.css +218 -0
- data/public/vendor/xterm/xterm.js +2 -0
- metadata +318 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
|
|
6
|
+
module Chocomint
|
|
7
|
+
module LLM
|
|
8
|
+
# 主 LLM にツール提案をさせる (DESIGN §5 Step1)。
|
|
9
|
+
# tool_use ブロックを優先し、無ければテキストから JSON を抽出する
|
|
10
|
+
# (LLM 出力形式に依存しない堅牢性: DESIGN Goals)。
|
|
11
|
+
class PrimaryClient
|
|
12
|
+
# 実行環境 (現在日時・OS・シェル) の説明。ローカルモデルが「今日の日付」や
|
|
13
|
+
# 「どの OS 上で動いているか」を誤解しないよう、システムプロンプトに毎回埋め込む。
|
|
14
|
+
# シェルは PowerShell を優先させる (Windows ネイティブ環境のため)。
|
|
15
|
+
def self.environment_prompt
|
|
16
|
+
<<~ENV.strip
|
|
17
|
+
実行環境:
|
|
18
|
+
- 現在日時: #{Time.now.strftime('%Y-%m-%d %H:%M:%S %Z')}
|
|
19
|
+
- OS: #{host_os_description}
|
|
20
|
+
- シェル: PowerShell (pwsh) を優先して使うこと。
|
|
21
|
+
コマンド実行が必要な場合は、原則として powershell ツール (または run_command) を使い、
|
|
22
|
+
PowerShell の構文で書いてください。bash 固有の構文は避けてください。
|
|
23
|
+
ENV
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# RbConfig の host_os を人間可読な OS 名に整える。
|
|
27
|
+
def self.host_os_description
|
|
28
|
+
host = RbConfig::CONFIG["host_os"]
|
|
29
|
+
case host
|
|
30
|
+
when /mswin|mingw|cygwin/ then "Windows (#{host})"
|
|
31
|
+
when /darwin/ then "macOS (#{host})"
|
|
32
|
+
when /linux/ then "Linux (#{host})"
|
|
33
|
+
else host.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 共通の作業ルール (パス・workspace の扱い)。両プロンプトで共有する。
|
|
38
|
+
WORKSPACE_RULES = <<~PROMPT.strip
|
|
39
|
+
パスを指定するツールでは、必ず作業ディレクトリからの相対パス (例: workspace/foo.txt) を
|
|
40
|
+
使ってください。/workspace や /home/user/workspace のような絶対パスは許可されません。
|
|
41
|
+
run_command / bash / powershell の作業ディレクトリはプロジェクトルートです。
|
|
42
|
+
成果物は必ず workspace/ 配下に作ってください。uv など出力先ディレクトリを取るコマンドは
|
|
43
|
+
必ず --directory workspace/... のように workspace 配下を明示してください
|
|
44
|
+
(例: run_command uv init --directory workspace/myproj)。
|
|
45
|
+
最新情報や外部ドキュメントの参照が必要な場合は fetch_url ツールを使ってください。
|
|
46
|
+
PROMPT
|
|
47
|
+
|
|
48
|
+
def self.system_prompt
|
|
49
|
+
<<~PROMPT
|
|
50
|
+
あなたはツール実行を提案するアシスタントです。
|
|
51
|
+
ユーザー要求を満たすために呼び出すべきツールを 1 つだけ選び、
|
|
52
|
+
その引数を決めてください。ツールを使って応答してください。
|
|
53
|
+
|
|
54
|
+
#{environment_prompt}
|
|
55
|
+
|
|
56
|
+
#{WORKSPACE_RULES}
|
|
57
|
+
PROMPT
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# マルチステップの逐次実行を促すシステムプロンプト。
|
|
61
|
+
# 1 回の応答で「次に実行すべき 1 ツール」だけを選ばせ、
|
|
62
|
+
# やることが残っていなければ finish ツールで完了を宣言させる。
|
|
63
|
+
def self.step_system_prompt
|
|
64
|
+
<<~PROMPT
|
|
65
|
+
あなたはユーザー要求を達成するために、ツールを 1 つずつ順番に実行していくアシスタントです。
|
|
66
|
+
これまでに完了したステップの一覧が与えられます。それを踏まえ、
|
|
67
|
+
「次に実行すべきツールを 1 つだけ」選び、その引数を決めてください。
|
|
68
|
+
|
|
69
|
+
ユーザー要求がすべて満たされ、もう実行すべきことが残っていない場合は、
|
|
70
|
+
finish ツールを呼び出して完了を宣言してください (それ以外のツールは呼ばないこと)。
|
|
71
|
+
まだ残作業がある場合は finish を呼ばず、次に必要なツールを 1 つだけ呼んでください。
|
|
72
|
+
|
|
73
|
+
#{environment_prompt}
|
|
74
|
+
|
|
75
|
+
#{WORKSPACE_RULES}
|
|
76
|
+
PROMPT
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# 完了宣言用の疑似ツール。実際には実行されず、Planner がループ終了の合図として使う。
|
|
80
|
+
FINISH_TOOL = "finish"
|
|
81
|
+
FINISH_TOOL_DEFINITION = {
|
|
82
|
+
"name" => FINISH_TOOL,
|
|
83
|
+
"description" => "ユーザー要求がすべて達成され、もう実行すべきツールが残っていないときに呼ぶ。",
|
|
84
|
+
"input_schema" => {
|
|
85
|
+
"type" => "object",
|
|
86
|
+
"properties" => {
|
|
87
|
+
"reason" => { "type" => "string", "description" => "完了と判断した理由 (任意)" },
|
|
88
|
+
"message" => { "type" => "string", "description" => "ユーザーへ伝える最終メッセージ (任意)" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}.freeze
|
|
92
|
+
|
|
93
|
+
def initialize(base_client:, tool_definitions:)
|
|
94
|
+
@client = base_client
|
|
95
|
+
@tool_definitions = tool_definitions
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# マルチステップ用: これまで完了したステップを踏まえ、次に実行すべき 1 ツールを提案する。
|
|
99
|
+
# 完了していれば tool="finish" を返す (Planner がループ終了に使う)。
|
|
100
|
+
# 戻り値: { "tool" => name, "arguments" => Hash }
|
|
101
|
+
# done_steps: [{ tool:, arguments:, result: }, ...] 形式の完了済みステップ列。
|
|
102
|
+
def propose_step(request, done_steps = [], trace_id: nil)
|
|
103
|
+
response = @client.create_message(
|
|
104
|
+
system: self.class.step_system_prompt,
|
|
105
|
+
tools: @tool_definitions + [FINISH_TOOL_DEFINITION],
|
|
106
|
+
messages: build_step_messages(request, done_steps),
|
|
107
|
+
trace_id: trace_id
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# ツール提案があればそれを優先。無ければモデルの自然文応答を「finish + メッセージ」
|
|
111
|
+
# として扱う。天気の問い合わせで場所を聞き返すなど、モデルがツールではなく
|
|
112
|
+
# テキストで応答するのは正当なので、これを失敗にせずユーザーへ返す (聞き返し対応)。
|
|
113
|
+
# ここでは JSON 抽出の失敗を致命エラーにしない (strict: false)。自然文に壊れた
|
|
114
|
+
# JSON 断片が混じっていても、聞き返し (from_text_reply) にフォールバックさせる。
|
|
115
|
+
from_tool_use(response) || from_text(response, strict: false) ||
|
|
116
|
+
from_text_reply(response) ||
|
|
117
|
+
raise(InvalidProposalError, "step proposal contains no valid tool call")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 戻り値: { "tool" => name, "arguments" => Hash }
|
|
121
|
+
# feedback: 初回〜前回までの全試行履歴 (再提案時に引数を変えさせるため / DESIGN §6)。
|
|
122
|
+
# [{ attempt:, proposal:, error: }, ...] 形式。旧来の単一 Hash も許容する。
|
|
123
|
+
def propose(request, feedback: nil, trace_id: nil)
|
|
124
|
+
response = @client.create_message(
|
|
125
|
+
system: self.class.system_prompt,
|
|
126
|
+
tools: @tool_definitions,
|
|
127
|
+
messages: build_messages(request, feedback),
|
|
128
|
+
trace_id: trace_id
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
from_tool_use(response) || from_text(response) ||
|
|
132
|
+
raise(InvalidProposalError, "proposal contains no valid tool call")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# マルチステップの提案メッセージ。要求と、これまで完了したステップの一覧を渡す。
|
|
138
|
+
# 完了ステップが無ければ最初のステップとして要求だけを渡す。
|
|
139
|
+
def build_step_messages(request, done_steps)
|
|
140
|
+
messages = [{ "role" => "user", "content" => request.to_s }]
|
|
141
|
+
steps = Array(done_steps)
|
|
142
|
+
return messages if steps.empty?
|
|
143
|
+
|
|
144
|
+
lines = steps.each_with_index.map do |s, i|
|
|
145
|
+
tool = s[:tool] || s["tool"]
|
|
146
|
+
args = s[:arguments] || s["arguments"]
|
|
147
|
+
result = s[:result] || s["result"]
|
|
148
|
+
" ステップ#{i + 1}: ツール=#{tool} 引数=#{JSON.generate(args)} " \
|
|
149
|
+
"結果=#{summarize_result(result)}"
|
|
150
|
+
end
|
|
151
|
+
messages << {
|
|
152
|
+
"role" => "user",
|
|
153
|
+
"content" => <<~MSG.strip
|
|
154
|
+
これまでに以下のステップが完了しています。
|
|
155
|
+
#{lines.join("\n")}
|
|
156
|
+
上記を踏まえ、ユーザー要求を達成するために次に実行すべきツールを 1 つだけ選んでください。
|
|
157
|
+
すでにすべて達成されている場合は finish ツールを呼んでください。
|
|
158
|
+
同じ引数のツールを無意味に繰り返さないでください。
|
|
159
|
+
MSG
|
|
160
|
+
}
|
|
161
|
+
messages
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# ステップ結果を提案プロンプト向けに 1 行へ要約する (exit_code と stdout 冒頭)。
|
|
165
|
+
RESULT_SUMMARY_LIMIT = 200
|
|
166
|
+
|
|
167
|
+
def summarize_result(result)
|
|
168
|
+
return "(なし)" unless result.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
code = result[:exit_code] || result["exit_code"]
|
|
171
|
+
out = (result[:stdout] || result["stdout"]).to_s.strip
|
|
172
|
+
err = (result[:stderr] || result["stderr"]).to_s.strip
|
|
173
|
+
parts = ["exit_code=#{code}"]
|
|
174
|
+
parts << "stdout=#{truncate(out)}" unless out.empty?
|
|
175
|
+
parts << "stderr=#{truncate(err)}" unless err.empty?
|
|
176
|
+
parts.join(" ")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def truncate(text)
|
|
180
|
+
text.length > RESULT_SUMMARY_LIMIT ? "#{text[0, RESULT_SUMMARY_LIMIT]}…" : text
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# 再提案時は初回から前回までの全試行 (提案とエラー) を添え、別の引数を試すよう促す。
|
|
184
|
+
def build_messages(request, feedback)
|
|
185
|
+
messages = [{ "role" => "user", "content" => request.to_s }]
|
|
186
|
+
history = normalize_history(feedback)
|
|
187
|
+
return messages if history.empty?
|
|
188
|
+
|
|
189
|
+
lines = history.map do |h|
|
|
190
|
+
prev = h[:proposal]
|
|
191
|
+
" 試行#{h[:attempt]}: 提案=#{prev ? JSON.generate(prev) : '(なし)'} / 失敗=#{h[:error]}"
|
|
192
|
+
end
|
|
193
|
+
hints = failure_hints(history)
|
|
194
|
+
messages << {
|
|
195
|
+
"role" => "user",
|
|
196
|
+
"content" => <<~MSG.strip
|
|
197
|
+
これまでの提案はすべて失敗しました。別の引数で再試行してください。
|
|
198
|
+
#{lines.join("\n")}
|
|
199
|
+
上記のいずれとも同じ引数を繰り返さず、パラメータを変えて提案してください。
|
|
200
|
+
#{hints}
|
|
201
|
+
MSG
|
|
202
|
+
}
|
|
203
|
+
messages
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# 直近の失敗理由から、繰り返されがちな誤りに対する具体的な修正ヒントを付加する
|
|
207
|
+
# (同じ種類の失敗を LLM が学習せず何度も繰り返すのを防ぐ / DESIGN §6)。
|
|
208
|
+
FAILURE_HINTS = {
|
|
209
|
+
/access denied.*outside allowed directories/ =>
|
|
210
|
+
"ヒント: パスは workspace/ で始まる相対パスにしてください (例: workspace/foo.txt)。" \
|
|
211
|
+
"workspace の外や絶対パスは使えません。",
|
|
212
|
+
/command not allowed/ =>
|
|
213
|
+
"ヒント: run_command で使えるコマンドは whitelist に登録されたものだけです。" \
|
|
214
|
+
"許可されていないコマンド (bash 等) を run_command で実行しようとしないでください。" \
|
|
215
|
+
"シェル機能が必要な場合は bash/powershell ツールを直接使ってください。",
|
|
216
|
+
/old_string not found|old_string is not unique/ =>
|
|
217
|
+
"ヒント: edit の old_string はファイルの現在の内容と完全一致し、かつ一意である必要があります。" \
|
|
218
|
+
"前回の置換が既に成功している可能性があるので、まず read_file で現在の内容を確認してください。"
|
|
219
|
+
}.freeze
|
|
220
|
+
|
|
221
|
+
def failure_hints(history)
|
|
222
|
+
errors = history.filter_map { |h| h[:error]&.to_s }
|
|
223
|
+
matched = FAILURE_HINTS.filter_map { |pattern, hint| hint if errors.any? { |e| e.match?(pattern) } }
|
|
224
|
+
matched.join("\n")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# feedback を試行履歴の配列に正規化する (旧来の単一 Hash も受理)。
|
|
228
|
+
def normalize_history(feedback)
|
|
229
|
+
case feedback
|
|
230
|
+
when nil then []
|
|
231
|
+
when Array then feedback
|
|
232
|
+
else [feedback]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def from_tool_use(response)
|
|
237
|
+
blocks = Array(response["content"])
|
|
238
|
+
tu = blocks.find { |b| b.is_a?(Hash) && b["type"] == "tool_use" }
|
|
239
|
+
return nil unless tu
|
|
240
|
+
|
|
241
|
+
args = tu["input"]
|
|
242
|
+
args = {} if args.nil?
|
|
243
|
+
raise InvalidProposalError, "arguments must be an object" unless args.is_a?(Hash)
|
|
244
|
+
|
|
245
|
+
{ "tool" => tu["name"], "arguments" => args }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# モデルがツールを使わず自然文だけで応答したとき、その文章を最終メッセージとして
|
|
249
|
+
# finish 提案に載せて返す。ツール不要で答えられた場合や、必要な情報 (場所など) を
|
|
250
|
+
# ユーザーに聞き返す場合がこれに当たる。thinking ブロックは除き本文テキストのみ拾う。
|
|
251
|
+
#
|
|
252
|
+
# モデルがツール呼び出し JSON を書き損じた残骸 ({..}, <|..|> 等) が混じることが
|
|
253
|
+
# あるため、それらを取り除いて読みやすくする。除去後に実質空になったら、
|
|
254
|
+
# 汎用の聞き返し文にフォールバックする (壊れた断片をそのまま見せない)。
|
|
255
|
+
def from_text_reply(response)
|
|
256
|
+
text = clean_reply_text(extract_text(response).to_s)
|
|
257
|
+
text = "うまく指示を解釈できませんでした。もう少し具体的に教えてください。" if text.empty?
|
|
258
|
+
|
|
259
|
+
{ "tool" => FINISH_TOOL, "arguments" => { "message" => text } }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# 自然文応答から、ツール呼び出し JSON の書き損じ残骸を取り除く。
|
|
263
|
+
def clean_reply_text(text)
|
|
264
|
+
text.gsub(/\{[^{}]*\}/, " ") # {reason:...} のような JSON 断片
|
|
265
|
+
.gsub(/<\|[^|]*\|>/, " ") # <|"|> のような特殊トークン
|
|
266
|
+
.gsub(/[ \t]{2,}/, " ")
|
|
267
|
+
.strip
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# content 内テキスト、または文字列レスポンスから JSON オブジェクトを拾う。
|
|
271
|
+
# strict: true なら JSON 抽出失敗 (壊れた JSON / tool・arguments 欠落) を
|
|
272
|
+
# InvalidProposalError にする。false なら nil を返し、呼び出し側の別経路
|
|
273
|
+
# (自然文応答の扱い) にフォールバックさせる。
|
|
274
|
+
def from_text(response, strict: true)
|
|
275
|
+
text = extract_text(response)
|
|
276
|
+
return nil if text.nil? || text.empty?
|
|
277
|
+
|
|
278
|
+
json = extract_json_object(text)
|
|
279
|
+
return nil if json.nil?
|
|
280
|
+
|
|
281
|
+
parsed = JSON.parse(json)
|
|
282
|
+
unless parsed.is_a?(Hash) && parsed["tool"] && parsed["arguments"].is_a?(Hash)
|
|
283
|
+
return nil unless strict
|
|
284
|
+
|
|
285
|
+
raise InvalidProposalError, "proposal JSON missing tool/arguments"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
{ "tool" => parsed["tool"], "arguments" => parsed["arguments"] }
|
|
289
|
+
rescue JSON::ParserError => e
|
|
290
|
+
return nil unless strict
|
|
291
|
+
|
|
292
|
+
raise InvalidProposalError, "proposal JSON invalid: #{e.message}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def extract_text(response)
|
|
296
|
+
blocks = Array(response["content"])
|
|
297
|
+
texts = blocks.filter_map do |b|
|
|
298
|
+
b["text"] if b.is_a?(Hash) && b["type"] == "text"
|
|
299
|
+
end
|
|
300
|
+
return texts.join("\n") unless texts.empty?
|
|
301
|
+
|
|
302
|
+
response["content"].is_a?(String) ? response["content"] : nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# 最初の { から対応する } までを素朴に切り出す。
|
|
306
|
+
def extract_json_object(text)
|
|
307
|
+
start = text.index("{")
|
|
308
|
+
return nil unless start
|
|
309
|
+
|
|
310
|
+
depth = 0
|
|
311
|
+
text[start..].each_char.with_index do |ch, i|
|
|
312
|
+
depth += 1 if ch == "{"
|
|
313
|
+
depth -= 1 if ch == "}"
|
|
314
|
+
return text[start, i + 1] if depth.zero?
|
|
315
|
+
end
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Chocomint
|
|
6
|
+
module LLM
|
|
7
|
+
# 副 LLM による意味的検証 (DESIGN §4)。1 または 0 のみを返させる。
|
|
8
|
+
class VerifierClient
|
|
9
|
+
PROMPT_TEMPLATE = <<~PROMPT
|
|
10
|
+
あなたは厳格な監査器です。
|
|
11
|
+
|
|
12
|
+
ユーザー要求:
|
|
13
|
+
%<request>s
|
|
14
|
+
|
|
15
|
+
期待条件:
|
|
16
|
+
%<expectations>s
|
|
17
|
+
|
|
18
|
+
実行結果:
|
|
19
|
+
%<result>s
|
|
20
|
+
|
|
21
|
+
要求が完全に満たされている場合のみ 1 を返してください。
|
|
22
|
+
少しでも不明点・不足・失敗の可能性がある場合は 0 を返してください。
|
|
23
|
+
exit_code が 0 で stdout が要求を満たしていれば、stderr に警告・ログめいた
|
|
24
|
+
文字列が含まれていても、それだけを理由に不合格にしないでください。
|
|
25
|
+
|
|
26
|
+
出力は 1 または 0 のみ。
|
|
27
|
+
PROMPT
|
|
28
|
+
|
|
29
|
+
# マルチステップ全体の達成判定プロンプト。
|
|
30
|
+
# 単一結果ではなく、これまで実行した全ステップを踏まえて要求全体を評価させる。
|
|
31
|
+
OVERALL_TEMPLATE = <<~PROMPT
|
|
32
|
+
あなたは厳格な監査器です。
|
|
33
|
+
|
|
34
|
+
ユーザー要求:
|
|
35
|
+
%<request>s
|
|
36
|
+
|
|
37
|
+
期待条件:
|
|
38
|
+
%<expectations>s
|
|
39
|
+
|
|
40
|
+
これまでに実行が完了したステップ (順番どおり):
|
|
41
|
+
%<steps>s
|
|
42
|
+
|
|
43
|
+
上記のステップ実行の結果として、ユーザー要求が「全体として完全に」満たされたかを判定してください。
|
|
44
|
+
まだ実行すべき作業が残っている、または要求の一部しか満たされていない場合は 0 を返してください。
|
|
45
|
+
要求が過不足なくすべて達成されている場合のみ 1 を返してください。
|
|
46
|
+
|
|
47
|
+
出力は 1 または 0 のみ。
|
|
48
|
+
PROMPT
|
|
49
|
+
|
|
50
|
+
def initialize(base_client:)
|
|
51
|
+
@client = base_client
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# マルチステップ全体の達成判定。戻り値: "1" / "0" / nil(不正 -> 未達成扱い)。
|
|
55
|
+
# done_steps: [{ tool:, arguments:, result: }, ...] 形式の完了済みステップ列。
|
|
56
|
+
def verify_overall(request:, done_steps:, expectations: nil, trace_id: nil)
|
|
57
|
+
prompt = format(
|
|
58
|
+
OVERALL_TEMPLATE,
|
|
59
|
+
request: request.to_s,
|
|
60
|
+
expectations: (expectations || "(なし)").to_s,
|
|
61
|
+
steps: serialize_steps(done_steps)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
response = @client.create_message(
|
|
65
|
+
messages: [{ "role" => "user", "content" => prompt }],
|
|
66
|
+
trace_id: trace_id
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
normalize(extract_text(response))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 戻り値: "1" / "0" / nil(不正出力 -> RETRY 扱い)
|
|
73
|
+
def verify(request:, result:, expectations: nil, trace_id: nil)
|
|
74
|
+
prompt = format(
|
|
75
|
+
PROMPT_TEMPLATE,
|
|
76
|
+
request: request.to_s,
|
|
77
|
+
expectations: (expectations || "(なし)").to_s,
|
|
78
|
+
result: serialize(result)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
response = @client.create_message(
|
|
82
|
+
messages: [{ "role" => "user", "content" => prompt }],
|
|
83
|
+
trace_id: trace_id
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
normalize(extract_text(response))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# strip 後に完全一致で "1"/"0" のみ許可、それ以外は nil。
|
|
92
|
+
def normalize(text)
|
|
93
|
+
return nil if text.nil?
|
|
94
|
+
|
|
95
|
+
t = text.strip
|
|
96
|
+
%w[1 0].include?(t) ? t : nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def extract_text(response)
|
|
100
|
+
blocks = Array(response["content"])
|
|
101
|
+
texts = blocks.filter_map do |b|
|
|
102
|
+
b["text"] if b.is_a?(Hash) && b["type"] == "text"
|
|
103
|
+
end
|
|
104
|
+
return texts.join.strip unless texts.empty?
|
|
105
|
+
|
|
106
|
+
response["content"].is_a?(String) ? response["content"] : nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def serialize(result)
|
|
110
|
+
result.is_a?(String) ? result : JSON.generate(result)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 完了ステップ列を判定プロンプト向けに整形する。
|
|
114
|
+
def serialize_steps(done_steps)
|
|
115
|
+
steps = Array(done_steps)
|
|
116
|
+
return "(なし)" if steps.empty?
|
|
117
|
+
|
|
118
|
+
steps.each_with_index.map do |s, i|
|
|
119
|
+
tool = s[:tool] || s["tool"]
|
|
120
|
+
args = s[:arguments] || s["arguments"]
|
|
121
|
+
result = s[:result] || s["result"]
|
|
122
|
+
"ステップ#{i + 1}: ツール=#{tool} 引数=#{serialize(args)} 結果=#{serialize(result)}"
|
|
123
|
+
end.join("\n")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module Chocomint
|
|
9
|
+
module Logger
|
|
10
|
+
# 全試行を SQLite に記録する (DESIGN §12)。
|
|
11
|
+
class SqliteLogger
|
|
12
|
+
def initialize(path)
|
|
13
|
+
FileUtils.mkdir_p(File.dirname(path)) unless File.dirname(path).empty?
|
|
14
|
+
@db = SQLite3::Database.new(path)
|
|
15
|
+
create_schema
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 1 attempt を 1 行として保存する。
|
|
19
|
+
def record(trace_id:, attempt:, request:, proposal:, result:, status:, verifier_output: nil)
|
|
20
|
+
@db.execute(
|
|
21
|
+
<<~SQL,
|
|
22
|
+
INSERT INTO executions
|
|
23
|
+
(trace_id, attempt, request, proposal, result, verifier_output, status, created_at)
|
|
24
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
25
|
+
SQL
|
|
26
|
+
[
|
|
27
|
+
trace_id,
|
|
28
|
+
attempt,
|
|
29
|
+
request.to_s,
|
|
30
|
+
serialize(proposal),
|
|
31
|
+
serialize(result),
|
|
32
|
+
verifier_output,
|
|
33
|
+
status,
|
|
34
|
+
Time.now.utc.iso8601
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# LLM への送信内容 (ヘッダ含む生ボディ全体) を 1 呼び出し 1 行で記録する。
|
|
40
|
+
# role: "primary" / "verifier"。body/headers は Hash を渡すと JSON 化する。
|
|
41
|
+
# 戻り値: 挿入した行の id (後で update_llm_payload_response に渡す)。
|
|
42
|
+
def record_llm_payload(role:, model:, request_body:, request_headers:, trace_id: nil)
|
|
43
|
+
@db.execute(
|
|
44
|
+
<<~SQL,
|
|
45
|
+
INSERT INTO llm_payloads
|
|
46
|
+
(trace_id, role, model, request_body, request_headers, created_at)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
48
|
+
SQL
|
|
49
|
+
[
|
|
50
|
+
trace_id,
|
|
51
|
+
role.to_s,
|
|
52
|
+
model.to_s,
|
|
53
|
+
serialize(request_body),
|
|
54
|
+
serialize(request_headers),
|
|
55
|
+
Time.now.utc.iso8601
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
@db.last_insert_row_id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 記録済みペイロード行に LLM レスポンス (HTTP ステータス + 生ボディ) を追記する。
|
|
62
|
+
# id は record_llm_payload の戻り値。body は文字列/Hash どちらも可。
|
|
63
|
+
def update_llm_payload_response(id, response_status:, response_body:)
|
|
64
|
+
return if id.nil?
|
|
65
|
+
|
|
66
|
+
@db.execute(
|
|
67
|
+
"UPDATE llm_payloads SET response_status = ?, response_body = ? WHERE id = ?",
|
|
68
|
+
[response_status, serialize(response_body), id]
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# LLM 送信ペイロードを新しい順に取得する。trace_id で絞り込み可能。
|
|
73
|
+
def recent_llm_payloads(limit: 100, offset: 0, trace_id: nil)
|
|
74
|
+
trace_id = normalize_text(trace_id)
|
|
75
|
+
where = trace_id ? "WHERE trace_id = ?" : ""
|
|
76
|
+
params = trace_id ? [trace_id, limit, offset] : [limit, offset]
|
|
77
|
+
@db.results_as_hash = true
|
|
78
|
+
@db.execute(
|
|
79
|
+
<<~SQL,
|
|
80
|
+
SELECT id, trace_id, role, model, request_body, request_headers,
|
|
81
|
+
response_status, response_body, created_at
|
|
82
|
+
FROM llm_payloads
|
|
83
|
+
#{where}
|
|
84
|
+
ORDER BY id DESC
|
|
85
|
+
LIMIT ? OFFSET ?
|
|
86
|
+
SQL
|
|
87
|
+
params
|
|
88
|
+
)
|
|
89
|
+
ensure
|
|
90
|
+
@db.results_as_hash = false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# trace_id に紐づくツール実行の正常性判定 (status / verifier_output) を
|
|
94
|
+
# attempt 昇順で返す。LLM ペイロード詳細で「判定結果」を併記するため。
|
|
95
|
+
def tool_outcomes(trace_id)
|
|
96
|
+
trace_id = normalize_text(trace_id)
|
|
97
|
+
return [] if trace_id.nil?
|
|
98
|
+
|
|
99
|
+
@db.results_as_hash = true
|
|
100
|
+
@db.execute(
|
|
101
|
+
<<~SQL,
|
|
102
|
+
SELECT attempt, status, verifier_output, proposal
|
|
103
|
+
FROM executions WHERE trace_id = ?
|
|
104
|
+
ORDER BY attempt ASC, id ASC
|
|
105
|
+
SQL
|
|
106
|
+
[trace_id]
|
|
107
|
+
)
|
|
108
|
+
ensure
|
|
109
|
+
@db.results_as_hash = false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# LLM ペイロード 1 件を id 指定で取得する。無ければ nil。
|
|
113
|
+
def llm_payload(id)
|
|
114
|
+
@db.results_as_hash = true
|
|
115
|
+
@db.execute(
|
|
116
|
+
<<~SQL,
|
|
117
|
+
SELECT id, trace_id, role, model, request_body, request_headers,
|
|
118
|
+
response_status, response_body, created_at
|
|
119
|
+
FROM llm_payloads WHERE id = ?
|
|
120
|
+
SQL
|
|
121
|
+
[id]
|
|
122
|
+
).first
|
|
123
|
+
ensure
|
|
124
|
+
@db.results_as_hash = false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# LLM ペイロードの総件数。trace_id 指定時はその trace の件数。
|
|
128
|
+
def llm_payload_count(trace_id: nil)
|
|
129
|
+
trace_id = normalize_text(trace_id)
|
|
130
|
+
if trace_id
|
|
131
|
+
@db.execute("SELECT COUNT(*) FROM llm_payloads WHERE trace_id = ?", [trace_id])
|
|
132
|
+
else
|
|
133
|
+
@db.execute("SELECT COUNT(*) FROM llm_payloads")
|
|
134
|
+
end.first.first
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 監査ログを新しい順に取得する。ブラウザ表示 (GET /logs) 用。
|
|
138
|
+
# trace_id を指定するとその trace のみに絞る。offset でページ送り。
|
|
139
|
+
# 返り値は各行を Hash(カラム名 => 値) にした配列。
|
|
140
|
+
def recent(limit: 100, offset: 0, trace_id: nil)
|
|
141
|
+
trace_id = normalize_text(trace_id)
|
|
142
|
+
where = trace_id ? "WHERE trace_id = ?" : ""
|
|
143
|
+
params = trace_id ? [trace_id, limit, offset] : [limit, offset]
|
|
144
|
+
@db.results_as_hash = true
|
|
145
|
+
@db.execute(
|
|
146
|
+
<<~SQL,
|
|
147
|
+
SELECT id, trace_id, attempt, request, proposal, result,
|
|
148
|
+
verifier_output, status, created_at
|
|
149
|
+
FROM executions
|
|
150
|
+
#{where}
|
|
151
|
+
ORDER BY id DESC
|
|
152
|
+
LIMIT ? OFFSET ?
|
|
153
|
+
SQL
|
|
154
|
+
params
|
|
155
|
+
)
|
|
156
|
+
ensure
|
|
157
|
+
@db.results_as_hash = false
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# 監査ログ 1 件を id 指定で取得する。無ければ nil。
|
|
161
|
+
def execution(id)
|
|
162
|
+
@db.results_as_hash = true
|
|
163
|
+
@db.execute(
|
|
164
|
+
<<~SQL,
|
|
165
|
+
SELECT id, trace_id, attempt, request, proposal, result,
|
|
166
|
+
verifier_output, status, created_at
|
|
167
|
+
FROM executions WHERE id = ?
|
|
168
|
+
SQL
|
|
169
|
+
[id]
|
|
170
|
+
).first
|
|
171
|
+
ensure
|
|
172
|
+
@db.results_as_hash = false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# ページネーション用の総件数。trace_id 指定時はその trace の件数。
|
|
176
|
+
def count(trace_id: nil)
|
|
177
|
+
trace_id = normalize_text(trace_id)
|
|
178
|
+
if trace_id
|
|
179
|
+
@db.execute("SELECT COUNT(*) FROM executions WHERE trace_id = ?", [trace_id])
|
|
180
|
+
else
|
|
181
|
+
@db.execute("SELECT COUNT(*) FROM executions")
|
|
182
|
+
end.first.first
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def close = @db.close
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# TEXT カラムと確実に比較できるよう UTF-8 文字列に正規化する。
|
|
190
|
+
# ASCII-8BIT(BINARY) のままだと sqlite3 が BLOB としてバインドし、
|
|
191
|
+
# TEXT カラムと一致しなくなる。
|
|
192
|
+
def normalize_text(str)
|
|
193
|
+
return nil if str.nil?
|
|
194
|
+
|
|
195
|
+
str.encoding == Encoding::UTF_8 ? str : str.dup.force_encoding("UTF-8")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def create_schema
|
|
199
|
+
@db.execute <<~SQL
|
|
200
|
+
CREATE TABLE IF NOT EXISTS executions (
|
|
201
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
202
|
+
trace_id TEXT NOT NULL,
|
|
203
|
+
attempt INTEGER NOT NULL,
|
|
204
|
+
request TEXT NOT NULL,
|
|
205
|
+
proposal TEXT NOT NULL,
|
|
206
|
+
result TEXT NOT NULL,
|
|
207
|
+
verifier_output TEXT,
|
|
208
|
+
status TEXT NOT NULL,
|
|
209
|
+
created_at TEXT NOT NULL
|
|
210
|
+
)
|
|
211
|
+
SQL
|
|
212
|
+
@db.execute <<~SQL
|
|
213
|
+
CREATE TABLE IF NOT EXISTS llm_payloads (
|
|
214
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
215
|
+
trace_id TEXT,
|
|
216
|
+
role TEXT NOT NULL,
|
|
217
|
+
model TEXT NOT NULL,
|
|
218
|
+
request_body TEXT NOT NULL,
|
|
219
|
+
request_headers TEXT NOT NULL,
|
|
220
|
+
response_status INTEGER,
|
|
221
|
+
response_body TEXT,
|
|
222
|
+
created_at TEXT NOT NULL
|
|
223
|
+
)
|
|
224
|
+
SQL
|
|
225
|
+
# 既存 DB へのカラム追加 (存在すれば無視)。
|
|
226
|
+
add_column_if_missing("llm_payloads", "response_status", "INTEGER")
|
|
227
|
+
add_column_if_missing("llm_payloads", "response_body", "TEXT")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def serialize(obj)
|
|
231
|
+
return obj if obj.is_a?(String)
|
|
232
|
+
|
|
233
|
+
JSON.generate(obj)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# 既存テーブルに列が無ければ追加する (旧 DB の後方互換マイグレーション)。
|
|
237
|
+
def add_column_if_missing(table, column, type)
|
|
238
|
+
cols = @db.execute("PRAGMA table_info(#{table})").map { |r| r[1] }
|
|
239
|
+
return if cols.include?(column)
|
|
240
|
+
|
|
241
|
+
@db.execute("ALTER TABLE #{table} ADD COLUMN #{column} #{type}")
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|