@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.
Files changed (128) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +24 -0
  3. package/dist/lib/browser/chunk-AKP45LQH.mjs +1394 -0
  4. package/dist/lib/browser/chunk-AKP45LQH.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +71 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/testing/index.mjs +601 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/node/chunk-G6CGWEMO.cjs +1418 -0
  11. package/dist/lib/node/chunk-G6CGWEMO.cjs.map +7 -0
  12. package/dist/lib/node/index.cjs +93 -0
  13. package/dist/lib/node/index.cjs.map +7 -0
  14. package/dist/lib/node/meta.json +1 -0
  15. package/dist/lib/node/testing/index.cjs +619 -0
  16. package/dist/lib/node/testing/index.cjs.map +7 -0
  17. package/dist/lib/node-esm/chunk-WUPUPEFB.mjs +1396 -0
  18. package/dist/lib/node-esm/chunk-WUPUPEFB.mjs.map +7 -0
  19. package/dist/lib/node-esm/index.mjs +72 -0
  20. package/dist/lib/node-esm/index.mjs.map +7 -0
  21. package/dist/lib/node-esm/meta.json +1 -0
  22. package/dist/lib/node-esm/testing/index.mjs +602 -0
  23. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  24. package/dist/types/src/ai-service/client.d.ts +48 -0
  25. package/dist/types/src/ai-service/client.d.ts.map +1 -0
  26. package/dist/types/src/ai-service/client.test.d.ts +2 -0
  27. package/dist/types/src/ai-service/client.test.d.ts.map +1 -0
  28. package/dist/types/src/ai-service/index.d.ts +3 -0
  29. package/dist/types/src/ai-service/index.d.ts.map +1 -0
  30. package/dist/types/src/ai-service/schema.d.ts +296 -0
  31. package/dist/types/src/ai-service/schema.d.ts.map +1 -0
  32. package/dist/types/src/ai-service/sse.d.ts +34 -0
  33. package/dist/types/src/ai-service/sse.d.ts.map +1 -0
  34. package/dist/types/src/bin/chat.d.ts +2 -0
  35. package/dist/types/src/bin/chat.d.ts.map +1 -0
  36. package/dist/types/src/conversation/conversation.d.ts +23 -0
  37. package/dist/types/src/conversation/conversation.d.ts.map +1 -0
  38. package/dist/types/src/conversation/conversation.test.d.ts +2 -0
  39. package/dist/types/src/conversation/conversation.test.d.ts.map +1 -0
  40. package/dist/types/src/conversation/index.d.ts +3 -0
  41. package/dist/types/src/conversation/index.d.ts.map +1 -0
  42. package/dist/types/src/conversation/types.d.ts +48 -0
  43. package/dist/types/src/conversation/types.d.ts.map +1 -0
  44. package/dist/types/src/cypher/ast.d.ts +56 -0
  45. package/dist/types/src/cypher/ast.d.ts.map +1 -0
  46. package/dist/types/src/cypher/combinators.d.ts +14 -0
  47. package/dist/types/src/cypher/combinators.d.ts.map +1 -0
  48. package/dist/types/src/cypher/combinators.test.d.ts +2 -0
  49. package/dist/types/src/cypher/combinators.test.d.ts.map +1 -0
  50. package/dist/types/src/cypher/echo-data-source.d.ts +16 -0
  51. package/dist/types/src/cypher/echo-data-source.d.ts.map +1 -0
  52. package/dist/types/src/cypher/execution-plan.d.ts +49 -0
  53. package/dist/types/src/cypher/execution-plan.d.ts.map +1 -0
  54. package/dist/types/src/cypher/execution-plan.test.d.ts +2 -0
  55. package/dist/types/src/cypher/execution-plan.test.d.ts.map +1 -0
  56. package/dist/types/src/cypher/index.d.ts +5 -0
  57. package/dist/types/src/cypher/index.d.ts.map +1 -0
  58. package/dist/types/src/cypher/parjs.d.ts +4 -0
  59. package/dist/types/src/cypher/parjs.d.ts.map +1 -0
  60. package/dist/types/src/cypher/parser.d.ts +6 -0
  61. package/dist/types/src/cypher/parser.d.ts.map +1 -0
  62. package/dist/types/src/cypher/query-executor.d.ts +26 -0
  63. package/dist/types/src/cypher/query-executor.d.ts.map +1 -0
  64. package/dist/types/src/cypher/query-executor.test.d.ts +2 -0
  65. package/dist/types/src/cypher/query-executor.test.d.ts.map +1 -0
  66. package/dist/types/src/cypher/schema.d.ts +12 -0
  67. package/dist/types/src/cypher/schema.d.ts.map +1 -0
  68. package/dist/types/src/cypher/schema.test.d.ts +2 -0
  69. package/dist/types/src/cypher/schema.test.d.ts.map +1 -0
  70. package/dist/types/src/index.d.ts +4 -0
  71. package/dist/types/src/index.d.ts.map +1 -0
  72. package/dist/types/src/sanity.test.d.ts +2 -0
  73. package/dist/types/src/sanity.test.d.ts.map +1 -0
  74. package/dist/types/src/testing/index.d.ts +5 -0
  75. package/dist/types/src/testing/index.d.ts.map +1 -0
  76. package/dist/types/src/testing/logger.d.ts +12 -0
  77. package/dist/types/src/testing/logger.d.ts.map +1 -0
  78. package/dist/types/src/testing/object-extraction.test.d.ts +1 -0
  79. package/dist/types/src/testing/object-extraction.test.d.ts.map +1 -0
  80. package/dist/types/src/testing/query-promts.d.ts +5 -0
  81. package/dist/types/src/testing/query-promts.d.ts.map +1 -0
  82. package/dist/types/src/testing/query.test.d.ts +2 -0
  83. package/dist/types/src/testing/query.test.d.ts.map +1 -0
  84. package/dist/types/src/testing/test-data.d.ts +29 -0
  85. package/dist/types/src/testing/test-data.d.ts.map +1 -0
  86. package/dist/types/src/testing/test-schema.d.ts +50 -0
  87. package/dist/types/src/testing/test-schema.d.ts.map +1 -0
  88. package/dist/types/src/util/index.d.ts +2 -0
  89. package/dist/types/src/util/index.d.ts.map +1 -0
  90. package/dist/types/src/util/trim.d.ts +5 -0
  91. package/dist/types/src/util/trim.d.ts.map +1 -0
  92. package/dist/types/tsconfig.tsbuildinfo +1 -0
  93. package/package.json +64 -0
  94. package/src/ai-service/client.test.ts +162 -0
  95. package/src/ai-service/client.ts +243 -0
  96. package/src/ai-service/index.ts +6 -0
  97. package/src/ai-service/schema.ts +260 -0
  98. package/src/ai-service/sse.ts +336 -0
  99. package/src/bin/chat.ts +79 -0
  100. package/src/conversation/conversation.test.ts +74 -0
  101. package/src/conversation/conversation.ts +134 -0
  102. package/src/conversation/index.ts +6 -0
  103. package/src/conversation/types.ts +90 -0
  104. package/src/cypher/ast.ts +75 -0
  105. package/src/cypher/combinators.test.ts +137 -0
  106. package/src/cypher/combinators.ts +149 -0
  107. package/src/cypher/echo-data-source.ts +105 -0
  108. package/src/cypher/execution-plan.test.ts +42 -0
  109. package/src/cypher/execution-plan.ts +208 -0
  110. package/src/cypher/index.ts +8 -0
  111. package/src/cypher/parjs-shims.d.ts +6 -0
  112. package/src/cypher/parjs.ts +10 -0
  113. package/src/cypher/parser.ts +19 -0
  114. package/src/cypher/query-executor.test.ts +47 -0
  115. package/src/cypher/query-executor.ts +145 -0
  116. package/src/cypher/schema.test.ts +17 -0
  117. package/src/cypher/schema.ts +82 -0
  118. package/src/index.ts +7 -0
  119. package/src/sanity.test.ts +9 -0
  120. package/src/testing/index.ts +8 -0
  121. package/src/testing/logger.ts +128 -0
  122. package/src/testing/object-extraction.test.ts +71 -0
  123. package/src/testing/query-promts.ts +75 -0
  124. package/src/testing/query.test.ts +90 -0
  125. package/src/testing/test-data.ts +366 -0
  126. package/src/testing/test-schema.ts +80 -0
  127. package/src/util/index.ts +5 -0
  128. package/src/util/trim.ts +15 -0
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Schema as S } from '@effect/schema';
6
+
7
+ import { toJsonSchema } from '@dxos/echo-schema';
8
+ import { JsonSchemaType } from '@dxos/echo-schema';
9
+ import type { SpaceId } from '@dxos/keys';
10
+ import { log } from '@dxos/log';
11
+
12
+ import { ObjectId, type Message } from '../ai-service';
13
+
14
+ export const createUserMessage = (spaceId: SpaceId, threadId: ObjectId, text: string): Message => ({
15
+ id: ObjectId.random(),
16
+ spaceId,
17
+ threadId,
18
+ role: 'user',
19
+ content: [{ type: 'text', text }],
20
+ });
21
+
22
+ export const LLMToolDefinition = S.Struct({
23
+ name: S.String,
24
+ description: S.String,
25
+ parameters: JsonSchemaType,
26
+ execute: S.Any,
27
+ });
28
+
29
+ export type LLMToolDefinition = {
30
+ name: string;
31
+ description: string;
32
+ parameters: JsonSchemaType;
33
+ execute: (params: unknown, context: ToolExecutionContext) => Promise<LLMToolResult>;
34
+ };
35
+
36
+ export type ToolExecutionContext = {};
37
+
38
+ export type LLMToolResult =
39
+ | { kind: 'success'; result: unknown }
40
+ | { kind: 'error'; message: string }
41
+ | { kind: 'break'; result: unknown };
42
+
43
+ export const LLMToolResult = Object.freeze({
44
+ /**
45
+ * The tool execution was successful.
46
+ * Gives the result back to the LLM.
47
+ */
48
+ Success: (result: unknown): LLMToolResult => ({ kind: 'success', result }),
49
+
50
+ Error: (message: string): LLMToolResult => ({ kind: 'error', message }),
51
+ /**
52
+ * Stop the conversation and return the result.
53
+ */
54
+ Break: (result: unknown): LLMToolResult => ({ kind: 'break', result }),
55
+ });
56
+
57
+ export type DefineToolParams<Params extends S.Schema.AnyNoContext> = {
58
+ name: string;
59
+ description: string;
60
+ schema: Params;
61
+ execute: (params: S.Schema.Type<Params>, context: ToolExecutionContext) => Promise<LLMToolResult>;
62
+ };
63
+
64
+ export const defineTool = <Params extends S.Schema.AnyNoContext>({
65
+ name,
66
+ description,
67
+ schema,
68
+ execute,
69
+ }: DefineToolParams<Params>): LLMToolDefinition => {
70
+ return {
71
+ name,
72
+ description,
73
+ parameters: toFunctionParameterSchema(schema),
74
+ execute: (params, context) => {
75
+ const sanitized = S.decodeSync(schema)(params);
76
+ return execute(sanitized, context);
77
+ },
78
+ };
79
+ };
80
+
81
+ const toFunctionParameterSchema = (schema: S.Schema.All) => {
82
+ const jsonSchema = toJsonSchema(schema);
83
+ log('tool schema', { jsonSchema });
84
+ for (const key in jsonSchema.properties) {
85
+ if (typeof jsonSchema.properties![key].description !== 'string') {
86
+ throw new Error(`Property missing description: ${key}`);
87
+ }
88
+ }
89
+ return jsonSchema;
90
+ };
@@ -0,0 +1,75 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ // TODO(burdon): Why interface not type?
6
+
7
+ export interface AstNode<Kind extends string> {
8
+ astKind: Kind;
9
+ }
10
+
11
+ export interface Identifier extends AstNode<'Identifier'> {
12
+ name: string;
13
+ }
14
+
15
+ export interface Property {
16
+ key: Identifier;
17
+ value: StringLiteral;
18
+ }
19
+
20
+ export interface Properties extends AstNode<'Properties'> {
21
+ properties: Property[];
22
+ }
23
+
24
+ export interface NodePattern extends AstNode<'NodePattern'> {
25
+ variable: Identifier | null;
26
+ label: Identifier;
27
+ properties: Properties | null;
28
+ }
29
+
30
+ export interface RelationshipPattern extends AstNode<'RelationshipPattern'> {
31
+ variable: Identifier | null;
32
+ label: Identifier;
33
+ properties: Properties | null;
34
+ }
35
+
36
+ export interface GraphPatternConnector extends AstNode<'GraphPatternConnector'> {
37
+ direction: '-' | '->' | '<-';
38
+ }
39
+
40
+ export interface GraphPattern extends AstNode<'GraphPattern'> {
41
+ segments: (NodePattern | RelationshipPattern)[] & {
42
+ separators: GraphPatternConnector[];
43
+ };
44
+ }
45
+
46
+ export interface MatchClause extends AstNode<'MatchClause'> {
47
+ pattern: GraphPattern;
48
+ }
49
+
50
+ export interface StringLiteral extends AstNode<'StringLiteral'> {
51
+ value: string;
52
+ }
53
+
54
+ export interface MemberExpression extends AstNode<'MemberExpression'> {
55
+ path: Identifier[];
56
+ }
57
+
58
+ export interface EqualsPredicate extends AstNode<'EqualsPredicate'> {
59
+ left: MemberExpression;
60
+ right: StringLiteral;
61
+ }
62
+
63
+ export interface WhereClause extends AstNode<'WhereClause'> {
64
+ predicates: EqualsPredicate[];
65
+ }
66
+
67
+ export interface ReturnClause extends AstNode<'ReturnClause'> {
68
+ fields: MemberExpression[];
69
+ }
70
+
71
+ export interface CypherQuery extends AstNode<'CypherQuery'> {
72
+ matchClause: MatchClause;
73
+ whereClause: WhereClause | null;
74
+ returnClause: ReturnClause;
75
+ }
@@ -0,0 +1,137 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ /* eslint-disable unused-imports/no-unused-vars */
6
+ /* eslint-disable @typescript-eslint/consistent-type-imports */
7
+
8
+ import { createRequire } from 'node:module';
9
+ import type { Parjser } from 'parjs';
10
+ import { describe, test } from 'vitest';
11
+
12
+ import { log } from '@dxos/log';
13
+
14
+ import {
15
+ cypherQuery,
16
+ identifier,
17
+ matchClause,
18
+ nodePattern,
19
+ properties,
20
+ returnClause,
21
+ whereClause,
22
+ } from './combinators';
23
+
24
+ describe('Cypher Combinators', () => {
25
+ // TODO(dmaretskyi): Must be like this or it breaks vitest.
26
+ const require = createRequire(import.meta.url);
27
+ const { string, regexp, whitespace } = require('parjs') as typeof import('parjs');
28
+ const { between, many, manySepBy, or, map, then, recover } =
29
+ require('parjs/combinators') as typeof import('parjs/combinators');
30
+
31
+ test('string', ({ expect }) => {
32
+ expect(string('hello').parse('hello')).toMatchObject({ isOk: true });
33
+ });
34
+
35
+ test('string with whitespace', ({ expect }) => {
36
+ const ws = whitespace(); // Parses whitespace
37
+ // many;
38
+ // const optionalWs = many(100)(ws);
39
+
40
+ expect(string('hello').parse('hello')).toMatchObject({ isOk: true });
41
+ });
42
+
43
+ test('optional vairable tag', ({ expect }) => {
44
+ const node = identifier.pipe(
45
+ then(':'),
46
+ recover(() => ({ kind: 'Soft' })),
47
+ or(''),
48
+ then(identifier),
49
+ between('(', ')'),
50
+ );
51
+ const relation = identifier.pipe(
52
+ then(':'),
53
+ recover(() => ({ kind: 'Soft' })),
54
+ or(''),
55
+ then(identifier),
56
+ between('[', ']'),
57
+ );
58
+ const parser = or(node)(relation);
59
+ const input = '[a:b]';
60
+ const result = parser.parse(input);
61
+ log.info('', { result });
62
+ });
63
+
64
+ test('properties', ({ expect }) => {
65
+ const input = "{name: 'John'}";
66
+ const result = properties.parse(input);
67
+ log.info('', { result });
68
+ expect(result).toMatchObject({ isOk: true });
69
+ });
70
+
71
+ test('node pattern', ({ expect }) => {
72
+ testParser(nodePattern, '(:Person)');
73
+ testParser(nodePattern, '(n:Person)');
74
+ testParser(nodePattern, "(n:Person {name: 'John'})");
75
+ });
76
+
77
+ test('match clause', ({ expect }) => {
78
+ testParser(matchClause, 'MATCH (n:Person)');
79
+ testParser(matchClause, 'MATCH (n:Person)-[r:KNOWS]->(m:Person)');
80
+ testParser(matchClause, 'MATCH (o:Org)-[:HAS_EMPLOYEE]->(e:Employee)');
81
+ });
82
+
83
+ test('where clause', ({ expect }) => {
84
+ testParser(whereClause, 'WHERE n.name = "John"');
85
+ testParser(whereClause, 'WHERE o.name = "DXOS" AND p.name = "Composer"');
86
+ });
87
+
88
+ test('return clause', ({ expect }) => {
89
+ testParser(returnClause, 'RETURN n, m');
90
+ testParser(returnClause, 'RETURN m.name AS employee');
91
+
92
+ const cases = ['RETURN n, m'];
93
+ for (const input of cases) {
94
+ const result = returnClause.parse(input);
95
+ log.info('', { result });
96
+ expect(result).toMatchObject({ isOk: true });
97
+ }
98
+ });
99
+
100
+ test('query', async ({ expect }) => {
101
+ testParser(cypherQuery, 'MATCH (n:Person) RETURN n');
102
+ testParser(
103
+ cypherQuery,
104
+ `
105
+ MATCH (n:Person {name: 'John'})-[r:KNOWS]->(m:Person)
106
+ RETURN n, m
107
+ `,
108
+ );
109
+ testParser(
110
+ cypherQuery,
111
+ `
112
+ MATCH (n:Person)-[r:KNOWS]->(m:Person)
113
+ WHERE n.name = "John"
114
+ RETURN n, m
115
+ `,
116
+ );
117
+ testParser(
118
+ cypherQuery,
119
+ 'MATCH (o:Org)-[:HAS_EMPLOYEE]->(e:Employee)-[:WORKS_ON]->(p:Project) WHERE o.name = "DXOS" AND p.name = "Composer" RETURN e.name, e.id, p.name, p.id',
120
+ );
121
+ testParser(
122
+ cypherQuery,
123
+ 'MATCH (n:Org {name: "DXOS"})-[:HAS_EMPLOYEE]->(m:Employee)<-[:WORKS_ON]-(p:Project {name: "Composer"}) RETURN m.name AS employee',
124
+ );
125
+ testParser(
126
+ cypherQuery,
127
+ "MATCH (org:Org {name: 'DXOS'})-[:ORG_EMPLOYEES]->(c:Contact)<-[:TASK_ASSIGNEE]-(t:Task)-[:TASK_PROJECT]->(p:Project {name: 'Composer'}) RETURN c.name",
128
+ );
129
+ });
130
+ });
131
+
132
+ const testParser = (parser: Parjser<any>, input: string) => {
133
+ const result = parser.parse(input);
134
+ if (!result.isOk) {
135
+ throw new Error(`Failed to parse: ${result}`);
136
+ }
137
+ };
@@ -0,0 +1,149 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ /* eslint-disable unused-imports/no-unused-vars */
6
+ /* eslint-disable @typescript-eslint/consistent-type-imports */
7
+
8
+ import type {
9
+ CypherQuery,
10
+ EqualsPredicate,
11
+ GraphPattern,
12
+ GraphPatternConnector,
13
+ Identifier,
14
+ MatchClause,
15
+ MemberExpression,
16
+ NodePattern,
17
+ Properties,
18
+ Property,
19
+ RelationshipPattern,
20
+ ReturnClause,
21
+ StringLiteral,
22
+ WhereClause,
23
+ } from './ast';
24
+
25
+ import { Parjser, parjs, parjsCombinators } from './parjs';
26
+
27
+ // Helper parsers
28
+ const ws = parjs.whitespace(); // Parses whitespace
29
+
30
+ const nothing = parjs.string('').pipe(parjsCombinators.map(() => null)); // Parses nothing
31
+
32
+ const optional = <T>(parser: Parjser<T>) =>
33
+ parser.pipe(
34
+ parjsCombinators.recover(() => ({ kind: 'Soft' })),
35
+ parjsCombinators.or(nothing),
36
+ ); // Makes a parser optional
37
+
38
+ const keyword = (word: string) => parjs.string(word).pipe(parjsCombinators.map(() => word)); // Parses specific keywords
39
+
40
+ // Identifiers (node labels, relationship types, variables, etc.)
41
+ export const identifier = parjs.regexp(/[a-zA-Z_][a-zA-Z0-9_]*/).pipe(
42
+ parjsCombinators.map((match): Identifier => ({ astKind: 'Identifier', name: match[0] })), // Extract the matched string
43
+ );
44
+
45
+ // Literals (numbers and strings)
46
+ const numberLiteral = parjs.regexp(/-?\d+(\.\d+)?/).pipe(parjsCombinators.map(Number)); // Numbers
47
+ const stringLiteral = parjs.regexp(/"([^"]*)"|'([^']*)'/).pipe(
48
+ parjsCombinators.map((match): StringLiteral => ({ astKind: 'StringLiteral', value: match[1] || match[2] })), // Extract content without quotes
49
+ );
50
+
51
+ // Node pattern (e.g., "(n:Person {name: 'John'})"
52
+ const variableTag = identifier.pipe(parjsCombinators.map((variable) => variable)).expects('variable tag');
53
+ export const label = identifier.pipe(parjsCombinators.map((name) => name));
54
+ export const property = identifier.pipe(
55
+ parjsCombinators.then(parjs.string(':'), ws, stringLiteral),
56
+ parjsCombinators.map(([key, , , value]): Property => ({ key, value })),
57
+ );
58
+ export const properties = property.pipe(
59
+ parjsCombinators.manySepBy(','),
60
+ parjsCombinators.between('{', '}'),
61
+ parjsCombinators.map((properties): Properties => ({ astKind: 'Properties', properties })),
62
+ );
63
+ export const nodePattern = optional(variableTag).pipe(
64
+ parjsCombinators.then(':'),
65
+ parjsCombinators.then(ws, label, ws),
66
+ parjsCombinators.then(optional(properties)),
67
+ parjsCombinators.between('(', ')'),
68
+ parjsCombinators.map(
69
+ ([[[variable], _, label], properties]): NodePattern => ({ astKind: 'NodePattern', variable, label, properties }),
70
+ ),
71
+ );
72
+
73
+ export const relationshipPattern = optional(variableTag).pipe(
74
+ parjsCombinators.then(':'),
75
+ parjsCombinators.then(ws, label, ws),
76
+ parjsCombinators.then(optional(properties)),
77
+ parjsCombinators.between('[', ']'),
78
+ parjsCombinators.map(
79
+ ([[[variable], _, label], properties]): RelationshipPattern => ({
80
+ astKind: 'RelationshipPattern',
81
+ variable,
82
+ label,
83
+ properties,
84
+ }),
85
+ ),
86
+ );
87
+
88
+ const graphPatternConnector = parjs.string('->').pipe(
89
+ parjsCombinators.or('<-' as const, '-' as const),
90
+ parjsCombinators.map((x): GraphPatternConnector => ({ astKind: 'GraphPatternConnector', direction: x })),
91
+ );
92
+
93
+ export const graphPattern = nodePattern.pipe(
94
+ parjsCombinators.or(relationshipPattern),
95
+ parjsCombinators.manySepBy(graphPatternConnector),
96
+ parjsCombinators.map((segments): GraphPattern => ({ astKind: 'GraphPattern', segments })),
97
+ );
98
+
99
+ export const matchClause = keyword('MATCH').pipe(
100
+ parjsCombinators.then(ws),
101
+ parjsCombinators.then(graphPattern),
102
+ parjsCombinators.map(([, pattern]): MatchClause => ({ astKind: 'MatchClause', pattern })),
103
+ );
104
+
105
+ const memberExpression = identifier.pipe(
106
+ parjsCombinators.manySepBy('.'),
107
+ parjsCombinators.map((path): MemberExpression => ({ astKind: 'MemberExpression', path })),
108
+ );
109
+
110
+ const equalsPredicate = memberExpression.pipe(
111
+ parjsCombinators.then(ws, parjs.string('='), ws, stringLiteral),
112
+ parjsCombinators.map(([left, , , , right]): EqualsPredicate => ({ astKind: 'EqualsPredicate', left, right })),
113
+ );
114
+
115
+ export const whereClause = keyword('WHERE').pipe(
116
+ parjsCombinators.then(
117
+ ws,
118
+ equalsPredicate.pipe(parjsCombinators.between(ws, ws), parjsCombinators.manySepBy(parjs.string('AND'))),
119
+ ws,
120
+ ),
121
+ parjsCombinators.map(([, , predicates]): WhereClause => ({ astKind: 'WhereClause', predicates })),
122
+ );
123
+
124
+ export const returnClause = keyword('RETURN').pipe(
125
+ parjsCombinators.then(
126
+ ws,
127
+ memberExpression.pipe(
128
+ parjsCombinators.then(
129
+ optional(parjs.string('AS').pipe(parjsCombinators.between(ws), parjsCombinators.then(identifier))),
130
+ ws,
131
+ ),
132
+ parjsCombinators.manySepBy(parjs.string(',').pipe(parjsCombinators.then(ws))),
133
+ ),
134
+ ws,
135
+ ),
136
+ parjsCombinators.map(([, , fields]): ReturnClause => ({ astKind: 'ReturnClause', fields: fields.map(([f]) => f) })),
137
+ );
138
+
139
+ export const cypherQuery = ws.pipe(
140
+ parjsCombinators.then(matchClause, ws, optional(whereClause), ws, returnClause, ws),
141
+ parjsCombinators.map(
142
+ ([, matchClause, , whereClause, , returnClause]): CypherQuery => ({
143
+ astKind: 'CypherQuery',
144
+ matchClause,
145
+ whereClause,
146
+ returnClause,
147
+ }),
148
+ ),
149
+ );
@@ -0,0 +1,105 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { raise } from '@dxos/debug';
6
+ import { type EchoDatabase, Filter, type ReactiveEchoObject } from '@dxos/echo-db';
7
+ import { type S, getSchemaTypename, StoredSchema, toJsonSchema } from '@dxos/echo-schema';
8
+ import { getSchema } from '@dxos/live-object';
9
+ import { log } from '@dxos/log';
10
+
11
+ import type { DataSource, Node, Relationship } from './query-executor';
12
+ import { formatInferredRelationshipLabel, formatNodeLabel, isReference } from './schema';
13
+
14
+ export class EchoDataSource implements DataSource {
15
+ constructor(private readonly _db: EchoDatabase) {}
16
+
17
+ @log.method()
18
+ async getNodes({ label }: { label?: string }): Promise<Node[]> {
19
+ if (!label) {
20
+ const { objects } = await this._db.query().run();
21
+ return objects.map(this._objectToNode);
22
+ }
23
+
24
+ const schema = (await this._getAllSchema()).find((s) => getSchemaTypename(s)?.endsWith(label));
25
+
26
+ if (!schema) {
27
+ return [];
28
+ }
29
+
30
+ const { objects } = await this._db.query(Filter.schema(schema)).run();
31
+ return objects.map(this._objectToNode);
32
+ }
33
+
34
+ @log.method()
35
+ async getRelationships({ label }: { label?: string }): Promise<Relationship[]> {
36
+ const syntheticRefRelationships = (await this._getAllSchema())
37
+ .filter((schema) => getSchemaTypename(schema) !== StoredSchema.typename)
38
+ .flatMap((schema) => {
39
+ const jsonSchema = toJsonSchema(schema);
40
+ const relationships = Object.entries(jsonSchema.properties!).filter(([key, prop]) => isReference(prop));
41
+
42
+ return relationships.map(([key, value]) => {
43
+ const relationshipLabel = formatInferredRelationshipLabel(
44
+ jsonSchema.typename ?? raise(new Error('Schema must have typename')),
45
+ key,
46
+ );
47
+
48
+ return {
49
+ label: relationshipLabel,
50
+ schema,
51
+ property: key,
52
+ };
53
+ });
54
+ });
55
+ const relationship = syntheticRefRelationships.find((r) => r.label === label);
56
+ if (!relationship) {
57
+ return []; // TODO(dmaretskyi): Handle real relationships.
58
+ }
59
+
60
+ const { objects } = await this._db.query(Filter.schema(relationship.schema)).run();
61
+ return (
62
+ await Promise.all(objects.map((object) => this._projectRefRelationship(object, relationship.property)))
63
+ ).flat();
64
+ }
65
+
66
+ private async _getAllSchema(): Promise<S.Schema.AnyNoContext[]> {
67
+ return [
68
+ ...(await this._db.schemaRegistry.query().run()),
69
+ // TODO(dmaretskyi): Remove once we can serialize recursive schema.
70
+ ...this._db.graph.schemaRegistry.schemas.filter((s) => getSchemaTypename(s)?.startsWith('example.org')),
71
+ ].filter((schema) => getSchemaTypename(schema) !== StoredSchema.typename);
72
+ }
73
+
74
+ private _objectToNode(object: ReactiveEchoObject<any>): Node {
75
+ const { id, ...properties } = object;
76
+ return {
77
+ id,
78
+ kind: 'node',
79
+ label: formatNodeLabel(getSchemaTypename(getSchema(object)!)!),
80
+ properties,
81
+ };
82
+ }
83
+
84
+ private async _projectRefRelationship(object: ReactiveEchoObject<any>, prop: string): Promise<Relationship[]> {
85
+ if (!object[prop]) {
86
+ return [];
87
+ }
88
+
89
+ const value = Array.isArray(object[prop]) ? object[prop] : [object[prop]];
90
+
91
+ return Promise.all(
92
+ value.map(async (ref): Promise<Relationship> => {
93
+ const target = await ref.load();
94
+ return {
95
+ id: `${object.id}-${prop}-${target.id}`,
96
+ kind: 'relationship',
97
+ label: formatInferredRelationshipLabel(getSchemaTypename(getSchema(object)!)!, prop),
98
+ properties: {},
99
+ source: this._objectToNode(object),
100
+ target: this._objectToNode(target),
101
+ };
102
+ }),
103
+ );
104
+ }
105
+ }
@@ -0,0 +1,42 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { log } from '@dxos/log';
8
+
9
+ import { createExecutionPlan } from './execution-plan';
10
+ import { parseCypherQuery } from './parser';
11
+
12
+ describe('Execution Plan', () => {
13
+ test('trivial', ({ expect }) => {
14
+ const plan = createExecutionPlan(parseCypherQuery('MATCH (n:Person) RETURN n'));
15
+ log.info('', { plan });
16
+ expect(plan.steps.map((step) => step.type)).toEqual(['NodeScan', 'ProduceResults']);
17
+ });
18
+
19
+ test('complex query', ({ expect }) => {
20
+ const plan = createExecutionPlan(
21
+ parseCypherQuery(
22
+ "MATCH (org:Org {name: 'DXOS'})-[:ORG_EMPLOYEES]->(c:Contact)<-[:TASK_ASSIGNEE]-(:Task)-[:TASK_PROJECT]->(p:Project {name: 'Composer'}) RETURN c.name",
23
+ ),
24
+ );
25
+ log.info('', { plan });
26
+ for (const step of plan.steps) {
27
+ log.info('', { step });
28
+ }
29
+ expect(plan.steps.map((step) => step.type)).toEqual([
30
+ 'NodeScan',
31
+ 'FilterByProperty',
32
+ 'RelationshipScan',
33
+ 'FilterByLabel',
34
+ 'RelationshipScan',
35
+ 'FilterByLabel',
36
+ 'RelationshipScan',
37
+ 'FilterByLabel',
38
+ 'FilterByProperty',
39
+ 'ProduceResults',
40
+ ]);
41
+ });
42
+ });