pwn 0.5.677 → 0.5.680
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/README.md +2 -2
- data/documentation/AI-Integration.md +4 -3
- data/documentation/Agent-Tool-Registry.md +27 -5
- data/documentation/CLI-Drivers.md +1 -1
- data/documentation/Configuration.md +65 -25
- data/documentation/Cron.md +12 -2
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Installation.md +12 -7
- data/documentation/Reinforcement-Learning.md +10 -6
- data/documentation/Skills-Memory-Learning.md +1 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/diagrams/agent-tool-registry.svg +156 -147
- data/documentation/diagrams/cron-scheduling.svg +149 -126
- data/documentation/diagrams/dot/agent-tool-registry.dot +2 -1
- data/documentation/diagrams/dot/cron-scheduling.dot +6 -3
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +14 -13
- data/documentation/pwn-REPL.md +1 -1
- data/documentation/pwn-ai-Agent.md +16 -9
- data/lib/pwn/ai/agent/loop.rb +42 -5
- data/lib/pwn/ai/agent/policy.rb +9 -3
- data/lib/pwn/ai/agent/registry.rb +95 -6
- data/lib/pwn/ai/anthropic.rb +3 -6
- data/lib/pwn/ai/gemini.rb +3 -6
- data/lib/pwn/ai/grok.rb +8 -8
- data/lib/pwn/ai/ollama.rb +3 -6
- data/lib/pwn/ai/open_ai.rb +3 -6
- data/lib/pwn/ai/open_web_ui.rb +3 -6
- data/lib/pwn/blockchain/btc.rb +2 -6
- data/lib/pwn/blockchain/eth.rb +2 -6
- data/lib/pwn/config.rb +4 -3
- data/lib/pwn/cron.rb +1 -1
- data/lib/pwn/plugins/black_duck_binary_analysis.rb +2 -4
- data/lib/pwn/plugins/jira_data_center.rb +2 -4
- data/lib/pwn/plugins/repl.rb +6 -6
- data/lib/pwn/plugins/tty_spinner.rb +71 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/registry_spec.rb +6 -0
- data/spec/lib/pwn/ai/grok_spec.rb +7 -0
- data/spec/lib/pwn/plugins/tty_spinner_spec.rb +13 -0
- data/third_party/pwn_rdoc.jsonl +8 -2
- metadata +3 -1
|
@@ -249,10 +249,10 @@
|
|
|
249
249
|
<!-- Dispatch->Curric -->
|
|
250
250
|
<g id="edge67" class="edge">
|
|
251
251
|
<title>Dispatch->Curric</title>
|
|
252
|
-
<path fill="none" stroke="#fb7185" stroke-width="1.3" stroke-dasharray="5,2" d="M982.
|
|
252
|
+
<path fill="none" stroke="#fb7185" stroke-width="1.3" stroke-dasharray="5,2" d="M982.71,-511.61C962.63,-506.78 940.54,-501.98 920,-498.62 885.68,-493.02 631.72,-496.29 608.25,-470.62 601.95,-463.74 601.66,-456.24 608.25,-449.62 618.31,-439.53 2647.42,-445.97 2661,-441.62 2703.86,-427.9 2744.93,-396.11 2771.77,-371.93"/>
|
|
253
253
|
<polygon fill="#fb7185" stroke="#fb7185" stroke-width="1.3" points="2773.51,-374.14 2777.51,-366.66 2769.72,-370.01 2773.51,-374.14"/>
|
|
254
|
-
<text xml:space="preserve" text-anchor="middle" x="
|
|
255
|
-
<text xml:space="preserve" text-anchor="middle" x="
|
|
254
|
+
<text xml:space="preserve" text-anchor="middle" x="637.12" y="-462.07" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">counterfactual</text>
|
|
255
|
+
<text xml:space="preserve" text-anchor="middle" x="637.12" y="-451.57" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">[REPEATING]</text>
|
|
256
256
|
</g>
|
|
257
257
|
<!-- Policy -->
|
|
258
258
|
<g id="node17" class="node">
|
|
@@ -273,9 +273,9 @@
|
|
|
273
273
|
<!-- Dispatch->Snapshot -->
|
|
274
274
|
<g id="edge17" class="edge">
|
|
275
275
|
<title>Dispatch->Snapshot</title>
|
|
276
|
-
<path fill="none" stroke="#f59e0b" stroke-width="1.3" d="M982.79,-511.
|
|
277
|
-
<polygon fill="#f59e0b" stroke="#f59e0b" stroke-width="1.3" points="834.
|
|
278
|
-
<text xml:space="preserve" text-anchor="middle" x="
|
|
276
|
+
<path fill="none" stroke="#f59e0b" stroke-width="1.3" d="M982.79,-511.08C962.71,-506.24 940.6,-501.57 920,-498.62 857.67,-489.73 397.41,-516.24 354,-470.62 347.57,-463.86 347.47,-456.29 354,-449.62 369.32,-433.99 727.57,-449.48 748,-441.62 781.44,-428.77 811.75,-401.69 832.99,-378.88"/>
|
|
277
|
+
<polygon fill="#f59e0b" stroke="#f59e0b" stroke-width="1.3" points="834.94,-380.91 838.24,-373.11 830.79,-377.15 834.94,-380.91"/>
|
|
278
|
+
<text xml:space="preserve" text-anchor="middle" x="366" y="-456.82" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">probe</text>
|
|
279
279
|
</g>
|
|
280
280
|
<!-- Dispatch->Observe -->
|
|
281
281
|
<g id="edge23" class="edge">
|
|
@@ -354,9 +354,10 @@
|
|
|
354
354
|
<!-- Result->Learning -->
|
|
355
355
|
<g id="edge13" class="edge">
|
|
356
356
|
<title>Result->Learning</title>
|
|
357
|
-
<path fill="none" stroke="#34d399" stroke-width="1.3" d="M1436.68,-515.
|
|
358
|
-
<polygon fill="#34d399" stroke="#34d399" stroke-width="1.3" points="1837.52,-381.18 1840.
|
|
359
|
-
<text xml:space="preserve" text-anchor="middle" x="
|
|
357
|
+
<path fill="none" stroke="#34d399" stroke-width="1.3" d="M1436.68,-515.78C1417.86,-509.34 1394.53,-502.33 1373,-498.62 1335.07,-492.1 707.41,-498.39 680.75,-470.62 674.29,-463.89 674.17,-456.25 680.75,-449.62 691.27,-439.03 1742.12,-447.14 1756,-441.62 1787.86,-428.96 1815.93,-402.06 1835.47,-379.27"/>
|
|
358
|
+
<polygon fill="#34d399" stroke="#34d399" stroke-width="1.3" points="1837.52,-381.18 1840.5,-373.24 1833.22,-377.59 1837.52,-381.18"/>
|
|
359
|
+
<text xml:space="preserve" text-anchor="middle" x="722.38" y="-462.07" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">auto_introspect</text>
|
|
360
|
+
<text xml:space="preserve" text-anchor="middle" x="722.38" y="-451.57" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">(deferred after reply)</text>
|
|
360
361
|
</g>
|
|
361
362
|
<!-- Reward -->
|
|
362
363
|
<g id="node15" class="node">
|
|
@@ -391,10 +392,10 @@
|
|
|
391
392
|
<!-- Result->Observe -->
|
|
392
393
|
<g id="edge18" class="edge">
|
|
393
394
|
<title>Result->Observe</title>
|
|
394
|
-
<path fill="none" stroke="#f59e0b" stroke-width="1.3" d="M1436.69,-515.
|
|
395
|
-
<polygon fill="#f59e0b" stroke="#f59e0b" stroke-width="1.3" points="1056.27,-368.
|
|
396
|
-
<text xml:space="preserve" text-anchor="middle" x="
|
|
397
|
-
<text xml:space="preserve" text-anchor="middle" x="
|
|
395
|
+
<path fill="none" stroke="#f59e0b" stroke-width="1.3" d="M1436.69,-515.68C1417.89,-509.21 1394.55,-502.21 1373,-498.62 1321.07,-489.99 462.42,-508.4 425.75,-470.62 419.25,-463.93 419.2,-456.27 425.75,-449.62 436.56,-438.66 966.02,-448.09 980,-441.62 1013.37,-426.2 1039.16,-391.34 1053.96,-367.03"/>
|
|
396
|
+
<polygon fill="#f59e0b" stroke="#f59e0b" stroke-width="1.3" points="1056.27,-368.62 1057.91,-360.3 1051.44,-365.78 1056.27,-368.62"/>
|
|
397
|
+
<text xml:space="preserve" text-anchor="middle" x="476.38" y="-462.07" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">auto_extrospect</text>
|
|
398
|
+
<text xml:space="preserve" text-anchor="middle" x="476.38" y="-451.57" font-family="sans-Serif" font-size="9.00" fill="#cbd5e1">(AUTO_SECTIONS only)</text>
|
|
398
399
|
</g>
|
|
399
400
|
<!-- Result->Verify -->
|
|
400
401
|
<g id="edge49" class="edge">
|
data/documentation/pwn-REPL.md
CHANGED
|
@@ -19,7 +19,7 @@ commands.
|
|
|
19
19
|
| Command | Implemented in | Purpose |
|
|
20
20
|
|---|---|---|
|
|
21
21
|
| `pwn-ai` | `Agent::Loop` | Enter the AI agent TUI |
|
|
22
|
-
| `pwn-vault` | `PWN::Plugins::Vault` | Decrypt
|
|
22
|
+
| `pwn-vault` | `PWN::Plugins::Vault` | Decrypt, edit `~/.pwn/pwn.yaml`, re-encrypt. Stays in the editor until `PWN::Config` accepts the file. |
|
|
23
23
|
| `pwn-asm` | `Plugins::Assembly` | Multiline asm ↔ opcodes workbench |
|
|
24
24
|
| `pwn-mesh` | `Meshtastic` gem | Meshtastic serial / MQTT client (Ruby ≥ 4.0; installed via `pwn setup`) |
|
|
25
25
|
| `pwn-ai-memory` | `PWN::Memory` | View/edit persistent memory |
|
|
@@ -38,9 +38,10 @@ $ pwn --ai "run bin/pwn_sast against ./src and push findings to DefectDojo"
|
|
|
38
38
|
chosen)` DPO preference pair** in `~/.pwn/preferences.jsonl`.
|
|
39
39
|
3. **Registry** hands Loop the tool schemas - the full set for frontier
|
|
40
40
|
engines, or `CORE_TOOLS` + top-K keyword-relevant when
|
|
41
|
-
`ai.agent.tool_router` is on
|
|
42
|
-
`Policy` after a pair has been visited
|
|
43
|
-
|
|
41
|
+
`ai.agent.tool_router` is on (default on for `ollama` / `openwebui`). Rank
|
|
42
|
+
can include a Q-advantage term from `Policy` after a pair has been visited
|
|
43
|
+
at least twice, plus `ai.agent.tool_preference` as a smaller tie-break.
|
|
44
|
+
Planning still owns the task list. *(local)* `Learning.exemplars_for` splices a compressed
|
|
44
45
|
prior-success trace as few-shot; *(local)* `plan_first` forces a numbered
|
|
45
46
|
tool plan before the first dispatch (optionally red-teamed by
|
|
46
47
|
`Curriculum.red_team_plan`).
|
|
@@ -80,8 +81,10 @@ $ pwn --ai "run bin/pwn_sast against ./src and push findings to DefectDojo"
|
|
|
80
81
|
outside world; a `:refuted` verify is itself recorded as a Mistakes
|
|
81
82
|
`assumption` fingerprint.
|
|
82
83
|
7. Results are appended to the message list; go to 5.
|
|
83
|
-
8. When the reply has *no* tool_calls it's the **final answer
|
|
84
|
-
`
|
|
84
|
+
8. When the reply has *no* tool_calls it's the **final answer**. With
|
|
85
|
+
`defer_introspect` on (default), the user-visible reply returns first and
|
|
86
|
+
`Learning.auto_introspect` runs on a background thread. Specs and cron stay
|
|
87
|
+
inline. Then *(local)*
|
|
85
88
|
`fact_check_local_final` auto-`extro_verify`s every CVE / version-shaped
|
|
86
89
|
claim in the answer; **`Reward.judge`** scores (request, final) with a cheap LLM ORM →
|
|
87
90
|
`{score, verdict, rationale, source}` (heuristic overlap only if the engine is unavailable); **`Policy.finish`** applies that judge score as
|
|
@@ -162,7 +165,7 @@ task_summary_interval_s: 8.0 # or every N seconds (verbose)
|
|
|
162
165
|
task_summary_verbose: false # mid-flight Progress: lines
|
|
163
166
|
task_summary_llm: true # LLM task decompose for autonomous goals (default on)
|
|
164
167
|
request_kind_llm: null # null = follow task_summary_llm; false = heuristic-only
|
|
165
|
-
max_iters:
|
|
168
|
+
max_iters: 75 # budget pressure may lower the effective cap (stricter on local engines)
|
|
166
169
|
```
|
|
167
170
|
|
|
168
171
|
|
|
@@ -176,8 +179,9 @@ max_iters: 25 # budget pressure may lower the effective cap (s
|
|
|
176
179
|
summary turn.
|
|
177
180
|
- Run `mistakes_list` before retrying something that failed last session -
|
|
178
181
|
the fix may already be recorded.
|
|
179
|
-
- `ai.agent.tool_router
|
|
180
|
-
a local model - that cuts mis-routing a lot.
|
|
182
|
+
- Leave `ai.agent.tool_router` and `ai.agent.plan_first` on auto when running
|
|
183
|
+
a local model - that cuts mis-routing a lot. Tune `ai.agent.tool_preference`
|
|
184
|
+
if you want recall / sessions ahead of `shell` on ties.
|
|
181
185
|
- Set `ai.reflect_engine:` to a frontier provider so lessons written to
|
|
182
186
|
`~/.pwn/memory.json` stay high-signal even when the executing engine is
|
|
183
187
|
local.
|
|
@@ -201,7 +205,10 @@ max_iters: 25 # budget pressure may lower the effective cap (s
|
|
|
201
205
|
| `reward_model` | `nil` | optional cheaper model id for `Reward.judge` / `.prm` (nil = active engine default) |
|
|
202
206
|
| `reward_llm_timeout` | `12` | seconds for the cheap ORM chat (clamped 2..30) |
|
|
203
207
|
| `verify_as_reward` | `nil` (auto) | browser-grounded claim sample policy |
|
|
204
|
-
| `local_introspect` | `:failure_only` | ollama end-of-turn introspect policy |
|
|
208
|
+
| `local_introspect` | `:failure_only` | ollama / openwebui end-of-turn introspect policy |
|
|
209
|
+
| `tool_preference` | memory_recall-first list | Rank bonus + Policy suggested-action order |
|
|
210
|
+
| `defer_introspect` | `true` | Post-answer Learning on a background thread |
|
|
211
|
+
| `prompt_cache` | `true` | Engine-native prefix cache (not Ollama / Open WebUI) |
|
|
205
212
|
|
|
206
213
|
Full detail: [Reinforcement Learning](Reinforcement-Learning.md).
|
|
207
214
|
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -1013,7 +1013,7 @@ module PWN
|
|
|
1013
1013
|
cwt_opts[:tool_choice] = has_tool_result && !still_acting ? 'auto' : 'required'
|
|
1014
1014
|
end
|
|
1015
1015
|
end
|
|
1016
|
-
response = mod.chat_with_tools(
|
|
1016
|
+
response = mod.chat_with_tools(cwt_opts)
|
|
1017
1017
|
publish_usage(response: response, engine: engine)
|
|
1018
1018
|
normalize_llm(response: response)
|
|
1019
1019
|
else
|
|
@@ -1404,9 +1404,46 @@ module PWN
|
|
|
1404
1404
|
private_class_method def self.answer_statement(opts = {})
|
|
1405
1405
|
request = opts[:request].to_s
|
|
1406
1406
|
session_id = opts[:session_id]
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1407
|
+
system_role_content = opts[:system_role_content].to_s
|
|
1408
|
+
engine = active_engine
|
|
1409
|
+
mod_name = ENGINE_MODS[engine]
|
|
1410
|
+
raise "ERROR: Unsupported AI engine for agent loop: #{engine}" unless mod_name
|
|
1411
|
+
|
|
1412
|
+
mod = Object.const_get(mod_name)
|
|
1413
|
+
q_sys = <<~SYS
|
|
1414
|
+
#{system_role_content}
|
|
1415
|
+
|
|
1416
|
+
INTENT: STATEMENT (this turn only)
|
|
1417
|
+
The user is making a statement — not an autonomous multi-step goal.
|
|
1418
|
+
Respond concisely in plain US English. Do NOT call tools unless a
|
|
1419
|
+
single factual lookup is strictly required and already present in
|
|
1420
|
+
context. Do NOT plan multi-step work. Do NOT invent task traces,
|
|
1421
|
+
planner monologue, rubocop, rake, or live recon.
|
|
1422
|
+
SYS
|
|
1423
|
+
|
|
1424
|
+
txt =
|
|
1425
|
+
if mod.respond_to?(:chat)
|
|
1426
|
+
r = mod.chat(
|
|
1427
|
+
request: request,
|
|
1428
|
+
system_role_content: q_sys,
|
|
1429
|
+
spinner: true
|
|
1430
|
+
)
|
|
1431
|
+
if r.is_a?(Hash)
|
|
1432
|
+
(r.dig(:choices, -1, :content) || r.dig(:choices, -1, :text) || r[:content]).to_s
|
|
1433
|
+
else
|
|
1434
|
+
r.to_s
|
|
1435
|
+
end
|
|
1436
|
+
else
|
|
1437
|
+
messages = [
|
|
1438
|
+
{ role: 'system', content: q_sys },
|
|
1439
|
+
{ role: 'user', content: request }
|
|
1440
|
+
]
|
|
1441
|
+
msg = call_engine(messages: messages, tools: nil)
|
|
1442
|
+
msg.is_a?(Hash) ? msg[:content].to_s : msg.to_s
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
txt = txt.to_s.strip
|
|
1446
|
+
txt = 'I do not have enough context to respond to your statement.' if txt.empty?
|
|
1410
1447
|
|
|
1411
1448
|
append_session(session_id: session_id, role: 'user', content: request)
|
|
1412
1449
|
append_session(session_id: session_id, role: 'assistant', content: txt)
|
|
@@ -1415,7 +1452,7 @@ module PWN
|
|
|
1415
1452
|
session_id: session_id,
|
|
1416
1453
|
request: request,
|
|
1417
1454
|
final: txt,
|
|
1418
|
-
predicted: 0.
|
|
1455
|
+
predicted: 0.85,
|
|
1419
1456
|
plan: [],
|
|
1420
1457
|
ts_state: nil
|
|
1421
1458
|
)
|
data/lib/pwn/ai/agent/policy.rb
CHANGED
|
@@ -572,16 +572,22 @@ module PWN
|
|
|
572
572
|
end
|
|
573
573
|
|
|
574
574
|
ev = evaluate(limit: opts[:limit] || 40)
|
|
575
|
+
fallback = %w[memory_recall sessions_view pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember]
|
|
576
|
+
pref = begin
|
|
577
|
+
PWN::AI::Agent::Registry.preference_order
|
|
578
|
+
rescue StandardError
|
|
579
|
+
[]
|
|
580
|
+
end
|
|
581
|
+
actions = pref.empty? ? fallback : pref
|
|
575
582
|
rec = begin
|
|
576
|
-
|
|
577
|
-
recs[:action]
|
|
583
|
+
recommend(actions: actions, epsilon: 0.0)[:action]
|
|
578
584
|
rescue StandardError
|
|
579
585
|
nil
|
|
580
586
|
end
|
|
581
587
|
lines << 'POLICY (R5 tabular Q / REINFORCE — advisory only, does not replace planning)'
|
|
582
588
|
lines << " episodes=#{s[:n_episodes]} states=#{s[:n_states]} pairs=#{s[:n_pairs]} updates=#{s[:n_updates]}"
|
|
583
589
|
lines << " mean_return=#{s[:mean_return] || '-'} mean|TD|=#{s[:mean_abs_td] || '-'} greedy_match=#{ev[:greedy_match] || '-'}"
|
|
584
|
-
lines << " current_state=#{current_state || '(none)'} suggest=#{rec || '-'}"
|
|
590
|
+
lines << " current_state=#{current_state || '(none)'} suggest=#{rec || '-'} tool_preference=#{actions.join(',')}"
|
|
585
591
|
"#{lines.join("\n")}\n"
|
|
586
592
|
rescue StandardError
|
|
587
593
|
''
|
|
@@ -43,6 +43,12 @@ module PWN
|
|
|
43
43
|
CORE_TOOLS = %w[shell pwn_eval memory_remember memory_recall
|
|
44
44
|
mistakes_record mistakes_resolve learning_note_outcome].freeze
|
|
45
45
|
|
|
46
|
+
# Operator-tunable tool order. Prefer these names when keyword-fit
|
|
47
|
+
# (or other rank features) tie. Overridable via opts[:order],
|
|
48
|
+
# opts[:preference], or PWN::Env[:ai][:agent][:tool_preference].
|
|
49
|
+
# Explicit nil/empty order disables preference (no Env/DEFAULT fallback).
|
|
50
|
+
DEFAULT_PREFERENCE = %w[memory_recall sessions_view pwn_eval shell mistakes_record mistakes_resolve learning_note_outcome memory_remember].freeze
|
|
51
|
+
|
|
46
52
|
@entries = {}
|
|
47
53
|
@discovered = false
|
|
48
54
|
|
|
@@ -100,7 +106,9 @@ module PWN
|
|
|
100
106
|
# tools = PWN::AI::Agent::Registry.definitions(
|
|
101
107
|
# enabled: 'optional - Array of toolset names to include; nil = all whose check passes',
|
|
102
108
|
# relevance: 'optional - user request; when set AND :tool_router is enabled, slim to CORE + top-K keyword matches',
|
|
103
|
-
# top_k: 'optional - keyword-ranked tools to keep beyond CORE (default 10)'
|
|
109
|
+
# top_k: 'optional - keyword-ranked tools to keep beyond CORE (default 10)',
|
|
110
|
+
# order: 'optional - preference list forwarded only when this key is present',
|
|
111
|
+
# preference: 'optional - alias of :order; same key-present rule'
|
|
104
112
|
# )
|
|
105
113
|
|
|
106
114
|
public_class_method def self.definitions(opts = {})
|
|
@@ -108,19 +116,84 @@ module PWN
|
|
|
108
116
|
enabled = enabled.map(&:to_s) if enabled
|
|
109
117
|
pool = @entries.values.select { |e| (enabled.nil? || enabled.include?(e.toolset)) && safe_check(entry: e) }
|
|
110
118
|
|
|
119
|
+
pref_fwd = {}
|
|
120
|
+
pref_fwd[:order] = opts[:order] if opts.key?(:order)
|
|
121
|
+
pref_fwd[:preference] = opts[:preference] if opts.key?(:preference)
|
|
122
|
+
|
|
111
123
|
if opts[:relevance] && router_enabled?
|
|
112
|
-
keep
|
|
124
|
+
keep = rank({ query: opts[:relevance], entries: pool }.merge(pref_fwd)).first(opts[:top_k] || 10).map(&:name)
|
|
113
125
|
names = (CORE_TOOLS + keep).uniq
|
|
114
126
|
pool = pool.select { |e| names.include?(e.name) }
|
|
115
127
|
end
|
|
116
128
|
|
|
129
|
+
pool = apply_preference({ items: pool }.merge(pref_fwd))
|
|
117
130
|
pool.map { |e| { type: 'function', function: e.schema } }
|
|
118
131
|
end
|
|
119
132
|
|
|
133
|
+
# Supported Method Parameters::
|
|
134
|
+
# order = PWN::AI::Agent::Registry.preference_order(
|
|
135
|
+
# order: 'optional - Array of tool names; explicit nil/[] disables',
|
|
136
|
+
# preference: 'optional - alias of :order. Explicit key disables Env/DEFAULT fallback'
|
|
137
|
+
# )
|
|
138
|
+
|
|
139
|
+
public_class_method def self.preference_order(opts = {})
|
|
140
|
+
explicit = opts.key?(:order) || opts.key?(:preference)
|
|
141
|
+
if explicit
|
|
142
|
+
raw = opts.key?(:preference) ? opts[:preference] : opts[:order]
|
|
143
|
+
return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
144
|
+
|
|
145
|
+
return Array(raw).map(&:to_s).reject(&:empty?)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
raw = nil
|
|
149
|
+
raw = PWN::Env.dig(:ai, :agent, :tool_preference) if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
150
|
+
raw = DEFAULT_PREFERENCE if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
151
|
+
Array(raw).map(&:to_s).reject(&:empty?)
|
|
152
|
+
rescue StandardError
|
|
153
|
+
DEFAULT_PREFERENCE.dup
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Supported Method Parameters::
|
|
157
|
+
# sorted = PWN::AI::Agent::Registry.apply_preference(
|
|
158
|
+
# items: 'required - Array of Entry objects or OpenAI tool hashes',
|
|
159
|
+
# order: 'optional - forwarded to preference_order only when the key is set',
|
|
160
|
+
# preference: 'optional - alias of :order; same key-present rule'
|
|
161
|
+
# )
|
|
162
|
+
#
|
|
163
|
+
# Stable sort: preferred names first, original index as the tie-break
|
|
164
|
+
# so equal-preference items keep their incoming order.
|
|
165
|
+
# Empty preference (explicit [] / nil) leaves items unsorted.
|
|
166
|
+
|
|
167
|
+
public_class_method def self.apply_preference(opts = {})
|
|
168
|
+
items = Array(opts[:items] || opts[:entries])
|
|
169
|
+
po = {}
|
|
170
|
+
po[:preference] = opts[:preference] if opts.key?(:preference)
|
|
171
|
+
po[:order] = opts[:order] if opts.key?(:order)
|
|
172
|
+
order = preference_order(po)
|
|
173
|
+
return items if order.empty?
|
|
174
|
+
|
|
175
|
+
index = {}
|
|
176
|
+
order.each_with_index { |name, i| index[name.to_s] = i }
|
|
177
|
+
fallback = order.length
|
|
178
|
+
items.sort_by.with_index do |item, orig|
|
|
179
|
+
name = if item.respond_to?(:name)
|
|
180
|
+
item.name.to_s
|
|
181
|
+
elsif item.is_a?(Hash)
|
|
182
|
+
(item.dig(:function, :name) ||
|
|
183
|
+
item.dig('function', 'name') ||
|
|
184
|
+
item[:name] || item['name']).to_s
|
|
185
|
+
else
|
|
186
|
+
item.to_s
|
|
187
|
+
end
|
|
188
|
+
[index.fetch(name, fallback), orig]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
120
192
|
# Supported Method Parameters::
|
|
121
193
|
# ranked = PWN::AI::Agent::Registry.rank(
|
|
122
194
|
# query: 'required - user request text',
|
|
123
|
-
# entries: 'optional - Entry pool to rank (default .all)'
|
|
195
|
+
# entries: 'optional - Entry pool to rank (default .all)',
|
|
196
|
+
# order: 'optional - preference list forwarded to preference_order'
|
|
124
197
|
# )
|
|
125
198
|
#
|
|
126
199
|
# C1 advantage-weighted router: score = α·keyword_sim + β·(tool
|
|
@@ -136,11 +209,23 @@ module PWN
|
|
|
136
209
|
|
|
137
210
|
tokens = query.scan(/[a-z0-9_]{3,}/).uniq
|
|
138
211
|
# C1 — advantage-weighted router:
|
|
139
|
-
# score =
|
|
212
|
+
# score = alpha*keyword_sim + beta*advantage + gamma*UCB(tool)
|
|
213
|
+
# + delta*prm_advantage + eps_q*Q_adv + zeta*pref_score
|
|
140
214
|
# UCB gives untried / low-N tools an exploration bonus so a single
|
|
141
215
|
# early failure (before its dep was installed) does not blacklist
|
|
142
216
|
# it forever; advantage prefers tools that outperform the fleet.
|
|
217
|
+
# zeta * pref_score (zeta=0.35) boosts Env/opts tool preference;
|
|
218
|
+
# alpha stays 1.0 so keyword fit remains the primary signal.
|
|
219
|
+
# zeta is 0 when preference is empty so ties are not pref-boosted.
|
|
143
220
|
alpha = 1.0
|
|
221
|
+
po = {}
|
|
222
|
+
po[:preference] = opts[:preference] if opts.key?(:preference)
|
|
223
|
+
po[:order] = opts[:order] if opts.key?(:order)
|
|
224
|
+
pref_order = preference_order(po)
|
|
225
|
+
zeta = pref_order.empty? ? 0.0 : 0.35
|
|
226
|
+
pref_index = {}
|
|
227
|
+
pref_order.each_with_index { |n, i| pref_index[n.to_s] = i }
|
|
228
|
+
pref_n = [pref_order.length, 1].max
|
|
144
229
|
# P4 — haircut advantage weight when reward proxy is hacked
|
|
145
230
|
trust = defined?(Metrics) && Metrics.respond_to?(:proxy_trust) ? Metrics.proxy_trust : 1.0
|
|
146
231
|
beta = 0.3 * trust
|
|
@@ -187,10 +272,14 @@ module PWN
|
|
|
187
272
|
else
|
|
188
273
|
0.0
|
|
189
274
|
end
|
|
190
|
-
[e
|
|
275
|
+
pidx = pref_index[e.name]
|
|
276
|
+
pref = pidx.nil? ? 0.0 : ((pref_n - pidx).to_f / pref_n)
|
|
277
|
+
total = (alpha * sim) + (beta * adv) + (gamma * ucb) + (delta * prm) + (eps_q * qadv) + (zeta * pref)
|
|
278
|
+
[e, sim, total]
|
|
191
279
|
end
|
|
280
|
+
# Equal keyword-fit (and equal total): preferred name first.
|
|
192
281
|
scored.reject { |_, sim, _| sim.zero? }
|
|
193
|
-
.sort_by { |
|
|
282
|
+
.sort_by { |e, _, s| [-s, pref_index.fetch(e.name, pref_n), e.name] }
|
|
194
283
|
.map(&:first)
|
|
195
284
|
end
|
|
196
285
|
|
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'rest-client'
|
|
5
|
-
require 'tty-spinner'
|
|
6
5
|
require 'securerandom'
|
|
7
6
|
require 'base64'
|
|
8
7
|
require 'digest'
|
|
@@ -418,10 +417,8 @@ module PWN
|
|
|
418
417
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
419
418
|
rest_client = browser_obj[:browser]::Request
|
|
420
419
|
|
|
421
|
-
if spinner
|
|
422
|
-
|
|
423
|
-
spin.auto_spin
|
|
424
|
-
end
|
|
420
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
421
|
+
spin.auto_spin if spinner
|
|
425
422
|
|
|
426
423
|
retry_count = 0
|
|
427
424
|
begin
|
|
@@ -479,7 +476,7 @@ module PWN
|
|
|
479
476
|
raise e
|
|
480
477
|
end
|
|
481
478
|
ensure
|
|
482
|
-
|
|
479
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
483
480
|
end
|
|
484
481
|
end
|
|
485
482
|
|
data/lib/pwn/ai/gemini.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'rest-client'
|
|
5
|
-
require 'tty-spinner'
|
|
6
5
|
require 'securerandom'
|
|
7
6
|
|
|
8
7
|
module PWN
|
|
@@ -55,10 +54,8 @@ module PWN
|
|
|
55
54
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
56
55
|
rest_client = browser_obj[:browser]::Request
|
|
57
56
|
|
|
58
|
-
if spinner
|
|
59
|
-
|
|
60
|
-
spin.auto_spin
|
|
61
|
-
end
|
|
57
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
58
|
+
spin.auto_spin if spinner
|
|
62
59
|
|
|
63
60
|
retry_count = 0
|
|
64
61
|
begin
|
|
@@ -103,7 +100,7 @@ module PWN
|
|
|
103
100
|
raise e
|
|
104
101
|
end
|
|
105
102
|
ensure
|
|
106
|
-
|
|
103
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
107
104
|
end
|
|
108
105
|
end
|
|
109
106
|
|
data/lib/pwn/ai/grok.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'rest-client'
|
|
5
|
-
require 'tty-spinner'
|
|
6
5
|
require 'uri'
|
|
7
6
|
require 'base64'
|
|
8
7
|
|
|
@@ -424,10 +423,8 @@ module PWN
|
|
|
424
423
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
425
424
|
rest_client = browser_obj[:browser]::Request
|
|
426
425
|
|
|
427
|
-
if spinner
|
|
428
|
-
|
|
429
|
-
spin.auto_spin
|
|
430
|
-
end
|
|
426
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
427
|
+
spin.auto_spin if spinner
|
|
431
428
|
|
|
432
429
|
retry_count = 0
|
|
433
430
|
begin
|
|
@@ -495,7 +492,7 @@ module PWN
|
|
|
495
492
|
end
|
|
496
493
|
"#{e.message}: #{e.response}"
|
|
497
494
|
ensure
|
|
498
|
-
|
|
495
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
499
496
|
end
|
|
500
497
|
|
|
501
498
|
# Supported Method Parameters::
|
|
@@ -695,6 +692,9 @@ module PWN
|
|
|
695
692
|
temp = opts[:temp].to_f
|
|
696
693
|
temp = engine[:temp].to_f.nonzero? || 1 if temp.zero?
|
|
697
694
|
|
|
695
|
+
spinner = opts[:spinner]
|
|
696
|
+
timeout = opts[:timeout]
|
|
697
|
+
|
|
698
698
|
conv_id = nil
|
|
699
699
|
if defined?(PWN::AI::Agent::PromptCache) &&
|
|
700
700
|
PWN::AI::Agent::PromptCache.enabled?(engine: :grok)
|
|
@@ -722,8 +722,8 @@ module PWN
|
|
|
722
722
|
http_method: :post,
|
|
723
723
|
rest_call: 'chat/completions',
|
|
724
724
|
http_body: http_body,
|
|
725
|
-
timeout:
|
|
726
|
-
spinner:
|
|
725
|
+
timeout: timeout,
|
|
726
|
+
spinner: spinner,
|
|
727
727
|
quiet: opts[:quiet],
|
|
728
728
|
headers: (conv_id ? { 'x-grok-conv-id' => conv_id } : nil)
|
|
729
729
|
)
|
data/lib/pwn/ai/ollama.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'base64'
|
|
5
5
|
require 'securerandom'
|
|
6
|
-
require 'tty-spinner'
|
|
7
6
|
|
|
8
7
|
module PWN
|
|
9
8
|
module AI
|
|
@@ -75,10 +74,8 @@ module PWN
|
|
|
75
74
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
76
75
|
rest_client = browser_obj[:browser]::Request
|
|
77
76
|
|
|
78
|
-
if spinner
|
|
79
|
-
|
|
80
|
-
spin.auto_spin
|
|
81
|
-
end
|
|
77
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
78
|
+
spin.auto_spin if spinner
|
|
82
79
|
|
|
83
80
|
retry_count = 0
|
|
84
81
|
begin
|
|
@@ -198,7 +195,7 @@ module PWN
|
|
|
198
195
|
raise e
|
|
199
196
|
end
|
|
200
197
|
ensure
|
|
201
|
-
|
|
198
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
202
199
|
end
|
|
203
200
|
|
|
204
201
|
# Pull a user-visible answer out of a reasoning/"thinking" channel.
|
data/lib/pwn/ai/open_ai.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'base64'
|
|
5
5
|
require 'securerandom'
|
|
6
|
-
require 'tty-spinner'
|
|
7
6
|
require 'digest'
|
|
8
7
|
require 'uri'
|
|
9
8
|
|
|
@@ -460,10 +459,8 @@ module PWN
|
|
|
460
459
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
461
460
|
rest_client = browser_obj[:browser]::Request
|
|
462
461
|
|
|
463
|
-
if spinner
|
|
464
|
-
|
|
465
|
-
spin.auto_spin
|
|
466
|
-
end
|
|
462
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
463
|
+
spin.auto_spin if spinner
|
|
467
464
|
|
|
468
465
|
retry_count = 0
|
|
469
466
|
begin
|
|
@@ -534,7 +531,7 @@ module PWN
|
|
|
534
531
|
end
|
|
535
532
|
"#{e.message}: #{e.response}"
|
|
536
533
|
ensure
|
|
537
|
-
|
|
534
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
538
535
|
end
|
|
539
536
|
|
|
540
537
|
# Supported Method Parameters::
|
data/lib/pwn/ai/open_web_ui.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'base64'
|
|
5
5
|
require 'securerandom'
|
|
6
|
-
require 'tty-spinner'
|
|
7
6
|
|
|
8
7
|
module PWN
|
|
9
8
|
module AI
|
|
@@ -87,10 +86,8 @@ module PWN
|
|
|
87
86
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
88
87
|
rest_client = browser_obj[:browser]::Request
|
|
89
88
|
|
|
90
|
-
if spinner
|
|
91
|
-
|
|
92
|
-
spin.auto_spin
|
|
93
|
-
end
|
|
89
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
90
|
+
spin.auto_spin if spinner
|
|
94
91
|
|
|
95
92
|
retry_count = 0
|
|
96
93
|
begin
|
|
@@ -226,7 +223,7 @@ module PWN
|
|
|
226
223
|
raise e
|
|
227
224
|
end
|
|
228
225
|
ensure
|
|
229
|
-
|
|
226
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
230
227
|
end
|
|
231
228
|
|
|
232
229
|
# Pull a user-visible answer out of a reasoning/"thinking" channel.
|
data/lib/pwn/blockchain/btc.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'base64'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'rest-client'
|
|
6
|
-
require 'tty-spinner'
|
|
7
6
|
require 'date'
|
|
8
7
|
|
|
9
8
|
module PWN
|
|
@@ -54,10 +53,7 @@ module PWN
|
|
|
54
53
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
55
54
|
rest_client = browser_obj[:browser]::Request
|
|
56
55
|
|
|
57
|
-
if spinner
|
|
58
|
-
spin = TTY::Spinner.new(format: :dots)
|
|
59
|
-
spin.auto_spin
|
|
60
|
-
end
|
|
56
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
61
57
|
|
|
62
58
|
retries = 0
|
|
63
59
|
case http_method.to_s.scrub.downcase.to_sym
|
|
@@ -109,7 +105,7 @@ module PWN
|
|
|
109
105
|
rescue StandardError => e
|
|
110
106
|
raise e
|
|
111
107
|
ensure
|
|
112
|
-
|
|
108
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
113
109
|
end
|
|
114
110
|
|
|
115
111
|
private_class_method def self.btc_rpc_call(opts = {})
|
data/lib/pwn/blockchain/eth.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'rest-client'
|
|
5
|
-
require 'tty-spinner'
|
|
6
5
|
|
|
7
6
|
module PWN
|
|
8
7
|
module Blockchain
|
|
@@ -41,10 +40,7 @@ module PWN
|
|
|
41
40
|
browser_obj = PWN::Plugins::TransparentBrowser.open(browser_type: :rest)
|
|
42
41
|
rest_client = browser_obj[:browser]::Request
|
|
43
42
|
|
|
44
|
-
if spinner
|
|
45
|
-
spin = TTY::Spinner.new(format: :dots)
|
|
46
|
-
spin.auto_spin
|
|
47
|
-
end
|
|
43
|
+
spin = PWN::Plugins::TTYSpinner.start if spinner
|
|
48
44
|
|
|
49
45
|
retries = 0
|
|
50
46
|
case http_method
|
|
@@ -96,7 +92,7 @@ module PWN
|
|
|
96
92
|
rescue StandardError => e
|
|
97
93
|
raise e
|
|
98
94
|
ensure
|
|
99
|
-
|
|
95
|
+
PWN::Plugins::TTYSpinner.stop(spin: spin)
|
|
100
96
|
end
|
|
101
97
|
|
|
102
98
|
# Supported Method Parameters::
|