girb 0.4.2 → 0.5.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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +33 -0
- data/README_ja.md +32 -0
- data/lib/girb/ai_client.rb +79 -4
- data/lib/girb/conversation_history.rb +8 -0
- data/lib/girb/debug_prompt_builder.rb +35 -6
- data/lib/girb/language_detector.rb +47 -0
- data/lib/girb/prompt_builder.rb +39 -4
- data/lib/girb/providers/base.rb +54 -2
- data/lib/girb/tools/evaluate_code.rb +17 -10
- data/lib/girb/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2de8ea7943a9f07d92d64077d4b00698d74173b77751461b96ef0f39f450f0f
|
|
4
|
+
data.tar.gz: dc43f1e3039aa9fb5869249c93b0e17a4e501a8b5e54e3e84f99996e8be94fd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0bc812210be8ee6e03a02a28972d1f1d9a9837543d02d2f9888e5b487972cbedccd173831e289b74d2e0a389f4e7ced5ce64f270278b8e2b67a06f4851bb5f57
|
|
7
|
+
data.tar.gz: 208ef2e119d7dc5da244bd016fdd8584aa2456724a91368d38d4efb206aacb2c5bace91dc23c7aa5ec002387d1f1e5272c2d4187285e0eb00d40ec44b4ad60b2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-06-16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Provider-agnostic error classification: `Providers::Base::Response` now carries an
|
|
8
|
+
`error_kind` (`:malformed_tool_call`/`:rate_limit`/`:transient`/`:timeout`/`:fatal`) with
|
|
9
|
+
`#self_correctable?` and `#transport_retryable?` predicates. Providers may set it explicitly;
|
|
10
|
+
a conservative fallback classifier infers it from the error message otherwise.
|
|
11
|
+
- Restored self-correction for malformed function calls: when the provider reports a malformed
|
|
12
|
+
tool call, girb feeds corrective guidance back to the model and retries (bounded by
|
|
13
|
+
`MAX_SELF_CORRECTION`). Transport errors (rate limit, etc.) are not retried this way.
|
|
14
|
+
- `LanguageDetector`: lightweight, dependency-free heuristic that locks the response language to
|
|
15
|
+
the user's original question.
|
|
16
|
+
- GitHub Actions CI running the test suite on Ruby 3.2 / 3.3 / 3.4.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- When the per-turn tool-call limit (`MAX_TOOL_ITERATIONS`) is reached, girb now makes one final
|
|
21
|
+
request with no tools available so the model produces a natural-language answer, instead of ending
|
|
22
|
+
silently on raw tool output.
|
|
23
|
+
- The response-language directive is now derived from the original question and placed at the end
|
|
24
|
+
of the system prompt, and is propagated through auto-continue / interrupt / limit turns so English
|
|
25
|
+
system messages no longer flip the output language (root fix for the recurring "responds in
|
|
26
|
+
Japanese" / wrong-language bug).
|
|
27
|
+
- `Gemfile` no longer hard-codes local provider-gem paths; a fresh clone can `bundle install` and run
|
|
28
|
+
the test suite without sibling repos (`GIRB_LOCAL_PROVIDERS=1` re-enables the local paths).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Stop recording the assistant message twice on a non-empty text response (avoids conversation-history
|
|
33
|
+
duplication, redundant context, and language drift).
|
|
34
|
+
- Tool execution no longer crashes the loop and dumps a raw backtrace when evaluated code raises a
|
|
35
|
+
`ScriptError` (e.g. `require` of an unavailable gem → `LoadError`). `evaluate_code` and the tool
|
|
36
|
+
dispatcher now catch `ScriptError` in addition to `StandardError`, returning a structured error so the
|
|
37
|
+
AI can recover. `Interrupt`/`SignalException` still propagate, so Ctrl+C handling is unaffected.
|
|
38
|
+
|
|
39
|
+
### Notes / follow-ups
|
|
40
|
+
|
|
41
|
+
- A user-defined `custom_prompt` is placed after the language directive, so an explicit user language
|
|
42
|
+
preference overrides automatic detection.
|
|
43
|
+
- The self-correction retry records an empty assistant turn plus a user-role feedback message to keep
|
|
44
|
+
provider alternation valid. Normalizing empty turns / consecutive-user handling per provider is left
|
|
45
|
+
to the provider adapter gems (a future provider-conformance test kit will cover this).
|
|
46
|
+
|
|
3
47
|
## [0.4.2] - 2026-02-13
|
|
4
48
|
|
|
5
49
|
### Added
|
data/README.md
CHANGED
|
@@ -398,6 +398,23 @@ class MyProvider < Girb::Providers::Base
|
|
|
398
398
|
end
|
|
399
399
|
```
|
|
400
400
|
|
|
401
|
+
### Reporting errors
|
|
402
|
+
|
|
403
|
+
On failure, return a `Response` with an `error` message. Optionally set `error_kind` to one of
|
|
404
|
+
`:malformed_tool_call`, `:rate_limit`, `:transient`, `:timeout`, or `:fatal` so girb can react
|
|
405
|
+
appropriately — in particular, `:malformed_tool_call` lets girb feed the model corrective guidance and
|
|
406
|
+
retry automatically:
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
Girb::Providers::Base::Response.new(
|
|
410
|
+
error: "the provider rejected the tool call",
|
|
411
|
+
error_kind: :malformed_tool_call
|
|
412
|
+
)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
If `error_kind` is omitted, girb infers a conservative category from the error message (defaulting to
|
|
416
|
+
`:fatal`).
|
|
417
|
+
|
|
401
418
|
---
|
|
402
419
|
|
|
403
420
|
## Requirements
|
|
@@ -414,3 +431,19 @@ MIT License
|
|
|
414
431
|
## Contributing
|
|
415
432
|
|
|
416
433
|
Bug reports and feature requests welcome at [GitHub Issues](https://github.com/rira100000000/girb/issues).
|
|
434
|
+
|
|
435
|
+
### Development
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
bundle install
|
|
439
|
+
bundle exec rspec
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The test suite does not require a provider gem. To run girb interactively against your local checkout
|
|
443
|
+
with a provider, opt in with `GIRB_LOCAL_PROVIDERS=1` (uses a local sibling checkout such as
|
|
444
|
+
`../girb-gemini` if present, otherwise the published gem):
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
GIRB_LOCAL_PROVIDERS=1 bundle install
|
|
448
|
+
GIRB_LOCAL_PROVIDERS=1 bundle exec girb
|
|
449
|
+
```
|
data/README_ja.md
CHANGED
|
@@ -396,6 +396,22 @@ class MyProvider < Girb::Providers::Base
|
|
|
396
396
|
end
|
|
397
397
|
```
|
|
398
398
|
|
|
399
|
+
### エラーの通知
|
|
400
|
+
|
|
401
|
+
失敗時は `error` メッセージを持つ `Response` を返します。任意で `error_kind` を
|
|
402
|
+
`:malformed_tool_call` / `:rate_limit` / `:transient` / `:timeout` / `:fatal` のいずれかに設定すると、
|
|
403
|
+
girb が適切に対応できます。特に `:malformed_tool_call` を返すと、girb がモデルに修正指示を与えて
|
|
404
|
+
自動的にリトライします:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
Girb::Providers::Base::Response.new(
|
|
408
|
+
error: "プロバイダーがツール呼び出しを拒否しました",
|
|
409
|
+
error_kind: :malformed_tool_call
|
|
410
|
+
)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`error_kind` を省略した場合は、エラーメッセージから保守的に分類されます(既定は `:fatal`)。
|
|
414
|
+
|
|
399
415
|
---
|
|
400
416
|
|
|
401
417
|
## 動作要件
|
|
@@ -412,3 +428,19 @@ MIT License
|
|
|
412
428
|
## 貢献
|
|
413
429
|
|
|
414
430
|
バグ報告や機能リクエストは [GitHub Issues](https://github.com/rira100000000/girb/issues) へお願いします。
|
|
431
|
+
|
|
432
|
+
### 開発
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
bundle install
|
|
436
|
+
bundle exec rspec
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
テストの実行にプロバイダー gem は不要です。ローカルのチェックアウトをプロバイダー付きで対話的に動かす
|
|
440
|
+
場合は `GIRB_LOCAL_PROVIDERS=1` を指定します(`../girb-gemini` などローカルの sibling があればそれを、
|
|
441
|
+
無ければ公開 gem を使用):
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
GIRB_LOCAL_PROVIDERS=1 bundle install
|
|
445
|
+
GIRB_LOCAL_PROVIDERS=1 bundle exec girb
|
|
446
|
+
```
|
data/lib/girb/ai_client.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "auto_continue"
|
|
4
|
+
require_relative "language_detector"
|
|
4
5
|
require_relative "conversation_history"
|
|
5
6
|
require_relative "session_persistence"
|
|
6
7
|
require_relative "providers/base"
|
|
@@ -10,6 +11,8 @@ require_relative "debug_prompt_builder"
|
|
|
10
11
|
module Girb
|
|
11
12
|
class AiClient
|
|
12
13
|
MAX_TOOL_ITERATIONS = 10
|
|
14
|
+
# Max provider-agnostic retries after a malformed tool call before giving up.
|
|
15
|
+
MAX_SELF_CORRECTION = 2
|
|
13
16
|
|
|
14
17
|
def initialize
|
|
15
18
|
@provider = Girb.configuration.provider!
|
|
@@ -21,6 +24,10 @@ module Girb
|
|
|
21
24
|
@irb_context = irb_context
|
|
22
25
|
@debug_mode = debug_mode
|
|
23
26
|
@reasoning_log = []
|
|
27
|
+
@self_correction_count = 0
|
|
28
|
+
# Lock the response language to the original question once, so English
|
|
29
|
+
# auto-continue / interrupt / limit messages can't re-anchor it.
|
|
30
|
+
@response_language = Girb::LanguageDetector.detect(question)
|
|
24
31
|
|
|
25
32
|
prompt_builder = create_prompt_builder(question, context)
|
|
26
33
|
@system_prompt = prompt_builder.system_prompt
|
|
@@ -98,9 +105,9 @@ module Girb
|
|
|
98
105
|
|
|
99
106
|
def create_prompt_builder(question, context)
|
|
100
107
|
if @debug_mode
|
|
101
|
-
DebugPromptBuilder.new(question, context)
|
|
108
|
+
DebugPromptBuilder.new(question, context, response_language: @response_language)
|
|
102
109
|
else
|
|
103
|
-
PromptBuilder.new(question, context)
|
|
110
|
+
PromptBuilder.new(question, context, response_language: @response_language)
|
|
104
111
|
end
|
|
105
112
|
end
|
|
106
113
|
|
|
@@ -136,6 +143,7 @@ module Girb
|
|
|
136
143
|
iterations += 1
|
|
137
144
|
if iterations > MAX_TOOL_ITERATIONS
|
|
138
145
|
puts "\n[girb] Tool iteration limit reached"
|
|
146
|
+
emit_tool_limit_summary
|
|
139
147
|
break
|
|
140
148
|
end
|
|
141
149
|
|
|
@@ -196,6 +204,23 @@ module Girb
|
|
|
196
204
|
|
|
197
205
|
if response.error && !response.function_call?
|
|
198
206
|
puts "[girb] API Error: #{response.error}" if Girb.configuration.debug
|
|
207
|
+
|
|
208
|
+
# Provider-agnostic self-correction: when the model emits a tool call
|
|
209
|
+
# the provider can't parse, feed it corrective guidance and retry.
|
|
210
|
+
# Bounded by MAX_SELF_CORRECTION; the outer loop's MAX_TOOL_ITERATIONS
|
|
211
|
+
# is the absolute backstop. Only malformed tool calls are retried here
|
|
212
|
+
# (transport errors like rate limits are not the model's to fix).
|
|
213
|
+
if response.self_correctable? && @self_correction_count < MAX_SELF_CORRECTION
|
|
214
|
+
@self_correction_count += 1
|
|
215
|
+
if Girb.configuration.debug
|
|
216
|
+
puts "[girb] Self-correcting malformed tool call " \
|
|
217
|
+
"(attempt #{@self_correction_count}/#{MAX_SELF_CORRECTION})"
|
|
218
|
+
end
|
|
219
|
+
ConversationHistory.add_assistant_message("")
|
|
220
|
+
ConversationHistory.add_user_message(self_correction_feedback)
|
|
221
|
+
next
|
|
222
|
+
end
|
|
223
|
+
|
|
199
224
|
ConversationHistory.add_assistant_message("")
|
|
200
225
|
error_summary = response.error.to_s.split(":").first || "Unknown error"
|
|
201
226
|
puts "[girb] Error: #{error_summary}"
|
|
@@ -262,7 +287,6 @@ module Girb
|
|
|
262
287
|
puts "[girb] Warning: Empty response"
|
|
263
288
|
end
|
|
264
289
|
else
|
|
265
|
-
ConversationHistory.add_assistant_message(full_text)
|
|
266
290
|
puts full_text
|
|
267
291
|
record_ai_response(full_text)
|
|
268
292
|
end
|
|
@@ -271,6 +295,53 @@ module Girb
|
|
|
271
295
|
end
|
|
272
296
|
end
|
|
273
297
|
|
|
298
|
+
# When the per-turn tool-call limit is hit, ask the model once more — with NO
|
|
299
|
+
# tools available — so it must produce a final natural-language answer instead
|
|
300
|
+
# of ending silently on raw tool output. Mirrors handle_irb_limit_reached.
|
|
301
|
+
def emit_tool_limit_summary
|
|
302
|
+
# Flush any pending tool calls into an assistant turn first, so the summary
|
|
303
|
+
# request lands AFTER the tool results in the conversation, not before.
|
|
304
|
+
# Guarded so we don't append an empty assistant turn when nothing is pending.
|
|
305
|
+
ConversationHistory.add_assistant_message("") if ConversationHistory.pending_tool_calls?
|
|
306
|
+
ConversationHistory.add_user_message(
|
|
307
|
+
"(System: Tool-call limit reached. Do NOT call any more tools. " \
|
|
308
|
+
"Summarize what you found and give your best final answer now.)"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
text = ""
|
|
312
|
+
begin
|
|
313
|
+
final = @provider.chat(
|
|
314
|
+
messages: ConversationHistory.to_normalized,
|
|
315
|
+
system_prompt: @system_prompt,
|
|
316
|
+
tools: [],
|
|
317
|
+
binding: @current_binding
|
|
318
|
+
)
|
|
319
|
+
text = final&.text.to_s
|
|
320
|
+
rescue ScriptError, StandardError => e
|
|
321
|
+
# Interrupt/SignalException are not caught here, so Ctrl+C still propagates.
|
|
322
|
+
puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Always close the turn with an assistant message — even on failure/empty —
|
|
326
|
+
# so the synthetic user request above is never left dangling (which would
|
|
327
|
+
# break alternation and leak the internal instruction into the next turn).
|
|
328
|
+
ConversationHistory.add_assistant_message(text)
|
|
329
|
+
|
|
330
|
+
if text.strip.empty?
|
|
331
|
+
puts "[girb] (No final summary was produced — see the tool output above for results.)"
|
|
332
|
+
else
|
|
333
|
+
puts text
|
|
334
|
+
record_ai_response(text)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def self_correction_feedback
|
|
339
|
+
"(System: Your previous tool call could not be parsed by the provider. " \
|
|
340
|
+
"This is a Ruby environment. Respond with a single well-formed tool call " \
|
|
341
|
+
"whose arguments are valid JSON — do not use Python syntax, and do not put " \
|
|
342
|
+
"prose where structured arguments are expected.)"
|
|
343
|
+
end
|
|
344
|
+
|
|
274
345
|
def execute_tool(tool_name, args)
|
|
275
346
|
tool_class = Tools.find_tool(tool_name)
|
|
276
347
|
|
|
@@ -286,7 +357,11 @@ module Girb
|
|
|
286
357
|
else
|
|
287
358
|
{ error: "No binding available for tool execution" }
|
|
288
359
|
end
|
|
289
|
-
rescue StandardError => e
|
|
360
|
+
rescue ScriptError, StandardError => e
|
|
361
|
+
# ScriptError (LoadError/NotImplementedError) is not a StandardError; catch
|
|
362
|
+
# both so a tool can never crash the loop and dump a raw backtrace. Interrupt
|
|
363
|
+
# and SignalException are not StandardError/ScriptError, so Ctrl+C still
|
|
364
|
+
# propagates to the interrupt handling above.
|
|
290
365
|
{ error: "Tool execution failed: #{e.class} - #{e.message}" }
|
|
291
366
|
end
|
|
292
367
|
|
|
@@ -45,6 +45,10 @@ module Girb
|
|
|
45
45
|
def to_normalized
|
|
46
46
|
instance.to_normalized
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
def pending_tool_calls?
|
|
50
|
+
instance.pending_tool_calls?
|
|
51
|
+
end
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
attr_reader :messages
|
|
@@ -82,6 +86,10 @@ module Girb
|
|
|
82
86
|
}.compact
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
def pending_tool_calls?
|
|
90
|
+
@pending_tool_calls.any?
|
|
91
|
+
end
|
|
92
|
+
|
|
85
93
|
def clear!
|
|
86
94
|
@messages.clear
|
|
87
95
|
@pending_tool_calls.clear
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "language_detector"
|
|
4
|
+
|
|
3
5
|
module Girb
|
|
4
6
|
class DebugPromptBuilder
|
|
5
7
|
SYSTEM_PROMPT = <<~PROMPT
|
|
@@ -189,18 +191,21 @@ module Girb
|
|
|
189
191
|
Non-navigation commands (break, info, bt) should come before navigation commands (step, next, continue).
|
|
190
192
|
PROMPT
|
|
191
193
|
|
|
192
|
-
def initialize(question, context)
|
|
194
|
+
def initialize(question, context, response_language: nil)
|
|
193
195
|
@question = question
|
|
194
196
|
@context = context
|
|
197
|
+
@response_language = response_language || LanguageDetector.detect(question)
|
|
195
198
|
end
|
|
196
199
|
|
|
197
200
|
def system_prompt
|
|
201
|
+
prompt = SYSTEM_PROMPT
|
|
202
|
+
# After the bulk of the prompt (so it isn't drowned out by English debug
|
|
203
|
+
# context), but before user-defined instructions so an explicit user
|
|
204
|
+
# language preference still wins.
|
|
205
|
+
prompt += "\n" + language_directive
|
|
198
206
|
custom = Girb.configuration&.custom_prompt
|
|
199
|
-
if custom && !custom.empty?
|
|
200
|
-
|
|
201
|
-
else
|
|
202
|
-
SYSTEM_PROMPT
|
|
203
|
-
end
|
|
207
|
+
prompt += "\n\n## User-Defined Instructions\n#{custom}" if custom && !custom.empty?
|
|
208
|
+
prompt
|
|
204
209
|
end
|
|
205
210
|
|
|
206
211
|
def user_message
|
|
@@ -215,6 +220,30 @@ module Girb
|
|
|
215
220
|
|
|
216
221
|
private
|
|
217
222
|
|
|
223
|
+
def language_directive
|
|
224
|
+
lang = @response_language
|
|
225
|
+
if lang == LanguageDetector::FALLBACK
|
|
226
|
+
<<~MSG
|
|
227
|
+
## Response Language
|
|
228
|
+
Respond in #{lang}: detect it from the user's ORIGINAL question and match it.
|
|
229
|
+
This is NOT determined by code, tool output, logs, stack traces, system
|
|
230
|
+
messages, or prior assistant turns. Keep identifiers, code, errors, and
|
|
231
|
+
quoted output unchanged.
|
|
232
|
+
(If the user's instructions below specify a language, follow those instead.)
|
|
233
|
+
MSG
|
|
234
|
+
else
|
|
235
|
+
<<~MSG
|
|
236
|
+
## Response Language
|
|
237
|
+
response_language for this conversation is: #{lang}.
|
|
238
|
+
It is derived ONLY from the user's original question — NOT from code, tool
|
|
239
|
+
output, logs, stack traces, system messages, or prior assistant turns.
|
|
240
|
+
Unless the user's instructions below say otherwise, your final answer
|
|
241
|
+
AND your one-line tool comments MUST be in #{lang}.
|
|
242
|
+
Keep identifiers, code, errors, and quoted output unchanged.
|
|
243
|
+
MSG
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
218
247
|
def build_context_section
|
|
219
248
|
<<~CONTEXT
|
|
220
249
|
### Source Location
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
# Lightweight, dependency-free heuristic for the response language.
|
|
5
|
+
#
|
|
6
|
+
# The goal is NOT accurate language identification — it is to stop the model
|
|
7
|
+
# from drifting to English when the user clearly wrote in Japanese (a recurring
|
|
8
|
+
# bug), without forcing a wrong language onto users of other languages.
|
|
9
|
+
#
|
|
10
|
+
# Returns one of:
|
|
11
|
+
# "Japanese" - high confidence (kana present)
|
|
12
|
+
# "English" - high confidence (Latin-only, no other scripts)
|
|
13
|
+
# "the user's language" - uncertain; fall back to "match the user" guidance
|
|
14
|
+
module LanguageDetector
|
|
15
|
+
FALLBACK = "the user's language"
|
|
16
|
+
|
|
17
|
+
# Common English function words. Latin-only text is only called "English"
|
|
18
|
+
# when one of these appears, so Spanish/French/German/romaji questions fall
|
|
19
|
+
# back to "match the user" instead of being forced into English.
|
|
20
|
+
# Note: single-letter "i" is intentionally excluded — with the /i flag it
|
|
21
|
+
# would match a Ruby loop variable `i` and misclassify code as English.
|
|
22
|
+
ENGLISH_MARKERS = /\b(?:the|is|are|was|were|what|why|how|does|do|did|this|that|these|those|
|
|
23
|
+
can|could|should|would|will|when|where|which|who|of|to|in|on|for|
|
|
24
|
+
with|and|or|not|please|explain|show|tell|my|it|its)\b/xi
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def detect(text)
|
|
29
|
+
s = text.to_s
|
|
30
|
+
return FALLBACK if s.strip.empty?
|
|
31
|
+
|
|
32
|
+
# Kana is unique to Japanese — any occurrence is a strong signal.
|
|
33
|
+
return "Japanese" if s.match?(/[\p{Hiragana}\p{Katakana}]/)
|
|
34
|
+
|
|
35
|
+
# Han without kana is ambiguous (could be Chinese); stay cautious.
|
|
36
|
+
return FALLBACK if s.match?(/\p{Han}/)
|
|
37
|
+
|
|
38
|
+
# Any other non-Latin script (Hangul, Cyrillic, Arabic, ...) -> don't guess.
|
|
39
|
+
return FALLBACK if s.match?(/[^\p{Latin}\p{Common}\p{Inherited}]/)
|
|
40
|
+
|
|
41
|
+
# Latin-only: only commit to English when a clear English marker is present.
|
|
42
|
+
return "English" if s.match?(ENGLISH_MARKERS)
|
|
43
|
+
|
|
44
|
+
FALLBACK
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/girb/prompt_builder.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "language_detector"
|
|
4
|
+
|
|
3
5
|
module Girb
|
|
4
6
|
class PromptBuilder
|
|
5
7
|
# Common prompt shared across all IRB modes
|
|
@@ -204,9 +206,13 @@ module Girb
|
|
|
204
206
|
- For simple one-shot investigations
|
|
205
207
|
PROMPT
|
|
206
208
|
|
|
207
|
-
def initialize(question, context)
|
|
209
|
+
def initialize(question, context, response_language: nil)
|
|
208
210
|
@question = question
|
|
209
211
|
@context = context
|
|
212
|
+
# Derived from the ORIGINAL user question and propagated by AiClient, so
|
|
213
|
+
# English system/continuation messages never re-anchor the output language.
|
|
214
|
+
# Falls back to detecting from this question for standalone callers.
|
|
215
|
+
@response_language = response_language || LanguageDetector.detect(question)
|
|
210
216
|
end
|
|
211
217
|
|
|
212
218
|
# Legacy single prompt format (for backward compatibility)
|
|
@@ -225,12 +231,17 @@ module Girb
|
|
|
225
231
|
def system_prompt
|
|
226
232
|
prompt = COMMON_PROMPT + "\n" + mode_specific_prompt + "\n" + CONTINUE_ANALYSIS_PROMPT
|
|
227
233
|
|
|
234
|
+
# Language directive goes after the bulk of the prompt so it isn't drowned
|
|
235
|
+
# out, but BEFORE user-defined instructions so an explicit user language
|
|
236
|
+
# preference still wins.
|
|
237
|
+
prompt += "\n" + language_directive
|
|
238
|
+
|
|
228
239
|
custom = Girb.configuration&.custom_prompt
|
|
229
240
|
if custom && !custom.empty?
|
|
230
|
-
prompt
|
|
231
|
-
else
|
|
232
|
-
prompt
|
|
241
|
+
prompt += "\n\n## User-Defined Instructions\n#{custom}"
|
|
233
242
|
end
|
|
243
|
+
|
|
244
|
+
prompt
|
|
234
245
|
end
|
|
235
246
|
|
|
236
247
|
# User message (context + question)
|
|
@@ -246,6 +257,30 @@ module Girb
|
|
|
246
257
|
|
|
247
258
|
private
|
|
248
259
|
|
|
260
|
+
def language_directive
|
|
261
|
+
lang = @response_language
|
|
262
|
+
if lang == LanguageDetector::FALLBACK
|
|
263
|
+
<<~MSG
|
|
264
|
+
## Response Language
|
|
265
|
+
Respond in #{lang}: detect it from the user's ORIGINAL question and match it.
|
|
266
|
+
This is NOT determined by code, tool output, logs, stack traces, system
|
|
267
|
+
messages, or prior assistant turns. Keep identifiers, code, errors, and
|
|
268
|
+
quoted output unchanged.
|
|
269
|
+
(If the user's instructions below specify a language, follow those instead.)
|
|
270
|
+
MSG
|
|
271
|
+
else
|
|
272
|
+
<<~MSG
|
|
273
|
+
## Response Language
|
|
274
|
+
response_language for this conversation is: #{lang}.
|
|
275
|
+
It is derived ONLY from the user's original question — NOT from code, tool
|
|
276
|
+
output, logs, stack traces, system messages, or prior assistant turns.
|
|
277
|
+
Unless the user's instructions below say otherwise, your final answer
|
|
278
|
+
AND your one-line tool comments MUST be in #{lang}.
|
|
279
|
+
Keep identifiers, code, errors, and quoted output unchanged.
|
|
280
|
+
MSG
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
249
284
|
def mode_specific_prompt
|
|
250
285
|
case detect_mode
|
|
251
286
|
when :breakpoint
|
data/lib/girb/providers/base.rb
CHANGED
|
@@ -32,18 +32,70 @@ module Girb
|
|
|
32
32
|
|
|
33
33
|
# Response object returned by chat method
|
|
34
34
|
class Response
|
|
35
|
-
|
|
35
|
+
# Normalized error categories. Providers SHOULD set error_kind explicitly;
|
|
36
|
+
# when omitted, a conservative fallback classification is applied.
|
|
37
|
+
# :malformed_tool_call - the model emitted an invalid/unparseable tool call
|
|
38
|
+
# :rate_limit - provider rate limited the request
|
|
39
|
+
# :transient - temporary provider/network failure, safe to retry
|
|
40
|
+
# :timeout - request timed out
|
|
41
|
+
# :fatal - non-recoverable (auth, bad request, unknown)
|
|
42
|
+
ERROR_KINDS = %i[malformed_tool_call rate_limit transient timeout fatal].freeze
|
|
43
|
+
|
|
44
|
+
attr_reader :text, :function_calls, :error, :raw_response, :error_kind
|
|
45
|
+
|
|
46
|
+
def initialize(text: nil, function_calls: nil, error: nil, raw_response: nil, error_kind: nil)
|
|
47
|
+
if error_kind && !ERROR_KINDS.include?(error_kind)
|
|
48
|
+
raise ArgumentError, "unknown error_kind: #{error_kind.inspect} (expected one of #{ERROR_KINDS.inspect})"
|
|
49
|
+
end
|
|
36
50
|
|
|
37
|
-
def initialize(text: nil, function_calls: nil, error: nil, raw_response: nil)
|
|
38
51
|
@text = text
|
|
39
52
|
@function_calls = function_calls || []
|
|
40
53
|
@error = error
|
|
41
54
|
@raw_response = raw_response
|
|
55
|
+
@error_kind = error_kind
|
|
56
|
+
@error_kind ||= classify_error(error) if error
|
|
42
57
|
end
|
|
43
58
|
|
|
44
59
|
def function_call?
|
|
45
60
|
@function_calls.any?
|
|
46
61
|
end
|
|
62
|
+
|
|
63
|
+
# The model can fix this itself by re-emitting a valid tool call.
|
|
64
|
+
# Drives provider-agnostic self-correction in the tool loop.
|
|
65
|
+
def self_correctable?
|
|
66
|
+
@error_kind == :malformed_tool_call
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The request can be retried at the transport layer without involving
|
|
70
|
+
# the model (no conversation-history change). Not wired into the loop
|
|
71
|
+
# yet; exposed for providers and a future transport-retry milestone.
|
|
72
|
+
def transport_retryable?
|
|
73
|
+
%i[rate_limit transient timeout].include?(@error_kind)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Conservative fallback used only when the provider does not set
|
|
79
|
+
# error_kind. Defaults to :fatal and classifies only a small set of
|
|
80
|
+
# unambiguous, provider-reported patterns, so a generic error message is
|
|
81
|
+
# never mistaken for a malformed tool call.
|
|
82
|
+
def classify_error(err)
|
|
83
|
+
s = err.to_s.downcase
|
|
84
|
+
return :malformed_tool_call if s.include?("malformed_function_call") || s.include?("malformed function call")
|
|
85
|
+
if s.include?("rate limit") || s.include?("rate_limit") || s.match?(/\b429\b/) ||
|
|
86
|
+
s.include?("too many requests") || s.include?("resource_exhausted") || s.include?("quota")
|
|
87
|
+
return :rate_limit
|
|
88
|
+
end
|
|
89
|
+
if s.include?("timed out") || s.include?("timeout") || s.include?("deadline exceeded") ||
|
|
90
|
+
s.include?("execution expired") || s.include?("etimedout")
|
|
91
|
+
return :timeout
|
|
92
|
+
end
|
|
93
|
+
if s.match?(/\b503\b/) || s.include?("service unavailable") || s.include?("unavailable") ||
|
|
94
|
+
s.include?("econnreset") || s.include?("connection reset")
|
|
95
|
+
return :transient
|
|
96
|
+
end
|
|
97
|
+
:fatal
|
|
98
|
+
end
|
|
47
99
|
end
|
|
48
100
|
|
|
49
101
|
# Normalized tool definition format
|
|
@@ -29,18 +29,34 @@ module Girb
|
|
|
29
29
|
def execute(binding, code:)
|
|
30
30
|
captured_output = StringIO.new
|
|
31
31
|
original_stdout = $stdout
|
|
32
|
-
|
|
32
|
+
error_response = nil
|
|
33
33
|
|
|
34
|
+
# A single ensure guarantees $stdout is restored on every exit path —
|
|
35
|
+
# including exceptions we intentionally let propagate (Interrupt,
|
|
36
|
+
# SignalException), which the rescue clauses below do NOT catch.
|
|
34
37
|
begin
|
|
38
|
+
$stdout = captured_output
|
|
35
39
|
result = binding.eval(code)
|
|
40
|
+
rescue SyntaxError => e
|
|
41
|
+
error_response = { code: code, error: "Syntax error: #{e.message}", success: false }
|
|
42
|
+
rescue ScriptError, StandardError => e
|
|
43
|
+
# ScriptError (LoadError/NotImplementedError) is NOT a StandardError,
|
|
44
|
+
# so without it such errors would escape and crash the tool loop.
|
|
45
|
+
error_response = { code: code, error: "#{e.class}: #{e.message}", backtrace: e.backtrace&.first(5), success: false }
|
|
36
46
|
ensure
|
|
37
47
|
$stdout = original_stdout
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
stdout_str = captured_output.string
|
|
41
51
|
# Also print captured output to the real console for user visibility
|
|
52
|
+
# (after $stdout is restored, so it reaches the real terminal).
|
|
42
53
|
print stdout_str unless stdout_str.empty?
|
|
43
54
|
|
|
55
|
+
if error_response
|
|
56
|
+
error_response[:stdout] = stdout_str unless stdout_str.empty?
|
|
57
|
+
return error_response
|
|
58
|
+
end
|
|
59
|
+
|
|
44
60
|
response = {
|
|
45
61
|
code: code,
|
|
46
62
|
result: safe_inspect(result),
|
|
@@ -49,15 +65,6 @@ module Girb
|
|
|
49
65
|
}
|
|
50
66
|
response[:stdout] = stdout_str unless stdout_str.empty?
|
|
51
67
|
response
|
|
52
|
-
rescue SyntaxError => e
|
|
53
|
-
$stdout = original_stdout if $stdout != original_stdout
|
|
54
|
-
{ code: code, error: "Syntax error: #{e.message}", success: false }
|
|
55
|
-
rescue StandardError => e
|
|
56
|
-
$stdout = original_stdout if $stdout != original_stdout
|
|
57
|
-
stdout_str = captured_output&.string
|
|
58
|
-
response = { code: code, error: "#{e.class}: #{e.message}", backtrace: e.backtrace&.first(5), success: false }
|
|
59
|
-
response[:stdout] = stdout_str if stdout_str && !stdout_str.empty?
|
|
60
|
-
response
|
|
61
68
|
end
|
|
62
69
|
end
|
|
63
70
|
end
|
data/lib/girb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: girb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rira100000000
|
|
@@ -95,6 +95,7 @@ files:
|
|
|
95
95
|
- lib/girb/exception_capture.rb
|
|
96
96
|
- lib/girb/girbrc_loader.rb
|
|
97
97
|
- lib/girb/irb_integration.rb
|
|
98
|
+
- lib/girb/language_detector.rb
|
|
98
99
|
- lib/girb/prompt_builder.rb
|
|
99
100
|
- lib/girb/providers/base.rb
|
|
100
101
|
- lib/girb/railtie.rb
|
|
@@ -138,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
139
|
- !ruby/object:Gem::Version
|
|
139
140
|
version: '0'
|
|
140
141
|
requirements: []
|
|
141
|
-
rubygems_version: 3.
|
|
142
|
+
rubygems_version: 3.6.9
|
|
142
143
|
specification_version: 4
|
|
143
144
|
summary: AI-powered IRB assistant
|
|
144
145
|
test_files: []
|