amfs-adapter-postgres 0.2.34__tar.gz → 0.2.40__tar.gz

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.
Files changed (29) hide show
  1. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/PKG-INFO +2 -2
  2. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/pyproject.toml +2 -2
  3. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/adapter.py +180 -9
  4. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/async_adapter.py +7 -2
  5. amfs_adapter_postgres-0.2.40/src/amfs_postgres/evidence_sql.py +468 -0
  6. amfs_adapter_postgres-0.2.40/src/amfs_postgres/migrations/012_evidence_from_model.sql +423 -0
  7. amfs_adapter_postgres-0.2.40/src/amfs_postgres/migrations/013_outcome_verified_by_backfill.sql +105 -0
  8. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/schema.sql +149 -17
  9. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/.gitignore +0 -0
  10. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/__init__.py +0 -0
  11. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/_fts.py +0 -0
  12. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/knn.py +0 -0
  13. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/001_initial.sql +0 -0
  14. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
  15. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
  16. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
  17. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
  18. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
  19. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
  20. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
  21. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
  22. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
  23. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
  24. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
  25. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
  26. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/retention.py +0 -0
  27. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/tenant_context.py +0 -0
  28. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/tenant_gucs.py +0 -0
  29. {amfs_adapter_postgres-0.2.34 → amfs_adapter_postgres-0.2.40}/src/amfs_postgres/trace_partitions.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: amfs-adapter-postgres
3
- Version: 0.2.34
3
+ Version: 0.2.40
4
4
  Summary: AMFS Postgres adapter with back-propagation triggers
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
7
- Requires-Dist: amfs-core>=0.3.44
7
+ Requires-Dist: amfs-core>=0.3.52
8
8
  Requires-Dist: psycopg-pool>=3.1
9
9
  Requires-Dist: psycopg[binary]<4,>=3.1
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "amfs-adapter-postgres"
3
- version = "0.2.34"
3
+ version = "0.2.40"
4
4
  description = "AMFS Postgres adapter with back-propagation triggers"
5
5
  requires-python = ">=3.11"
6
6
  license = "Apache-2.0"
7
7
  dependencies = [
8
8
  # 0.3.14 adds the capture fields this adapter now persists and selects back,
9
9
  # tool_calls among them.
10
- "amfs-core>=0.3.44",
10
+ "amfs-core>=0.3.52",
11
11
  "psycopg[binary]>=3.1,<4",
12
12
  "psycopg-pool>=3.1",
13
13
  ]
@@ -34,8 +34,8 @@ from amfs_core.exclusions import (
34
34
  AGENT_ID_NOT_EXCLUDED_SQL,
35
35
  ENTITY_PATH_NOT_EXCLUDED_SQL,
36
36
  )
37
- from amfs_core.actions import recorded_environment
38
- from amfs_core.evidence import cited_entries, outcome_model
37
+ from amfs_core.actions import entity_paths_of, recorded_environment
38
+ from amfs_core.evidence import cited_entries, outcome_model, outcome_read_keys, outcome_verified_by
39
39
  from amfs_core.models import (
40
40
  OUTCOME_MULTIPLIERS,
41
41
  Agent,
@@ -274,6 +274,20 @@ _LOCAL_EVIDENCE_SQL = (
274
274
  _OUTCOME_TASK_TEXT_SQL = (
275
275
  Path(__file__).parent / "migrations" / "011_outcome_task_text.sql"
276
276
  ).read_text(encoding="utf-8")
277
+ #: Migration 012 — the evidence functions rendered from ``amfs_core.evidence``
278
+ #: (``evidence_sql.render_evidence_sql``), replacing the hand-written bodies
279
+ #: of 008/009 with the same arithmetic read off the Python model. Applied
280
+ #: last so its CREATE OR REPLACE is what stands; folded into the fingerprint
281
+ #: so a regenerated file re-applies.
282
+ _EVIDENCE_FROM_MODEL_SQL = (
283
+ Path(__file__).parent / "migrations" / "012_evidence_from_model.sql"
284
+ ).read_text(encoding="utf-8")
285
+ #: Migration 013 — ``verified_by`` copied from the sealed trace onto the
286
+ #: outcomes committed before 012 gave them the column. One pass, recorded in
287
+ #: ``amfs_backfills``; every later application returns at the first SELECT.
288
+ _VERIFIED_BY_BACKFILL_SQL = (
289
+ Path(__file__).parent / "migrations" / "013_outcome_verified_by_backfill.sql"
290
+ ).read_text(encoding="utf-8")
277
291
  #: Characters of ``task_input`` kept on the outcome row. Read for word
278
292
  #: statistics — which of a query's words recur across this entity's tasks —
279
293
  #: so the head of the request is all that is needed, and a pasted log or
@@ -620,6 +634,11 @@ _EVIDENCE_COLUMNS: tuple[str, ...] = (
620
634
  "success_count", "failure_count", "evidence_success", "evidence_failure",
621
635
  "last_outcome", "last_outcome_at", "discredited_at", "prior_confidence",
622
636
  )
637
+ #: Credit by uplift (migration 012): maintained by the trigger for the keys
638
+ #: an outcome cites, read by ``amfs_core.evidence.uplift_credit``. Not
639
+ #: persisted on write — a rewritten claim starts from the uniform split and
640
+ #: earns its own measurement the next time a session cites it.
641
+ _UPLIFT_COLUMNS: tuple[str, ...] = ("uplift", "uplift_sessions")
623
642
  _EVIDENCE_SELECT = ", outcome_count, " + ", ".join(_EVIDENCE_COLUMNS)
624
643
 
625
644
  #: The columns ``_row_to_entry`` reads, named so that entry-returning SELECTs
@@ -630,8 +649,8 @@ _EVIDENCE_SELECT = ", outcome_count, " + ", ".join(_EVIDENCE_COLUMNS)
630
649
  #: unscoped ``list()`` over a 20k-row namespace moved ~80 MB per call, and a
631
650
  #: dashboard page that fans out to several such calls took 20-35 s while the
632
651
  #: database sat at 1% CPU (2026-09-17). Every column here is created by
633
- #: schema.sql or ensured by ``_apply_migrations`` (including migration 008),
634
- #: so the projection is safe on any bootstrapped database.
652
+ #: schema.sql or ensured by ``_apply_migrations`` (including migrations 008
653
+ #: and 012), so the projection is safe on any bootstrapped database.
635
654
  _ENTRY_COLUMNS: tuple[str, ...] = (
636
655
  "id",
637
656
  "namespace",
@@ -658,6 +677,7 @@ _ENTRY_COLUMNS: tuple[str, ...] = (
658
677
  "branch",
659
678
  "is_artifact",
660
679
  *_EVIDENCE_COLUMNS,
680
+ *_UPLIFT_COLUMNS,
661
681
  )
662
682
  ENTRY_SELECT = ", ".join(_ENTRY_COLUMNS)
663
683
  #: For the window before ``_apply_migrations`` adds is_artifact; the decoder
@@ -804,6 +824,7 @@ class PostgresAdapter(AdapterABC):
804
824
  self._has_outcome_embedding_col = False
805
825
  self._has_outcome_env_col = False
806
826
  self._has_task_text_col = False
827
+ self._has_verified_by_col = False
807
828
  self._pgvector_version: tuple[int, ...] | None = None
808
829
  # When an embedder is provided, embeddings are computed at write time and
809
830
  # persisted, so ANN retrieval (semantic_search / pgvector HNSW) works
@@ -949,7 +970,8 @@ class PostgresAdapter(AdapterABC):
949
970
  return hashlib.sha256(
950
971
  (
951
972
  _SCHEMA_SQL + migrations + _OUTCOME_EVIDENCE_SQL + _ACTION_PRIORS_SQL
952
- + _LOCAL_EVIDENCE_SQL + _OUTCOME_TASK_TEXT_SQL
973
+ + _LOCAL_EVIDENCE_SQL + _OUTCOME_TASK_TEXT_SQL + _EVIDENCE_FROM_MODEL_SQL
974
+ + _VERIFIED_BY_BACKFILL_SQL
953
975
  ).encode("utf-8")
954
976
  ).hexdigest()
955
977
 
@@ -1317,6 +1339,12 @@ class PostgresAdapter(AdapterABC):
1317
1339
  cur.execute(_LOCAL_EVIDENCE_SQL)
1318
1340
  # The task text on outcomes: see migration 011.
1319
1341
  cur.execute(_OUTCOME_TASK_TEXT_SQL)
1342
+ # The evidence functions from the Python model: see migration 012.
1343
+ cur.execute(_EVIDENCE_FROM_MODEL_SQL)
1344
+ # verified_by onto the outcomes that predate the column: see 013.
1345
+ # After 012 (the column) and after the traces gained session_metadata
1346
+ # above, which is where the value is copied from.
1347
+ cur.execute(_VERIFIED_BY_BACKFILL_SQL)
1320
1348
  # task_embedding on outcomes follows the entries' embedding dimension,
1321
1349
  # so it is derived here rather than declared in the migration.
1322
1350
  cur.execute(
@@ -1776,7 +1804,8 @@ class PostgresAdapter(AdapterABC):
1776
1804
  SELECT column_name FROM information_schema.columns
1777
1805
  WHERE table_name = 'amfs_outcomes'
1778
1806
  AND column_name IN (
1779
- 'actions_taken', 'task_embedding', 'environment', 'task_text'
1807
+ 'actions_taken', 'task_embedding', 'environment', 'task_text',
1808
+ 'verified_by'
1780
1809
  )
1781
1810
  """,
1782
1811
  )
@@ -1785,6 +1814,7 @@ class PostgresAdapter(AdapterABC):
1785
1814
  self._has_outcome_embedding_col = "task_embedding" in found
1786
1815
  self._has_outcome_env_col = "environment" in found
1787
1816
  self._has_task_text_col = "task_text" in found
1817
+ self._has_verified_by_col = "verified_by" in found
1788
1818
 
1789
1819
  # ------------------------------------------------------------------
1790
1820
  # read
@@ -2659,8 +2689,13 @@ class PostgresAdapter(AdapterABC):
2659
2689
  params: list[Any] = [self._namespace]
2660
2690
 
2661
2691
  if query.entity_path is not None:
2662
- conditions.append("entity_path = %s")
2663
- params.append(query.entity_path)
2692
+ if getattr(query, "include_descendants", False):
2693
+ clause, clause_params = descendants_sql("entity_path", query.entity_path)
2694
+ conditions.append(clause)
2695
+ params.extend(clause_params)
2696
+ else:
2697
+ conditions.append("entity_path = %s")
2698
+ params.append(query.entity_path)
2664
2699
  else:
2665
2700
  conditions.append(_EXCLUDE_SHARED_PATHS)
2666
2701
  if query.min_confidence > 0:
@@ -2840,6 +2875,7 @@ class PostgresAdapter(AdapterABC):
2840
2875
  limit: int = 10,
2841
2876
  visible_agents: Collection[str] | None = None,
2842
2877
  agent: str | None = None,
2878
+ writer: str | None = None,
2843
2879
  ) -> dict[str, Any]:
2844
2880
  """What memory has been reused since a point in time, and by whom.
2845
2881
 
@@ -2871,6 +2907,13 @@ class PostgresAdapter(AdapterABC):
2871
2907
  would be filtering a list already cut to ``limit`` by reuse volume, so a
2872
2908
  quiet agent's handoffs would be missing — and indistinguishable from that
2873
2909
  agent having none.
2910
+
2911
+ ``writer`` is the same narrowing from the other side: the window as seen
2912
+ by one agent AS WRITER — the reads of memory it wrote. ``cross_surface``
2913
+ is then exactly how many times another agent took its memory, and
2914
+ ``by_agent`` names who. It composes with ``agent`` and with the
2915
+ visibility scope the same way, and an agent the caller may not see
2916
+ answers nothing.
2874
2917
  """
2875
2918
  empty: dict[str, Any] = {
2876
2919
  "reuses": 0,
@@ -2896,6 +2939,11 @@ class PostgresAdapter(AdapterABC):
2896
2939
  return empty
2897
2940
  scope += " AND reused_by = %s"
2898
2941
  scope_params = (*scope_params, agent)
2942
+ if writer:
2943
+ if visible_agents is not None and writer not in visible_agents:
2944
+ return empty
2945
+ scope += " AND written_by = %s"
2946
+ scope_params = (*scope_params, writer)
2899
2947
 
2900
2948
  try:
2901
2949
  # Every column is aliased and read by name: the pool's row factory is
@@ -3889,13 +3937,29 @@ class PostgresAdapter(AdapterABC):
3889
3937
  json.dumps(dict(record.causal_entry_versions or {})),
3890
3938
  ]
3891
3939
  if self._has_action_cols:
3940
+ # The entities this outcome is about. The SDK derives them
3941
+ # from the cited keys; a record built without them (an
3942
+ # older client, a direct adapter caller) gets the same
3943
+ # derivation here, because the trigger's uplift refresh
3944
+ # (migration 012) finds an entity's sessions by this column.
3945
+ entity_paths = list(record.entity_paths or []) or entity_paths_of(
3946
+ sorted(outcome_read_keys(record))
3947
+ )
3892
3948
  columns += ["actions_taken", "entity_paths", "situation"]
3893
3949
  placeholders += ["%s::jsonb", "%s", "%s"]
3894
3950
  params += [
3895
3951
  json.dumps(list(record.actions_taken or []), default=str),
3896
- list(record.entity_paths or []),
3952
+ entity_paths,
3897
3953
  record.situation,
3898
3954
  ]
3955
+ # Who decided the outcome when it was not the agent (CI, a
3956
+ # monitor, a reviewer); the trigger weighs a verified outcome
3957
+ # by VERIFIED_EVIDENCE_WEIGHT (migration 012).
3958
+ verified_by = outcome_verified_by(record)
3959
+ if self._has_verified_by_col and verified_by:
3960
+ columns.append("verified_by")
3961
+ placeholders.append("%s")
3962
+ params.append(verified_by)
3899
3963
  if getattr(self, "_has_outcome_env_col", False):
3900
3964
  env = recorded_environment({"session_metadata": record.session_metadata or {}})
3901
3965
  if env:
@@ -3972,6 +4036,111 @@ class PostgresAdapter(AdapterABC):
3972
4036
  row = cur.fetchone()
3973
4037
  return int(row["cnt"]) if row else 0
3974
4038
 
4039
+ def learning_series(
4040
+ self,
4041
+ *,
4042
+ since: datetime,
4043
+ agent: str | None = None,
4044
+ entity_path: str | None = None,
4045
+ limit: int = 5000,
4046
+ visible_agents: Collection[str] | None = None,
4047
+ ) -> dict[str, Any]:
4048
+ """The rows ``amfs_core.learning.learning_curve`` reads: the newest
4049
+ ``limit`` outcomes in the window, returned oldest first, and the regime
4050
+ shifts among the entries they cited.
4051
+
4052
+ ``sessions`` carries ``(at, outcome_type, agent_id, verified_by)`` per
4053
+ outcome. When the window holds more than ``limit`` outcomes the series
4054
+ keeps the most recent ones — the curve is about where the agent is
4055
+ now — and says so with ``truncated``; the shifts are then bounded by
4056
+ the oldest session kept, so every shift reported falls inside the
4057
+ sessions returned. ``shifts`` is one row per moment a cited entry was
4058
+ discredited — distinct ``(entity_path, key, discredited_at)`` across
4059
+ the entry's versions, since the step copies the timestamp forward
4060
+ until evidence lifts it. "Cited" counts the terminal causal keys and
4061
+ the failed attempts' keys alike, as the uplift refresh does.
4062
+ ``visible_agents`` scopes everything to the agents the caller may see,
4063
+ and ``agent`` narrows on top of it, never instead of it; an agent
4064
+ outside the scope answers an empty series.
4065
+ """
4066
+ empty: dict[str, Any] = {"sessions": [], "shifts": [], "truncated": False}
4067
+ if visible_agents is not None and not visible_agents:
4068
+ return empty
4069
+ conditions = ["namespace = %s", "committed_at >= %s"]
4070
+ params: list[Any] = [self._namespace, since]
4071
+ if visible_agents is not None:
4072
+ conditions.append("agent_id = ANY(%s)")
4073
+ params.append(list(visible_agents))
4074
+ if agent:
4075
+ if visible_agents is not None and agent not in visible_agents:
4076
+ return empty
4077
+ conditions.append("agent_id = %s")
4078
+ params.append(agent)
4079
+ if entity_path:
4080
+ if self._has_action_cols:
4081
+ conditions.append("entity_paths @> ARRAY[%s]::text[]")
4082
+ params.append(entity_path)
4083
+ else:
4084
+ conditions.append(
4085
+ "EXISTS (SELECT 1 FROM unnest(causal_entry_keys) AS ek WHERE ek LIKE %s)"
4086
+ )
4087
+ params.append(f"{entity_path}/%")
4088
+ where = " AND ".join(conditions)
4089
+ verified = "verified_by" if self._has_verified_by_col else "NULL::text AS verified_by"
4090
+ cap = max(1, int(limit))
4091
+ # Newest first under the cap, so a busy account's series ends at its
4092
+ # current sessions rather than stopping at the start of the window;
4093
+ # reversed below so the curve reads oldest to newest.
4094
+ newest = f"""SELECT id, committed_at, outcome_type, agent_id, causal_entry_keys, attempts
4095
+ FROM amfs_outcomes
4096
+ WHERE {where}
4097
+ ORDER BY committed_at DESC, id DESC
4098
+ LIMIT %s"""
4099
+ with self._pool.connection() as conn, conn.cursor() as cur:
4100
+ # One row past the cap tells "exactly the cap" from "more than
4101
+ # it"; a window holding exactly ``limit`` outcomes is complete.
4102
+ cur.execute(
4103
+ f"""WITH win AS ({newest})
4104
+ SELECT committed_at AS at, outcome_type, agent_id, {verified}
4105
+ FROM amfs_outcomes
4106
+ WHERE id IN (SELECT id FROM win)
4107
+ ORDER BY committed_at ASC, id ASC""",
4108
+ (*params, cap + 1),
4109
+ )
4110
+ sessions = [dict(row) for row in cur.fetchall()]
4111
+ if not sessions:
4112
+ return empty
4113
+ truncated = len(sessions) > cap
4114
+ if truncated:
4115
+ del sessions[0]
4116
+ # A shift before the oldest session kept has no "before" in this
4117
+ # series; bound the shifts by what the sessions actually cover.
4118
+ shift_since = max(since, sessions[0]["at"]) if truncated else since
4119
+ shift_conditions = ["e.namespace = %s", "e.discredited_at IS NOT NULL", "e.discredited_at >= %s"]
4120
+ shift_params: list[Any] = [self._namespace, shift_since]
4121
+ if entity_path:
4122
+ shift_conditions.append("e.entity_path = %s")
4123
+ shift_params.append(entity_path)
4124
+ cur.execute(
4125
+ f"""WITH win AS ({newest}),
4126
+ cited AS (
4127
+ SELECT DISTINCT x AS spec
4128
+ FROM win o, unnest(COALESCE(o.causal_entry_keys, '{{}}')) AS x
4129
+ UNION
4130
+ SELECT DISTINCT jsonb_array_elements_text(
4131
+ COALESCE(a->'causal_entry_keys', '[]'::jsonb))
4132
+ FROM win o, jsonb_array_elements(COALESCE(o.attempts, '[]'::jsonb)) AS a
4133
+ )
4134
+ SELECT DISTINCT e.entity_path, e.key, e.discredited_at AS at
4135
+ FROM amfs_memory_entries e
4136
+ JOIN cited c ON c.spec = e.entity_path || '/' || e.key
4137
+ WHERE {" AND ".join(shift_conditions)}
4138
+ ORDER BY e.discredited_at ASC""",
4139
+ (*params, cap, *shift_params),
4140
+ )
4141
+ shifts = [dict(row) for row in cur.fetchall()]
4142
+ return {"sessions": sessions, "shifts": shifts, "truncated": truncated}
4143
+
3975
4144
  def list_outcomes(
3976
4145
  self,
3977
4146
  *,
@@ -4935,6 +5104,8 @@ class PostgresAdapter(AdapterABC):
4935
5104
  last_outcome_at=row.get("last_outcome_at"),
4936
5105
  discredited_at=row.get("discredited_at"),
4937
5106
  validators=_validators_of(row),
5107
+ uplift=float(row["uplift"]) if row.get("uplift") is not None else None,
5108
+ uplift_sessions=int(row.get("uplift_sessions") or 0),
4938
5109
  recall_count=row.get("recall_count", 0),
4939
5110
  priority_score=float(row["priority_score"]) if row.get("priority_score") is not None else None,
4940
5111
  tier=row.get("tier", 3),
@@ -716,8 +716,13 @@ class AsyncPostgresAdapter:
716
716
  ]
717
717
  params: list[Any] = [self._namespace]
718
718
  if query.entity_path is not None:
719
- conditions.append("entity_path = %s")
720
- params.append(query.entity_path)
719
+ if getattr(query, "include_descendants", False):
720
+ clause, clause_params = descendants_sql("entity_path", query.entity_path)
721
+ conditions.append(clause)
722
+ params.extend(clause_params)
723
+ else:
724
+ conditions.append("entity_path = %s")
725
+ params.append(query.entity_path)
721
726
  else:
722
727
  conditions.append(_EXCLUDE_SHARED_PATHS)
723
728
  if query.min_confidence > 0: