@iloveagents/foundry-web-graph 0.1.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/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/neo4j.d.ts +52 -0
- package/dist/adapters/neo4j.js +149 -0
- package/dist/assistant-ui/graph-client-tools.d.ts +11 -0
- package/dist/assistant-ui/graph-client-tools.js +261 -0
- package/dist/assistant-ui/graph-context.d.ts +38 -0
- package/dist/assistant-ui/graph-context.js +113 -0
- package/dist/assistant-ui/graph-panel-content.d.ts +45 -0
- package/dist/assistant-ui/graph-panel-content.js +28 -0
- package/dist/assistant-ui/graph-panel.d.ts +11 -0
- package/dist/assistant-ui/graph-panel.js +121 -0
- package/dist/assistant-ui/graph-provenance.d.ts +17 -0
- package/dist/assistant-ui/graph-provenance.js +31 -0
- package/dist/assistant-ui/graph-result-seeder.d.ts +73 -0
- package/dist/assistant-ui/graph-result-seeder.js +157 -0
- package/dist/assistant-ui/graph-tool-registry.d.ts +25 -0
- package/dist/assistant-ui/graph-tool-registry.js +15 -0
- package/dist/assistant-ui/graph-tool-ui.d.ts +54 -0
- package/dist/assistant-ui/graph-tool-ui.js +77 -0
- package/dist/assistant-ui/index.d.ts +17 -0
- package/dist/assistant-ui/index.js +17 -0
- package/dist/assistant-ui/merge-into-panel.d.ts +21 -0
- package/dist/assistant-ui/merge-into-panel.js +72 -0
- package/dist/assistant-ui/register-graph-panel.d.ts +8 -0
- package/dist/assistant-ui/register-graph-panel.js +18 -0
- package/dist/caption-placement.d.ts +119 -0
- package/dist/caption-placement.js +146 -0
- package/dist/graph-canvas-paint.d.ts +90 -0
- package/dist/graph-canvas-paint.js +186 -0
- package/dist/graph-canvas.d.ts +49 -0
- package/dist/graph-canvas.js +533 -0
- package/dist/graph-inspector.d.ts +42 -0
- package/dist/graph-inspector.js +106 -0
- package/dist/graph-legend.d.ts +19 -0
- package/dist/graph-legend.js +20 -0
- package/dist/graph-notice.d.ts +19 -0
- package/dist/graph-notice.js +30 -0
- package/dist/graph-table.d.ts +28 -0
- package/dist/graph-table.js +57 -0
- package/dist/graph-toolbar.d.ts +22 -0
- package/dist/graph-toolbar.js +8 -0
- package/dist/graph-tooltip.d.ts +4 -0
- package/dist/graph-tooltip.js +55 -0
- package/dist/graph-view.d.ts +94 -0
- package/dist/graph-view.js +338 -0
- package/dist/graph-workspace.d.ts +55 -0
- package/dist/graph-workspace.js +100 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/model.d.ts +206 -0
- package/dist/model.js +369 -0
- package/dist/styles.css +97 -0
- package/dist/theme.d.ts +36 -0
- package/dist/theme.js +83 -0
- package/dist/use-element-size.d.ts +13 -0
- package/dist/use-element-size.js +32 -0
- package/dist/use-graph-model.d.ts +101 -0
- package/dist/use-graph-model.js +164 -0
- package/dist/use-graph-styling.d.ts +55 -0
- package/dist/use-graph-styling.js +156 -0
- package/dist/use-graph-theme.d.ts +11 -0
- package/dist/use-graph-theme.js +54 -0
- package/dist/use-graph-view-state.d.ts +42 -0
- package/dist/use-graph-view-state.js +145 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iLoveAgents, a brand of Leitwolf GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @iloveagents/foundry-web-graph
|
|
2
|
+
|
|
3
|
+
Interactive graph visualization for [Foundry UI](https://github.com/iLoveAgents/foundry-ui).
|
|
4
|
+
|
|
5
|
+
A force-directed graph view with the interactions people expect from a graph
|
|
6
|
+
explorer — colour and caption by type, a filtering legend, hover, select,
|
|
7
|
+
expand, search, pin, zoom-to-fit — plus an optional assistant-ui tool card and
|
|
8
|
+
side-panel renderer so an agent can put a graph in front of the user.
|
|
9
|
+
|
|
10
|
+
**Source-neutral by construction.** The root export takes a plain payload and
|
|
11
|
+
knows nothing about where the data came from. Neo4j is one adapter, in its own
|
|
12
|
+
entry point; the chat integration is in another. Import only what you use.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @iloveagents/foundry-web-graph
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Tailwind v4 hosts: point Tailwind at this package
|
|
19
|
+
|
|
20
|
+
`styles.css` carries the `--graph-1..8` palette and the rules Tailwind cannot
|
|
21
|
+
express — it is **not** the component styling. `GraphView` is built from
|
|
22
|
+
utility classes, and Tailwind v4 does not scan `node_modules`, so without a
|
|
23
|
+
`@source` directive the graph renders with no spacing, borders or typography:
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
@import "tailwindcss";
|
|
27
|
+
@source "../node_modules/@iloveagents/foundry-web-graph/dist";
|
|
28
|
+
|
|
29
|
+
@import "@iloveagents/foundry-web-graph/styles.css";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
(Adjust the relative path to your stylesheet\'s location. Apps scaffolded by
|
|
33
|
+
`create-foundry-ui-app --preset neo4j` already have this.)
|
|
34
|
+
|
|
35
|
+
## Drawing a graph
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { GraphView } from "@iloveagents/foundry-web-graph";
|
|
39
|
+
import "@iloveagents/foundry-web-graph/styles.css";
|
|
40
|
+
|
|
41
|
+
const data = {
|
|
42
|
+
kind: "foundry.graph",
|
|
43
|
+
version: 1,
|
|
44
|
+
nodes: [
|
|
45
|
+
{ id: "1", labels: ["Person"], properties: { name: "Ada" } },
|
|
46
|
+
{ id: "2", labels: ["Project"], properties: { name: "Analytical Engine" } },
|
|
47
|
+
],
|
|
48
|
+
edges: [{ id: "e1", source: "1", target: "2", type: "WORKED_ON" }],
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
<GraphView data={data} height={480} onExpandNode={(node) => loadNeighbours(node.id)} />;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`onExpandNode` is a callback: the view never issues a query of its own. Feed a
|
|
55
|
+
larger payload back through `data` and the layout keeps the positions of nodes
|
|
56
|
+
that were already there.
|
|
57
|
+
|
|
58
|
+
Colours come from your app's design tokens, so light and dark mode work without
|
|
59
|
+
configuration. The stylesheet is optional — without it the graph falls back to
|
|
60
|
+
the host's `--chart-*` tokens, and then to a built-in palette.
|
|
61
|
+
|
|
62
|
+
## In a chat
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { defineChatModule } from "@iloveagents/foundry-web-shell";
|
|
66
|
+
import {
|
|
67
|
+
makeGraphToolUI,
|
|
68
|
+
registerGraphPanelRenderer,
|
|
69
|
+
} from "@iloveagents/foundry-web-graph/assistant-ui";
|
|
70
|
+
|
|
71
|
+
const CypherToolUI = makeGraphToolUI({ toolName: "graph_read_cypher" });
|
|
72
|
+
|
|
73
|
+
export const graphModule = defineChatModule({
|
|
74
|
+
name: "graph",
|
|
75
|
+
useInit: registerGraphPanelRenderer,
|
|
76
|
+
toolUIs: <CypherToolUI />,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`makeGraphToolUI` is a factory because assistant-ui binds a literal tool name at
|
|
81
|
+
construction — so the card works for any tool that returns the payload, whatever
|
|
82
|
+
it is called. A result that is not graph-shaped degrades to an ordinary tool
|
|
83
|
+
card rather than throwing.
|
|
84
|
+
|
|
85
|
+
`registerGraphPanelRenderer()` teaches the tool panel to render
|
|
86
|
+
`type: "graph"` content, which is what the card's **Open in panel** action
|
|
87
|
+
produces. It is idempotent.
|
|
88
|
+
|
|
89
|
+
## Letting the agent work in the graph
|
|
90
|
+
|
|
91
|
+
The panel can be a workspace the agent and the user share rather than a picture
|
|
92
|
+
the agent hands over. `registerGraphClientTools()` adds four browser-executed
|
|
93
|
+
tools:
|
|
94
|
+
|
|
95
|
+
| Tool | What the agent can do |
|
|
96
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
97
|
+
| `graph_get_view` | Read what is on screen: counts, types, the selected node, the active search |
|
|
98
|
+
| `graph_show` | Put a new graph in the panel — positions of nodes already there are kept, so a refined query reads as a change |
|
|
99
|
+
| `graph_select_node` | Select and centre a node, by id or by caption |
|
|
100
|
+
| `graph_search` | Highlight matches and dim the rest |
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import {
|
|
104
|
+
registerGraphClientTools,
|
|
105
|
+
registerGraphPanelRenderer,
|
|
106
|
+
} from "@iloveagents/foundry-web-graph/assistant-ui";
|
|
107
|
+
|
|
108
|
+
export const graphModule = defineChatModule({
|
|
109
|
+
name: "graph",
|
|
110
|
+
useInit: () => {
|
|
111
|
+
registerGraphPanelRenderer();
|
|
112
|
+
registerGraphClientTools();
|
|
113
|
+
},
|
|
114
|
+
toolUIs: <CypherToolUI />,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The user's own clicks land in the same state, so "tell me about this node"
|
|
119
|
+
works without either side restating which node. Read `graphWorkspaceStore`
|
|
120
|
+
directly if you want to build your own UI on top of it.
|
|
121
|
+
|
|
122
|
+
## From Neo4j
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { graphFromNeo4j } from "@iloveagents/foundry-web-graph/adapters/neo4j";
|
|
126
|
+
|
|
127
|
+
const payload = graphFromNeo4j(records);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Structurally typed — it does not import `neo4j-driver`, so a visualization
|
|
131
|
+
package never drags a database client into your bundle. It walks arbitrary
|
|
132
|
+
result rows and collects anything node- or relationship-shaped, so `RETURN *`
|
|
133
|
+
works as well as a careful projection.
|
|
134
|
+
|
|
135
|
+
Most apps will not need this: a backend that emits the payload directly is the
|
|
136
|
+
better arrangement. See the `neo4j` starter — `npm create foundry-ui-app my-app -- --preset neo4j`.
|
|
137
|
+
|
|
138
|
+
## The payload
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
interface GraphPayload {
|
|
142
|
+
kind: "foundry.graph";
|
|
143
|
+
version: 1;
|
|
144
|
+
nodes: {
|
|
145
|
+
id: string;
|
|
146
|
+
labels?: string[];
|
|
147
|
+
caption?: string;
|
|
148
|
+
properties?: Record<string, unknown>;
|
|
149
|
+
}[];
|
|
150
|
+
edges: {
|
|
151
|
+
id: string;
|
|
152
|
+
source: string;
|
|
153
|
+
target: string;
|
|
154
|
+
type?: string;
|
|
155
|
+
properties?: Record<string, unknown>;
|
|
156
|
+
}[];
|
|
157
|
+
truncated?: boolean;
|
|
158
|
+
stats?: { nodeCount?: number; edgeCount?: number };
|
|
159
|
+
legend?: Record<string, { color?: string; captionKey?: string }>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`kind` and `version` are what let a tool card look at an arbitrary result and
|
|
164
|
+
decide whether to render it. `isGraphPayload` checks both strictly;
|
|
165
|
+
`coerceGraphPayload` is the tolerant version that also accepts a JSON string or
|
|
166
|
+
a `{ graph: … }` envelope.
|
|
167
|
+
|
|
168
|
+
A payload must never carry a top-level `error` key — the AG-UI runner treats
|
|
169
|
+
that as a failed tool call. Report partial results with `truncated`. The type
|
|
170
|
+
declares `error?: never` so this fails at compile time rather than in a demo.
|
|
171
|
+
|
|
172
|
+
## Large graphs
|
|
173
|
+
|
|
174
|
+
`GraphView` caps what it draws (1,500 nodes / 3,000 relationships by default,
|
|
175
|
+
via `limits`). When a payload overflows it keeps the **connected core** — seeded
|
|
176
|
+
at the best-connected node and expanded through its neighbours — rather than an
|
|
177
|
+
arbitrary prefix, and reports what was withheld. Taking the first N instead
|
|
178
|
+
produces a screen of unconnected dots, which reads as "there is nothing here"
|
|
179
|
+
rather than "there is too much here".
|
|
180
|
+
|
|
181
|
+
The canvas is not readable by assistive technology, so the same graph is always
|
|
182
|
+
available as a table (`GraphTable`, or `forceTable` on `GraphView`).
|
|
183
|
+
|
|
184
|
+
## Peer dependencies
|
|
185
|
+
|
|
186
|
+
`react`, `react-dom` and `lucide-react` are required. `@assistant-ui/react` is
|
|
187
|
+
optional and only needed for the `/assistant-ui` entry point.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { graphFromNeo4j, isNeo4jNode, isNeo4jRelationship, toGraphEdge, toGraphNode, type Neo4jNodeLike, type Neo4jRelationshipLike, } from "./neo4j.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { graphFromNeo4j, isNeo4jNode, isNeo4jRelationship, toGraphEdge, toGraphNode, } from "./neo4j.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neo4j → {@link GraphPayload}.
|
|
3
|
+
*
|
|
4
|
+
* The ONLY module in this package that knows Neo4j exists. Everything under
|
|
5
|
+
* `src/` outside `adapters/` stays store-agnostic; `src/__tests__/no-store-leak.test.ts`
|
|
6
|
+
* enforces that, so the visualizer keeps working for callers who have never
|
|
7
|
+
* heard of Cypher.
|
|
8
|
+
*
|
|
9
|
+
* This mirrors the server-side mapping the `neo4j-aura` starter preset does in
|
|
10
|
+
* `agent/app/graph_payload.py`. Most apps will not need it — their backend
|
|
11
|
+
* already emits the payload. It exists for the ones that query Neo4j straight
|
|
12
|
+
* from the browser (an HTTP Query API call, a fixture, a test).
|
|
13
|
+
*/
|
|
14
|
+
import { type GraphEdge, type GraphNode, type GraphPayload } from "../model.js";
|
|
15
|
+
/**
|
|
16
|
+
* Structural shape of a driver node. Deliberately NOT the driver's own type —
|
|
17
|
+
* importing `neo4j-driver` for a type would put a store client in the
|
|
18
|
+
* dependency tree of a visualization package. Anything with these fields
|
|
19
|
+
* works, including the plain JSON the HTTP Query API returns.
|
|
20
|
+
*/
|
|
21
|
+
export interface Neo4jNodeLike {
|
|
22
|
+
elementId?: string;
|
|
23
|
+
identity?: unknown;
|
|
24
|
+
labels?: string[];
|
|
25
|
+
properties?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface Neo4jRelationshipLike {
|
|
28
|
+
elementId?: string;
|
|
29
|
+
identity?: unknown;
|
|
30
|
+
type?: string;
|
|
31
|
+
start?: unknown;
|
|
32
|
+
end?: unknown;
|
|
33
|
+
startNodeElementId?: string;
|
|
34
|
+
endNodeElementId?: string;
|
|
35
|
+
properties?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export declare const isNeo4jNode: (value: unknown) => value is Neo4jNodeLike;
|
|
38
|
+
export declare const isNeo4jRelationship: (value: unknown) => value is Neo4jRelationshipLike;
|
|
39
|
+
export declare function toGraphNode(node: Neo4jNodeLike): GraphNode | null;
|
|
40
|
+
export declare function toGraphEdge(rel: Neo4jRelationshipLike): GraphEdge | null;
|
|
41
|
+
/**
|
|
42
|
+
* Walk arbitrary Cypher result rows and collect every node and relationship,
|
|
43
|
+
* however deeply nested.
|
|
44
|
+
*
|
|
45
|
+
* A row is whatever the query returned: bare entities, a path object, a list
|
|
46
|
+
* from `collect()`, a map from a projection. Rather than enumerate those
|
|
47
|
+
* shapes, this recurses and picks out anything that looks like an entity —
|
|
48
|
+
* which is what makes `RETURN *` work as well as a careful projection.
|
|
49
|
+
*
|
|
50
|
+
* Duplicates collapse by id: the same node returned on ten rows is one node.
|
|
51
|
+
*/
|
|
52
|
+
export declare function graphFromNeo4j(rows: unknown): GraphPayload;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neo4j → {@link GraphPayload}.
|
|
3
|
+
*
|
|
4
|
+
* The ONLY module in this package that knows Neo4j exists. Everything under
|
|
5
|
+
* `src/` outside `adapters/` stays store-agnostic; `src/__tests__/no-store-leak.test.ts`
|
|
6
|
+
* enforces that, so the visualizer keeps working for callers who have never
|
|
7
|
+
* heard of Cypher.
|
|
8
|
+
*
|
|
9
|
+
* This mirrors the server-side mapping the `neo4j-aura` starter preset does in
|
|
10
|
+
* `agent/app/graph_payload.py`. Most apps will not need it — their backend
|
|
11
|
+
* already emits the payload. It exists for the ones that query Neo4j straight
|
|
12
|
+
* from the browser (an HTTP Query API call, a fixture, a test).
|
|
13
|
+
*/
|
|
14
|
+
import { GRAPH_PAYLOAD_KIND, GRAPH_PAYLOAD_VERSION, } from "../model.js";
|
|
15
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
/**
|
|
17
|
+
* Normalize an id. The driver hands back `elementId` (a string) on modern
|
|
18
|
+
* versions and `identity` (an Integer object, or a bigint) on older ones;
|
|
19
|
+
* JSON round-trips turn the latter into `{low, high}`. All of them have to
|
|
20
|
+
* collapse to the same string, or edges stop matching their nodes.
|
|
21
|
+
*/
|
|
22
|
+
function toId(value) {
|
|
23
|
+
if (typeof value === "string" && value.length > 0)
|
|
24
|
+
return value;
|
|
25
|
+
if (typeof value === "bigint")
|
|
26
|
+
return String(value);
|
|
27
|
+
// A `number` only when it can still BE the id. Past 2**53-1 a double has
|
|
28
|
+
// already lost the low bits — by the time this runs the rounding has
|
|
29
|
+
// happened, so `String(value)` faithfully stringifies the wrong number and
|
|
30
|
+
// two distinct ids collapse to one key. Same consequence as the `low`-only
|
|
31
|
+
// bug below: it does not fail, it points edges at the wrong nodes. A caller
|
|
32
|
+
// holding larger ids has exact forms available — a string, a bigint, an
|
|
33
|
+
// `elementId`, or `{low, high}`.
|
|
34
|
+
if (typeof value === "number")
|
|
35
|
+
return Number.isSafeInteger(value) ? String(value) : null;
|
|
36
|
+
if (isRecord(value)) {
|
|
37
|
+
// An elementId is exact; prefer it whenever the record carries one.
|
|
38
|
+
if (typeof value.elementId === "string" && value.elementId.length > 0) {
|
|
39
|
+
return value.elementId;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value.low === "number") {
|
|
42
|
+
// A Neo4j Integer is a 64-bit value split across two 32-bit words, and
|
|
43
|
+
// `low` alone is only the bottom half. Dropping `high` makes every id
|
|
44
|
+
// past 2^32 collide with one below it — and a collision here does not
|
|
45
|
+
// fail, it silently points edges at the wrong nodes. BigInt, because
|
|
46
|
+
// the whole point is values a double cannot hold exactly.
|
|
47
|
+
const low = BigInt(value.low >>> 0);
|
|
48
|
+
const high = typeof value.high === "number" ? BigInt(value.high) : 0n;
|
|
49
|
+
return String((high << 32n) + low);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export const isNeo4jNode = (value) => isRecord(value) &&
|
|
55
|
+
Array.isArray(value.labels) &&
|
|
56
|
+
toId(value.elementId ?? value.identity) !== null;
|
|
57
|
+
export const isNeo4jRelationship = (value) => isRecord(value) &&
|
|
58
|
+
typeof value.type === "string" &&
|
|
59
|
+
toId(value.elementId ?? value.identity) !== null &&
|
|
60
|
+
toId(value.startNodeElementId ?? value.start) !== null &&
|
|
61
|
+
toId(value.endNodeElementId ?? value.end) !== null;
|
|
62
|
+
export function toGraphNode(node) {
|
|
63
|
+
const id = toId(node.elementId ?? node.identity);
|
|
64
|
+
if (id === null)
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
labels: node.labels ?? [],
|
|
69
|
+
properties: node.properties ?? {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function toGraphEdge(rel) {
|
|
73
|
+
const id = toId(rel.elementId ?? rel.identity);
|
|
74
|
+
const source = toId(rel.startNodeElementId ?? rel.start);
|
|
75
|
+
const target = toId(rel.endNodeElementId ?? rel.end);
|
|
76
|
+
if (id === null || source === null || target === null)
|
|
77
|
+
return null;
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
source,
|
|
81
|
+
target,
|
|
82
|
+
type: rel.type,
|
|
83
|
+
properties: rel.properties ?? {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Walk arbitrary Cypher result rows and collect every node and relationship,
|
|
88
|
+
* however deeply nested.
|
|
89
|
+
*
|
|
90
|
+
* A row is whatever the query returned: bare entities, a path object, a list
|
|
91
|
+
* from `collect()`, a map from a projection. Rather than enumerate those
|
|
92
|
+
* shapes, this recurses and picks out anything that looks like an entity —
|
|
93
|
+
* which is what makes `RETURN *` work as well as a careful projection.
|
|
94
|
+
*
|
|
95
|
+
* Duplicates collapse by id: the same node returned on ten rows is one node.
|
|
96
|
+
*/
|
|
97
|
+
export function graphFromNeo4j(rows) {
|
|
98
|
+
const nodes = new Map();
|
|
99
|
+
const edges = new Map();
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const visit = (value, depth) => {
|
|
102
|
+
if (value === null || value === undefined || depth > 12)
|
|
103
|
+
return;
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
for (const item of value)
|
|
106
|
+
visit(item, depth + 1);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!isRecord(value))
|
|
110
|
+
return;
|
|
111
|
+
// Cycle guard: paths reference their own segments' nodes.
|
|
112
|
+
if (seen.has(value))
|
|
113
|
+
return;
|
|
114
|
+
seen.add(value);
|
|
115
|
+
if (isNeo4jNode(value)) {
|
|
116
|
+
const node = toGraphNode(value);
|
|
117
|
+
if (node)
|
|
118
|
+
nodes.set(node.id, node);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isNeo4jRelationship(value)) {
|
|
122
|
+
const edge = toGraphEdge(value);
|
|
123
|
+
if (edge) {
|
|
124
|
+
edges.set(edge.id, edge);
|
|
125
|
+
// `MATCH ()-[r]->() RETURN r` is an ordinary query, and the driver
|
|
126
|
+
// gives back relationships with no node objects at all. Without a
|
|
127
|
+
// placeholder for each end, every edge dangles, `pruneDanglingEdges`
|
|
128
|
+
// drops the lot, and a perfectly good result renders as an empty
|
|
129
|
+
// graph. `setdefault` semantics: a real node visited later — in the
|
|
130
|
+
// same result or a merge — overwrites the stub and brings its labels
|
|
131
|
+
// and properties with it.
|
|
132
|
+
if (!nodes.has(edge.source))
|
|
133
|
+
nodes.set(edge.source, { id: edge.source });
|
|
134
|
+
if (!nodes.has(edge.target))
|
|
135
|
+
nodes.set(edge.target, { id: edge.target });
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const nested of Object.values(value))
|
|
140
|
+
visit(nested, depth + 1);
|
|
141
|
+
};
|
|
142
|
+
visit(rows, 0);
|
|
143
|
+
return {
|
|
144
|
+
kind: GRAPH_PAYLOAD_KIND,
|
|
145
|
+
version: GRAPH_PAYLOAD_VERSION,
|
|
146
|
+
nodes: [...nodes.values()],
|
|
147
|
+
edges: [...edges.values()],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ClientToolEntry } from "@iloveagents/foundry-agent";
|
|
2
|
+
export declare const GRAPH_CLIENT_TOOLS: ClientToolEntry[];
|
|
3
|
+
/**
|
|
4
|
+
* Register the graph client tools globally.
|
|
5
|
+
*
|
|
6
|
+
* Global rather than page-scoped: the panel is available from every route, so
|
|
7
|
+
* scoping them to one page would make the agent's ability to drive the graph
|
|
8
|
+
* depend on where the user happens to be standing. Re-registering by the same
|
|
9
|
+
* name replaces the entry, so this is safe to call from a `useInit`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerGraphClientTools(): void;
|