ModerRAS commited on
Commit
fb3fd2d
·
1 Parent(s): 94dfb39

Add Pi RPC title-boundary reviewer backend

Browse files
tools/rust_dmhy_template_apply/README.md CHANGED
@@ -53,9 +53,9 @@ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml --
53
  --audit-max-count 50
54
  ```
55
 
56
- Review ambiguous title-boundary decisions with a local LM Studio model. This is
57
- an optional final-pass reviewer: it writes a decision cache JSONL and does not
58
- modify recipes or training data directly.
59
 
60
  ```powershell
61
  cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
@@ -69,6 +69,17 @@ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml --
69
  --limit 50
70
  ```
71
 
 
 
 
 
 
 
 
 
 
 
 
72
  Optional controls:
73
 
74
  ```powershell
 
53
  --audit-max-count 50
54
  ```
55
 
56
+ Review ambiguous title-boundary decisions with a local LM Studio model or with
57
+ Pi RPC mode. This is an optional final-pass reviewer: it writes a decision cache
58
+ JSONL and does not modify recipes or training data directly.
59
 
60
  ```powershell
61
  cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
 
69
  --limit 50
70
  ```
71
 
72
+ ```powershell
73
+ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
74
+ --review-title-boundaries-pi-rpc `
75
+ --recipes reports\dmhy_template_recipes.full_top5000.seed.jsonl `
76
+ --title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_decisions.jsonl `
77
+ --pi-rpc-model openai/gpt-5.5 `
78
+ --title-boundary-min-similarity 0.25 `
79
+ --title-boundary-max-similarity 0.85 `
80
+ --limit 50
81
+ ```
82
+
83
  Optional controls:
84
 
85
  ```powershell
tools/rust_dmhy_template_apply/src/main.rs CHANGED
@@ -10,6 +10,7 @@ use std::collections::{HashMap, HashSet};
10
  use std::fs::{self, File};
11
  use std::io::{BufRead, BufReader, BufWriter, Read, Write};
12
  use std::path::PathBuf;
 
13
  use std::sync::atomic::{AtomicUsize, Ordering};
14
 
15
  #[derive(Parser, Debug)]
@@ -25,6 +26,8 @@ struct Args {
25
  rich_annotations: bool,
26
  #[arg(long)]
27
  review_title_boundaries_lmstudio: bool,
 
 
28
  #[arg(long, default_value = "datasets/AnimeName/dmhy_list.jsonl")]
29
  input: PathBuf,
30
  #[arg(long, default_value = "reports/dmhy_template_recipes.seed.jsonl")]
@@ -76,6 +79,8 @@ struct Args {
76
  lmstudio_temperature: f64,
77
  #[arg(long, default_value_t = 120)]
78
  lmstudio_timeout_seconds: u64,
 
 
79
  #[arg(long, default_value_t = 50)]
80
  audit_max_count: u64,
81
  #[arg(long)]
@@ -329,8 +334,8 @@ fn main() -> Result<()> {
329
  if args.rich_annotations {
330
  return run_rich_annotations(&args);
331
  }
332
- if args.review_title_boundaries_lmstudio {
333
- return run_review_title_boundaries_lmstudio(&args);
334
  }
335
  if args.expand != "all" && args.expand != "sample" {
336
  bail!("--expand must be all or sample");
@@ -1171,7 +1176,7 @@ fn run_rich_annotations(args: &Args) -> Result<()> {
1171
  Ok(())
1172
  }
1173
 
1174
- fn run_review_title_boundaries_lmstudio(args: &Args) -> Result<()> {
1175
  if !(0.0..=1.0).contains(&args.title_boundary_min_similarity)
1176
  || !(0.0..=1.0).contains(&args.title_boundary_max_similarity)
1177
  || args.title_boundary_min_similarity > args.title_boundary_max_similarity
@@ -1188,10 +1193,18 @@ fn run_review_title_boundaries_lmstudio(args: &Args) -> Result<()> {
1188
  let mut writer = BufWriter::new(File::create(&args.title_boundary_decisions_output)?);
1189
  let mut reviewed = 0usize;
1190
  let mut skipped = 0usize;
 
 
 
1191
  let endpoint = format!(
1192
  "{}/chat/completions",
1193
  args.lmstudio_base_url.trim_end_matches('/')
1194
  );
 
 
 
 
 
1195
 
1196
  for recipe in recipes {
1197
  let template_id = recipe
@@ -1226,27 +1239,34 @@ fn run_review_title_boundaries_lmstudio(args: &Args) -> Result<()> {
1226
  }
1227
  }
1228
  let prompt = title_boundary_review_prompt(&recipe, &decision);
1229
- let response = call_lmstudio_title_boundary(
1230
- &endpoint,
1231
- &args.lmstudio_model,
1232
- args.lmstudio_temperature,
1233
- args.lmstudio_timeout_seconds,
1234
- &prompt,
1235
- )
1236
- .with_context(|| {
1237
- format!(
1238
- "LM Studio title-boundary review failed for template {template_id} via {endpoint}"
 
1239
  )
1240
- })?;
1241
- let content = response
1242
- .get("choices")
1243
- .and_then(Value::as_array)
1244
- .and_then(|choices| choices.first())
1245
- .and_then(|choice| choice.get("message"))
1246
- .and_then(|message| message.get("content"))
1247
- .and_then(Value::as_str)
1248
- .unwrap_or("");
1249
- let parsed = parse_lmstudio_json_object(content).unwrap_or_else(|| {
 
 
 
 
 
 
1250
  json!({
1251
  "decision": "skip",
1252
  "kind": "parse_error",
@@ -1265,8 +1285,8 @@ fn run_review_title_boundaries_lmstudio(args: &Args) -> Result<()> {
1265
  "model_kind": parsed.get("kind").cloned().unwrap_or(Value::Null),
1266
  "confidence": parsed.get("confidence").cloned().unwrap_or(Value::Null),
1267
  "reason": parsed.get("reason").cloned().unwrap_or(Value::Null),
1268
- "source": "lmstudio",
1269
- "model": args.lmstudio_model,
1270
  "reviewed_at": Utc::now().to_rfc3339(),
1271
  });
1272
  serde_json::to_writer(&mut writer, &output)?;
@@ -1280,8 +1300,9 @@ fn run_review_title_boundaries_lmstudio(args: &Args) -> Result<()> {
1280
  "output": args.title_boundary_decisions_output.to_string_lossy(),
1281
  "reviewed": reviewed,
1282
  "skipped": skipped,
1283
- "model": args.lmstudio_model,
1284
- "base_url": args.lmstudio_base_url,
 
1285
  "similarity_range": [args.title_boundary_min_similarity, args.title_boundary_max_similarity],
1286
  "generated_at": Utc::now().to_rfc3339(),
1287
  });
@@ -1369,6 +1390,149 @@ fn title_boundary_review_prompt(recipe: &Value, decision: &Value) -> String {
1369
  )
1370
  }
1371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1372
  fn call_lmstudio_title_boundary(
1373
  endpoint: &str,
1374
  model: &str,
 
10
  use std::fs::{self, File};
11
  use std::io::{BufRead, BufReader, BufWriter, Read, Write};
12
  use std::path::PathBuf;
13
+ use std::process::{Child, ChildStdin, Command, Stdio};
14
  use std::sync::atomic::{AtomicUsize, Ordering};
15
 
16
  #[derive(Parser, Debug)]
 
26
  rich_annotations: bool,
27
  #[arg(long)]
28
  review_title_boundaries_lmstudio: bool,
29
+ #[arg(long)]
30
+ review_title_boundaries_pi_rpc: bool,
31
  #[arg(long, default_value = "datasets/AnimeName/dmhy_list.jsonl")]
32
  input: PathBuf,
33
  #[arg(long, default_value = "reports/dmhy_template_recipes.seed.jsonl")]
 
79
  lmstudio_temperature: f64,
80
  #[arg(long, default_value_t = 120)]
81
  lmstudio_timeout_seconds: u64,
82
+ #[arg(long)]
83
+ pi_rpc_model: Option<String>,
84
  #[arg(long, default_value_t = 50)]
85
  audit_max_count: u64,
86
  #[arg(long)]
 
334
  if args.rich_annotations {
335
  return run_rich_annotations(&args);
336
  }
337
+ if args.review_title_boundaries_lmstudio || args.review_title_boundaries_pi_rpc {
338
+ return run_review_title_boundaries(&args);
339
  }
340
  if args.expand != "all" && args.expand != "sample" {
341
  bail!("--expand must be all or sample");
 
1176
  Ok(())
1177
  }
1178
 
1179
+ fn run_review_title_boundaries(args: &Args) -> Result<()> {
1180
  if !(0.0..=1.0).contains(&args.title_boundary_min_similarity)
1181
  || !(0.0..=1.0).contains(&args.title_boundary_max_similarity)
1182
  || args.title_boundary_min_similarity > args.title_boundary_max_similarity
 
1193
  let mut writer = BufWriter::new(File::create(&args.title_boundary_decisions_output)?);
1194
  let mut reviewed = 0usize;
1195
  let mut skipped = 0usize;
1196
+ if args.review_title_boundaries_lmstudio && args.review_title_boundaries_pi_rpc {
1197
+ bail!("choose only one title-boundary review backend: LM Studio or Pi RPC");
1198
+ }
1199
  let endpoint = format!(
1200
  "{}/chat/completions",
1201
  args.lmstudio_base_url.trim_end_matches('/')
1202
  );
1203
+ let mut pi_rpc = if args.review_title_boundaries_pi_rpc {
1204
+ Some(PiRpcClient::start(args.pi_rpc_model.as_deref())?)
1205
+ } else {
1206
+ None
1207
+ };
1208
 
1209
  for recipe in recipes {
1210
  let template_id = recipe
 
1239
  }
1240
  }
1241
  let prompt = title_boundary_review_prompt(&recipe, &decision);
1242
+ let content = if let Some(client) = pi_rpc.as_mut() {
1243
+ client.review(&prompt).with_context(|| {
1244
+ format!("Pi RPC title-boundary review failed for template {template_id}")
1245
+ })?
1246
+ } else {
1247
+ let response = call_lmstudio_title_boundary(
1248
+ &endpoint,
1249
+ &args.lmstudio_model,
1250
+ args.lmstudio_temperature,
1251
+ args.lmstudio_timeout_seconds,
1252
+ &prompt,
1253
  )
1254
+ .with_context(|| {
1255
+ format!(
1256
+ "LM Studio title-boundary review failed for template {template_id} via {endpoint}"
1257
+ )
1258
+ })?;
1259
+ response
1260
+ .get("choices")
1261
+ .and_then(Value::as_array)
1262
+ .and_then(|choices| choices.first())
1263
+ .and_then(|choice| choice.get("message"))
1264
+ .and_then(|message| message.get("content"))
1265
+ .and_then(Value::as_str)
1266
+ .unwrap_or("")
1267
+ .to_string()
1268
+ };
1269
+ let parsed = parse_lmstudio_json_object(&content).unwrap_or_else(|| {
1270
  json!({
1271
  "decision": "skip",
1272
  "kind": "parse_error",
 
1285
  "model_kind": parsed.get("kind").cloned().unwrap_or(Value::Null),
1286
  "confidence": parsed.get("confidence").cloned().unwrap_or(Value::Null),
1287
  "reason": parsed.get("reason").cloned().unwrap_or(Value::Null),
1288
+ "source": if args.review_title_boundaries_pi_rpc { "pi-rpc" } else { "lmstudio" },
1289
+ "model": if args.review_title_boundaries_pi_rpc { args.pi_rpc_model.as_deref().unwrap_or("pi-default") } else { args.lmstudio_model.as_str() },
1290
  "reviewed_at": Utc::now().to_rfc3339(),
1291
  });
1292
  serde_json::to_writer(&mut writer, &output)?;
 
1300
  "output": args.title_boundary_decisions_output.to_string_lossy(),
1301
  "reviewed": reviewed,
1302
  "skipped": skipped,
1303
+ "model": if args.review_title_boundaries_pi_rpc { args.pi_rpc_model.as_deref().unwrap_or("pi-default") } else { args.lmstudio_model.as_str() },
1304
+ "backend": if args.review_title_boundaries_pi_rpc { "pi-rpc" } else { "lmstudio" },
1305
+ "base_url": if args.review_title_boundaries_pi_rpc { "pi --mode rpc" } else { args.lmstudio_base_url.as_str() },
1306
  "similarity_range": [args.title_boundary_min_similarity, args.title_boundary_max_similarity],
1307
  "generated_at": Utc::now().to_rfc3339(),
1308
  });
 
1390
  )
1391
  }
1392
 
1393
+ struct PiRpcClient {
1394
+ child: Child,
1395
+ stdin: Option<ChildStdin>,
1396
+ stdout: BufReader<std::process::ChildStdout>,
1397
+ next_id: usize,
1398
+ }
1399
+
1400
+ impl PiRpcClient {
1401
+ fn start(model: Option<&str>) -> Result<Self> {
1402
+ let mut command = Command::new("pi");
1403
+ command.args(["--mode", "rpc", "--no-session"]);
1404
+ if let Some(model) = model {
1405
+ command.args(["--model", model]);
1406
+ }
1407
+ let mut child = command
1408
+ .stdin(Stdio::piped())
1409
+ .stdout(Stdio::piped())
1410
+ .stderr(Stdio::inherit())
1411
+ .spawn()
1412
+ .context("failed to start pi --mode rpc")?;
1413
+ let stdin = child
1414
+ .stdin
1415
+ .take()
1416
+ .ok_or_else(|| anyhow::anyhow!("failed to open Pi RPC stdin"))?;
1417
+ let stdout = child
1418
+ .stdout
1419
+ .take()
1420
+ .ok_or_else(|| anyhow::anyhow!("failed to open Pi RPC stdout"))?;
1421
+ Ok(Self {
1422
+ child,
1423
+ stdin: Some(stdin),
1424
+ stdout: BufReader::new(stdout),
1425
+ next_id: 0,
1426
+ })
1427
+ }
1428
+
1429
+ fn review(&mut self, prompt: &str) -> Result<String> {
1430
+ let prompt_id = self.next_request_id("prompt");
1431
+ self.send(json!({"id": prompt_id, "type": "prompt", "message": prompt}))?;
1432
+ let mut accepted = false;
1433
+ loop {
1434
+ let event = self.read_event()?;
1435
+ if event.get("type").and_then(Value::as_str) == Some("response")
1436
+ && event.get("id").and_then(Value::as_str) == Some(prompt_id.as_str())
1437
+ {
1438
+ if !event
1439
+ .get("success")
1440
+ .and_then(Value::as_bool)
1441
+ .unwrap_or(false)
1442
+ {
1443
+ bail!("Pi RPC prompt rejected: {event}");
1444
+ }
1445
+ accepted = true;
1446
+ continue;
1447
+ }
1448
+ if event.get("type").and_then(Value::as_str) == Some("agent_end") && accepted {
1449
+ break;
1450
+ }
1451
+ }
1452
+ let text = self.get_last_assistant_text()?;
1453
+ self.new_session()?;
1454
+ Ok(text)
1455
+ }
1456
+
1457
+ fn get_last_assistant_text(&mut self) -> Result<String> {
1458
+ let id = self.next_request_id("last");
1459
+ self.send(json!({"id": id, "type": "get_last_assistant_text"}))?;
1460
+ loop {
1461
+ let event = self.read_event()?;
1462
+ if event.get("type").and_then(Value::as_str) == Some("response")
1463
+ && event.get("id").and_then(Value::as_str) == Some(id.as_str())
1464
+ {
1465
+ if !event
1466
+ .get("success")
1467
+ .and_then(Value::as_bool)
1468
+ .unwrap_or(false)
1469
+ {
1470
+ bail!("Pi RPC get_last_assistant_text failed: {event}");
1471
+ }
1472
+ return Ok(event
1473
+ .get("data")
1474
+ .and_then(|data| data.get("text"))
1475
+ .and_then(Value::as_str)
1476
+ .unwrap_or("")
1477
+ .to_string());
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ fn new_session(&mut self) -> Result<()> {
1483
+ let id = self.next_request_id("new");
1484
+ self.send(json!({"id": id, "type": "new_session"}))?;
1485
+ loop {
1486
+ let event = self.read_event()?;
1487
+ if event.get("type").and_then(Value::as_str) == Some("response")
1488
+ && event.get("id").and_then(Value::as_str) == Some(id.as_str())
1489
+ {
1490
+ if !event
1491
+ .get("success")
1492
+ .and_then(Value::as_bool)
1493
+ .unwrap_or(false)
1494
+ {
1495
+ bail!("Pi RPC new_session failed: {event}");
1496
+ }
1497
+ return Ok(());
1498
+ }
1499
+ }
1500
+ }
1501
+
1502
+ fn next_request_id(&mut self, prefix: &str) -> String {
1503
+ self.next_id += 1;
1504
+ format!("{prefix}-{}", self.next_id)
1505
+ }
1506
+
1507
+ fn send(&mut self, value: Value) -> Result<()> {
1508
+ let stdin = self
1509
+ .stdin
1510
+ .as_mut()
1511
+ .ok_or_else(|| anyhow::anyhow!("Pi RPC stdin is closed"))?;
1512
+ serde_json::to_writer(&mut *stdin, &value)?;
1513
+ stdin.write_all(b"\n")?;
1514
+ stdin.flush()?;
1515
+ Ok(())
1516
+ }
1517
+
1518
+ fn read_event(&mut self) -> Result<Value> {
1519
+ let mut line = String::new();
1520
+ let bytes = self.stdout.read_line(&mut line)?;
1521
+ if bytes == 0 {
1522
+ bail!("Pi RPC process exited");
1523
+ }
1524
+ let line = line.trim_end_matches(['\r', '\n']);
1525
+ serde_json::from_str(line).with_context(|| format!("invalid Pi RPC JSON line: {line}"))
1526
+ }
1527
+ }
1528
+
1529
+ impl Drop for PiRpcClient {
1530
+ fn drop(&mut self) {
1531
+ let _ = self.stdin.take();
1532
+ let _ = self.child.wait();
1533
+ }
1534
+ }
1535
+
1536
  fn call_lmstudio_title_boundary(
1537
  endpoint: &str,
1538
  model: &str,