flowscript-agents 0.1.0__py3-none-any.whl
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/__init__.py +21 -0
- flowscript_agents/crewai.py +409 -0
- flowscript_agents/google_adk.py +258 -0
- flowscript_agents/langgraph.py +280 -0
- flowscript_agents/memory.py +504 -0
- flowscript_agents/openai_agents.py +170 -0
- flowscript_agents-0.1.0.dist-info/METADATA +285 -0
- flowscript_agents-0.1.0.dist-info/RECORD +9 -0
- flowscript_agents-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FlowScript LangGraph Integration.
|
|
3
|
+
|
|
4
|
+
Implements LangGraph's BaseStore interface, making FlowScript memory
|
|
5
|
+
available as a drop-in store for LangGraph agents and LangMem.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from flowscript_agents.langgraph import FlowScriptStore
|
|
9
|
+
|
|
10
|
+
store = FlowScriptStore("./agent-memory.json")
|
|
11
|
+
# Use as LangGraph store — nodes stored as items, queries available via .memory
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Any, Iterable
|
|
20
|
+
|
|
21
|
+
from langgraph.store.base import (
|
|
22
|
+
BaseStore,
|
|
23
|
+
GetOp,
|
|
24
|
+
Item,
|
|
25
|
+
ListNamespacesOp,
|
|
26
|
+
MatchCondition,
|
|
27
|
+
Op,
|
|
28
|
+
PutOp,
|
|
29
|
+
Result,
|
|
30
|
+
SearchItem,
|
|
31
|
+
SearchOp,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from .memory import Memory
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FlowScriptStore(BaseStore):
|
|
38
|
+
"""LangGraph BaseStore backed by FlowScript reasoning memory.
|
|
39
|
+
|
|
40
|
+
Each item stored maps to a FlowScript node. The store provides standard
|
|
41
|
+
LangGraph get/put/search/delete operations, plus access to FlowScript's
|
|
42
|
+
semantic queries (why, tensions, blocked, alternatives, whatIf) via
|
|
43
|
+
the .memory property.
|
|
44
|
+
|
|
45
|
+
Namespaces map to FlowScript node metadata:
|
|
46
|
+
- Items are stored as nodes with namespace encoded in provenance
|
|
47
|
+
- Search uses FlowScript's content matching
|
|
48
|
+
- Full FlowScript query engine available via .memory.query
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, file_path: str | None = None) -> None:
|
|
52
|
+
super().__init__()
|
|
53
|
+
if file_path:
|
|
54
|
+
self._memory = Memory.load_or_create(file_path)
|
|
55
|
+
else:
|
|
56
|
+
self._memory = Memory()
|
|
57
|
+
# In-memory index: namespace+key → node_id + value
|
|
58
|
+
self._items: dict[tuple[tuple[str, ...], str], _StoredItem] = {}
|
|
59
|
+
# Rebuild index from loaded memory
|
|
60
|
+
self._rebuild_index()
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def memory(self) -> Memory:
|
|
64
|
+
"""Access the underlying FlowScript Memory for semantic queries.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
tensions = store.memory.query.tensions()
|
|
68
|
+
blocked = store.memory.query.blocked()
|
|
69
|
+
"""
|
|
70
|
+
return self._memory
|
|
71
|
+
|
|
72
|
+
def _rebuild_index(self) -> None:
|
|
73
|
+
"""Rebuild the namespace/key index from loaded memory nodes."""
|
|
74
|
+
for ref in self._memory.nodes:
|
|
75
|
+
node = ref.node
|
|
76
|
+
if node.ext and "langgraph_ns" in node.ext:
|
|
77
|
+
ns = tuple(node.ext["langgraph_ns"])
|
|
78
|
+
key = node.ext.get("langgraph_key", node.id)
|
|
79
|
+
value = node.ext.get("langgraph_value", {"content": node.content})
|
|
80
|
+
created = node.provenance.timestamp
|
|
81
|
+
self._items[(ns, key)] = _StoredItem(
|
|
82
|
+
node_id=node.id,
|
|
83
|
+
namespace=ns,
|
|
84
|
+
key=key,
|
|
85
|
+
value=value,
|
|
86
|
+
created_at=_parse_dt(created),
|
|
87
|
+
updated_at=_parse_dt(created),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
|
91
|
+
results: list[Result] = []
|
|
92
|
+
for op in ops:
|
|
93
|
+
if isinstance(op, GetOp):
|
|
94
|
+
results.append(self._handle_get(op))
|
|
95
|
+
elif isinstance(op, PutOp):
|
|
96
|
+
self._handle_put(op)
|
|
97
|
+
results.append(None)
|
|
98
|
+
elif isinstance(op, SearchOp):
|
|
99
|
+
results.append(self._handle_search(op))
|
|
100
|
+
elif isinstance(op, ListNamespacesOp):
|
|
101
|
+
results.append(self._handle_list_namespaces(op))
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError(f"Unknown op type: {type(op)}")
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
|
107
|
+
# Synchronous implementation is sufficient for file-based store
|
|
108
|
+
return self.batch(ops)
|
|
109
|
+
|
|
110
|
+
def _handle_get(self, op: GetOp) -> Item | None:
|
|
111
|
+
stored = self._items.get((op.namespace, op.key))
|
|
112
|
+
if stored is None:
|
|
113
|
+
return None
|
|
114
|
+
return Item(
|
|
115
|
+
namespace=stored.namespace,
|
|
116
|
+
key=stored.key,
|
|
117
|
+
value=stored.value,
|
|
118
|
+
created_at=stored.created_at,
|
|
119
|
+
updated_at=stored.updated_at,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _handle_put(self, op: PutOp) -> None:
|
|
123
|
+
ns = op.namespace
|
|
124
|
+
key = op.key
|
|
125
|
+
|
|
126
|
+
if op.value is None:
|
|
127
|
+
# Delete: remove from index AND from FlowScript graph
|
|
128
|
+
stored = self._items.pop((ns, key), None)
|
|
129
|
+
if stored:
|
|
130
|
+
self._memory.remove_node(stored.node_id)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Upsert: create or update
|
|
134
|
+
existing = self._items.get((ns, key))
|
|
135
|
+
now = datetime.now(timezone.utc)
|
|
136
|
+
|
|
137
|
+
# Determine content for the FlowScript node
|
|
138
|
+
content = op.value.get("content") or op.value.get("memory") or json.dumps(op.value)
|
|
139
|
+
|
|
140
|
+
if existing:
|
|
141
|
+
# Remove old node, create new one with updated content
|
|
142
|
+
self._memory.remove_node(existing.node_id)
|
|
143
|
+
ref = self._memory.thought(content)
|
|
144
|
+
node = ref.node
|
|
145
|
+
node.ext = node.ext or {}
|
|
146
|
+
node.ext["langgraph_ns"] = list(ns)
|
|
147
|
+
node.ext["langgraph_key"] = key
|
|
148
|
+
node.ext["langgraph_value"] = op.value
|
|
149
|
+
existing.node_id = ref.id
|
|
150
|
+
existing.value = op.value
|
|
151
|
+
existing.updated_at = now
|
|
152
|
+
else:
|
|
153
|
+
# Create new node
|
|
154
|
+
ref = self._memory.thought(content)
|
|
155
|
+
node = ref.node
|
|
156
|
+
node.ext = node.ext or {}
|
|
157
|
+
node.ext["langgraph_ns"] = list(ns)
|
|
158
|
+
node.ext["langgraph_key"] = key
|
|
159
|
+
node.ext["langgraph_value"] = op.value
|
|
160
|
+
|
|
161
|
+
self._items[(ns, key)] = _StoredItem(
|
|
162
|
+
node_id=ref.id,
|
|
163
|
+
namespace=ns,
|
|
164
|
+
key=key,
|
|
165
|
+
value=op.value,
|
|
166
|
+
created_at=now,
|
|
167
|
+
updated_at=now,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _handle_search(self, op: SearchOp) -> list[SearchItem]:
|
|
171
|
+
results: list[SearchItem] = []
|
|
172
|
+
prefix = op.namespace_prefix
|
|
173
|
+
|
|
174
|
+
for (ns, key), stored in self._items.items():
|
|
175
|
+
# Check namespace prefix match
|
|
176
|
+
if len(ns) < len(prefix) or ns[: len(prefix)] != prefix:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Apply filter
|
|
180
|
+
if op.filter:
|
|
181
|
+
if not _matches_filter(stored.value, op.filter):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Score by query match (simple content matching)
|
|
185
|
+
score = 0.0
|
|
186
|
+
if op.query:
|
|
187
|
+
content_str = json.dumps(stored.value).lower()
|
|
188
|
+
query_lower = op.query.lower()
|
|
189
|
+
if query_lower in content_str:
|
|
190
|
+
# Simple relevance: ratio of query length to content length
|
|
191
|
+
score = len(query_lower) / max(len(content_str), 1)
|
|
192
|
+
else:
|
|
193
|
+
# No match — skip if query was specified
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
results.append(
|
|
197
|
+
SearchItem(
|
|
198
|
+
namespace=stored.namespace,
|
|
199
|
+
key=stored.key,
|
|
200
|
+
value=stored.value,
|
|
201
|
+
created_at=stored.created_at,
|
|
202
|
+
updated_at=stored.updated_at,
|
|
203
|
+
score=score,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Sort by score descending, then by recency descending
|
|
208
|
+
results.sort(key=lambda x: (-(x.score or 0), -x.updated_at.timestamp()))
|
|
209
|
+
|
|
210
|
+
# Apply offset and limit
|
|
211
|
+
return results[op.offset : op.offset + op.limit]
|
|
212
|
+
|
|
213
|
+
def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
|
|
214
|
+
namespaces: set[tuple[str, ...]] = set()
|
|
215
|
+
|
|
216
|
+
for (ns, _key) in self._items:
|
|
217
|
+
# Apply max_depth truncation
|
|
218
|
+
effective_ns = ns
|
|
219
|
+
if op.max_depth is not None:
|
|
220
|
+
effective_ns = ns[: op.max_depth]
|
|
221
|
+
|
|
222
|
+
# Apply match conditions
|
|
223
|
+
matches = True
|
|
224
|
+
for cond in op.match_conditions:
|
|
225
|
+
if cond.match_type == "prefix":
|
|
226
|
+
if len(effective_ns) < len(cond.path) or effective_ns[: len(cond.path)] != tuple(cond.path):
|
|
227
|
+
matches = False
|
|
228
|
+
break
|
|
229
|
+
elif cond.match_type == "suffix":
|
|
230
|
+
if len(effective_ns) < len(cond.path) or effective_ns[-len(cond.path) :] != tuple(cond.path):
|
|
231
|
+
matches = False
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
if matches:
|
|
235
|
+
namespaces.add(effective_ns)
|
|
236
|
+
|
|
237
|
+
sorted_ns = sorted(namespaces)
|
|
238
|
+
return sorted_ns[op.offset : op.offset + op.limit]
|
|
239
|
+
|
|
240
|
+
def save(self) -> None:
|
|
241
|
+
"""Persist the store to disk."""
|
|
242
|
+
self._memory.save()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class _StoredItem:
|
|
246
|
+
"""Internal storage for items with their metadata."""
|
|
247
|
+
|
|
248
|
+
__slots__ = ("node_id", "namespace", "key", "value", "created_at", "updated_at")
|
|
249
|
+
|
|
250
|
+
def __init__(
|
|
251
|
+
self,
|
|
252
|
+
node_id: str,
|
|
253
|
+
namespace: tuple[str, ...],
|
|
254
|
+
key: str,
|
|
255
|
+
value: dict[str, Any],
|
|
256
|
+
created_at: datetime,
|
|
257
|
+
updated_at: datetime,
|
|
258
|
+
) -> None:
|
|
259
|
+
self.node_id = node_id
|
|
260
|
+
self.namespace = namespace
|
|
261
|
+
self.key = key
|
|
262
|
+
self.value = value
|
|
263
|
+
self.created_at = created_at
|
|
264
|
+
self.updated_at = updated_at
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _matches_filter(value: dict[str, Any], filter_dict: dict[str, Any]) -> bool:
|
|
268
|
+
"""Check if a value dict matches all filter criteria."""
|
|
269
|
+
for k, v in filter_dict.items():
|
|
270
|
+
if k not in value or value[k] != v:
|
|
271
|
+
return False
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _parse_dt(s: str) -> datetime:
|
|
276
|
+
"""Parse ISO-8601 datetime string."""
|
|
277
|
+
try:
|
|
278
|
+
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
279
|
+
except (ValueError, AttributeError):
|
|
280
|
+
return datetime.now(timezone.utc)
|