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.
- package/lib/index.js +30 -1
- package/lib/modules/standby.js +61 -0
- package/package.json +1 -1
- package/python/biocore_memory/store/tiering.py +14 -3
- package/python/biocore_model/sanitize.py +6 -1
- package/python/biocore_model/tests/test_cache_series.py +137 -0
- package/python/biocore_model/tests/test_lifecycle.py +153 -0
- package/python/biocore_model/tests/test_retrieval_plus.py +143 -0
- package/python/biocore_model/tests/test_standby.py +199 -0
- package/python/biocore_runtime/__init__.py +3 -0
- package/python/biocore_runtime/cache_stats.py +161 -0
- package/python/biocore_runtime/lifecycle.py +233 -0
- package/python/biocore_runtime/pipeline.py +134 -3
- package/python/biocore_runtime/retrieval_plus.py +352 -0
- package/python/biocore_runtime/standby.py +125 -18
- package/python/data/signals.json +3266 -206
|
@@ -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":
|
|
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.
|
|
62
|
-
"standby_require_open_loop":
|
|
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"),
|
|
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.
|
|
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"),
|
|
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
|
-
|
|
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["
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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":
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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))
|