nexo-brain 7.30.21 → 7.30.23

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.
@@ -11,12 +11,34 @@ import datetime as _dt
11
11
  import hashlib
12
12
  import json
13
13
  import os
14
+ import time as _time
14
15
  from pathlib import Path
15
16
  from typing import Any
16
17
 
17
18
 
18
19
  OPEN_STATES = {"open", "waiting", "verified"}
19
20
  FINAL_STATES = {"closed", "rejected", "stale"}
21
+ STATE_ALIASES = {
22
+ "ready": "open",
23
+ "waiting_user": "waiting",
24
+ "blocked": "waiting",
25
+ "done": "closed",
26
+ }
27
+ READINESS_STATES = {
28
+ "available",
29
+ "missing_tool",
30
+ "missing_credential",
31
+ "needs_user_permission",
32
+ "unsafe",
33
+ "external_blocker",
34
+ "unknown",
35
+ }
36
+ TRIAGE_STATES = OPEN_STATES | FINAL_STATES
37
+
38
+
39
+ def _canonical_state(value: Any) -> str:
40
+ clean = str(value or "").strip().lower()
41
+ return STATE_ALIASES.get(clean, clean)
20
42
 
21
43
 
22
44
  def _now_iso() -> str:
@@ -236,7 +258,7 @@ def _upsert_candidate(conn, item: dict[str, Any]) -> bool:
236
258
 
237
259
 
238
260
  def _record_event(conn, item_id: str, event_type: str, from_state: str, to_state: str, note: str, evidence: str = "") -> None:
239
- event_id = _hash_id("CIE", f"{item_id}:{event_type}:{from_state}:{to_state}:{_now_iso()}:{note}", 24)
261
+ event_id = _hash_id("CIE", f"{item_id}:{event_type}:{from_state}:{to_state}:{_time.time_ns()}:{note}", 24)
240
262
  conn.execute(
241
263
  """
242
264
  INSERT INTO closure_item_events (
@@ -247,6 +269,17 @@ def _record_event(conn, item_id: str, event_type: str, from_state: str, to_state
247
269
  )
248
270
 
249
271
 
272
+ def _readiness_counts(conn) -> dict[str, int]:
273
+ if not _table_exists(conn, "closure_capability_readiness"):
274
+ return {}
275
+ return {
276
+ row["status"]: row["n"]
277
+ for row in conn.execute(
278
+ "SELECT status, COUNT(*) AS n FROM closure_capability_readiness GROUP BY status"
279
+ ).fetchall()
280
+ }
281
+
282
+
250
283
  def _protocol_task_candidates(conn, limit: int) -> list[dict[str, Any]]:
251
284
  if not _table_exists(conn, "protocol_tasks"):
252
285
  return []
@@ -469,6 +502,7 @@ def refresh_closure_items(conn=None, *, limit_per_adapter: int = 250) -> dict[st
469
502
  run_migrations(conn)
470
503
  candidates: list[dict[str, Any]] = []
471
504
  adapter_counts: dict[str, int] = {}
505
+ adapter_errors: dict[str, str] = {}
472
506
  adapters = [
473
507
  ("protocol_tasks", lambda: _protocol_task_candidates(conn, limit_per_adapter)),
474
508
  ("followups", lambda: _followup_candidates(conn, limit_per_adapter)),
@@ -479,8 +513,9 @@ def refresh_closure_items(conn=None, *, limit_per_adapter: int = 250) -> dict[st
479
513
  for name, adapter in adapters:
480
514
  try:
481
515
  produced = adapter()
482
- except Exception:
516
+ except Exception as exc:
483
517
  produced = []
518
+ adapter_errors[name] = f"{type(exc).__name__}: {exc}"
484
519
  adapter_counts[name] = len(produced)
485
520
  candidates.extend(produced)
486
521
 
@@ -493,17 +528,34 @@ def refresh_closure_items(conn=None, *, limit_per_adapter: int = 250) -> dict[st
493
528
  return {
494
529
  "ok": True,
495
530
  "adapters": adapter_counts,
531
+ "adapter_errors": adapter_errors,
496
532
  "observed": len(candidates),
497
533
  "created": created,
498
534
  }
499
535
 
500
536
 
501
- def closure_next(conn=None, *, limit: int = 10, include_waiting: bool = False, source: str = "", kind: str = "") -> list[dict[str, Any]]:
537
+ def closure_next(
538
+ conn=None,
539
+ *,
540
+ limit: int = 10,
541
+ include_waiting: bool = False,
542
+ source: str = "",
543
+ kind: str = "",
544
+ state: str = "",
545
+ max_risk: float | None = None,
546
+ area: str = "",
547
+ ) -> list[dict[str, Any]]:
502
548
  if conn is None:
503
549
  from db import get_db
504
550
 
505
551
  conn = get_db()
506
- states = ("open", "verified", "waiting") if include_waiting else ("open", "verified")
552
+ clean_state = _canonical_state(state)
553
+ if clean_state:
554
+ if clean_state not in OPEN_STATES | FINAL_STATES:
555
+ return []
556
+ states = (clean_state,)
557
+ else:
558
+ states = ("open", "verified", "waiting") if include_waiting else ("open", "verified")
507
559
  clauses = [f"state IN ({','.join('?' for _ in states)})"]
508
560
  params: list[Any] = list(states)
509
561
  if source:
@@ -512,6 +564,24 @@ def closure_next(conn=None, *, limit: int = 10, include_waiting: bool = False, s
512
564
  if kind:
513
565
  clauses.append("kind = ?")
514
566
  params.append(kind)
567
+ if max_risk is not None:
568
+ try:
569
+ risk_limit = float(max_risk)
570
+ except Exception:
571
+ risk_limit = 0.0
572
+ if risk_limit > 0:
573
+ clauses.append("risk_score <= ?")
574
+ params.append(max(0.0, min(risk_limit, 1.0)))
575
+ clean_area = str(area or "").strip()
576
+ if clean_area:
577
+ like_area = f"%{clean_area}%"
578
+ clauses.append(
579
+ "("
580
+ "source_primary = ? OR kind = ? OR owner = ? OR capability_required = ? "
581
+ "OR title LIKE ? OR summary LIKE ? OR source_payload_json LIKE ?"
582
+ ")"
583
+ )
584
+ params.extend([clean_area, clean_area, clean_area, clean_area, like_area, like_area, like_area])
515
585
  params.append(max(1, min(int(limit or 10), 100)))
516
586
  rows = conn.execute(
517
587
  f"""
@@ -547,6 +617,7 @@ def closure_status(conn=None, *, refresh: bool = True, limit: int = 10) -> dict[
547
617
  "schema": "nexo.closure.status.v1",
548
618
  "refreshed": refresh_result,
549
619
  "counts": counts,
620
+ "capability_readiness": _readiness_counts(conn),
550
621
  "open_total": sum(int(counts.get(state, 0)) for state in OPEN_STATES),
551
622
  "by_kind": by_kind,
552
623
  "next": closure_next(conn, limit=limit, include_waiting=True),
@@ -576,8 +647,13 @@ def closure_item_get(item_id: str, conn=None) -> dict[str, Any] | None:
576
647
  "SELECT * FROM closure_item_events WHERE closure_item_id = ? ORDER BY created_at DESC LIMIT 50",
577
648
  (payload["id"],),
578
649
  ).fetchall()
650
+ links = conn.execute(
651
+ "SELECT * FROM closure_item_links WHERE closure_item_id = ? ORDER BY created_at DESC, link_type, link_id",
652
+ (payload["id"],),
653
+ ).fetchall() if _table_exists(conn, "closure_item_links") else []
579
654
  payload["sources"] = [_as_dict(row) for row in sources]
580
655
  payload["events"] = [_as_dict(row) for row in events]
656
+ payload["links"] = [_as_dict(row) for row in links]
581
657
  return payload
582
658
 
583
659
 
@@ -641,12 +717,238 @@ def closure_close_item(item_id: str, *, reason: str = "completed", conn=None) ->
641
717
  return {"ok": True, "id": item["id"], "state": final_state}
642
718
 
643
719
 
644
- def _write_daily_snapshot(conn) -> None:
720
+ def closure_link_item(
721
+ item_id: str,
722
+ *,
723
+ link_type: str,
724
+ link_id: str,
725
+ relation: str = "related",
726
+ conn=None,
727
+ ) -> dict[str, Any]:
728
+ if conn is None:
729
+ from db import get_db
730
+
731
+ conn = get_db()
732
+ item = closure_item_get(item_id, conn)
733
+ if not item:
734
+ return {"ok": False, "error": "closure item not found"}
735
+ clean_type = str(link_type or "").strip()
736
+ clean_id = str(link_id or "").strip()
737
+ clean_relation = str(relation or "related").strip() or "related"
738
+ if not clean_type or not clean_id:
739
+ return {"ok": False, "error": "link_type and link_id are required"}
740
+ now = _now_iso()
741
+ link_pk = _hash_id("CIL", f"{item['id']}:{clean_type}:{clean_id}:{clean_relation}", 24)
742
+ conn.execute(
743
+ """
744
+ INSERT INTO closure_item_links (
745
+ id, closure_item_id, link_type, link_id, relation, created_at
746
+ ) VALUES (?, ?, ?, ?, ?, ?)
747
+ ON CONFLICT(closure_item_id, link_type, link_id, relation) DO UPDATE SET
748
+ created_at = excluded.created_at
749
+ """,
750
+ (link_pk, item["id"], clean_type, clean_id, clean_relation, now),
751
+ )
752
+ _record_event(
753
+ conn,
754
+ item["id"],
755
+ "linked",
756
+ item["state"],
757
+ item["state"],
758
+ f"Linked {clean_type}:{clean_id} as {clean_relation}.",
759
+ )
760
+ conn.commit()
761
+ return {
762
+ "ok": True,
763
+ "id": link_pk,
764
+ "closure_item_id": item["id"],
765
+ "link_type": clean_type,
766
+ "link_id": clean_id,
767
+ "relation": clean_relation,
768
+ }
769
+
770
+
771
+ def closure_set_capability_readiness(
772
+ capability: str,
773
+ *,
774
+ status: str = "unknown",
775
+ reason: str = "",
776
+ evidence: str = "",
777
+ expires_at: str = "",
778
+ conn=None,
779
+ ) -> dict[str, Any]:
780
+ if conn is None:
781
+ from db import get_db
782
+
783
+ conn = get_db()
784
+ clean_capability = str(capability or "").strip()
785
+ clean_status = str(status or "unknown").strip()
786
+ if not clean_capability:
787
+ return {"ok": False, "error": "capability is required"}
788
+ if clean_status not in READINESS_STATES:
789
+ return {"ok": False, "error": f"invalid readiness status: {clean_status}"}
790
+ now = _now_iso()
791
+ row_id = _hash_id("CCR", clean_capability, 20)
792
+ conn.execute(
793
+ """
794
+ INSERT INTO closure_capability_readiness (
795
+ id, capability, status, reason, verified_at, verification_evidence, expires_at
796
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
797
+ ON CONFLICT(capability) DO UPDATE SET
798
+ status = excluded.status,
799
+ reason = excluded.reason,
800
+ verified_at = excluded.verified_at,
801
+ verification_evidence = excluded.verification_evidence,
802
+ expires_at = excluded.expires_at
803
+ """,
804
+ (row_id, clean_capability, clean_status, str(reason or ""), now, str(evidence or ""), str(expires_at or "")),
805
+ )
806
+ conn.commit()
807
+ return {"ok": True, "id": row_id, "capability": clean_capability, "status": clean_status}
808
+
809
+
810
+ def closure_triage_item(
811
+ item_id: str,
812
+ *,
813
+ state: str = "",
814
+ kind: str = "",
815
+ blocker_reason: str = "",
816
+ next_action: str = "",
817
+ evidence_required: str = "",
818
+ owner: str = "",
819
+ capability_required: str = "",
820
+ capability_status: str = "",
821
+ duplicate_of: str = "",
822
+ conn=None,
823
+ ) -> dict[str, Any]:
824
+ if conn is None:
825
+ from db import get_db
826
+
827
+ conn = get_db()
828
+ item = closure_item_get(item_id, conn)
829
+ if not item:
830
+ return {"ok": False, "error": "closure item not found"}
831
+ updates: dict[str, Any] = {}
832
+ requested_state = str(state or "").strip()
833
+ clean_state = _canonical_state(requested_state)
834
+ if clean_state:
835
+ if clean_state not in TRIAGE_STATES:
836
+ return {"ok": False, "error": f"invalid state: {requested_state}"}
837
+ updates["state"] = clean_state
838
+ for column, value in (
839
+ ("kind", kind),
840
+ ("blocker_reason", blocker_reason),
841
+ ("next_action", next_action),
842
+ ("evidence_required", evidence_required),
843
+ ("owner", owner),
844
+ ("capability_required", capability_required),
845
+ ("capability_status", capability_status),
846
+ ):
847
+ clean_value = str(value or "").strip()
848
+ if clean_value:
849
+ updates[column] = clean_value
850
+ clean_duplicate = str(duplicate_of or "").strip()
851
+ if clean_duplicate:
852
+ target = closure_item_get(clean_duplicate, conn)
853
+ if not target:
854
+ return {"ok": False, "error": "duplicate target not found"}
855
+ closure_link_item(item["id"], link_type="closure_item", link_id=target["id"], relation="duplicate_of", conn=conn)
856
+ updates["state"] = "stale"
857
+ updates["close_reason"] = f"duplicate_of:{target['id']}"
858
+ updates["closed_at"] = _now_iso()
859
+ if not updates:
860
+ return {"ok": False, "error": "no triage changes supplied"}
861
+ now = _now_iso()
862
+ updates["updated_at"] = now
863
+ if set(updates) - {"updated_at"}:
864
+ updates["last_progress_at"] = now
865
+ assignments = ", ".join(f"{column} = ?" for column in updates)
866
+ params = list(updates.values()) + [item["id"]]
867
+ conn.execute(f"UPDATE closure_items SET {assignments} WHERE id = ?", params)
868
+ to_state = updates.get("state", item["state"])
869
+ _record_event(
870
+ conn,
871
+ item["id"],
872
+ "triaged",
873
+ item["state"],
874
+ to_state,
875
+ "Closure item triaged: " + ", ".join(sorted(column for column in updates if column != "updated_at")),
876
+ )
877
+ conn.commit()
878
+ result = {"ok": True, "id": item["id"], "state": to_state, "updated": sorted(updates)}
879
+ if requested_state and requested_state != clean_state:
880
+ result["requested_state"] = requested_state
881
+ return result
882
+
883
+
884
+ def closure_snapshot(conn=None, *, refresh: bool = True, snapshot_date: str = "", limit: int = 10) -> dict[str, Any]:
885
+ if conn is None:
886
+ from db import get_db
887
+ from db._schema import run_migrations
888
+
889
+ conn = get_db()
890
+ run_migrations(conn)
891
+ if refresh:
892
+ refresh_closure_items(conn)
893
+ date_key = str(snapshot_date or _today()).strip()[:10]
894
+ if date_key != _today():
895
+ _write_daily_snapshot_for_date(conn, snapshot_date=date_key, limit=limit)
896
+ else:
897
+ _write_daily_snapshot(conn, limit=limit)
898
+ row = conn.execute(
899
+ "SELECT * FROM closure_daily_snapshots WHERE snapshot_date = ?",
900
+ (date_key,),
901
+ ).fetchone()
902
+ payload = _as_dict(row) if row else {}
903
+ if payload.get("top_items_json"):
904
+ try:
905
+ payload["top_items"] = json.loads(payload["top_items_json"])
906
+ except Exception:
907
+ payload["top_items"] = []
908
+ payload["capability_readiness"] = _readiness_counts(conn)
909
+ return {"ok": bool(payload), "snapshot": payload}
910
+
911
+
912
+ def _write_daily_snapshot_for_date(conn, *, snapshot_date: str, limit: int = 10) -> None:
913
+ counts = {
914
+ row["state"]: row["n"]
915
+ for row in conn.execute("SELECT state, COUNT(*) AS n FROM closure_items GROUP BY state").fetchall()
916
+ }
917
+ top = closure_next(conn, limit=limit, include_waiting=True)
918
+ conn.execute(
919
+ """
920
+ INSERT OR REPLACE INTO closure_daily_snapshots (
921
+ snapshot_date, total_open, total_verified, total_waiting, total_closed,
922
+ top_items_json, created_at
923
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
924
+ """,
925
+ (
926
+ snapshot_date,
927
+ int(counts.get("open", 0)),
928
+ int(counts.get("verified", 0)),
929
+ int(counts.get("waiting", 0)),
930
+ int(counts.get("closed", 0)),
931
+ _safe_json([
932
+ {
933
+ "id": item.get("id"),
934
+ "title": item.get("title"),
935
+ "priority_score": item.get("priority_score"),
936
+ "state": item.get("state"),
937
+ }
938
+ for item in top
939
+ ]),
940
+ _now_iso(),
941
+ ),
942
+ )
943
+ conn.commit()
944
+
945
+
946
+ def _write_daily_snapshot(conn, *, limit: int = 10) -> None:
645
947
  counts = {
646
948
  row["state"]: row["n"]
647
949
  for row in conn.execute("SELECT state, COUNT(*) AS n FROM closure_items GROUP BY state").fetchall()
648
950
  }
649
- top = closure_next(conn, limit=10, include_waiting=True)
951
+ top = closure_next(conn, limit=limit, include_waiting=True)
650
952
  conn.execute(
651
953
  """
652
954
  INSERT OR REPLACE INTO closure_daily_snapshots (
@@ -679,7 +981,15 @@ def handle_closure_status(refresh: bool = True, limit: int = 10) -> str:
679
981
  return json.dumps(closure_status(refresh=refresh, limit=limit), indent=2, ensure_ascii=False)
680
982
 
681
983
 
682
- def handle_closure_next(limit: int = 10, include_waiting: bool = False, source: str = "", kind: str = "") -> str:
984
+ def handle_closure_next(
985
+ limit: int = 10,
986
+ include_waiting: bool = False,
987
+ source: str = "",
988
+ kind: str = "",
989
+ state: str = "",
990
+ max_risk: float | None = None,
991
+ area: str = "",
992
+ ) -> str:
683
993
  from db import get_db
684
994
  from db._schema import run_migrations
685
995
 
@@ -688,7 +998,16 @@ def handle_closure_next(limit: int = 10, include_waiting: bool = False, source:
688
998
  refresh_closure_items(conn)
689
999
  return json.dumps({
690
1000
  "ok": True,
691
- "items": closure_next(conn, limit=limit, include_waiting=include_waiting, source=source, kind=kind),
1001
+ "items": closure_next(
1002
+ conn,
1003
+ limit=limit,
1004
+ include_waiting=include_waiting,
1005
+ source=source,
1006
+ kind=kind,
1007
+ state=state,
1008
+ max_risk=max_risk,
1009
+ area=area,
1010
+ ),
692
1011
  }, indent=2, ensure_ascii=False)
693
1012
 
694
1013
 
@@ -702,6 +1021,68 @@ def handle_closure_item_get(item_id: str) -> str:
702
1021
  return json.dumps({"ok": bool(item), "item": item}, indent=2, ensure_ascii=False)
703
1022
 
704
1023
 
1024
+ def handle_closure_triage(
1025
+ item_id: str,
1026
+ state: str = "",
1027
+ kind: str = "",
1028
+ blocker_reason: str = "",
1029
+ next_action: str = "",
1030
+ evidence_required: str = "",
1031
+ owner: str = "",
1032
+ capability_required: str = "",
1033
+ capability_status: str = "",
1034
+ duplicate_of: str = "",
1035
+ ) -> str:
1036
+ from db import get_db
1037
+ from db._schema import run_migrations
1038
+
1039
+ conn = get_db()
1040
+ run_migrations(conn)
1041
+ return json.dumps(
1042
+ closure_triage_item(
1043
+ item_id,
1044
+ state=state,
1045
+ kind=kind,
1046
+ blocker_reason=blocker_reason,
1047
+ next_action=next_action,
1048
+ evidence_required=evidence_required,
1049
+ owner=owner,
1050
+ capability_required=capability_required,
1051
+ capability_status=capability_status,
1052
+ duplicate_of=duplicate_of,
1053
+ conn=conn,
1054
+ ),
1055
+ indent=2,
1056
+ ensure_ascii=False,
1057
+ )
1058
+
1059
+
1060
+ def handle_closure_link(item_id: str, link_type: str, link_id: str, relation: str = "related") -> str:
1061
+ from db import get_db
1062
+ from db._schema import run_migrations
1063
+
1064
+ conn = get_db()
1065
+ run_migrations(conn)
1066
+ return json.dumps(
1067
+ closure_link_item(item_id, link_type=link_type, link_id=link_id, relation=relation, conn=conn),
1068
+ indent=2,
1069
+ ensure_ascii=False,
1070
+ )
1071
+
1072
+
1073
+ def handle_closure_snapshot(refresh: bool = True, snapshot_date: str = "", limit: int = 10) -> str:
1074
+ from db import get_db
1075
+ from db._schema import run_migrations
1076
+
1077
+ conn = get_db()
1078
+ run_migrations(conn)
1079
+ return json.dumps(
1080
+ closure_snapshot(conn, refresh=refresh, snapshot_date=snapshot_date, limit=limit),
1081
+ indent=2,
1082
+ ensure_ascii=False,
1083
+ )
1084
+
1085
+
705
1086
  def handle_closure_verify(item_id: str, evidence: str) -> str:
706
1087
  from db import get_db
707
1088
  from db._schema import run_migrations