@dxos/assistant 0.7.4-df37807
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 +8 -0
- package/README.md +24 -0
- package/dist/lib/browser/chunk-AKP45LQH.mjs +1394 -0
- package/dist/lib/browser/chunk-AKP45LQH.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +71 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/testing/index.mjs +601 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node/chunk-G6CGWEMO.cjs +1418 -0
- package/dist/lib/node/chunk-G6CGWEMO.cjs.map +7 -0
- package/dist/lib/node/index.cjs +93 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/testing/index.cjs +619 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/lib/node-esm/chunk-WUPUPEFB.mjs +1396 -0
- package/dist/lib/node-esm/chunk-WUPUPEFB.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +72 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +602 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/ai-service/client.d.ts +48 -0
- package/dist/types/src/ai-service/client.d.ts.map +1 -0
- package/dist/types/src/ai-service/client.test.d.ts +2 -0
- package/dist/types/src/ai-service/client.test.d.ts.map +1 -0
- package/dist/types/src/ai-service/index.d.ts +3 -0
- package/dist/types/src/ai-service/index.d.ts.map +1 -0
- package/dist/types/src/ai-service/schema.d.ts +296 -0
- package/dist/types/src/ai-service/schema.d.ts.map +1 -0
- package/dist/types/src/ai-service/sse.d.ts +34 -0
- package/dist/types/src/ai-service/sse.d.ts.map +1 -0
- package/dist/types/src/bin/chat.d.ts +2 -0
- package/dist/types/src/bin/chat.d.ts.map +1 -0
- package/dist/types/src/conversation/conversation.d.ts +23 -0
- package/dist/types/src/conversation/conversation.d.ts.map +1 -0
- package/dist/types/src/conversation/conversation.test.d.ts +2 -0
- package/dist/types/src/conversation/conversation.test.d.ts.map +1 -0
- package/dist/types/src/conversation/index.d.ts +3 -0
- package/dist/types/src/conversation/index.d.ts.map +1 -0
- package/dist/types/src/conversation/types.d.ts +48 -0
- package/dist/types/src/conversation/types.d.ts.map +1 -0
- package/dist/types/src/cypher/ast.d.ts +56 -0
- package/dist/types/src/cypher/ast.d.ts.map +1 -0
- package/dist/types/src/cypher/combinators.d.ts +14 -0
- package/dist/types/src/cypher/combinators.d.ts.map +1 -0
- package/dist/types/src/cypher/combinators.test.d.ts +2 -0
- package/dist/types/src/cypher/combinators.test.d.ts.map +1 -0
- package/dist/types/src/cypher/echo-data-source.d.ts +16 -0
- package/dist/types/src/cypher/echo-data-source.d.ts.map +1 -0
- package/dist/types/src/cypher/execution-plan.d.ts +49 -0
- package/dist/types/src/cypher/execution-plan.d.ts.map +1 -0
- package/dist/types/src/cypher/execution-plan.test.d.ts +2 -0
- package/dist/types/src/cypher/execution-plan.test.d.ts.map +1 -0
- package/dist/types/src/cypher/index.d.ts +5 -0
- package/dist/types/src/cypher/index.d.ts.map +1 -0
- package/dist/types/src/cypher/parjs.d.ts +4 -0
- package/dist/types/src/cypher/parjs.d.ts.map +1 -0
- package/dist/types/src/cypher/parser.d.ts +6 -0
- package/dist/types/src/cypher/parser.d.ts.map +1 -0
- package/dist/types/src/cypher/query-executor.d.ts +26 -0
- package/dist/types/src/cypher/query-executor.d.ts.map +1 -0
- package/dist/types/src/cypher/query-executor.test.d.ts +2 -0
- package/dist/types/src/cypher/query-executor.test.d.ts.map +1 -0
- package/dist/types/src/cypher/schema.d.ts +12 -0
- package/dist/types/src/cypher/schema.d.ts.map +1 -0
- package/dist/types/src/cypher/schema.test.d.ts +2 -0
- package/dist/types/src/cypher/schema.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/sanity.test.d.ts +2 -0
- package/dist/types/src/sanity.test.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/logger.d.ts +12 -0
- package/dist/types/src/testing/logger.d.ts.map +1 -0
- package/dist/types/src/testing/object-extraction.test.d.ts +1 -0
- package/dist/types/src/testing/object-extraction.test.d.ts.map +1 -0
- package/dist/types/src/testing/query-promts.d.ts +5 -0
- package/dist/types/src/testing/query-promts.d.ts.map +1 -0
- package/dist/types/src/testing/query.test.d.ts +2 -0
- package/dist/types/src/testing/query.test.d.ts.map +1 -0
- package/dist/types/src/testing/test-data.d.ts +29 -0
- package/dist/types/src/testing/test-data.d.ts.map +1 -0
- package/dist/types/src/testing/test-schema.d.ts +50 -0
- package/dist/types/src/testing/test-schema.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/trim.d.ts +5 -0
- package/dist/types/src/util/trim.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +64 -0
- package/src/ai-service/client.test.ts +162 -0
- package/src/ai-service/client.ts +243 -0
- package/src/ai-service/index.ts +6 -0
- package/src/ai-service/schema.ts +260 -0
- package/src/ai-service/sse.ts +336 -0
- package/src/bin/chat.ts +79 -0
- package/src/conversation/conversation.test.ts +74 -0
- package/src/conversation/conversation.ts +134 -0
- package/src/conversation/index.ts +6 -0
- package/src/conversation/types.ts +90 -0
- package/src/cypher/ast.ts +75 -0
- package/src/cypher/combinators.test.ts +137 -0
- package/src/cypher/combinators.ts +149 -0
- package/src/cypher/echo-data-source.ts +105 -0
- package/src/cypher/execution-plan.test.ts +42 -0
- package/src/cypher/execution-plan.ts +208 -0
- package/src/cypher/index.ts +8 -0
- package/src/cypher/parjs-shims.d.ts +6 -0
- package/src/cypher/parjs.ts +10 -0
- package/src/cypher/parser.ts +19 -0
- package/src/cypher/query-executor.test.ts +47 -0
- package/src/cypher/query-executor.ts +145 -0
- package/src/cypher/schema.test.ts +17 -0
- package/src/cypher/schema.ts +82 -0
- package/src/index.ts +7 -0
- package/src/sanity.test.ts +9 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/logger.ts +128 -0
- package/src/testing/object-extraction.test.ts +71 -0
- package/src/testing/query-promts.ts +75 -0
- package/src/testing/query.test.ts +90 -0
- package/src/testing/test-data.ts +366 -0
- package/src/testing/test-schema.ts +80 -0
- package/src/util/index.ts +5 -0
- package/src/util/trim.ts +15 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { JsonPath } from '@dxos/echo-schema';
|
|
6
|
+
import { invariant } from '@dxos/invariant';
|
|
7
|
+
import { log } from '@dxos/log';
|
|
8
|
+
|
|
9
|
+
import type { GraphPatternConnector, MatchClause, ReturnClause, WhereClause } from './ast';
|
|
10
|
+
import type { ParsedQuery } from './parser';
|
|
11
|
+
|
|
12
|
+
export type VariableName = string & { __VariableName: never };
|
|
13
|
+
|
|
14
|
+
export type ExecutionPlanStep =
|
|
15
|
+
| {
|
|
16
|
+
type: 'NodeScan';
|
|
17
|
+
label: string;
|
|
18
|
+
into: VariableName;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: 'FilterByLabel';
|
|
22
|
+
variable: VariableName;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: 'FilterByProperty';
|
|
27
|
+
variable: VariableName;
|
|
28
|
+
property: JsonPath;
|
|
29
|
+
value: unknown;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
/**
|
|
33
|
+
* Scans (anchor)-[relationshipInto:label]->(nodeInto).
|
|
34
|
+
*/
|
|
35
|
+
type: 'RelationshipScan';
|
|
36
|
+
label: string;
|
|
37
|
+
anchor: VariableName;
|
|
38
|
+
relationshipInto: VariableName;
|
|
39
|
+
nodeInto: VariableName;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `in`: (anchor)<-[relationshipInto:label]-(nodeInto)
|
|
43
|
+
* `out`: (anchor)-[relationshipInto:label]->(nodeInto)
|
|
44
|
+
* `both`: (anchor)-[relationshipInto:label]-(nodeInto)
|
|
45
|
+
*/
|
|
46
|
+
direction: 'in' | 'out' | 'both';
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
type: 'ProduceResults';
|
|
50
|
+
fields: {
|
|
51
|
+
name: string;
|
|
52
|
+
variable: VariableName;
|
|
53
|
+
path: JsonPath;
|
|
54
|
+
}[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ExecutionPlan = {
|
|
58
|
+
steps: ExecutionPlanStep[];
|
|
59
|
+
output: {
|
|
60
|
+
fields: string[];
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const createExecutionPlan = (query: ParsedQuery): ExecutionPlan => {
|
|
65
|
+
return new ExecutionPlanBuilder().build(query);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
class ExecutionPlanBuilder {
|
|
69
|
+
private counter = 0;
|
|
70
|
+
|
|
71
|
+
newVariable(): VariableName {
|
|
72
|
+
return `anon_${this.counter++}` as VariableName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
build(query: ParsedQuery): ExecutionPlan {
|
|
76
|
+
log('Creating execution plan for query', { query });
|
|
77
|
+
return {
|
|
78
|
+
steps: [
|
|
79
|
+
...this.createStepsForMatchClause(query.ast.matchClause),
|
|
80
|
+
...(query.ast.whereClause ? this.createStepsForWhereClause(query.ast.whereClause) : []),
|
|
81
|
+
...this.createStepsForReturnClause(query.ast.returnClause),
|
|
82
|
+
],
|
|
83
|
+
output: {
|
|
84
|
+
fields: this.getOutputFields(query.ast.returnClause),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
createStepsForMatchClause(match: MatchClause): ExecutionPlanStep[] {
|
|
90
|
+
invariant(match.pattern.segments.length > 0);
|
|
91
|
+
const firstSegment = match.pattern.segments[0];
|
|
92
|
+
invariant(firstSegment.astKind === 'NodePattern');
|
|
93
|
+
let lastNodeVariable = (firstSegment.variable?.name as VariableName) ?? this.newVariable();
|
|
94
|
+
|
|
95
|
+
const steps: ExecutionPlanStep[] = [];
|
|
96
|
+
steps.push({
|
|
97
|
+
type: 'NodeScan',
|
|
98
|
+
label: firstSegment.label.name,
|
|
99
|
+
into: lastNodeVariable,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (firstSegment.properties) {
|
|
103
|
+
for (const property of firstSegment.properties.properties) {
|
|
104
|
+
steps.push({
|
|
105
|
+
type: 'FilterByProperty',
|
|
106
|
+
variable: lastNodeVariable,
|
|
107
|
+
property: property.key.name as JsonPath,
|
|
108
|
+
value: property.value.value,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = 1; i < match.pattern.segments.length; i++) {
|
|
114
|
+
const relationshipSegment = match.pattern.segments[i++];
|
|
115
|
+
invariant(relationshipSegment.astKind === 'RelationshipPattern');
|
|
116
|
+
const nodeSegment = match.pattern.segments[i];
|
|
117
|
+
invariant(nodeSegment.astKind === 'NodePattern');
|
|
118
|
+
const leftSeparator = match.pattern.segments.separators[i - 2];
|
|
119
|
+
const rightSeparator = match.pattern.segments.separators[i - 1];
|
|
120
|
+
const direction = getDirection(leftSeparator.direction, rightSeparator.direction);
|
|
121
|
+
|
|
122
|
+
const relationshipVariable = (relationshipSegment.variable?.name as VariableName) ?? this.newVariable();
|
|
123
|
+
const nodeVariable = (nodeSegment.variable?.name as VariableName) ?? this.newVariable();
|
|
124
|
+
|
|
125
|
+
steps.push({
|
|
126
|
+
type: 'RelationshipScan',
|
|
127
|
+
label: relationshipSegment.label.name,
|
|
128
|
+
direction,
|
|
129
|
+
anchor: lastNodeVariable,
|
|
130
|
+
relationshipInto: relationshipVariable,
|
|
131
|
+
nodeInto: nodeVariable,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (relationshipSegment.properties) {
|
|
135
|
+
for (const property of relationshipSegment.properties.properties) {
|
|
136
|
+
steps.push({
|
|
137
|
+
type: 'FilterByProperty',
|
|
138
|
+
variable: relationshipVariable,
|
|
139
|
+
property: property.key.name as JsonPath,
|
|
140
|
+
value: property.value.value,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (nodeSegment.label) {
|
|
145
|
+
steps.push({
|
|
146
|
+
type: 'FilterByLabel',
|
|
147
|
+
variable: nodeVariable,
|
|
148
|
+
label: nodeSegment.label.name,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (nodeSegment.properties) {
|
|
152
|
+
for (const property of nodeSegment.properties.properties) {
|
|
153
|
+
steps.push({
|
|
154
|
+
type: 'FilterByProperty',
|
|
155
|
+
variable: nodeVariable,
|
|
156
|
+
property: property.key.name as JsonPath,
|
|
157
|
+
value: property.value.value,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lastNodeVariable = nodeVariable;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return steps;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
createStepsForWhereClause(where: WhereClause): ExecutionPlanStep[] {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
createStepsForReturnClause(returnClause: ReturnClause): ExecutionPlanStep[] {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
type: 'ProduceResults',
|
|
176
|
+
fields: returnClause.fields.map((field) => ({
|
|
177
|
+
name: field.path.map((p) => p.name).join('_'),
|
|
178
|
+
variable: field.path[0].name as VariableName,
|
|
179
|
+
path:
|
|
180
|
+
field.path.length > 1
|
|
181
|
+
? (field.path
|
|
182
|
+
.slice(1)
|
|
183
|
+
.map((p) => p.name)
|
|
184
|
+
.join('.') as JsonPath)
|
|
185
|
+
: ('$' as JsonPath),
|
|
186
|
+
})),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getOutputFields(returnClause: ReturnClause): string[] {
|
|
192
|
+
return returnClause.fields.map((field) => field.path.map((p) => p.name).join('_'));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const getDirection = (
|
|
197
|
+
left: GraphPatternConnector['direction'],
|
|
198
|
+
right: GraphPatternConnector['direction'],
|
|
199
|
+
): 'in' | 'out' | 'both' => {
|
|
200
|
+
if (left === '-' && right === '->') {
|
|
201
|
+
return 'out';
|
|
202
|
+
} else if (left === '<-' && right === '-') {
|
|
203
|
+
return 'in';
|
|
204
|
+
} else if (left === '-' && right === '-') {
|
|
205
|
+
return 'both';
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`Invalid direction: ()${left}[]${right}()`);
|
|
208
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { CypherQuery } from './ast';
|
|
6
|
+
import { cypherQuery } from './combinators';
|
|
7
|
+
|
|
8
|
+
export type ParsedQuery = {
|
|
9
|
+
ast: CypherQuery;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const parseCypherQuery = (query: string): ParsedQuery => {
|
|
13
|
+
const parseResult = cypherQuery.parse(query);
|
|
14
|
+
if (parseResult.isOk) {
|
|
15
|
+
return { ast: parseResult.value };
|
|
16
|
+
} else {
|
|
17
|
+
throw new Error(`Failed to parse query: ${parseResult}`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { log } from '@dxos/log';
|
|
8
|
+
|
|
9
|
+
import { executeQuery } from './query-executor';
|
|
10
|
+
import { createTestData } from '../testing';
|
|
11
|
+
|
|
12
|
+
test('run', async ({ expect }) => {
|
|
13
|
+
const dataSource = createTestData();
|
|
14
|
+
// for (const node of dataSource.nodes) {
|
|
15
|
+
// log.info('node', { id: node.id, label: node.label, name: node.properties.name });
|
|
16
|
+
// }
|
|
17
|
+
// for (const relationship of dataSource.relationships) {
|
|
18
|
+
// log.info('relationship', {
|
|
19
|
+
// source: relationship.source.id,
|
|
20
|
+
// target: relationship.target.id,
|
|
21
|
+
// label: relationship.label,
|
|
22
|
+
// });
|
|
23
|
+
// }
|
|
24
|
+
|
|
25
|
+
const result = await executeQuery(
|
|
26
|
+
dataSource,
|
|
27
|
+
`
|
|
28
|
+
MATCH (org:Org {name: 'DXOS'})-[:ORG_EMPLOYEES]->(c:Contact)<-[:TASK_ASSIGNEE]-(t:Task)-[:TASK_PROJECT]->(p:Project {name: 'Composer'})
|
|
29
|
+
RETURN c.name, t.name
|
|
30
|
+
`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(result).toEqual([
|
|
34
|
+
{ c_name: 'Rich', t_name: 'Create form builder' },
|
|
35
|
+
{
|
|
36
|
+
c_name: 'Josiah',
|
|
37
|
+
t_name: 'Optimize startup performance',
|
|
38
|
+
},
|
|
39
|
+
{ c_name: 'Rich', t_name: 'Implement dark mode' },
|
|
40
|
+
{
|
|
41
|
+
c_name: 'Josiah',
|
|
42
|
+
t_name: 'Add support for custom themes',
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
log.info('result', { result });
|
|
47
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { JsonPath } from '@dxos/echo-schema';
|
|
6
|
+
import { log } from '@dxos/log';
|
|
7
|
+
import { mapValues } from '@dxos/util';
|
|
8
|
+
|
|
9
|
+
import { createExecutionPlan, type ExecutionPlan, type VariableName } from './execution-plan';
|
|
10
|
+
import { parseCypherQuery } from './parser';
|
|
11
|
+
|
|
12
|
+
export interface DataSource {
|
|
13
|
+
getNodes: (params: { label?: string }) => Promise<Node[]>;
|
|
14
|
+
getRelationships: (params: { label?: string }) => Promise<Relationship[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Node = {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: 'node';
|
|
20
|
+
label: string;
|
|
21
|
+
properties: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Relationship = {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: 'relationship';
|
|
27
|
+
label: string;
|
|
28
|
+
properties: Record<string, unknown>;
|
|
29
|
+
source: Node;
|
|
30
|
+
target: Node;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const executeQueryPlan = async (
|
|
34
|
+
dataSource: DataSource,
|
|
35
|
+
plan: ExecutionPlan,
|
|
36
|
+
): Promise<Record<string, unknown>[]> => {
|
|
37
|
+
let buffer: Record<VariableName, Node | Relationship>[] = [];
|
|
38
|
+
const results: Record<string, unknown>[] = [];
|
|
39
|
+
|
|
40
|
+
for (const step of plan.steps) {
|
|
41
|
+
log('begin step', { step });
|
|
42
|
+
|
|
43
|
+
switch (step.type) {
|
|
44
|
+
case 'NodeScan': {
|
|
45
|
+
const nodes = await dataSource.getNodes({ label: step.label });
|
|
46
|
+
buffer.push(...nodes.map((node) => ({ [step.into]: node })));
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case 'FilterByLabel': {
|
|
50
|
+
buffer = buffer.filter((item) => {
|
|
51
|
+
const node = item[step.variable];
|
|
52
|
+
return node && node.kind === 'node' && node.label === step.label;
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'FilterByProperty': {
|
|
57
|
+
buffer = buffer.filter((item) => {
|
|
58
|
+
const node = item[step.variable];
|
|
59
|
+
return node && node.kind === 'node' && node.properties[step.property] === step.value;
|
|
60
|
+
});
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'RelationshipScan': {
|
|
64
|
+
const relationships = await dataSource.getRelationships({ label: step.label });
|
|
65
|
+
for (const tuple of [...buffer]) {
|
|
66
|
+
if (tuple[step.anchor].kind !== 'node') {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const filteredRelationships = relationships.filter((relationship) => {
|
|
70
|
+
switch (step.direction) {
|
|
71
|
+
case 'in':
|
|
72
|
+
return relationship.target.id === tuple[step.anchor].id;
|
|
73
|
+
case 'out':
|
|
74
|
+
return relationship.source.id === tuple[step.anchor].id;
|
|
75
|
+
case 'both':
|
|
76
|
+
return (
|
|
77
|
+
relationship.source.id === tuple[step.anchor].id || relationship.target.id === tuple[step.anchor].id
|
|
78
|
+
);
|
|
79
|
+
default:
|
|
80
|
+
throw new Error(`Unknown direction: ${step.direction}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
for (let i = 0; i < filteredRelationships.length; i++) {
|
|
84
|
+
const relationship = filteredRelationships[i];
|
|
85
|
+
const newTuple = i === 0 ? tuple : { ...tuple };
|
|
86
|
+
if (newTuple !== tuple) {
|
|
87
|
+
buffer.push(newTuple);
|
|
88
|
+
}
|
|
89
|
+
newTuple[step.relationshipInto] = relationship;
|
|
90
|
+
switch (step.direction) {
|
|
91
|
+
case 'in':
|
|
92
|
+
newTuple[step.nodeInto] = relationship.source;
|
|
93
|
+
break;
|
|
94
|
+
case 'out':
|
|
95
|
+
newTuple[step.nodeInto] = relationship.target;
|
|
96
|
+
break;
|
|
97
|
+
case 'both':
|
|
98
|
+
newTuple[step.nodeInto] =
|
|
99
|
+
relationship.source.id === newTuple[step.anchor].id ? relationship.target : relationship.source;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'ProduceResults': {
|
|
107
|
+
for (const tuple of buffer) {
|
|
108
|
+
const result: Record<string, unknown> = {};
|
|
109
|
+
for (const field of step.fields) {
|
|
110
|
+
result[field.name] = jsonPathGet(tuple[field.variable].properties, field.path);
|
|
111
|
+
}
|
|
112
|
+
results.push(result);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
log('finish step', { buffer: buffer.map((tuple) => mapValues(tuple, (x) => x.id)), results });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const executeQuery = (dataSource: DataSource, query: string): Promise<Record<string, unknown>[]> => {
|
|
125
|
+
const plan = createExecutionPlan(parseCypherQuery(query));
|
|
126
|
+
return executeQueryPlan(dataSource, plan);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// TODO(dmaretskyi): Extract.
|
|
130
|
+
const jsonPathGet = (obj: Record<string, unknown>, path: JsonPath): unknown => {
|
|
131
|
+
if (path === '' || path === '$') {
|
|
132
|
+
return obj;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parts = path.split('.');
|
|
136
|
+
if (parts[0] === '$') {
|
|
137
|
+
parts.shift();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let result: any = obj;
|
|
141
|
+
for (const part of parts) {
|
|
142
|
+
result = result?.[part];
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { toJsonSchema } from '@dxos/echo-schema';
|
|
8
|
+
|
|
9
|
+
import { formatJsonSchemaForLLM } from './schema';
|
|
10
|
+
import { Contact, Org, Project, Task } from '../testing';
|
|
11
|
+
|
|
12
|
+
test('Org-Project-Task-Contact schema', async ({ expect }) => {
|
|
13
|
+
const jsonSchema = [Org, Project, Task, Contact].map((schema) => toJsonSchema(schema));
|
|
14
|
+
const result = formatJsonSchemaForLLM(jsonSchema);
|
|
15
|
+
// log.info('result', { result });
|
|
16
|
+
expect(result).toHaveLength(8);
|
|
17
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { raise } from '@dxos/debug';
|
|
6
|
+
import { JSON_SCHEMA_ECHO_REF_ID, type JsonSchemaType } from '@dxos/echo-schema';
|
|
7
|
+
import { DXN } from '@dxos/keys';
|
|
8
|
+
|
|
9
|
+
import { trim } from '../util';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Formats ECHO schema to be consumed by LLM.
|
|
13
|
+
* Schema is formatted in cypher format.
|
|
14
|
+
* Objects are represented as nodes.
|
|
15
|
+
* References are represented as relationships.
|
|
16
|
+
*/
|
|
17
|
+
export const formatJsonSchemaForLLM = (schemaTypes: JsonSchemaType[]): string[] => {
|
|
18
|
+
return schemaTypes.flatMap((schema) => [
|
|
19
|
+
formatNodeSchema(schema),
|
|
20
|
+
...formatInferredRelationships(schema, schemaTypes),
|
|
21
|
+
]);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const formatNodeSchema = (schema: JsonSchemaType) => {
|
|
25
|
+
const label = formatNodeLabel(schema.typename ?? raise(new Error('Schema must have $id')));
|
|
26
|
+
const properties = Object.entries(schema.properties!).filter(([key, prop]) => key !== 'id' && !isReference(prop));
|
|
27
|
+
|
|
28
|
+
return trim`
|
|
29
|
+
<node>
|
|
30
|
+
(:${label})
|
|
31
|
+
|
|
32
|
+
<description>${schema.description}</description>
|
|
33
|
+
|
|
34
|
+
<properties>
|
|
35
|
+
${properties.map(([key, value]) => {
|
|
36
|
+
return trim`
|
|
37
|
+
<${key}>
|
|
38
|
+
<type>${value.type}</type>
|
|
39
|
+
<description>${value.description}</description>
|
|
40
|
+
</${key}>
|
|
41
|
+
`;
|
|
42
|
+
})}
|
|
43
|
+
</properties>
|
|
44
|
+
</node>
|
|
45
|
+
`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const formatInferredRelationships = (schema: JsonSchemaType, allSchema: JsonSchemaType[]): string[] => {
|
|
49
|
+
const nodeLabel = formatNodeLabel(schema.typename ?? raise(new Error('Schema must have $id')));
|
|
50
|
+
const relationships = Object.entries(schema.properties!).filter(([key, prop]) => isReference(prop));
|
|
51
|
+
|
|
52
|
+
return relationships.map(([key, value]) => {
|
|
53
|
+
const relationshipLabel = formatInferredRelationshipLabel(
|
|
54
|
+
schema.typename ?? raise(new Error('Schema must have $id')),
|
|
55
|
+
key,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// TODO(dmaretskyi): Registered schema don't get reference types updated.
|
|
59
|
+
const targetSchema = allSchema.find((schema) => `dxn:type/${schema.typename}` === value.reference?.schema.$ref);
|
|
60
|
+
const targetLabel = targetSchema ? formatNodeLabel(targetSchema?.typename ?? '') : '';
|
|
61
|
+
|
|
62
|
+
return trim`
|
|
63
|
+
<relationship>
|
|
64
|
+
(:${nodeLabel})-[:${relationshipLabel}]->(:${targetLabel})
|
|
65
|
+
|
|
66
|
+
<description>${value.description}</description>
|
|
67
|
+
</relationship>
|
|
68
|
+
`;
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const formatNodeLabel = (typename: string) => {
|
|
73
|
+
return typename.replace(/^example\.com\/type\//g, '');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const formatInferredRelationshipLabel = (typename: string, property: string) => {
|
|
77
|
+
const nodeLabel = formatNodeLabel(typename);
|
|
78
|
+
return `${nodeLabel}_${property}`.toUpperCase();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const isReference = (schema: JsonSchemaType) =>
|
|
82
|
+
schema.$id === JSON_SCHEMA_ECHO_REF_ID || (schema.type === 'array' && schema.items?.$id === JSON_SCHEMA_ECHO_REF_ID);
|
package/src/index.ts
ADDED