@dxos/assistant 0.8.3 → 0.8.4-main.2e9d522
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/dist/lib/browser/index.mjs +760 -1087
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +760 -1087
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/context/binder.d.ts +34 -0
- package/dist/types/src/context/binder.d.ts.map +1 -0
- package/dist/types/src/context/binding.d.ts +56 -0
- package/dist/types/src/context/binding.d.ts.map +1 -0
- package/dist/types/src/context/index.d.ts +3 -0
- package/dist/types/src/context/index.d.ts.map +1 -0
- package/dist/types/src/conversation/conversation.d.ts +38 -0
- package/dist/types/src/conversation/conversation.d.ts.map +1 -0
- package/dist/types/src/conversation/index.d.ts +2 -0
- package/dist/types/src/conversation/index.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +38 -0
- package/dist/types/src/errors.d.ts.map +1 -0
- package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +1 -1
- package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +135 -41
- package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +1 -1
- package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts +1 -1
- package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts.map +1 -1
- package/dist/types/src/extraction/extraction.d.ts +100 -42
- package/dist/types/src/extraction/extraction.d.ts.map +1 -1
- package/dist/types/src/functions/index.d.ts +2 -0
- package/dist/types/src/functions/index.d.ts.map +1 -0
- package/dist/types/src/functions/services.d.ts +6 -0
- package/dist/types/src/functions/services.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/research/exa.d.ts +10 -5
- package/dist/types/src/research/exa.d.ts.map +1 -1
- package/dist/types/src/research/graph.d.ts +18 -8
- package/dist/types/src/research/graph.d.ts.map +1 -1
- package/dist/types/src/research/research.d.ts +1 -1
- package/dist/types/src/research/research.d.ts.map +1 -1
- package/dist/types/src/session/session.d.ts +183 -184
- package/dist/types/src/session/session.d.ts.map +1 -1
- package/dist/types/src/session/version-pin.d.ts +2 -2
- package/dist/types/src/session/version-pin.d.ts.map +1 -1
- package/dist/types/src/templates/index.d.ts +2 -0
- package/dist/types/src/templates/index.d.ts.map +1 -0
- package/dist/types/src/templates/prompt.d.ts +30 -0
- package/dist/types/src/templates/prompt.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +10 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -30
- package/src/context/binder.ts +103 -0
- package/src/context/binding.ts +32 -0
- package/src/context/index.ts +6 -0
- package/src/conversation/conversation.ts +96 -0
- package/src/conversation/index.ts +5 -0
- package/src/errors.ts +10 -0
- package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +6 -18
- package/src/extraction/extraction-llm-function/extraction-llm-function.ts +19 -14
- package/src/extraction/extraction-ner-function/extraction-ner-function.ts +6 -3
- package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +2 -2
- package/src/extraction/extraction-ner-function/named-entity-recognition.ts +1 -1
- package/src/functions/index.ts +5 -0
- package/src/functions/services.ts +139 -0
- package/src/index.ts +4 -1
- package/src/research/exa.ts +56 -59
- package/src/research/graph.test.ts +50 -1
- package/src/research/graph.ts +85 -53
- package/src/research/instructions-research.tpl +1 -0
- package/src/research/research.test.ts +27 -27
- package/src/research/research.ts +50 -52
- package/src/session/__snapshots__/session.test.ts.snap +32 -0
- package/src/session/session.test.ts +99 -18
- package/src/session/session.ts +230 -443
- package/src/session/version-pin.test.ts +2 -21
- package/src/session/version-pin.ts +5 -5
- package/src/summarization/summarization.test.ts +75 -88
- package/src/templates/assistant.tpl +111 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/prompt.ts +45 -0
- package/src/types.ts +40 -1
- package/dist/lib/node/index.cjs +0 -2178
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/types/src/blueprint/ai.test.d.ts +0 -2
- package/dist/types/src/blueprint/ai.test.d.ts.map +0 -1
- package/dist/types/src/blueprint/blueprint.d.ts +0 -55
- package/dist/types/src/blueprint/blueprint.d.ts.map +0 -1
- package/dist/types/src/blueprint/blueprint.test.d.ts +0 -2
- package/dist/types/src/blueprint/blueprint.test.d.ts.map +0 -1
- package/dist/types/src/blueprint/index.d.ts +0 -4
- package/dist/types/src/blueprint/index.d.ts.map +0 -1
- package/dist/types/src/blueprint/logger.d.ts +0 -25
- package/dist/types/src/blueprint/logger.d.ts.map +0 -1
- package/dist/types/src/blueprint/machine.d.ts +0 -143
- package/dist/types/src/blueprint/machine.d.ts.map +0 -1
- package/dist/types/src/blueprint/test-data.d.ts +0 -3
- package/dist/types/src/blueprint/test-data.d.ts.map +0 -1
- package/src/blueprint/ai.test.ts +0 -156
- package/src/blueprint/blueprint.test.ts +0 -151
- package/src/blueprint/blueprint.ts +0 -75
- package/src/blueprint/index.ts +0 -7
- package/src/blueprint/logger.ts +0 -155
- package/src/blueprint/machine.ts +0 -251
- package/src/blueprint/test-data.ts +0 -55
package/src/research/exa.ts
CHANGED
|
@@ -2,78 +2,75 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { AiTool, AiToolkit } from '@effect/ai';
|
|
6
|
+
import { Effect, Schema } from 'effect';
|
|
6
7
|
import Exa from 'exa-js';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { CredentialsService } from '@dxos/functions';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import { SEARCH_RESULTS } from '../testing';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const createExaTool = ({ apiKey }: CreateExaToolOptions) => {
|
|
18
|
-
const exa = new Exa(apiKey);
|
|
19
|
-
|
|
20
|
-
return createTool('search', {
|
|
21
|
-
name: 'web_search',
|
|
14
|
+
export class ExaToolkit extends AiToolkit.make(
|
|
15
|
+
AiTool.make('exa_search', {
|
|
22
16
|
description: 'Search the web for information',
|
|
23
|
-
|
|
17
|
+
parameters: {
|
|
24
18
|
query: Schema.String.annotations({ description: 'The query to search for' }),
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
},
|
|
20
|
+
success: Schema.Unknown,
|
|
21
|
+
failure: Schema.Never,
|
|
22
|
+
}).addRequirement<CredentialsService>(),
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
export const LiveExaHandler = ExaToolkit.toLayer({
|
|
26
|
+
exa_search: Effect.fn(function* ({ query }) {
|
|
27
|
+
const credential = yield* CredentialsService.getCredential({ service: 'exa.ai' });
|
|
28
|
+
const exa = new Exa(credential.apiKey);
|
|
29
|
+
|
|
30
|
+
const context = yield* Effect.promise(async () =>
|
|
31
|
+
exa.searchAndContents(query, {
|
|
28
32
|
type: 'auto',
|
|
29
33
|
text: {
|
|
30
34
|
maxCharacters: 3_000,
|
|
31
35
|
},
|
|
32
36
|
livecrawl: 'always',
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
null as (typeof SEARCH_RESULTS)[0] | null,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
return ToolResult.Success(result);
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
};
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
log.info('exa search', { query, costDollars: context.costDollars });
|
|
41
|
+
|
|
42
|
+
return context;
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const MockExaHandler = ExaToolkit.toLayer({
|
|
47
|
+
exa_search: Effect.fn(function* ({ query }) {
|
|
48
|
+
// Find result with closest matching autoprompt using weighted Levenshtein distance
|
|
49
|
+
const result = SEARCH_RESULTS.reduce(
|
|
50
|
+
(closest, current) => {
|
|
51
|
+
if (!current.autopromptString) {
|
|
52
|
+
return closest;
|
|
53
|
+
}
|
|
54
|
+
if (!closest) {
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Calculate Levenshtein distance
|
|
59
|
+
const dist1 = levenshteinDistance(query, current.autopromptString);
|
|
60
|
+
const dist2 = levenshteinDistance(query, closest.autopromptString || '');
|
|
61
|
+
|
|
62
|
+
// Weight by length of the longer string to normalize
|
|
63
|
+
const weight1 = dist1 / Math.max(query.length, current.autopromptString.length);
|
|
64
|
+
const weight2 = dist2 / Math.max(query.length, closest.autopromptString?.length || 0);
|
|
65
|
+
|
|
66
|
+
return weight1 < weight2 ? current : closest;
|
|
67
|
+
},
|
|
68
|
+
null as (typeof SEARCH_RESULTS)[0] | null,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
77
74
|
|
|
78
75
|
const levenshteinDistance = (str1: string, str2: string): number => {
|
|
79
76
|
const m = str1.length;
|
|
@@ -2,14 +2,63 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { AnthropicClient } from '@effect/ai-anthropic';
|
|
6
|
+
import { FetchHttpClient } from '@effect/platform';
|
|
7
|
+
import { describe, it } from '@effect/vitest';
|
|
8
|
+
import { Config, Effect, Layer, pipe } from 'effect';
|
|
9
|
+
|
|
10
|
+
import { AiService, AiServiceRouter, ToolExecutionService, ToolResolverService } from '@dxos/ai';
|
|
11
|
+
import { tapHttpErrors } from '@dxos/ai/testing';
|
|
12
|
+
import { TestHelpers } from '@dxos/effect';
|
|
13
|
+
import { DatabaseService } from '@dxos/functions';
|
|
14
|
+
import { log } from '@dxos/log';
|
|
15
|
+
import { DataType } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
import { makeGraphWriterHandler, makeGraphWriterToolkit } from './graph';
|
|
18
|
+
import { AiSession } from '../session';
|
|
6
19
|
|
|
7
20
|
// import { type EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
8
21
|
|
|
22
|
+
const TestLayer = pipe(
|
|
23
|
+
AiService.model('@anthropic/claude-3-5-sonnet-20241022'),
|
|
24
|
+
Layer.provideMerge(DatabaseService.notAvailable),
|
|
25
|
+
Layer.provideMerge(ToolResolverService.layerEmpty),
|
|
26
|
+
Layer.provideMerge(ToolExecutionService.layerEmpty),
|
|
27
|
+
Layer.provide(AiServiceRouter.AiServiceRouter),
|
|
28
|
+
Layer.provide(
|
|
29
|
+
AnthropicClient.layerConfig({
|
|
30
|
+
apiKey: Config.redacted('ANTHROPIC_API_KEY'),
|
|
31
|
+
transformClient: tapHttpErrors,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
Layer.provide(FetchHttpClient.layer),
|
|
35
|
+
);
|
|
36
|
+
|
|
9
37
|
describe('graph', () => {
|
|
10
38
|
// let builder: EchoTestBuilder;
|
|
11
39
|
// test('findRelatedSchema', async () => {
|
|
12
40
|
// const db = await createEchoDatabase();
|
|
13
41
|
// const relatedSchemas = await findRelatedSchema(db, Schema.Struct({}));
|
|
14
42
|
// });
|
|
43
|
+
|
|
44
|
+
it.effect.skip(
|
|
45
|
+
'calculator',
|
|
46
|
+
Effect.fn(
|
|
47
|
+
function* ({ expect }) {
|
|
48
|
+
const graphWriteToolkit = makeGraphWriterToolkit({ schema: [DataType.Project] });
|
|
49
|
+
|
|
50
|
+
const session = new AiSession();
|
|
51
|
+
const response = yield* session
|
|
52
|
+
.run({
|
|
53
|
+
prompt: 'What is 10 + 20?',
|
|
54
|
+
history: [],
|
|
55
|
+
toolkit: graphWriteToolkit,
|
|
56
|
+
})
|
|
57
|
+
.pipe(Effect.provide(makeGraphWriterHandler(graphWriteToolkit)));
|
|
58
|
+
log.info('response', { response });
|
|
59
|
+
},
|
|
60
|
+
Effect.provide(TestLayer),
|
|
61
|
+
TestHelpers.runIf(process.env.ANTHROPIC_API_KEY),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
15
64
|
});
|
package/src/research/graph.ts
CHANGED
|
@@ -2,30 +2,31 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { AiTool, AiToolkit } from '@effect/ai';
|
|
6
|
+
import { Context, Effect, identity, Option, Schema, SchemaAST } from 'effect';
|
|
6
7
|
|
|
7
|
-
import { createTool, ToolResult } from '@dxos/ai';
|
|
8
8
|
import type { Obj, Relation } from '@dxos/echo';
|
|
9
|
-
import {
|
|
9
|
+
import { Filter, Query } from '@dxos/echo';
|
|
10
10
|
import { type EchoDatabase, type Queue } from '@dxos/echo-db';
|
|
11
11
|
import { isEncodedReference } from '@dxos/echo-protocol';
|
|
12
12
|
import {
|
|
13
|
+
create,
|
|
13
14
|
EntityKind,
|
|
15
|
+
getEntityKind,
|
|
16
|
+
getSchemaDXN,
|
|
14
17
|
getSchemaTypename,
|
|
15
18
|
getTypeAnnotation,
|
|
16
19
|
getTypeIdentifierAnnotation,
|
|
17
|
-
create,
|
|
18
|
-
getEntityKind,
|
|
19
|
-
getSchemaDXN,
|
|
20
20
|
ObjectId,
|
|
21
21
|
ReferenceAnnotationId,
|
|
22
|
-
type BaseObject,
|
|
23
22
|
RelationSourceDXNId,
|
|
24
|
-
RelationTargetDXNId,
|
|
25
23
|
RelationSourceId,
|
|
24
|
+
RelationTargetDXNId,
|
|
26
25
|
RelationTargetId,
|
|
26
|
+
type BaseObject,
|
|
27
27
|
} from '@dxos/echo-schema';
|
|
28
28
|
import { mapAst } from '@dxos/effect';
|
|
29
|
+
import { ContextQueueService, DatabaseService } from '@dxos/functions';
|
|
29
30
|
import { DXN } from '@dxos/keys';
|
|
30
31
|
import { log } from '@dxos/log';
|
|
31
32
|
import { deepMapValues, isNonNullable } from '@dxos/util';
|
|
@@ -96,33 +97,80 @@ const isSchemaAddressableByDxn = (schema: Schema.Schema.AnyNoContext, dxn: DXN):
|
|
|
96
97
|
/**
|
|
97
98
|
* Perform vector search in the local database.
|
|
98
99
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// TODO(dmaretskyi): Rename `GraphReadToolkit`.
|
|
101
|
+
export class LocalSearchToolkit extends AiToolkit.make(
|
|
102
|
+
AiTool.make('search_local_search', {
|
|
102
103
|
description: 'Search the local database for information using a vector index',
|
|
103
|
-
|
|
104
|
+
parameters: {
|
|
104
105
|
query: Schema.String.annotations({
|
|
105
106
|
description: 'The query to search for. Could be a question or a topic or a set of keywords.',
|
|
106
107
|
}),
|
|
107
|
-
}),
|
|
108
|
-
execute: async ({ query }) => {
|
|
109
|
-
const { objects } = await db.query(Query.select(Filter.text(query, { type: 'vector' }))).run();
|
|
110
|
-
const results = [...objects];
|
|
111
|
-
if (queue) {
|
|
112
|
-
const queueObjects = await queue.queryObjects();
|
|
113
|
-
// TODO(dmaretskyi): Text search on the queue.
|
|
114
|
-
results.push(...queueObjects);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return ToolResult.Success(`
|
|
118
|
-
<local_context>
|
|
119
|
-
${JSON.stringify(results, null, 2)}
|
|
120
|
-
</local_context>
|
|
121
|
-
`);
|
|
122
108
|
},
|
|
123
|
-
|
|
109
|
+
success: Schema.Unknown,
|
|
110
|
+
failure: Schema.Never,
|
|
111
|
+
}).addRequirement<DatabaseService>(),
|
|
112
|
+
) {}
|
|
113
|
+
|
|
114
|
+
export const LocalSearchHandler = LocalSearchToolkit.toLayer({
|
|
115
|
+
search_local_search: Effect.fn(function* ({ query }) {
|
|
116
|
+
const { objects } = yield* DatabaseService.runQuery(Query.select(Filter.text(query, { type: 'vector' })));
|
|
117
|
+
const results = [...objects];
|
|
118
|
+
|
|
119
|
+
const contextQueue = yield* Effect.serviceOption(ContextQueueService);
|
|
120
|
+
if (Option.isSome(contextQueue)) {
|
|
121
|
+
const queueObjects = yield* Effect.promise(() => contextQueue.value.contextQueue.queryObjects());
|
|
122
|
+
// TODO(dmaretskyi): Text search on the queue.
|
|
123
|
+
results.push(...queueObjects);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `
|
|
127
|
+
<local_context>
|
|
128
|
+
${JSON.stringify(results, null, 2)}
|
|
129
|
+
</local_context>
|
|
130
|
+
`;
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Attached as an annotation to the writer tool.
|
|
136
|
+
*/
|
|
137
|
+
class GraphWriterSchema extends Context.Tag('GraphWriterSchema')<
|
|
138
|
+
GraphWriterSchema,
|
|
139
|
+
{ schema: Schema.Schema.AnyNoContext[] }
|
|
140
|
+
>() {}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Forms typed objects that can be written to the graph database.
|
|
144
|
+
*/
|
|
145
|
+
export const makeGraphWriterToolkit = ({ schema }: { schema: Schema.Schema.AnyNoContext[] }) => {
|
|
146
|
+
return AiToolkit.make(
|
|
147
|
+
AiTool.make('graph_writer', {
|
|
148
|
+
description: 'Write to the local graph database',
|
|
149
|
+
parameters: createExtractionSchema(schema).fields,
|
|
150
|
+
success: Schema.Unknown,
|
|
151
|
+
failure: Schema.Never,
|
|
152
|
+
})
|
|
153
|
+
.addRequirement<DatabaseService>()
|
|
154
|
+
.annotateContext(Context.make(GraphWriterSchema, { schema })),
|
|
155
|
+
);
|
|
124
156
|
};
|
|
125
157
|
|
|
158
|
+
export const makeGraphWriterHandler = (toolkit: ReturnType<typeof makeGraphWriterToolkit>) => {
|
|
159
|
+
const { schema } = Context.get(
|
|
160
|
+
toolkit.tools.graph_writer.annotations as Context.Context<GraphWriterSchema>,
|
|
161
|
+
GraphWriterSchema,
|
|
162
|
+
);
|
|
163
|
+
return toolkit.toLayer({
|
|
164
|
+
graph_writer: Effect.fn(function* (input) {
|
|
165
|
+
const { db } = yield* DatabaseService;
|
|
166
|
+
const contextQueue = yield* Effect.serviceOption(ContextQueueService);
|
|
167
|
+
const data = yield* Effect.promise(() =>
|
|
168
|
+
sanitizeObjects(schema, input as any, db, contextQueue.pipe(Option.getOrUndefined)?.contextQueue),
|
|
169
|
+
);
|
|
170
|
+
return data as Obj.Any[];
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
};
|
|
126
174
|
/**
|
|
127
175
|
* Create a schema for structured data extraction.
|
|
128
176
|
*/
|
|
@@ -282,15 +330,21 @@ const SoftRef = Schema.Struct({
|
|
|
282
330
|
const preprocessSchema = (schema: Schema.Schema.AnyNoContext) => {
|
|
283
331
|
const isRelationSchema = getEntityKind(schema) === 'relation';
|
|
284
332
|
|
|
285
|
-
const go = (ast: SchemaAST.AST): SchemaAST.AST => {
|
|
333
|
+
const go = (ast: SchemaAST.AST, visited = new Set<SchemaAST.AST>()): SchemaAST.AST => {
|
|
334
|
+
if (visited.has(ast)) {
|
|
335
|
+
// Already visited this node, prevent infinite recursion.
|
|
336
|
+
return ast;
|
|
337
|
+
}
|
|
338
|
+
visited.add(ast);
|
|
339
|
+
|
|
286
340
|
if (SchemaAST.getAnnotation(ast, ReferenceAnnotationId).pipe(Option.isSome)) {
|
|
287
341
|
return SoftRef.ast;
|
|
288
342
|
}
|
|
289
343
|
|
|
290
|
-
return mapAst(ast, go);
|
|
344
|
+
return mapAst(ast, (child) => go(child, visited));
|
|
291
345
|
};
|
|
292
346
|
|
|
293
|
-
return Schema.make<any, any, never>(mapAst(schema.ast, go)).pipe(
|
|
347
|
+
return Schema.make<any, any, never>(mapAst(schema.ast, (ast) => go(ast))).pipe(
|
|
294
348
|
Schema.omit('id'),
|
|
295
349
|
Schema.extend(
|
|
296
350
|
Schema.Struct({
|
|
@@ -313,25 +367,3 @@ const preprocessSchema = (schema: Schema.Schema.AnyNoContext) => {
|
|
|
313
367
|
: identity<Schema.Schema.AnyNoContext>,
|
|
314
368
|
);
|
|
315
369
|
};
|
|
316
|
-
|
|
317
|
-
export const createGraphWriterTool = ({
|
|
318
|
-
db,
|
|
319
|
-
queue,
|
|
320
|
-
schema,
|
|
321
|
-
onDone = async (x) => x,
|
|
322
|
-
}: {
|
|
323
|
-
db: EchoDatabase;
|
|
324
|
-
queue?: Queue;
|
|
325
|
-
schema: Schema.Schema.AnyNoContext[];
|
|
326
|
-
onDone?: (data: Obj.Any[]) => Promise<any>;
|
|
327
|
-
}) => {
|
|
328
|
-
return createTool('graph', {
|
|
329
|
-
name: 'writer',
|
|
330
|
-
description: 'Write to the local graph database',
|
|
331
|
-
schema: createExtractionSchema(schema),
|
|
332
|
-
execute: async (input) => {
|
|
333
|
-
const data = await sanitizeObjects(schema, input as any, db, queue);
|
|
334
|
-
return ToolResult.Success(await onDone(data as Obj.Any[]));
|
|
335
|
-
},
|
|
336
|
-
});
|
|
337
|
-
};
|
|
@@ -25,6 +25,7 @@ The Research Agent always follows these principles:
|
|
|
25
25
|
- The Research Agent creates relations and references between new objects found and what's already in the database.
|
|
26
26
|
- The Research Agent does not create objects that are already in the database.
|
|
27
27
|
- The Research Agent re-uses existing object IDs as references when enriching existing objects.
|
|
28
|
+
- The Research Agent ALWAYS calls the `graph_writer` at the end to save the data. This conversation will be delted, so only the data written to the graph will be preserved.
|
|
28
29
|
|
|
29
30
|
The Research Agent may be asked for:
|
|
30
31
|
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { AnthropicClient } from '@effect/ai-anthropic';
|
|
6
|
+
import { FetchHttpClient } from '@effect/platform';
|
|
7
|
+
import { Config, Layer, ManagedRuntime } from 'effect';
|
|
5
8
|
import { inspect } from 'node:util';
|
|
6
|
-
import { beforeAll, describe, test } from 'vitest';
|
|
9
|
+
import { afterAll, beforeAll, describe, test } from 'vitest';
|
|
7
10
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
11
|
+
import { AiService, AiServiceRouter, structuredOutputParser } from '@dxos/ai';
|
|
12
|
+
import { EXA_API_KEY, tapHttpErrors } from '@dxos/ai/testing';
|
|
10
13
|
import { Obj } from '@dxos/echo';
|
|
11
14
|
import { type EchoDatabase } from '@dxos/echo-db';
|
|
12
15
|
import { EchoTestBuilder } from '@dxos/echo-db/testing';
|
|
@@ -17,38 +20,35 @@ import { DataType, DataTypes } from '@dxos/schema';
|
|
|
17
20
|
import { createExtractionSchema, getSanitizedSchemaName } from './graph';
|
|
18
21
|
import { researchFn } from './research';
|
|
19
22
|
|
|
20
|
-
const REMOTE_AI = true;
|
|
21
23
|
const MOCK_SEARCH = false;
|
|
22
24
|
|
|
25
|
+
const AnthropicLayer = AnthropicClient.layerConfig({
|
|
26
|
+
apiKey: Config.redacted('ANTHROPIC_API_KEY'),
|
|
27
|
+
transformClient: tapHttpErrors,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const AiServiceLayer = AiServiceRouter.AiServiceRouter.pipe(
|
|
31
|
+
Layer.provide(AnthropicLayer),
|
|
32
|
+
Layer.provide(FetchHttpClient.layer),
|
|
33
|
+
);
|
|
34
|
+
|
|
23
35
|
describe.skip('Research', () => {
|
|
36
|
+
let runtime: ManagedRuntime.ManagedRuntime<AiService, any>;
|
|
24
37
|
let builder: EchoTestBuilder;
|
|
25
38
|
let db: EchoDatabase;
|
|
26
39
|
let executor: FunctionExecutor;
|
|
27
40
|
|
|
28
41
|
beforeAll(async () => {
|
|
42
|
+
runtime = ManagedRuntime.make(AiServiceLayer);
|
|
43
|
+
|
|
29
44
|
// TODO(dmaretskyi): Helper to scaffold this from a config.
|
|
30
45
|
builder = await new EchoTestBuilder().open();
|
|
31
46
|
|
|
32
47
|
db = (await builder.createDatabase({ indexing: { vector: true } })).db;
|
|
33
|
-
db.graph.schemaRegistry.addSchema(DataTypes);
|
|
34
48
|
|
|
35
49
|
executor = new FunctionExecutor(
|
|
36
50
|
new ServiceContainer().setServices({
|
|
37
|
-
ai:
|
|
38
|
-
client: REMOTE_AI
|
|
39
|
-
? new EdgeAiServiceClient({
|
|
40
|
-
endpoint: AI_SERVICE_ENDPOINT.REMOTE,
|
|
41
|
-
defaultGenerationOptions: {
|
|
42
|
-
// model: '@anthropic/claude-sonnet-4-20250514',
|
|
43
|
-
model: '@anthropic/claude-3-5-sonnet-20241022',
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
: new OllamaAiServiceClient({
|
|
47
|
-
overrides: {
|
|
48
|
-
model: 'llama3.1:8b',
|
|
49
|
-
},
|
|
50
|
-
}),
|
|
51
|
-
},
|
|
51
|
+
ai: await runtime.runPromise(AiService),
|
|
52
52
|
credentials: new ConfiguredCredentialsService([{ service: 'exa.ai', apiKey: EXA_API_KEY }]),
|
|
53
53
|
database: { db },
|
|
54
54
|
tracing: TracingService.console,
|
|
@@ -56,13 +56,13 @@ describe.skip('Research', () => {
|
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await runtime.dispose();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// TODO(dmaretskyi): Refactor using effect.
|
|
64
|
+
test.only('should generate a research report', { timeout: 300_000 }, async () => {
|
|
65
|
+
db.add(Obj.make(DataType.Organization, { name: 'Notion', website: 'https://www.notion.com' }));
|
|
66
66
|
await db.flush({ indexes: true });
|
|
67
67
|
|
|
68
68
|
const result = await executor.invoke(researchFn, {
|
package/src/research/research.ts
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// TODO(burdon): Fix.
|
|
6
|
+
// @ts-nocheck
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import { AiToolkit } from '@effect/ai';
|
|
9
|
+
import { Effect, Layer, Schema } from 'effect';
|
|
10
|
+
|
|
11
|
+
import { AiService, ConsolePrinter } from '@dxos/ai';
|
|
12
|
+
import { defineFunction, TracingService } from '@dxos/functions';
|
|
10
13
|
import { log } from '@dxos/log';
|
|
11
14
|
import { DataTypes } from '@dxos/schema';
|
|
12
15
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
16
|
+
import { ExaToolkit, LiveExaHandler, MockExaHandler } from './exa';
|
|
17
|
+
import { LocalSearchHandler, LocalSearchToolkit, makeGraphWriterHandler, makeGraphWriterToolkit } from './graph';
|
|
15
18
|
// TODO(dmaretskyi): Vite build bug with instruction files with the same filename getting mixed-up
|
|
16
19
|
import PROMPT from './instructions-research.tpl?raw';
|
|
17
|
-
import {
|
|
20
|
+
import { AiSession } from '../session';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Exec external service and return the results as a Subgraph.
|
|
@@ -34,56 +37,51 @@ export const researchFn = defineFunction({
|
|
|
34
37
|
}),
|
|
35
38
|
}),
|
|
36
39
|
outputSchema: Schema.Struct({
|
|
37
|
-
|
|
38
|
-
result: Schema.Any,
|
|
40
|
+
result: Schema.Unknown,
|
|
39
41
|
}),
|
|
40
|
-
handler:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const { db } = context.getService(DatabaseService);
|
|
44
|
-
// const queues = context.getService(QueueService);
|
|
45
|
-
const tracing = context.getService(TracingService);
|
|
46
|
-
|
|
47
|
-
tracing.write(create(AgentStatusReport, { message: 'Researching...' }));
|
|
48
|
-
|
|
49
|
-
const exaCredential = await credentials.getCredential({ service: 'exa.ai' });
|
|
50
|
-
const searchTool = mockSearch ? createMockExaTool() : createExaTool({ apiKey: exaCredential.apiKey! });
|
|
42
|
+
handler: Effect.fnUntraced(
|
|
43
|
+
function* ({ data: { query, mockSearch } }) {
|
|
44
|
+
yield* TracingService.emitStatus({ message: 'Researching...' });
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
const graphWriteToolkit = makeGraphWriterToolkit({ schema: DataTypes });
|
|
47
|
+
const toolkit = yield* AiToolkit.merge(ExaToolkit, LocalSearchToolkit, graphWriteToolkit).pipe(
|
|
48
|
+
Effect.provide(
|
|
49
|
+
Layer.mergeAll(
|
|
50
|
+
mockSearch ? MockExaHandler : LiveExaHandler,
|
|
51
|
+
LocalSearchHandler,
|
|
52
|
+
makeGraphWriterHandler(graphWriteToolkit),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const printer = new ConsolePrinter();
|
|
58
|
+
const session = new AiSession();
|
|
59
|
+
session.message.on((message) => printer.printMessage(message));
|
|
60
|
+
session.userMessage.on((message) => printer.printMessage(message));
|
|
61
|
+
session.block.on((block) => printer.printContentBlock(block));
|
|
62
|
+
session.streamEvent.on((event) => log('stream', { event }));
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
prompt: query,
|
|
74
|
-
});
|
|
64
|
+
// TODO(dmaretskyi): Consider adding this pattern as the "Graph" output mode for the session.
|
|
65
|
+
const result = yield* session.run({
|
|
66
|
+
prompt: query,
|
|
67
|
+
history: [],
|
|
68
|
+
systemPrompt: PROMPT,
|
|
69
|
+
toolkit,
|
|
70
|
+
});
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
return {
|
|
73
|
+
result,
|
|
74
|
+
};
|
|
79
75
|
|
|
80
|
-
|
|
76
|
+
// queues.contextQueue!.append(data);
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
// return {
|
|
79
|
+
// result: `
|
|
80
|
+
// The research results are placed in the following objects:
|
|
81
|
+
// ${data.map((object, id) => `[obj_${id}][dxn:echo:@:${object.id}]`).join('\n')}
|
|
82
|
+
// `,
|
|
83
|
+
// };
|
|
84
|
+
},
|
|
85
|
+
Effect.provide(AiService.model('@anthropic/claude-3-5-sonnet-20241022')),
|
|
86
|
+
),
|
|
89
87
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`AiSession > no tools 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"blocks": ReactiveArray [
|
|
7
|
+
{
|
|
8
|
+
"_tag": "text",
|
|
9
|
+
"text": "Hello world!",
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
"created": "Sun, 20 Jul 2025 10:54:08 GMT",
|
|
13
|
+
"id": "01K0KQGFTFV3GBKGEWZW67K8HK",
|
|
14
|
+
"sender": {
|
|
15
|
+
"role": "user",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"blocks": ReactiveArray [
|
|
20
|
+
{
|
|
21
|
+
"_tag": "text",
|
|
22
|
+
"text": "Hi there! How can I help you today?",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
"created": "2025-07-20T10:54:10.801Z",
|
|
26
|
+
"id": "01K0KQGHVHJ37D4YG45WG090WV",
|
|
27
|
+
"sender": {
|
|
28
|
+
"role": "assistant",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
`;
|