dsh-biocore 0.3.2 → 0.4.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.
@@ -53,13 +53,15 @@ DEFAULTS = {
53
53
  "standby_model_key": "",
54
54
  "standby_every_minutes": 30,
55
55
  "standby_active_hours": "09:00-23:00",
56
- "standby_max_per_day": 6,
56
+ "standby_max_per_day": 3,
57
+ "standby_cooldown_minutes": 120,
58
+ "standby_max_no_reply": 2,
57
59
  "standby_channel": "outbox",
58
60
  "standby_timeout": 20.0,
59
61
  "standby_store": "",
60
62
  "standby_session_id": "standby",
61
- "standby_min_trust": 0.6,
62
- "standby_require_open_loop": True,
63
+ "standby_min_trust": 0.0,
64
+ "standby_require_open_loop": False,
63
65
  }
64
66
 
65
67
 
@@ -102,13 +104,17 @@ def normalize_config(config=None):
102
104
  out["standby_every_minutes"] = max(
103
105
  1, min(24 * 60, int(_num(out.get("standby_every_minutes"), 30))))
104
106
  out["standby_max_per_day"] = max(
105
- 1, min(100, int(_num(out.get("standby_max_per_day"), 6))))
107
+ 1, min(100, int(_num(out.get("standby_max_per_day"), 3))))
108
+ out["standby_cooldown_minutes"] = max(
109
+ 1, min(24 * 60, int(_num(out.get("standby_cooldown_minutes"), 120))))
110
+ out["standby_max_no_reply"] = max(
111
+ 1, min(10, int(_num(out.get("standby_max_no_reply"), 2))))
106
112
  out["standby_timeout"] = max(
107
113
  0.2, min(120.0, _num(out.get("standby_timeout"), 20.0)))
108
114
  out["standby_min_trust"] = max(
109
- 0.0, min(1.0, _num(out.get("standby_min_trust"), 0.6)))
115
+ 0.0, min(1.0, _num(out.get("standby_min_trust"), 0.0)))
110
116
  out["standby_require_open_loop"] = _as_bool(
111
- out.get("standby_require_open_loop"), True)
117
+ out.get("standby_require_open_loop"), False)
112
118
  out["standby_session_id"] = out["standby_session_id"] or "standby"
113
119
  return out
114
120
 
@@ -198,8 +204,9 @@ def load_session_state(store, session_id):
198
204
  return _pipeline.apply_intero_to_state(state)
199
205
 
200
206
 
201
- def build_prompt(state, memories, persona_text="", channel="outbox"):
202
- """Full persona (never truncated) + thinking rule + state + recent memories."""
207
+ def build_prompt(state, memories, persona_text="", channel="outbox",
208
+ digest=""):
209
+ """Full persona (never truncated) + thinking rule + state + memories + digest."""
203
210
  parts = []
204
211
  if persona_text:
205
212
  parts.append(str(persona_text))
@@ -212,6 +219,8 @@ def build_prompt(state, memories, persona_text="", channel="outbox"):
212
219
  recent = [str(item).strip() for item in (memories or []) if str(item).strip()]
213
220
  if recent:
214
221
  parts.append("最近记得:" + ";".join(recent[:3]))
222
+ if digest:
223
+ parts.append("上次对话:" + str(digest)[:60])
215
224
  prompt = "\n\n".join(parts)
216
225
  return prompt
217
226
 
@@ -341,6 +350,45 @@ def _bookkeep(store, now, state, sent=False, reason="", text=""):
341
350
  return data
342
351
 
343
352
 
353
+ def _settle_reply_bookkeeping(store, state, session_state, now, cfg):
354
+ """Count unanswered sends; two in a row block standby."""
355
+ awaiting = bool(state.get("awaiting_reply"))
356
+ streak = int(_num(state.get("no_reply_streak"), 0))
357
+ if not awaiting:
358
+ return streak, state
359
+ last_sent = _num(state.get("last_sent_ts"), 0.0)
360
+ last_human = _num((session_state or {}).get("last_human_ts"), 0.0)
361
+ if last_human > last_sent:
362
+ streak = 0
363
+ elif now - last_sent > cfg["standby_cooldown_minutes"] * 60.0:
364
+ streak += 1
365
+ else:
366
+ return streak, state
367
+ state = dict(state)
368
+ state["awaiting_reply"] = False
369
+ state["no_reply_streak"] = streak
370
+ try:
371
+ _write_json(_bookkeeping_path(store), state)
372
+ except Exception:
373
+ pass
374
+ return streak, state
375
+
376
+
377
+ def _last_digest(session_state):
378
+ for key in ("last_user_text", "last_digest", "last_line"):
379
+ value = str((session_state or {}).get(key) or "").strip()
380
+ if value:
381
+ return value[:60]
382
+ return ""
383
+
384
+
385
+ def _template_line(state):
386
+ intero = (state or {}).get("intero") if isinstance((state or {}).get("intero"), dict) else {}
387
+ if _num(intero.get("tension"), 0.5) >= 0.65:
388
+ return "在忙吗?刚刚想起你,不用急着回。"
389
+ return "刚刚想起你,今天还好吗?"
390
+
391
+
344
392
  def tick(now=None, config=None, transport=None, proactive=None):
345
393
  """One standby tick. Returns {ok, sent, reason, text?, channel, outbox?}."""
346
394
  cfg = normalize_config(config)
@@ -358,16 +406,22 @@ def tick(now=None, config=None, transport=None, proactive=None):
358
406
  result["reason"] = "outside_active_hours"
359
407
  return result
360
408
  state = load_state(store)
409
+ session_state = load_session_state(store, session_id)
410
+ streak, state = _settle_reply_bookkeeping(store, state, session_state,
411
+ now_value, cfg)
412
+ result["no_reply_streak"] = streak
413
+ if streak >= cfg["standby_max_no_reply"]:
414
+ result["reason"] = "no_reply_streak"
415
+ return result
361
416
  if len(_sent_today(state, now_value)) >= cfg["standby_max_per_day"]:
362
417
  result["reason"] = "daily_cap"
363
418
  return result
364
419
  last_attempt = _num(state.get("last_attempt_ts"), 0.0)
365
420
  last_sent = _num(state.get("last_sent_ts"), 0.0)
366
- cooldown_until = max(last_attempt, last_sent) + cfg["standby_every_minutes"] * 60.0
421
+ cooldown_until = max(last_attempt, last_sent) + cfg["standby_cooldown_minutes"] * 60.0
367
422
  if cooldown_until > now_value:
368
423
  result["reason"] = "cooldown"
369
424
  return result
370
- session_state = load_session_state(store, session_id)
371
425
  last_human = _num(session_state.get("last_human_ts"),
372
426
  now_value - max(cfg["standby_every_minutes"], 180) * 60.0)
373
427
  switches = {
@@ -399,22 +453,26 @@ def tick(now=None, config=None, transport=None, proactive=None):
399
453
  memories = _recent_memories(store, session_id)
400
454
  persona = _persona_text(store)
401
455
  prompt = build_prompt(session_state, memories, persona,
402
- cfg["standby_channel"])
456
+ cfg["standby_channel"],
457
+ digest=_last_digest(session_state))
403
458
  text, reason = generate(prompt, cfg, transport=transport)
459
+ fallback = False
404
460
  if reason != "ok" or not text:
405
- _bookkeep(store, now_value, state, sent=False, reason=reason)
406
- result["reason"] = reason
407
- return result
461
+ text = _template_line(session_state)
462
+ reason = "template_fallback"
463
+ fallback = True
408
464
  outbox = _outbox_path(store)
409
465
  try:
410
466
  outbox.parent.mkdir(parents=True, exist_ok=True)
411
467
  row = {
412
468
  "ts": round(now_value, 3),
413
469
  "text": text,
414
- "reason": "ok",
470
+ "reason": reason,
415
471
  "state_snapshot": session_state,
416
472
  "channel": cfg["standby_channel"],
417
473
  "model": cfg["standby_model"],
474
+ "fallback": fallback,
475
+ "prompt": prompt,
418
476
  }
419
477
  with outbox.open("a", encoding="utf-8") as handle:
420
478
  handle.write(json.dumps(row, ensure_ascii=False) + "\n")
@@ -422,12 +480,61 @@ def tick(now=None, config=None, transport=None, proactive=None):
422
480
  _bookkeep(store, now_value, state, sent=False, reason="outbox_write_failed")
423
481
  result["reason"] = "outbox_write_failed"
424
482
  return result
425
- data = _bookkeep(store, now_value, state, sent=True, reason="ok", text=text)
426
- result.update({"sent": True, "reason": "ok", "text": text,
427
- "outbox": str(outbox), "sent_today": len(_sent_today(data, now_value))})
483
+ state = dict(state)
484
+ state["awaiting_reply"] = True
485
+ state["no_reply_streak"] = 0
486
+ data = _bookkeep(store, now_value, state, sent=True, reason=reason, text=text)
487
+ data["awaiting_reply"] = True
488
+ data["no_reply_streak"] = 0
489
+ try:
490
+ _write_json(_bookkeeping_path(store), data)
491
+ except Exception:
492
+ pass
493
+ result.update({"sent": True, "reason": reason, "text": text,
494
+ "fallback": fallback, "prompt": prompt,
495
+ "outbox": str(outbox),
496
+ "sent_today": len(_sent_today(data, now_value))})
428
497
  return result
429
498
 
430
499
 
500
+ def _results_path(store):
501
+ return _proactive_dir(store) / "results.jsonl"
502
+
503
+
504
+ def _count_lines(path):
505
+ try:
506
+ if path is None or not Path(path).exists():
507
+ return 0
508
+ return sum(1 for line in Path(path).read_text(encoding="utf-8").splitlines()
509
+ if line.strip())
510
+ except Exception:
511
+ return 0
512
+
513
+
514
+ def summary(store, now=None, enabled=False):
515
+ """Numbers for /state; never generates or sends."""
516
+ stamp = _num(now, time.time())
517
+ state = load_state(store)
518
+ sent_today = len(_sent_today(state, stamp))
519
+ pending = max(0, _count_lines(_outbox_path(store))
520
+ - _count_lines(_results_path(store)))
521
+ cfg = normalize_config({})
522
+ last_attempt = _num(state.get("last_attempt_ts"), 0.0)
523
+ next_at = (last_attempt + cfg["standby_every_minutes"] * 60.0
524
+ if last_attempt > 0 else 0.0)
525
+ return {
526
+ "enabled": bool(enabled),
527
+ "next_at": round(next_at, 3),
528
+ "pending": pending,
529
+ "sent_today": sent_today,
530
+ "last_reason": str(state.get("last_reason") or ""),
531
+ "no_reply_streak": int(_num(state.get("no_reply_streak"), 0)),
532
+ "awaiting_reply": bool(state.get("awaiting_reply")),
533
+ "cooldown_minutes": cfg["standby_cooldown_minutes"],
534
+ "max_per_day": cfg["standby_max_per_day"],
535
+ }
536
+
537
+
431
538
  def sleep_loop(config=None, once=False):
432
539
  cfg = normalize_config(config)
433
540
  interval = max(60, int(cfg["standby_every_minutes"] * 60))