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,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chocomint
|
|
4
|
+
module EditHandlers
|
|
5
|
+
# /edit のシングルページ HTML。実体は public/edit/index.html (CSS/JS も同ディレクトリ) で、
|
|
6
|
+
# ここでは読み込むだけ。__WS_CONFIG__ はサーバー側で WebSocket 接続情報 (JSON) に置換される。
|
|
7
|
+
EDIT_HTML_TEMPLATE = File.read(File.expand_path("../../public/edit/index.html", __dir__))
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chocomint
|
|
4
|
+
# すべての chocomint 例外の基底。
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# 主 LLM の提案 JSON が不正 (DESIGN §14: FAIL)。
|
|
8
|
+
class InvalidProposalError < Error; end
|
|
9
|
+
|
|
10
|
+
# 未知のツール名 (DESIGN §14: FAIL)。
|
|
11
|
+
class UnknownToolError < Error; end
|
|
12
|
+
|
|
13
|
+
# リトライ上限超過 (DESIGN §14: FAIL)。
|
|
14
|
+
class RetryLimitExceededError < Error; end
|
|
15
|
+
|
|
16
|
+
# PathGuard によるアクセス拒否 (DESIGN §13)。ツール内で捕捉され exit_code=1 になる。
|
|
17
|
+
class PathAccessError < Error; end
|
|
18
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chocomint
|
|
4
|
+
# config から各コンポーネントを組み立てて Orchestrator を返す。
|
|
5
|
+
module Factory
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# config から PathGuard を組み立てる (edit UI がファイルアクセスに使う)。
|
|
9
|
+
def build_path_guard(config, base_dir: Dir.pwd)
|
|
10
|
+
Security::PathGuard.new(config.allowed_dirs, base_dir: base_dir)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build(config, base_dir: Dir.pwd)
|
|
14
|
+
path_guard = build_path_guard(config, base_dir: base_dir)
|
|
15
|
+
|
|
16
|
+
registry = Tools::Registry.new
|
|
17
|
+
registry.register(Tools::WriteFile.new(path_guard: path_guard,
|
|
18
|
+
max_file_bytes: config.max_file_bytes))
|
|
19
|
+
registry.register(Tools::ReadFile.new(path_guard: path_guard,
|
|
20
|
+
max_file_bytes: config.max_file_bytes))
|
|
21
|
+
registry.register(Tools::ListDir.new(path_guard: path_guard))
|
|
22
|
+
registry.register(Tools::AppendFile.new(path_guard: path_guard,
|
|
23
|
+
max_file_bytes: config.max_file_bytes))
|
|
24
|
+
registry.register(Tools::DeleteFile.new(path_guard: path_guard))
|
|
25
|
+
registry.register(Tools::FileInfo.new(path_guard: path_guard))
|
|
26
|
+
registry.register(Tools::MakeDir.new(path_guard: path_guard))
|
|
27
|
+
registry.register(Tools::RunCommand.new(allowed_commands: config.allowed_commands,
|
|
28
|
+
timeout_sec: config.timeout_sec,
|
|
29
|
+
max_output_bytes: config.max_output_bytes,
|
|
30
|
+
cwd: base_dir,
|
|
31
|
+
path_guard: path_guard,
|
|
32
|
+
allow_all: config.allow_all_commands?))
|
|
33
|
+
registry.register(Tools::Edit.new(path_guard: path_guard,
|
|
34
|
+
max_file_bytes: config.max_file_bytes))
|
|
35
|
+
registry.register(Tools::Grep.new(path_guard: path_guard,
|
|
36
|
+
max_output_bytes: config.max_output_bytes,
|
|
37
|
+
base_dir: base_dir))
|
|
38
|
+
registry.register(Tools::Glob.new(path_guard: path_guard, base_dir: base_dir))
|
|
39
|
+
registry.register(Tools::Ls.new(path_guard: path_guard))
|
|
40
|
+
registry.register(Tools::Shell.build("bash", timeout_sec: config.timeout_sec,
|
|
41
|
+
max_output_bytes: config.max_output_bytes,
|
|
42
|
+
cwd: base_dir,
|
|
43
|
+
path_guard: path_guard))
|
|
44
|
+
registry.register(Tools::Shell.build("powershell", timeout_sec: config.timeout_sec,
|
|
45
|
+
max_output_bytes: config.max_output_bytes,
|
|
46
|
+
cwd: base_dir,
|
|
47
|
+
path_guard: path_guard))
|
|
48
|
+
|
|
49
|
+
# ロガーは LLM クライアントにも注入するため先に生成する。
|
|
50
|
+
logger = Logger::SqliteLogger.new(config.sqlite_path)
|
|
51
|
+
|
|
52
|
+
# ウェブサイト参照ツールは取得した本文を LLM (ChatClient) に要約・抽出させる。
|
|
53
|
+
registry.register(Tools::FetchUrl.new(chat_client: chat_client(config, logger),
|
|
54
|
+
timeout_sec: config.timeout_sec))
|
|
55
|
+
|
|
56
|
+
primary = build_primary(config, registry, logger)
|
|
57
|
+
verifier = build_verifier_client(config, logger)
|
|
58
|
+
semantic = Validator::SemanticValidator.new(
|
|
59
|
+
verifier_client: verifier, enabled: config.verifier_enabled?, path_guard: path_guard
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
machine = Validator::MachineValidator.new(
|
|
63
|
+
path_guard: path_guard, max_output_bytes: config.max_output_bytes
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
orchestrator = Orchestrator.new(
|
|
67
|
+
primary: primary,
|
|
68
|
+
registry: registry,
|
|
69
|
+
machine_validator: machine,
|
|
70
|
+
semantic_validator: semantic,
|
|
71
|
+
logger: logger,
|
|
72
|
+
max_retry: config.max_retry,
|
|
73
|
+
timeout_sec: config.timeout_sec
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# マルチステップ (複数ツールを順に実行する要求) に対応する外側ループ。
|
|
77
|
+
# 単一ツールで完結する要求は 1 ステップ目の全体判定で即完了する。
|
|
78
|
+
Planner.new(
|
|
79
|
+
primary: primary,
|
|
80
|
+
orchestrator: orchestrator,
|
|
81
|
+
verifier: config.verifier_enabled? ? verifier : nil,
|
|
82
|
+
logger: logger,
|
|
83
|
+
max_steps: config.max_steps,
|
|
84
|
+
step_retry: config.max_retry
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 会話 (ツール不要の普通の質問) に答えるための生クライアント。
|
|
89
|
+
# /edit チャットで、タスク判定が「会話」だったときに直接応答させるのに使う。
|
|
90
|
+
def build_chat_client(config, base_dir: Dir.pwd)
|
|
91
|
+
chat_client(config, Logger::SqliteLogger.new(config.sqlite_path))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ChatClient を組み立てる。build 内では既存 logger を再利用するため logger を受け取る
|
|
95
|
+
# (fetch_url ツールと /edit チャットの双方で使う)。
|
|
96
|
+
def chat_client(config, logger)
|
|
97
|
+
client = LLM::BaseClient.new(
|
|
98
|
+
base_url: config.llm.fetch("base_url"),
|
|
99
|
+
model: config.llm.fetch("model"),
|
|
100
|
+
api_key: api_key(config.llm),
|
|
101
|
+
max_tokens: config.llm.fetch("max_tokens", 1024),
|
|
102
|
+
timeout: config.timeout_sec,
|
|
103
|
+
payload_logger: logger,
|
|
104
|
+
role: "chat"
|
|
105
|
+
)
|
|
106
|
+
LLM::ChatClient.new(base_client: client)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_primary(config, registry, logger)
|
|
110
|
+
client = LLM::BaseClient.new(
|
|
111
|
+
base_url: config.llm.fetch("base_url"),
|
|
112
|
+
model: config.llm.fetch("model"),
|
|
113
|
+
api_key: api_key(config.llm),
|
|
114
|
+
max_tokens: config.llm.fetch("max_tokens", 1024),
|
|
115
|
+
timeout: config.timeout_sec,
|
|
116
|
+
payload_logger: logger,
|
|
117
|
+
role: "primary"
|
|
118
|
+
)
|
|
119
|
+
LLM::PrimaryClient.new(base_client: client,
|
|
120
|
+
tool_definitions: registry.tool_definitions)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# verifier が無効なら nil を返す (Planner / SemanticValidator 双方でそのまま扱える)。
|
|
124
|
+
def build_verifier_client(config, logger)
|
|
125
|
+
return nil unless config.verifier_enabled?
|
|
126
|
+
|
|
127
|
+
# verifier は primary と同じモデルを共有する (VRAM 節約のため単一モデルに統一)。
|
|
128
|
+
client = LLM::BaseClient.new(
|
|
129
|
+
base_url: config.llm.fetch("base_url"),
|
|
130
|
+
model: config.llm.fetch("model"),
|
|
131
|
+
api_key: api_key(config.llm),
|
|
132
|
+
max_tokens: config.verifier.fetch("max_tokens", 1024),
|
|
133
|
+
timeout: config.timeout_sec,
|
|
134
|
+
payload_logger: logger,
|
|
135
|
+
role: "verifier"
|
|
136
|
+
)
|
|
137
|
+
LLM::VerifierClient.new(base_client: client)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def api_key(llm_config)
|
|
141
|
+
env_name = llm_config["api_key_env"]
|
|
142
|
+
env_name && ENV[env_name]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Chocomint
|
|
7
|
+
module LLM
|
|
8
|
+
# Anthropic Messages API 互換クライアント (DESIGN §10)。
|
|
9
|
+
# base_url を差し替えるだけで Ollama / Claude / OpenRouter などに対応する。
|
|
10
|
+
# リトライは orchestrator の責務なので、ここは単発呼び出しに徹する。
|
|
11
|
+
class BaseClient
|
|
12
|
+
# payload_logger: LLM 送信内容を記録するロガー (record_llm_payload を持つ / 任意)。
|
|
13
|
+
# role: ペイロードの識別子 ("primary" / "verifier")。ログ表示で区別するため。
|
|
14
|
+
def initialize(base_url:, model:, api_key: nil, max_tokens: 1024, timeout: 60,
|
|
15
|
+
payload_logger: nil, role: nil)
|
|
16
|
+
@base_url = base_url.to_s.chomp("/")
|
|
17
|
+
@model = model
|
|
18
|
+
@api_key = api_key
|
|
19
|
+
@max_tokens = max_tokens
|
|
20
|
+
@timeout = timeout
|
|
21
|
+
@payload_logger = payload_logger
|
|
22
|
+
@role = role
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# messages: Anthropic 形式の配列。tools / system は任意。
|
|
26
|
+
# trace_id: 監査ログと突き合わせるための識別子 (任意)。
|
|
27
|
+
# 戻り値は生のレスポンス Hash (content 配列を含む)。
|
|
28
|
+
def create_message(messages:, tools: nil, system: nil, trace_id: nil)
|
|
29
|
+
body = {
|
|
30
|
+
"model" => @model,
|
|
31
|
+
"max_tokens" => @max_tokens,
|
|
32
|
+
"messages" => messages
|
|
33
|
+
}
|
|
34
|
+
body["tools"] = tools if tools
|
|
35
|
+
body["system"] = system if system
|
|
36
|
+
|
|
37
|
+
headers = {
|
|
38
|
+
"content-type" => "application/json",
|
|
39
|
+
"anthropic-version" => "2023-06-01"
|
|
40
|
+
}
|
|
41
|
+
headers["x-api-key"] = @api_key if @api_key && !@api_key.empty?
|
|
42
|
+
|
|
43
|
+
# 送信直前に、LLM へ渡す全内容 (ボディ + ヘッダ) をそのまま記録する。
|
|
44
|
+
# 戻り値の行 id にレスポンスを後から追記する。
|
|
45
|
+
payload_id = log_payload(body, headers, trace_id)
|
|
46
|
+
|
|
47
|
+
response = begin
|
|
48
|
+
connection.post("#{@base_url}/messages") do |req|
|
|
49
|
+
headers.each { |k, v| req.headers[k] = v }
|
|
50
|
+
req.body = JSON.generate(body)
|
|
51
|
+
end
|
|
52
|
+
rescue Faraday::ConnectionFailed => e
|
|
53
|
+
# Ollama 等のローカル LLM サーバーが起動していない場合にここに来る。
|
|
54
|
+
# 生の接続拒否メッセージは分かりにくいため、原因が伝わる日本語メッセージに置き換える。
|
|
55
|
+
log_response(payload_id, nil, e.message)
|
|
56
|
+
raise Chocomint::Error,
|
|
57
|
+
"LLM サーバーに接続できません (#{@base_url})。Ollama 等の LLM サーバーが" \
|
|
58
|
+
"起動しているか確認してください。"
|
|
59
|
+
rescue Faraday::Error => e
|
|
60
|
+
# その他の接続失敗・タイムアウト等。orchestrator の RETRY 扱いに乗せるため
|
|
61
|
+
# Chocomint::Error でラップする (素通りすると server.rb の例外処理網から漏れる)。
|
|
62
|
+
log_response(payload_id, nil, e.message)
|
|
63
|
+
raise Chocomint::Error, "LLM request failed: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 成功・失敗を問わず LLM レスポンス (ステータス + 生ボディ) を記録する。
|
|
67
|
+
log_response(payload_id, response.status, response.body)
|
|
68
|
+
|
|
69
|
+
unless response.success?
|
|
70
|
+
raise Chocomint::Error, "LLM HTTP #{response.status}: #{response.body}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
JSON.parse(response.body)
|
|
74
|
+
rescue JSON::ParserError => e
|
|
75
|
+
raise Chocomint::Error, "LLM response is not valid JSON: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# ロガーが無い / 記録失敗時も LLM 呼び出し本体は止めない (堅牢性優先)。
|
|
81
|
+
# 戻り値: 記録した行 id (ロガー無し・失敗時は nil)。
|
|
82
|
+
def log_payload(body, headers, trace_id)
|
|
83
|
+
return nil unless @payload_logger
|
|
84
|
+
|
|
85
|
+
@payload_logger.record_llm_payload(
|
|
86
|
+
role: @role, model: @model, trace_id: trace_id,
|
|
87
|
+
request_body: body, request_headers: headers
|
|
88
|
+
)
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
warn "llm payload logging failed: #{e.message}"
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# LLM レスポンスを対応するペイロード行に追記する。記録失敗は無視する。
|
|
95
|
+
def log_response(payload_id, status, response_body)
|
|
96
|
+
return unless @payload_logger && payload_id
|
|
97
|
+
|
|
98
|
+
@payload_logger.update_llm_payload_response(
|
|
99
|
+
payload_id, response_status: status, response_body: response_body
|
|
100
|
+
)
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
warn "llm response logging failed: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def connection
|
|
106
|
+
@connection ||= Faraday.new do |f|
|
|
107
|
+
f.options.timeout = @timeout
|
|
108
|
+
f.options.open_timeout = @timeout
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
|
|
6
|
+
module Chocomint
|
|
7
|
+
module LLM
|
|
8
|
+
# /edit チャットの「普通の質問」応答を担う軽量クライアント。
|
|
9
|
+
#
|
|
10
|
+
# 役割は 2 つ:
|
|
11
|
+
# 1. classify — ユーザー指示が「ファイル操作/コマンド実行を要するタスク」か、
|
|
12
|
+
# 「ツール不要の普通の質問・会話」かを 1 語で判定する。
|
|
13
|
+
# 2. answer — 会話と判定された指示に、ツールを使わず自然文で直接答える。
|
|
14
|
+
#
|
|
15
|
+
# タスクと判定された指示は呼び出し側 (EditHandlers) が Planner に渡す。
|
|
16
|
+
class ChatClient
|
|
17
|
+
# 分類プロンプト。ローカル小型モデルでも安定するよう、出力を 1 語に強制する。
|
|
18
|
+
CLASSIFY_SYSTEM_PROMPT = <<~PROMPT
|
|
19
|
+
あなたはユーザーの指示を 2 種類に分類する分類器です。
|
|
20
|
+
- "task": 実際の操作が必要なもの。ファイルの作成・編集・削除、
|
|
21
|
+
コマンド・プログラムの実行、外部ツールでの調査 (ping・実行・ビルド・
|
|
22
|
+
テスト・検索・一覧取得など) を伴うもの。「〜して」「〜を実行して」
|
|
23
|
+
「〜した結果をまとめて/教えて」のように、動かして結果を得る依頼はすべて task。
|
|
24
|
+
具体的な URL が示されていて、その内容を参照・要約する依頼も task。
|
|
25
|
+
- "chat": 操作を一切伴わず、言葉の説明・質問・相談・雑談だけで済むもの。
|
|
26
|
+
知識の説明や意見を求めるものはこちら。
|
|
27
|
+
また、今日の天気・最新ニュース・現在の株価のように「その場では調べる対象
|
|
28
|
+
(場所・URL など) が特定できない、リアルタイム情報を尋ねる質問」も chat。
|
|
29
|
+
これらは会話として聞き返し、必要な情報を確認するのが適切なため。
|
|
30
|
+
迷ったら task を選ぶこと (操作が必要なのに chat と誤ると何も実行されないため)。
|
|
31
|
+
出力は task または chat のいずれか 1 語のみ。他の文字は一切出力しないこと。
|
|
32
|
+
PROMPT
|
|
33
|
+
|
|
34
|
+
# 会話応答プロンプト。エディタ上のアシスタントとして簡潔に日本語で答えさせる。
|
|
35
|
+
# 現在日時・OS・シェルの情報は answer 実行時に environment_prompt を付加する。
|
|
36
|
+
ANSWER_SYSTEM_PROMPT = <<~PROMPT
|
|
37
|
+
あなたはコードエディタに組み込まれた親切なアシスタントです。
|
|
38
|
+
ユーザーの質問や相談に、簡潔で正確な日本語で答えてください。
|
|
39
|
+
実際のファイル操作やコマンド実行は行いません (この応答では行えません)。
|
|
40
|
+
操作が必要な場合は、その旨と具体的な手順を説明してください。
|
|
41
|
+
今日の天気・最新ニュースなどリアルタイム情報は、あなた自身は把握していません。
|
|
42
|
+
推測で答えず、参照したいウェブページの URL を教えてもらえれば内容を調べられる旨を
|
|
43
|
+
案内し、必要なら場所や対象を聞き返してください。
|
|
44
|
+
コマンド例を示すときは PowerShell (pwsh) の構文を優先してください。
|
|
45
|
+
絵文字は一切使わないでください。
|
|
46
|
+
PROMPT
|
|
47
|
+
|
|
48
|
+
# 実行結果の要約プロンプト。実行済みのツール列とその結果をもとに、
|
|
49
|
+
# ユーザー要求に対して何を行い・どうなったかを自然文でまとめさせる。
|
|
50
|
+
SUMMARIZE_SYSTEM_PROMPT = <<~PROMPT
|
|
51
|
+
あなたはコードエディタのアシスタントです。ユーザー要求を達成するために
|
|
52
|
+
いくつかのツール (ファイル編集やコマンド実行) を実行しました。
|
|
53
|
+
実行したツールとその結果をふまえ、「何をして・結果どうなったか」を
|
|
54
|
+
ユーザー向けに簡潔な日本語でまとめてください。
|
|
55
|
+
コマンドの生出力をそのまま貼るのではなく、要点を短く説明してください。
|
|
56
|
+
絵文字は一切使わないでください。
|
|
57
|
+
PROMPT
|
|
58
|
+
|
|
59
|
+
# ウェブページ内容の抽出プロンプト。fetch_url ツールが取得した本文テキストから、
|
|
60
|
+
# ユーザーの目的 (query) に沿って必要な情報を抜き出し・要約させる。
|
|
61
|
+
EXTRACT_SYSTEM_PROMPT = <<~PROMPT
|
|
62
|
+
あなたはウェブページの内容を読み取るアシスタントです。あるページの本文テキストと、
|
|
63
|
+
ユーザーが知りたいこと (目的) が渡されます。本文から目的に関係する情報だけを
|
|
64
|
+
抜き出し、簡潔で正確な日本語にまとめてください。
|
|
65
|
+
本文に答えが書かれていない場合は、その旨を正直に述べてください。
|
|
66
|
+
推測で補わず、本文に書かれている事実だけを根拠にしてください。
|
|
67
|
+
絵文字は一切使わないでください。
|
|
68
|
+
PROMPT
|
|
69
|
+
|
|
70
|
+
# 会話履歴の圧縮プロンプト。これまでのやり取りを、後から見返して文脈を
|
|
71
|
+
# 思い出せる短い要約に畳み込ませる (/compact 用)。
|
|
72
|
+
COMPACT_SYSTEM_PROMPT = <<~PROMPT
|
|
73
|
+
あなたはコードエディタのアシスタントです。これまでのユーザーとの
|
|
74
|
+
やり取りの記録を渡します。後から読み返して文脈を思い出せるよう、
|
|
75
|
+
「何を相談し・何を行い・どう決着したか」の要点を、簡潔な日本語の
|
|
76
|
+
箇条書きでまとめてください。未解決の課題や次の一手があれば残してください。
|
|
77
|
+
細部の生出力は省き、重要な事実と結論だけを残します。
|
|
78
|
+
絵文字は一切使わないでください。
|
|
79
|
+
PROMPT
|
|
80
|
+
|
|
81
|
+
def initialize(base_client:)
|
|
82
|
+
@client = base_client
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 指示を "task" / "chat" に分類する。判定不能時は安全側の "task" を返す
|
|
86
|
+
# (タスクを会話扱いして取りこぼすより、会話をタスク扱いして Planner に流す方が無難)。
|
|
87
|
+
def classify(instruction, trace_id: nil)
|
|
88
|
+
response = @client.create_message(
|
|
89
|
+
system: CLASSIFY_SYSTEM_PROMPT,
|
|
90
|
+
messages: [{ "role" => "user", "content" => instruction.to_s }],
|
|
91
|
+
trace_id: trace_id
|
|
92
|
+
)
|
|
93
|
+
text = extract_text(response).to_s.downcase
|
|
94
|
+
text.include?("chat") ? "chat" : "task"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 会話としての自然文回答を返す。現在日時・OS・シェルの情報を毎回付加する
|
|
98
|
+
# (ローカルモデルが日付や実行環境を誤解しないようにするため)。
|
|
99
|
+
def answer(instruction, context: nil, trace_id: nil)
|
|
100
|
+
content = context.to_s.empty? ? instruction.to_s : "#{context}\n\n#{instruction}"
|
|
101
|
+
system_prompt = "#{ANSWER_SYSTEM_PROMPT}\n#{PrimaryClient.environment_prompt}"
|
|
102
|
+
response = @client.create_message(
|
|
103
|
+
system: system_prompt,
|
|
104
|
+
messages: [{ "role" => "user", "content" => content }],
|
|
105
|
+
trace_id: trace_id
|
|
106
|
+
)
|
|
107
|
+
text = strip_emoji(extract_text(response))
|
|
108
|
+
raise Chocomint::Error, "chat response is empty" if text.nil? || text.empty?
|
|
109
|
+
|
|
110
|
+
text
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 実行済みステップ列を自然文でまとめる。失敗しても要約なしで続行できるよう
|
|
114
|
+
# 例外時は nil を返す (呼び出し側は生の結果表示にフォールバックする)。
|
|
115
|
+
# steps: [{ tool:, arguments:, status:, output: }, ...] 形式。
|
|
116
|
+
def summarize(request, steps, trace_id: nil)
|
|
117
|
+
content = <<~MSG
|
|
118
|
+
ユーザー要求:
|
|
119
|
+
#{request}
|
|
120
|
+
|
|
121
|
+
実行したステップ:
|
|
122
|
+
#{format_steps(steps)}
|
|
123
|
+
MSG
|
|
124
|
+
response = @client.create_message(
|
|
125
|
+
system: SUMMARIZE_SYSTEM_PROMPT,
|
|
126
|
+
messages: [{ "role" => "user", "content" => content }],
|
|
127
|
+
trace_id: trace_id
|
|
128
|
+
)
|
|
129
|
+
text = strip_emoji(extract_text(response))
|
|
130
|
+
text.nil? || text.empty? ? nil : text
|
|
131
|
+
rescue Chocomint::Error
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ウェブページ本文 (text) から、目的 (query) に沿った情報を抽出・要約する。
|
|
136
|
+
# fetch_url ツールから呼ばれる。抽出できないときは Chocomint::Error を投げる。
|
|
137
|
+
def extract(query, text, url: nil, trace_id: nil)
|
|
138
|
+
content = <<~MSG
|
|
139
|
+
対象URL: #{url}
|
|
140
|
+
知りたいこと: #{query.to_s.empty? ? '(指定なし。ページの要点をまとめる)' : query}
|
|
141
|
+
|
|
142
|
+
ページ本文:
|
|
143
|
+
#{text}
|
|
144
|
+
MSG
|
|
145
|
+
response = @client.create_message(
|
|
146
|
+
system: EXTRACT_SYSTEM_PROMPT,
|
|
147
|
+
messages: [{ "role" => "user", "content" => content }],
|
|
148
|
+
trace_id: trace_id
|
|
149
|
+
)
|
|
150
|
+
result = strip_emoji(extract_text(response))
|
|
151
|
+
raise Chocomint::Error, "extract response is empty" if result.nil? || result.empty?
|
|
152
|
+
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# これまでの会話ログ (プレーンテキスト) を短い要約に畳み込む (/compact 用)。
|
|
157
|
+
# 要約できないときは Chocomint::Error を投げる (呼び出し側で 500 にする)。
|
|
158
|
+
def compact(transcript, trace_id: nil)
|
|
159
|
+
response = @client.create_message(
|
|
160
|
+
system: COMPACT_SYSTEM_PROMPT,
|
|
161
|
+
messages: [{ "role" => "user", "content" => transcript.to_s }],
|
|
162
|
+
trace_id: trace_id
|
|
163
|
+
)
|
|
164
|
+
text = strip_emoji(extract_text(response))
|
|
165
|
+
raise Chocomint::Error, "compact response is empty" if text.nil? || text.empty?
|
|
166
|
+
|
|
167
|
+
text
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
# モデルがプロンプト指示に反して絵文字を混ぜた場合の保険。
|
|
173
|
+
# 絵文字・記号ピクトグラム・異体字セレクタ・ZWJ を除去し、余分な空白を整える。
|
|
174
|
+
EMOJI_PATTERN = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]/
|
|
175
|
+
|
|
176
|
+
def strip_emoji(text)
|
|
177
|
+
return text if text.nil?
|
|
178
|
+
|
|
179
|
+
text.gsub(EMOJI_PATTERN, "").gsub(/[ \t]{2,}/, " ").strip
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
SUMMARY_OUTPUT_LIMIT = 500
|
|
183
|
+
|
|
184
|
+
def format_steps(steps)
|
|
185
|
+
Array(steps).each_with_index.map do |s, i|
|
|
186
|
+
tool = s[:tool] || s["tool"]
|
|
187
|
+
status = s[:status] || s["status"]
|
|
188
|
+
out = (s[:output] || s["output"]).to_s.strip
|
|
189
|
+
out = "#{out[0, SUMMARY_OUTPUT_LIMIT]}…" if out.length > SUMMARY_OUTPUT_LIMIT
|
|
190
|
+
line = "#{i + 1}. #{tool} (#{status})"
|
|
191
|
+
out.empty? ? line : "#{line}\n 結果: #{out}"
|
|
192
|
+
end.join("\n")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def extract_text(response)
|
|
196
|
+
blocks = Array(response["content"])
|
|
197
|
+
texts = blocks.filter_map do |b|
|
|
198
|
+
b["text"] if b.is_a?(Hash) && b["type"] == "text"
|
|
199
|
+
end
|
|
200
|
+
return texts.join("\n") unless texts.empty?
|
|
201
|
+
|
|
202
|
+
response["content"].is_a?(String) ? response["content"] : nil
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|