flowscript-agents 0.2.7__tar.gz → 0.2.8__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.
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/PKG-INFO +1 -1
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/__init__.py +5 -1
- flowscript_agents-0.2.8/flowscript_agents/cloud.py +325 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/extract.py +30 -1
- flowscript_agents-0.2.8/flowscript_agents/fixpoint.py +339 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/pyproject.toml +1 -1
- flowscript_agents-0.2.8/tests/test_cloud.py +312 -0
- flowscript_agents-0.2.8/tests/test_fixpoint.py +671 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/.github/workflows/test.yml +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/.gitignore +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/AUDIT_TRAIL_DESIGN.md +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/README.md +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/adapters.md +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/api-reference.md +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/audit-trail.md +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/brand/logo-512.png +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/brand/social-preview.png +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/flowscript-demo.png +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/docs/lifecycle.md +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/examples/CLAUDE.md.example +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/examples/langgraph_live_test.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/examples/temporal_e2e_test.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/audit.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/camel_ai.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/crewai.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/__init__.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/_utils.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/consolidate.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/index.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/providers.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/embeddings/search.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/explain.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/google_adk.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/haystack.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/langgraph.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/llamaindex.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/mcp.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/memory.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/openai_agents.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/pydantic_ai.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/query.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/smolagents.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/tool-integrity.json +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/types.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/flowscript_agents/unified.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/scripts/validate_dedup_threshold.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/conftest.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_audit.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_camel_ai.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_consolidation.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_crewai.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_embeddings.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_explain.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_google_adk.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_haystack.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_langgraph.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_llamaindex.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_mcp.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_memory.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_openai_agents.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_pydantic_ai.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_smolagents.py +0 -0
- {flowscript_agents-0.2.7 → flowscript_agents-0.2.8}/tests/test_temporal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowscript-agents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Complete agent memory: reasoning queries + vector search + auto-extraction. Decision intelligence for LangGraph, CrewAI, Google ADK, OpenAI Agents SDK, Pydantic AI, smolagents, LlamaIndex, Haystack, and CAMEL-AI.
|
|
5
5
|
Project-URL: Homepage, https://flowscript.org
|
|
6
6
|
Project-URL: Repository, https://github.com/phillipclapham/flowscript-agents
|
|
@@ -27,6 +27,7 @@ Usage:
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
from .audit import AuditConfig, AuditQueryResult, AuditVerifyResult
|
|
30
|
+
from .cloud import CloudClient, CloudFlushResult, CloudWitness
|
|
30
31
|
from .memory import (
|
|
31
32
|
Memory,
|
|
32
33
|
MemoryOptions,
|
|
@@ -44,12 +45,15 @@ from .memory import (
|
|
|
44
45
|
from .unified import UnifiedMemory
|
|
45
46
|
from .explain import explain
|
|
46
47
|
|
|
47
|
-
__version__ = "0.2.
|
|
48
|
+
__version__ = "0.2.8"
|
|
48
49
|
__all__ = [
|
|
49
50
|
"explain",
|
|
50
51
|
"AuditConfig",
|
|
51
52
|
"AuditQueryResult",
|
|
52
53
|
"AuditVerifyResult",
|
|
54
|
+
"CloudClient",
|
|
55
|
+
"CloudFlushResult",
|
|
56
|
+
"CloudWitness",
|
|
53
57
|
"Memory",
|
|
54
58
|
"MemoryOptions",
|
|
55
59
|
"NodeRef",
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CloudClient — Batching client for FlowScript Cloud API.
|
|
3
|
+
|
|
4
|
+
Sends audit trail events to api.flowscript.org for independent cryptographic
|
|
5
|
+
witnessing. Events are buffered locally and flushed in batches.
|
|
6
|
+
|
|
7
|
+
Integration with AuditWriter via on_event callback:
|
|
8
|
+
|
|
9
|
+
from flowscript_agents import AuditConfig
|
|
10
|
+
from flowscript_agents.cloud import CloudClient
|
|
11
|
+
|
|
12
|
+
cloud = CloudClient(api_key="fsk_...", namespace="myorg/myagent")
|
|
13
|
+
config = AuditConfig(on_event=cloud.queue_event, on_event_async=True)
|
|
14
|
+
writer = AuditWriter(Path("agent.json"), config)
|
|
15
|
+
# Events are automatically queued and flushed to Cloud
|
|
16
|
+
|
|
17
|
+
# At shutdown:
|
|
18
|
+
cloud.flush() # Send any remaining buffered events
|
|
19
|
+
|
|
20
|
+
Manual usage:
|
|
21
|
+
|
|
22
|
+
cloud = CloudClient(api_key="fsk_...", namespace="myorg/myagent")
|
|
23
|
+
cloud.queue_event(entry_dict)
|
|
24
|
+
result = cloud.flush()
|
|
25
|
+
print(result.accepted, result.witness)
|
|
26
|
+
|
|
27
|
+
Configuration via environment:
|
|
28
|
+
|
|
29
|
+
FLOWSCRIPT_API_KEY=fsk_...
|
|
30
|
+
FLOWSCRIPT_CLOUD_URL=https://api.flowscript.org (default)
|
|
31
|
+
FLOWSCRIPT_NAMESPACE=myorg/myagent
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
import threading
|
|
40
|
+
import urllib.error
|
|
41
|
+
import urllib.request
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from typing import Any, Optional
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Configuration
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
DEFAULT_CLOUD_URL = "https://api.flowscript.org"
|
|
51
|
+
DEFAULT_BATCH_SIZE = 100
|
|
52
|
+
DEFAULT_FLUSH_INTERVAL = 5.0 # seconds (for future auto-flush)
|
|
53
|
+
USER_AGENT = "flowscript-agents-python/0.2.8"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Result Types
|
|
58
|
+
# =============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CloudFlushResult:
|
|
63
|
+
"""Result of a flush operation."""
|
|
64
|
+
|
|
65
|
+
accepted: int
|
|
66
|
+
witness: Optional[dict[str, Any]] = None
|
|
67
|
+
error: Optional[str] = None
|
|
68
|
+
status_code: Optional[int] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CloudWitness:
|
|
73
|
+
"""Witness attestation from Cloud."""
|
|
74
|
+
|
|
75
|
+
id: str
|
|
76
|
+
chain_head_seq: int
|
|
77
|
+
chain_head_hash: str
|
|
78
|
+
witnessed_at: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# CloudClient
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CloudClient:
|
|
87
|
+
"""Batching client for FlowScript Cloud event ingestion.
|
|
88
|
+
|
|
89
|
+
Events are buffered locally and sent in batches to minimize network
|
|
90
|
+
round-trips. Each flush returns a witness attestation from the Cloud
|
|
91
|
+
service, proving independent verification of the hash chain.
|
|
92
|
+
|
|
93
|
+
Thread-safe: multiple threads can call queue_event() concurrently.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
api_key: FlowScript Cloud API key (fsk_...). Falls back to
|
|
97
|
+
FLOWSCRIPT_API_KEY env var.
|
|
98
|
+
namespace: Agent namespace in "owner/agent" format. Falls back to
|
|
99
|
+
FLOWSCRIPT_NAMESPACE env var.
|
|
100
|
+
endpoint: Cloud API base URL. Falls back to FLOWSCRIPT_CLOUD_URL
|
|
101
|
+
env var, then https://api.flowscript.org.
|
|
102
|
+
batch_size: Flush automatically when buffer reaches this size.
|
|
103
|
+
Default 100.
|
|
104
|
+
timeout: HTTP request timeout in seconds. Default 30.
|
|
105
|
+
on_witness: Optional callback invoked with each CloudWitness after
|
|
106
|
+
successful flush. Use for logging or monitoring.
|
|
107
|
+
on_error: Optional callback invoked on flush errors. Receives the
|
|
108
|
+
CloudFlushResult with error details. Default: print to stderr.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
api_key: Optional[str] = None,
|
|
114
|
+
namespace: Optional[str] = None,
|
|
115
|
+
endpoint: Optional[str] = None,
|
|
116
|
+
batch_size: int = DEFAULT_BATCH_SIZE,
|
|
117
|
+
timeout: int = 30,
|
|
118
|
+
on_witness: Optional[callable] = None,
|
|
119
|
+
on_error: Optional[callable] = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
self._api_key = api_key or os.environ.get("FLOWSCRIPT_API_KEY", "")
|
|
122
|
+
self._namespace = namespace or os.environ.get("FLOWSCRIPT_NAMESPACE", "")
|
|
123
|
+
self._endpoint = (
|
|
124
|
+
endpoint or os.environ.get("FLOWSCRIPT_CLOUD_URL", DEFAULT_CLOUD_URL)
|
|
125
|
+
).rstrip("/")
|
|
126
|
+
self._batch_size = batch_size
|
|
127
|
+
self._timeout = timeout
|
|
128
|
+
self._on_witness = on_witness
|
|
129
|
+
self._on_error = on_error
|
|
130
|
+
self._buffer: list[str] = []
|
|
131
|
+
self._lock = threading.Lock()
|
|
132
|
+
self._total_sent = 0
|
|
133
|
+
self._total_accepted = 0
|
|
134
|
+
self._last_witness: Optional[CloudWitness] = None
|
|
135
|
+
|
|
136
|
+
if not self._api_key:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"API key required. Pass api_key= or set FLOWSCRIPT_API_KEY env var."
|
|
139
|
+
)
|
|
140
|
+
if not self._namespace:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"Namespace required. Pass namespace= or set FLOWSCRIPT_NAMESPACE env var. "
|
|
143
|
+
"Format: owner/agent"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def buffered_count(self) -> int:
|
|
148
|
+
"""Number of events currently buffered."""
|
|
149
|
+
with self._lock:
|
|
150
|
+
return len(self._buffer)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def total_sent(self) -> int:
|
|
154
|
+
"""Total events sent to Cloud (may include retransmissions)."""
|
|
155
|
+
return self._total_sent
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def total_accepted(self) -> int:
|
|
159
|
+
"""Total events accepted by Cloud (excludes replays)."""
|
|
160
|
+
return self._total_accepted
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def last_witness(self) -> Optional[CloudWitness]:
|
|
164
|
+
"""Most recent witness attestation received."""
|
|
165
|
+
return self._last_witness
|
|
166
|
+
|
|
167
|
+
# -------------------------------------------------------------------------
|
|
168
|
+
# Queue + Flush
|
|
169
|
+
# -------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def queue_event(self, entry: dict[str, Any]) -> None:
|
|
172
|
+
"""Queue an audit event for batch upload to Cloud.
|
|
173
|
+
|
|
174
|
+
This is designed to be used as the AuditConfig.on_event callback.
|
|
175
|
+
The entry dict is re-serialized to canonical JSON (matching the
|
|
176
|
+
AuditWriter's serialization) for hash-chain-compatible transmission.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
entry: Audit event dict as produced by AuditWriter.write()
|
|
180
|
+
"""
|
|
181
|
+
# Re-serialize to canonical JSON — identical to AuditWriter line 390.
|
|
182
|
+
# Python json.dumps with sort_keys=True is deterministic for the same dict.
|
|
183
|
+
json_line = json.dumps(entry, sort_keys=True, separators=(",", ":"))
|
|
184
|
+
|
|
185
|
+
with self._lock:
|
|
186
|
+
self._buffer.append(json_line)
|
|
187
|
+
if len(self._buffer) >= self._batch_size:
|
|
188
|
+
self._flush_locked()
|
|
189
|
+
|
|
190
|
+
def flush(self) -> Optional[CloudFlushResult]:
|
|
191
|
+
"""Send all buffered events to Cloud.
|
|
192
|
+
|
|
193
|
+
Returns None if buffer is empty. Otherwise returns CloudFlushResult
|
|
194
|
+
with accepted count and witness attestation.
|
|
195
|
+
|
|
196
|
+
Safe to call from any thread, including atexit handlers.
|
|
197
|
+
"""
|
|
198
|
+
with self._lock:
|
|
199
|
+
return self._flush_locked()
|
|
200
|
+
|
|
201
|
+
def _flush_locked(self) -> Optional[CloudFlushResult]:
|
|
202
|
+
"""Internal flush — must be called with self._lock held."""
|
|
203
|
+
if not self._buffer:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
events = list(self._buffer)
|
|
207
|
+
self._buffer.clear()
|
|
208
|
+
|
|
209
|
+
# Build request body
|
|
210
|
+
body = json.dumps({
|
|
211
|
+
"namespace": self._namespace,
|
|
212
|
+
"events": events,
|
|
213
|
+
}).encode("utf-8")
|
|
214
|
+
|
|
215
|
+
url = f"{self._endpoint}/v1/events"
|
|
216
|
+
req = urllib.request.Request(
|
|
217
|
+
url,
|
|
218
|
+
data=body,
|
|
219
|
+
headers={
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
222
|
+
"User-Agent": USER_AGENT,
|
|
223
|
+
},
|
|
224
|
+
method="POST",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
229
|
+
resp_body = json.loads(resp.read().decode("utf-8"))
|
|
230
|
+
self._total_sent += len(events)
|
|
231
|
+
accepted = resp_body.get("accepted", 0)
|
|
232
|
+
self._total_accepted += accepted
|
|
233
|
+
|
|
234
|
+
# Parse witness
|
|
235
|
+
witness_data = resp_body.get("witness")
|
|
236
|
+
witness = None
|
|
237
|
+
if witness_data:
|
|
238
|
+
witness = CloudWitness(
|
|
239
|
+
id=witness_data["id"],
|
|
240
|
+
chain_head_seq=witness_data["chain_head_seq"],
|
|
241
|
+
chain_head_hash=witness_data["chain_head_hash"],
|
|
242
|
+
witnessed_at=witness_data["witnessed_at"],
|
|
243
|
+
)
|
|
244
|
+
self._last_witness = witness
|
|
245
|
+
if self._on_witness:
|
|
246
|
+
try:
|
|
247
|
+
self._on_witness(witness)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
print(
|
|
250
|
+
f"CloudClient: on_witness callback failed: {e}",
|
|
251
|
+
file=sys.stderr,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return CloudFlushResult(
|
|
255
|
+
accepted=accepted,
|
|
256
|
+
witness=witness_data,
|
|
257
|
+
status_code=resp.status,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
except urllib.error.HTTPError as e:
|
|
261
|
+
error_body = ""
|
|
262
|
+
try:
|
|
263
|
+
error_body = e.read().decode("utf-8")
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
result = CloudFlushResult(
|
|
268
|
+
accepted=0,
|
|
269
|
+
error=f"HTTP {e.code}: {error_body}",
|
|
270
|
+
status_code=e.code,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# On 409 (chain break), don't retry — this is a data integrity issue
|
|
274
|
+
# that needs human attention. On 4xx, also don't retry (client error).
|
|
275
|
+
# On 5xx, put events back in buffer for retry.
|
|
276
|
+
if e.code >= 500:
|
|
277
|
+
self._buffer = events + self._buffer # prepend for order preservation
|
|
278
|
+
|
|
279
|
+
self._handle_error(result)
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
except (urllib.error.URLError, OSError, TimeoutError) as e:
|
|
283
|
+
# Network error — put events back for retry
|
|
284
|
+
self._buffer = events + self._buffer
|
|
285
|
+
result = CloudFlushResult(
|
|
286
|
+
accepted=0,
|
|
287
|
+
error=f"Network error: {e}",
|
|
288
|
+
)
|
|
289
|
+
self._handle_error(result)
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
def _handle_error(self, result: CloudFlushResult) -> None:
|
|
293
|
+
"""Handle flush errors."""
|
|
294
|
+
if self._on_error:
|
|
295
|
+
try:
|
|
296
|
+
self._on_error(result)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
else:
|
|
300
|
+
print(
|
|
301
|
+
f"CloudClient: flush failed: {result.error}",
|
|
302
|
+
file=sys.stderr,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# -------------------------------------------------------------------------
|
|
306
|
+
# Convenience
|
|
307
|
+
# -------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def send_events(self, entries: list[dict[str, Any]]) -> CloudFlushResult:
|
|
310
|
+
"""Send a list of events immediately (no buffering).
|
|
311
|
+
|
|
312
|
+
Useful for backfill or one-shot uploads.
|
|
313
|
+
"""
|
|
314
|
+
for entry in entries:
|
|
315
|
+
json_line = json.dumps(entry, sort_keys=True, separators=(",", ":"))
|
|
316
|
+
with self._lock:
|
|
317
|
+
self._buffer.append(json_line)
|
|
318
|
+
return self.flush() or CloudFlushResult(accepted=0)
|
|
319
|
+
|
|
320
|
+
def health(self) -> dict[str, Any]:
|
|
321
|
+
"""Check Cloud API health. Returns health response dict."""
|
|
322
|
+
url = f"{self._endpoint}/v1/health"
|
|
323
|
+
req = urllib.request.Request(url, method="GET", headers={"User-Agent": USER_AGENT})
|
|
324
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
325
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
@@ -611,6 +611,18 @@ class AutoExtract:
|
|
|
611
611
|
4. Create extraction relationships for surviving nodes
|
|
612
612
|
5. Apply extraction states for surviving nodes
|
|
613
613
|
"""
|
|
614
|
+
# Capture pre-extraction graph hash for attestation.
|
|
615
|
+
# MUST be before node creation — the certificate's initial_graph_hash
|
|
616
|
+
# must represent the graph BEFORE any extraction/consolidation work.
|
|
617
|
+
# Otherwise ADD actions (novel nodes already in memory from step 1)
|
|
618
|
+
# produce initial_hash == final_hash, contradicting delta > 0.
|
|
619
|
+
pre_hash = None
|
|
620
|
+
try:
|
|
621
|
+
from ..fixpoint import FixpointContext
|
|
622
|
+
pre_hash = FixpointContext._compute_graph_hash_static(self._memory)
|
|
623
|
+
except Exception:
|
|
624
|
+
pass # Attestation is optional — consolidation always runs
|
|
625
|
+
|
|
614
626
|
# Create all nodes and embed them
|
|
615
627
|
node_refs: list[NodeRef | None] = []
|
|
616
628
|
extracted_node_dicts: list[dict[str, Any]] = []
|
|
@@ -624,11 +636,28 @@ class AutoExtract:
|
|
|
624
636
|
"content": extracted_node.content,
|
|
625
637
|
})
|
|
626
638
|
|
|
627
|
-
# Run consolidation
|
|
639
|
+
# Run consolidation (always runs — this is the core operation)
|
|
628
640
|
consolidation_result = self._consolidation_engine.consolidate(
|
|
629
641
|
extracted_node_dicts, node_refs
|
|
630
642
|
)
|
|
631
643
|
|
|
644
|
+
# Wrap with fixpoint attestation (fail-open: never blocks consolidation)
|
|
645
|
+
try:
|
|
646
|
+
from ..fixpoint import FixpointContext
|
|
647
|
+
|
|
648
|
+
with FixpointContext(self._memory, name="consolidation", constraint="L1",
|
|
649
|
+
_pre_hash=pre_hash) as fix_ctx:
|
|
650
|
+
delta = (
|
|
651
|
+
consolidation_result.nodes_added
|
|
652
|
+
+ consolidation_result.nodes_updated
|
|
653
|
+
+ consolidation_result.nodes_related
|
|
654
|
+
+ consolidation_result.nodes_resolved
|
|
655
|
+
)
|
|
656
|
+
fix_ctx.record_iteration(delta)
|
|
657
|
+
fix_ctx.record_iteration(0) # convergence marker (degenerate @fix)
|
|
658
|
+
except Exception as e:
|
|
659
|
+
print(f"AutoExtract: fixpoint attestation failed: {e}", file=sys.stderr)
|
|
660
|
+
|
|
632
661
|
# Create extraction relationships for surviving nodes only
|
|
633
662
|
# (consolidation may have removed some via UPDATE/NONE)
|
|
634
663
|
rels_created = self._create_extraction_relationships(extraction, node_refs)
|